面向C程序員的現代C++(二)

面向C程序員的現代C++(二)

歡迎回來!在第一部分中,我討論了std: stringstd::vector如何與C交互,包括與C標準庫qsort調用交互。我們還發現C++std::sort比Cqsort快40%,因爲C++能夠內聯比較函數

在這一部分中,我們將繼續使用C++特性,您可以使用這些特性來爲代碼“逐行添加”,而不必立即使用所有1400頁的“C++編程語言”。

這裏討論的各種代碼示例可以在GitHub上找到。

如果你有任何你喜歡的東西,你希望看到討論或問題,請聯繫@PowerDNS_Bert或[email protected]

命名空間

命名空間允許名稱相同的事物同時存在。由於C++定義了許多函數和類,這些函數和類可能會與您在C中使用的名稱發生衝突,因此這對我們來說是非常重要的。因此,C++庫就在std:: namespace中,使編譯C代碼變得更容易。

爲了節省大量的輸入,可以導入整個std::namespaces通過using namespace std,或者選擇單個名稱:using std::thread

C++本身確實有一些關鍵字,比如class、throw、catch和reintrepret_cast,它們可能會與現有的C代碼發生衝突。

C++又一個名稱是“帶類的C”,由一個轉換器把這個新的C++轉換成普通的C語言。有趣的是,這個翻譯本身就是用“帶有類的C語言編寫的”。

大多數高級C項目已經使用了與C++幾乎完全相同的類。在最簡單的形式中,類只不過是具有一些調用約定的結構體。(繼承和虛函數使情況變得複雜,這些可選技術將在第3部分中討論)。

典型的現代C代碼將定義一個描述某樣東西的結構,然後有一組函數接受指向該結構的指針作爲第一個參數:

struct Circle
{
    int x, y;
    int size;
    Canvas* canvas;
    ...
};

void setCanvas(Circle* circle, Canvas* canvas);
void positionCircle(Circle* circle, int x, int y);
void paintCircle(Circle* circle);

許多C項目實際上甚至會使這些結構(部分)變得不透明,這表明API用戶不應該看到這些內部結構。這是通過在.h中向前聲明一個結構體來實現的,但是從來沒有定義它。sqlite3句柄就是這種技術的一個很好的例子。

一個C++類就像上面的struct一樣佈局,實際上,如果它包含了方法(成員函數),那麼這些內部調用的方式完全相同:

class Circle
{
public:
    Circle(Canvas* canvas);  // "constructor"
    void position(int x, int y);
    void paint();
private:
    int d_x, d_y;
    int d_size;
    Canvas* d_canvas;
};

void Circle::paint()
{
    d_canvas->drawCircle(d_x, d_y, d_size);
}

如果我們看“水下”Circle::position(1,2)實際上被稱爲Circle::position(Circle*this,int x, int y)。沒有比這個更神奇的(或頭頂上的)了。此外,Circle::paintCircle::position函數在範圍內具有d_x、d_y、d_size和d_canvas。

兩者之間區別之一是這些“私有成員變量”不能從外部訪問。例如,當x中的任何更改需要與畫布協調時,這可能是有用的,而且我們不希望用戶在不知情的情況下更改x。如前所述,許多C項目都通過技巧實現同樣的不透明——這只是一種更簡單的方法。

到目前爲止,類僅僅是語法糖和一些範圍規則。然而. .

資源獲取是初始化(RAII)

大多數現代語言需要執行垃圾收集是因爲很難跟蹤內存。這導致週期性的GC運行,有可能“阻止世界”。儘管技術正在改進,GC仍然是一個令人擔憂的問題,尤其是在一個多核心的世界中。

儘管C和C++不進行垃圾收集,但仍然是非常困難的,在所有(錯誤)條件下跟蹤每個內存分配是非常困難的。C++有複雜的方法來幫助您,這些方法構建在稱爲構造函數和析構函數的原語之上。

SmartFP 就是一個例子,我們將在接下來的章節中對其進行補充,這樣它就會變得更加有用和安全:

struct SmartFP
{
    SmartFP(const char* fname, const char* mode)
    {
        d_fp = fopen(fname, mode);
    }
    ~SmartFP()
    {
        if(d_fp)
            fclose(d_fp);
    }
    FILE* d_fp;
};

注意: 結構體和類是一樣的,除了所有的東西都是“公共的”。

典型的使用SmartFP:

void func()
{
    SmartFP fp("/etc/passwd", "r");
    if(!fp.d_fp)
        // do error things

    char line[512];
    while(fgets(line, sizeof(line), fp.d_fp)) {
        // do things with line
    }
    // note, no fclose
}

就像這樣編寫的,當SmartFP對象實例化時,就會發生對fopen()的實際調用。它調用構造函數,構造函數的名稱與結構本身相同:SmartFP。

然後,我們可以像往常一樣使用存儲在類中的文件*。最後,當fp超出範圍時,調用它的析構函數SmartFP::~SmartFP(),如果d_fp在構造函數中成功打開,它將爲我們fclose()。

這樣寫,代碼有兩個巨大的優點:1)文件指針永遠不會泄漏2)我們確切地知道何時關閉。帶有垃圾收集的語言也保證了“1”,但交付“2”需要努力或需要真正的工作。

這種使用帶有構造函數和析構函數的類或結構來擁有資源的技術稱爲資源獲取初始化或RAII,它被廣泛使用。對於更大的c++項目來說,在構造函數/析構函數對之外不包含對new或delete(或malloc/free)的單個調用是很常見的。或者事實上。

智能指針

內存泄漏是每個項目的禍害。即使使用垃圾收集,也可以將千兆字節的內存用於顯示聊天消息的單個窗口。

C++提供了許多所謂的智能指針,它們都有各自的優勢。最“做我想做的”智能指針是std:: shared_ptr,它最基本的形式是:

void func(Canvas* canvas)
{
    std::shared_ptr<Circle> ptr(new Circle(canvas));
    // or better:
    auto ptr = std::make_shared<Circle>(canvas)
}

第一個表單顯示了使用C++編寫malloc的方法,在本例中,爲Circle實例分配內存,並使用canvas參數構造它。如前所述,大多數現代C++項目很少使用“naked new”語句,而是將它們封裝在負責(de)分配的基礎設施中。

第二種方法不僅打字更少,而且效率更高。

不過,shared_ptr有更多的妙招:

// make a vector of shared pointers to Circle instances
std::vector<std::shared_ptr<Circle> > circles;

void func(Canvas* canvas)
{
    auto ptr = std::make_shared<Circle>(canvas)
    circles.push_back(ptr);
    ptr->draw();
}

這首先定義了一個std::shared_ptrs的向量,然後創建一個shared_ptr並將其存儲在Circle向量中。當func返回時,ptr超出了範圍,但是由於它的副本在矢量圓中,所以Circle對象仍然是活着的。因此shared_ptr是一個引用計數智能指針。

shared_ptr還有一個很好的功能:

void func()
{
        FILE *fp = fopen("/etc/passwd", "r");
        if(!fp)
          ; // do error things

        std::shared_ptr<FILE> ptr(fp, fclose);

        char buf[1024];
        fread(buf, sizeof(buf), 1, ptr.get());
}

在這裏,我們使用名爲fclose的自定義刪除器創建shared_ptr。這意味着如果需要,ptr知道如何在自己之後進行清理,並且使用一行創建了一個引用計數文件句柄。

有了這個,我們現在就能明白爲什麼我們之前定義的SmartFP不是很安全了。可以對它進行復制,一旦該複製超出範圍,它也將關閉相同的文件*。shared_ptr使我們不必考慮這些事情。

std::shared_ptr的缺點是它使用內存進行實際的引用計數,這對於多線程操作也是安全的。它還必須存儲一個可選的自定義刪除程序。

c++還提供了其他智能指針,其中最相關的是std::unique_ptr。我們通常不需要實際的引用計數,只需要“在超出範圍時進行清理”。這就是std::unique_ptr所提供的,幾乎爲零開銷。還有一些工具可以將std: unique_ptr“移動”到存儲中,以便它在範圍內。我們稍後再討論這個問題。

線程、原子

每次我用C或更老的C++創建一個帶有pthread_create的線程時,我都會感覺很糟糕。必須填滿所有的數據,才能通過一個空指針來啓動線程,這感覺很傻很危險。

C++在本機線程系統之上提供了一個強大的層,以使這一切更容易和更安全。此外,它還可以輕鬆地從線程中獲取數據。

一個小示例:

double factorial(unsigned int limit)
{
        double ret = 1;
        for(unsigned int n = 1 ; n <= limit ; ++n)
                ret *= n;
        return ret;
}


int main()
{
      auto future1 = std::async(factorial, 19);
      auto future2 = std::async(factorial, 12);      
      double result = future1.get() + future2.get();

      std::cout<<"Result is: " << result << std::endl;
}

如果不需要返回代碼,那麼啓動一個線程就像:

std::thread t(factorial, 19);
    t.join(); // or t.detach()

與C11一樣,C++也提供原子操作。這些就像定義std::atomic<uint64_t> packetcounter一樣簡單。packetcounter上的操作是原子性的,如果需要特定的模式(例如構建無鎖數據結構),可以使用一套廣泛的方法來詢問或更新packetcounter。

注意,與在C中一樣,將計數器從多個線程中聲明爲volatile沒有任何用處。需要全原子或顯式鎖定。

Looking

就像跟蹤內存分配一樣,確保在所有代碼頁上釋放鎖是很困難的。像往常一樣,賴伊伸出援手:

std::mutex g_pages_mutex;
std::map<std::string, std::string> g_pages;

void func()
{
    std::lock_guard<std::mutex> guard(g_pages_mutex);
    g_pages[url] = result;
}

上面的守護對象將在需要的時候將g_pages_mutex鎖定很長時間,但是無論是否通過錯誤,在func()完成時總是會釋放它。

錯誤處理

老實說,錯誤處理在任何語言中都是一個難以解決的問題。我們可以用檢查來對代碼進行謎語,每次檢查時,我都想知道“如果失敗了,程序實際上應該做什麼”。選項很少是好的——忽略、提示用戶、重新啓動程序或記錄消息,希望有人閱讀它。

C++提供的異常在任何情況下都比檢查每個返回代碼有一些好處。異常的好處是,與返回代碼不同,它在默認情況下不會被忽略。首先讓我們更新SmartFP,它會拋出異常:

std::string stringerror()
{
    return strerror(errno);
}

struct SmartFP
{
        SmartFP(const char* fname, const char* mode)
        {
                d_fp = fopen(fname, mode);
                if(!d_fp)
                    throw std::runtime_error("Can't open file: " + stringerror());
        }
        ~SmartFP()
        {
                fclose(d_fp);
        }
        FILE* d_fp;
};

如果我們現在創建了一個SmartFP,並且它沒有拋出異常,我們知道使用它是很好的。對於錯誤報告,我們可以捕獲異常:

void func2()
{
        SmartFP fp("nosuchfile", "r");

        char line[512];
        while(fgets(line, sizeof(line), fp.d_fp)) {
                // do things with line
        }       
        // note, no fclose
}

void func()
{
    func2();
}

int main()
try {
    func();
}
catch(std::exception& e) {
    std::cerr<< "Fatal error: " << e.what() << std::endl;
}

這顯示了一個從SmartFP::SmartFP中拋出的異常,該異常隨後“通過”func2()和func()以在main()中被捕獲。關於fallthrough的好處是,錯誤總是會被注意到,而不像簡單的返回代碼可以忽略。然而,不利的一面是,異常可能會在離拋出異常的地方很遠的地方被“捕獲”,這可能會導致意外。這通常會導致良好的錯誤記錄。

結合RAII,異常是一種非常強大的技術,可以安全地獲取資源並處理錯誤。

可以拋出異常的代碼比不能拋出異常的代碼稍慢一些,但它幾乎不會出現在概要文件中。實際上拋出異常是相當重的,所以只在錯誤條件下使用它。

大多數調試器可以在拋出異常時中斷,這是一種強大的調試技術。在gdb中,這是通過接球完成的。

如前所述,沒有任何錯誤處理技術是完美的。一個看起來很有希望的東西是std::expect工作或boost::expect,它創建的函數具有返回代碼或拋出異常(如果不查看它們)。

總結

在“C++爲C程序員”的第2部分中,我們展示了類是如何在C中得到很好的應用的,除了C++使它更容易。此外,C++類(和結構)可以有構造函數和析構函數,這些函數對於確保在需要時獲取和釋放資源非常有用。

基於這些基本類型,C++提供了各種智能和開銷的智能指針,涵蓋了大多數需求。

此外,C++爲線程、原子和鎖定提供了良好的支持。最後,異常是處理錯誤的一種強大方法。

如果你有任何你喜歡的東西,你希望看到討論或問題,請聯繫@PowerDNS_Bert或[email protected]

請繼續關注第3部分!

注:原文MODERN C++ FOR C PROGRAMMERS: PART 1.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章