深入理解C++11讀書筆記(一)

C++11相對於C++98/03顯著增強的特點:

  1. 通過內存模型、線程、原子操作等來支持本地並行編程
  2. 通過統一初始化表達式、auto、declytype、移動語義來統一對泛型編程的支持
  3. 通過constexpr、POD(概念)等更好的支持系統編程
  4. 通過內聯命名空間、繼承構造函數和右值引用等、以更好的支持庫的構建

新標準的誕生

1、宏__cplusplus使用

(參考https://blog.csdn.net/zzwdkxx/article/details/44244535)

#ifdef __cplusplus
extern “c” {
#endif
//一些代碼
#ifdef __cplusplus
}
#endif

使用上述定義的頭文件可以在.c文件中編譯,也可以在.cpp文件中編譯,它是C/C++混用頭文件的典型做法

extern “C”的作用如下:

(1)、核心作用:實現C和C++的混合編程。extern “C”提供一個鏈接交換指定符號,用於告訴C++這段函數是C函數,extern “C”後面的函數不使用C++的名字修飾,而是使用C。

(2)、C++支持函數重載,C不支持函數重載。函數被C++編譯後在庫中的名字與C語言不同。如void add(int a, int b),該函數在C編譯器編譯後,庫中名字爲_add,而C++編譯器則會生成add_int_int的名字。故C++提供C鏈接交換指定符號extern “C”來解決名字匹配問題。(extern c當中的C++不允許函數重載,因爲是按C語言的方式編譯)

(3)、被extern “C”限定的函數或變量是extern類型,extern是C/C++語言中表明函數和全局變量作用範圍(可見性)的關鍵字,此關鍵字告訴編譯器,該聲明的函數可以在本模塊或其它模塊使用。被extern “C”修飾的變量和函數按照C語言方式編譯和鏈接。

(4)、與extern對應的關鍵字是static,被static修飾的全局變量和函數只能在本模塊中使用。如果一個函數或變量只能在本模塊中使用時,不能用extern “C”修飾

 

舉個例子:

animal.h animal.cpp mian.c

目標:C語言調用animal.cpp提供的C++語言

//animal.h

#ifndef __ANIMAL_H__  //防止被重複包含
#define __ANIMAL_H__
#ifdef __cplusplus
extern "C" {
#endif
class ANIMAL{
public:
        ANIMAL(char* );
        ~ANIMAL();
        char* getname(void);
private:
        char* name;
};
void print(void);
#ifdef __cplusplus
}
#endif
#endif  // __ANIMAL_H__
//animal.cpp

#include "animal.h"
#include <iostream>
using namespace std;
ANIMAL::ANIMAL(char* data)//構造函數
{       name = new char[64];
        strcpy(name, data);
}
ANIMAL::~ANIMAL() //析構函數
{
        if(name)
        {
                delete[] name;
                name = NULL;
        }
}
char* ANIMAL::getname(void)
{        return name;
}
void print(void) //對外接口,而且必須有一個非類中方法,才能被C調用
{
        ANIMAL animal("dog");
        char* animal_name = animal.getname();
        cout << "animal name is :" << animal_name << endl;
}
//main.c

int main(void)
{       print();
        return 0;
}

注意,要想c語言調用c++,一定要用extern “C”使其按C語言的方式編譯

先用C++方式編譯animal.cpp爲animal.o

g++ -c animal.cpp

然後用gcc編譯出可執行文件

gcc -lstdc++ main.c animal.o -o main

注意:此時一定要加上-lstdc++,鏈接C++的標準庫,否則編譯無法通過。

./main

animal name is :dog

 

2、靜態斷言與static_assert

靜態斷言即在編譯時期的斷言

  1. 通過”除0”會導致編譯器報錯這個特性來實現靜態斷言
#define assert_static(e) \
do { \
        enum {assert_static__ = 1 / (3)}; \
 	} while (0)
  1. 使用c++11提供的static_assert(e,wrongInfo)

使用場景:

template <typename t, typename u> int bit_copy(t& a, t& b) {
    static_assert(sizeof(b) == sizeof(a), “the parameters of bit_copy must have same width”);
    memcpy(&a, &b, sizeof(b));
};
int main() {
    int a = 10;
    double b;
    bit_copy(a, b);
}

這樣,不用等到運行時期,在模板實例化(編譯時期)就能發現錯誤

注: static_assert的斷言表達式的結果必須實在編譯時期可以己算的表達式,即常量表達式。

下述例子使用變量就會導語法錯誤。

int positive(const int n) {
    static_assert(n > 0, “value must > 0”);
}

 

3、noexcept修飾符與noexcept操作符

  1. noexcept修飾符
#include<iostream>
using namespace std;
void Throw() {throw 1;}
void NoBlockThrow() {Throw();}
void BlockThrow() noexcept {Throw();}

int main()
{
    try {
        Throw();
    }
    catch(...) {
        cout << "Pound throw." << endl;
    }

    try {
        NoBlockThrow();
    }
    catch(...) {
        cout << "Throw is not blocked." << endl;
    }

    try {
        BlockThrow();
    }
    catch(...) {
        cout << "Pound throw 1." << endl;
    }
}

上述例子輸出結果如下:

Pound throw.

Throw is not blocked.

terminate called after throwing an instance of 'int'

已放棄

可以看到調用BlockThrow時,對於拋出的異常並沒有輸出Pound throw 1.,這是因爲noexcept修飾的函數不會拋出異常,其與之前表示函數不會派出異常的動態異常聲明throw()相比較有一定的優勢,noexcept修飾的函數假如拋出了異常,會直接調用std::terminate()函數來終止程序的運行。而基於異常機制的throw()會帶來額外開銷:拋出異常導致函數棧依次被展開,並且依次調用已構造的自動變量的析構函數。所以noexcept效率會高一些

    2. noexcept操作符

   noexcept(e),假如e爲不會拋出異常,返回true,否則會返回false。

   其通常用於模板

   

template <class T>

   void fun() noexcept(noexcept(T())) {}

   這裏fun函數是否會拋出異常取決於T()是否會拋出異常,第二個noexcept爲操作符,假如T()不會拋出異常,則返回True,否則返回false。這樣可以使函數模板根據條件實現noexcept修飾的版本或無noexcept修飾的版本。

 

4、快速初始化成員變量

在C++98中,只允許對類中靜態成員常量進行初始化,並且靜態成員也只能時整形或者枚舉型才能就地初始化,非靜態的成員變量必須在構造函數中進行

在C++11中,還允許使用”=”和”{}”進行就地的非晶態成員變量初始化(不能像初始化列表一樣使用”()”)。

例如: struct init {int a = 1; double b {1.2};};

同時,C++11對於C++98的初始化列表這個手段也同樣支持,並且兩者可以共用而不發生衝突。初始化列表的效果優先於就地初始化。

struct Mem {
    Mem() {cout << “Mem default, num:” << num << endl;}
    Mem(int i): num(i) {cout << “Mem, num: ” << num << endl;}

    int num = 2; //使用=初始化非靜態成員
}

假如 Mem member;由於是調用默認構造函數,且沒有初始化列表,那麼member.num = 2;

假如 Mem member(19);由於調用了Mem(int i),且有初始化列表,其優先級較高,member.num = 19。

 

那麼對於非靜態成員變量進行就地初始化的好處是什麼呢?其可以大大降低程序員的工作量降低編程的複雜性。下述例子很清楚的描述其好處

class Mem {
public:
    Mem(int i): m(i) {}
private:
    int m;
};

class Group {
public:
    Group() {}  //不需要初始化data、mem、name成員了
    Group(int a): data(a) {}  //不需要初始化mem、name成員了
    Group(Mem m) : mem(m) {} //不需要初始化data、name成員了
    Group(int a, Mem m, string n): data(a), mem(m), name(n) {}
private:
    int data = 1;
    Mem mem{0}; //注意不能像平常初始化一樣調用”()”
    string name{“Group”};
};

可以想象,假如使用C++98,就不得不在每個構造函數當中編寫初始化列表了,而C++11的編譯器通過對非靜態成員變量就地初始化,可以避免重複的在多個構造函數中編寫初始化列表了

 

但是對於非常量的靜態成員變量,C++11與C++98是保持了一致的。必須到頭文件以外去定義它以保證類靜態成員的定義只存在於一個目標文件中。

 

爲什麼C++11之前,只有靜態常量整型數據成員纔可以在類中初始化呢?

只有靜態常量整型數據成員,纔可以在類中初始化
這是因爲,當時認爲,類定義中的數據定義,是一種聲明,不是數據定義,並沒有分配內存。

當用類 定義對象(變量,常量)時候,纔開始定義數據(分配內存)。
靜態常量整型數據成員
1)不是對象的一部分
2)可以產生常量表達式,所以可以在類中初始化。---否則,用它作爲數組的大小,就不合適了。
靜態常量整型數據成員,能夠用來當作常量表達式使用,不在內部定義的話,則該常量表達式未定義,就不能使用了。

 

5、非靜態成員的sizeof

C++98,對於類的非靜態成員變量,不允許直接對其進行sizeof操作。只允許對類實例對象的成員進行sizeof操作。舉個例子:

class A {
public:
    int num;
};
A a;
sizoef(A::num)  //c++98 wrong c++11 right
sizeof(a.num)  //all right

在沒有定義類實例的時候,要獲得類成員的大小,通常採用如下方法:

sizeof(((A*)0)->hand);

這裏將0強制轉換爲一個A類指針,然後對指針解引用獲得成員變量,在進行sizeof操作求得該成員變量的大小。

 

6、 擴展的friend方法

class Poly;
typedef Poly P;
class LiLei {
    friend class Poly; //C++98通過,c++11通過
}
class Jim {
    friend Poly; //C++98失敗,c++11通過
}
class HanMeiMei {
    friend P; //C++98失敗,c++11通過
}

friend關鍵字用於聲明類的友元,友元類(方法)可以無視類中的成員的屬性盡情的訪問成員,由上圖可以看到C++98必須使用”class”字眼才能聲明友元類,C++11則不需要,這爲我們帶來了一個好處:可以在類模板中聲明友元類。這通常可以用於測試用例(正常情況下友元類破壞封裝,少用爲妙)

template <typename T> class DefenderT {
public:
friend T;
void Defence(int x, int y) {}
void Tackle(int x, int y) {}
private:
    int pos_x = 15;
    int pos_y = 0;
    int speed = 2;
    int stamina = 120;
};

template <typename T> class AttackerT {
public:
    friend T;
    void Move(int x, int y) {}
    void SpeedUp(float ratio) {}

private:
    int pos_x = 0;
    int pos_y = 10;
    int speed = 3;
    int stamina = 100;
};

using Defender = DefenderT<int>; // 用using和typedef的作用一樣
using Attacker = AttackerT<int>; // 當實例化時爲內置類型時,友元失效(無友元類)

#ifdef UNIT_TEST
class Validator {
public:
    void Validate(int x, int y, DefenderTest &d) {} // 內部實現利用d的私有成員記性校驗
    void Validate(int x, int y, AttackerTest &d) {} // 內部實現利用a的私有成員記性校驗
};

using DefenderTest = DefenderT<Validator>; // 測試專用的定義,Validator類成爲友元類
using AttackerTest = AttackerT<Validator>;

int main() {
    DefenderTest d;
    AttackerTest a;
    a.Move(15, 30);
    d.Defence(15, 30);
    a.SpeedUp(1.5f);
    d.Defence(15, 30);

    Validator v;
    v.Validate(7, 0, d);
    v.Validate(1, -10, a);
    return 0;
}

7、final/override控制

final用於繼承類中,當某個繼承類的虛函數帶有final關鍵字,此繼承類的子類都不能在重載此函數

struct Object {
    virtual void fun() = 0;
}

struct Base : public Object {
    void fun() final; // 聲明爲final
}

struct Derived : public Base {
    void fun(); // 無法通過編譯
}

好處:思考一下,某個繼承類實現了一套完備的打印日誌的接虛函數口,當多個子類都繼承自此繼承類時,爲了防止子類重載此接口而保證大家都是統一的打印格式,就可以採取final關鍵字而防止子類繼續重載此接口

 

override也時用於繼承類中,當子類某個函數聲明爲override時,表明此函數時重載基類的虛函數。

struct Object {
    virtual void fun() = 0;
}

struct Base : public Object {
    void fun() override;
}

struct Derived : public Base {
    void func() override; // 無法通過編譯
}

上面示例可以看到override的好處,假如Derived類的方法不帶有oveeride關鍵字,是可以通過編譯的,即使你本意是想重載fun函數,但編譯器無法識別你的意圖,會認爲你在重新實現一個接口。當使用了override關鍵字告訴編譯器你是想重載函數時,編譯器此時纔會發現函數名func寫錯,應當爲fun。

 

8、模板函數的默認模板參數

C++98中模板類聲明的時候,可以提供默認模板參數,但是模板函數不行。C++11對模板函數支持提供默認模板參數

void DefParm(int m = 3) {} // C++98編譯通過, C++11編譯通過
template <typename T = int> // C++98編譯通過, C++11編譯通過
class DefClass {};
template <typename T = int> // C++98編譯失敗, C++11編譯通過
void DefTempParm() {};
並且其與類模板有些差異,函數模板制定默認值不必遵從”從右往左”的規則
template <typename T1, typename T2 = int> class DefClass1;
template <typename T1 = int, typename T2> class DefClass1; // 無法編譯通過

template <typename T, int i = 0> class DefClass3;
template <int i = 0, typename T> class DefClass4; // 無法編譯通過

template <typename T1 = int, typename T2> void DefFunc1(T1 a, T2 b); // 可行
template <int i = 0, typename T> void DefFunc2(T a); // 可行

需要注意的是,其不能根據默認形參推導出來模板參數的類型,只能通過實參推導

void g() {
    f(1, 'c'); // f<int, char>(1, 'c'), 實參1和'c'推導
    f(1); // f<int, double>(1, 0), 使用了默認模板參數doubl,int通過實參1推導
    f(); // 錯誤,無法通過默認形參推導類型
    f<int>(); // f<int, double>(0, 0), 使用了默認模板參數double
    f<int, char>(); // f<int, char>(0, 0)
}

9、外部模板

都知道在聲明變量的時候由’extern’關鍵字。其作用是爲了跨模塊使用全局變量。具體的舉個例子:a.c中定義了一個全局變量int i,b.c中想使用這個全局變量就可以作一個外部聲明extern int i。這樣在編譯的時候,a.o中數據區會實實在在保存i數據,而在b.o中只是記錄了i符號會引用其他目標文件(a.o)的數據區中名爲i的數據。這樣一來,在鏈接a.o和b.o爲單個可執行文件d的時候,d當中的數據區也只會有一個i的數據(供a.c和b.c的代碼共享)。假如不在b.c進行extern int i的聲明,那麼會報錯,因爲無法確定相同的符號是否會合並。

而模板函數實例化重複和變量重複不同(不進行extern聲明),代碼重複是允許的,只是在鏈接的時候會通過編譯器的輔助手段來將重複的函數刪除掉,以這樣的方式解決模板實例化產生的代碼冗餘問題。具體情況見下圖:

這樣就會有編譯性能上的損耗(1、在每個模塊當中會進行模板實例化。2、鏈接的時候去要刪除多份重複的模板實例化函數的函數)。但是在C++11中提供了外部模板機制之後,性能就會改進很多.

具體的代碼編寫方式和顯示實例化有關,舉例子:某個頭文件test.h當中有一個模板函數聲明:

template <typename T> void fun(T) {}

我們只需要顯示實例化聲明template void fun<int>(int)頭部增加extern,就會像extern聲明變量一樣告知編譯器此模板實例化的定義在其他模塊

extern template void fun<int>(int); // 外部模板必須結合顯示實例化實現

這樣以後int相關實例化的fun都只會有一份模板實例化。

// test1.cpp
#include "test.h"
template void fun<int>(int); // 顯示實例化
void test1() {fun(3);} 

// test2.cpp
#include "test.h"
extern template void fun<int>(int); // 外部模板的聲明
void test1() {fun(3);} 

具體的外部模板聲明的模板函數的編譯和鏈接行爲如下圖所示:

10、局部和匿名類型作模板實參

代碼清晰的顯示其支持的行爲:

template <typename T> class X {};
template <typename T> void TempFun(T t) {};
struct A{} a;
struct {int i;}b; // 匿名類型變量b
typedef struct {int i;}B; // 匿名類型B

void Fun()
{
    struct C {} c; //局部類型C和局部類型變量c

    X<A> x1; // C++98編譯通過, C++11通過
    X<B> x2; // C++98編譯不通過, C++11通過
    X<C> x3; // C++98編譯不通過, C++11通過

    TempFun(a); // C++98編譯通過, C++11通過
    TempFun(b); // C++98編譯不通過, C++11通過
    TempFun(c); // C++98編譯不通過, C++11通過
}

 

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