C++那些問題

參考鏈接:

  • static靜態成員變量不能在類的內部初始化。在類的內部只是聲明,定義必須在類定義體的外部,通常在類的實現文件中初始化。

  • 由於聲明爲static的變量只被初始化一次,因爲它們在單獨的靜態存儲中分配了空間,因此類中的靜態變量由對象共享。對於不同的對象,不能有相同靜態變量的多個副本。也是因爲這個原因,靜態變量不能使用構造函數初始化。

  • 類中的靜態變量應由用戶使用類外的類名和範圍解析運算符顯式初始化

  • 靜態對象的範圍是貫穿程序的生命週期

  • 一個對象的this指針並不是對象本身的一部分,不會影響sizeof(對象)的結果。

  • 在類的非靜態成員函數中返回類對象本身的時候,直接使用 return *this。

  • 當參數與成員變量名相同時,如this->n = n (不能寫成n = n)。

  • this在成員函數的開始執行前構造,在成員的執行結束後清除。

  • this類型爲const A* const。A爲類。

  • 內聯能提高函數效率,但並不是所有的函數都定義成內聯函數!內聯是以代碼膨脹(複製)爲代價,僅僅省去了函數調用的開銷,從而提高函數的執行效率。

  • 虛函數可以是內聯函數,內聯是可以修飾虛函數的,但是當虛函數表現多態性的時候不能內聯。

  • 內聯是在編譯器建議編譯器內聯,而虛函數的多態性在運行期,編譯器無法知道運行期調用哪個代碼,因此虛函數表現爲多態性時(運行期)不可以內聯。

  • inline virtual 唯一可以內聯的時候是:編譯器知道所調用的對象是哪個類(如 Base::who()),這隻有在編譯器具有實際對象而不是對象的指針或引用時纔會發生。

  • sizeof: 普通繼承,派生類繼承了所有基類的函數與成員,要按照字節對齊來計算大小

  • sizeof:虛函數繼承,不管是單繼承還是多繼承,都是繼承了基類的vptr。(32位操作系統4字節,64位操作系統 8字節)!

  • sizeof: 靜態變量不影響類的大小

  • sizeof:對於包含虛函數的類,不管有多少個虛函數,只有一個虛指針,vptr的大小。

  • sizeof: 派生類虛繼承多個虛函數,會繼承所有虛函數的vptr。

  • 抽象類中:在成員函數內可以調用純虛函數,在構造函數/析構函數內部不能使用純虛函數。

  • 如果一個類從抽象類派生而來,它必須實現了基類中的所有純虛函數,才能成爲非抽象類。

  • 抽象類至少包含一個純虛函數

  • 不能創建抽象類的對象

  • 抽象類的指針和引用 指向 由抽象類派生出來的類的對象

  • 派生類沒有實現純虛函數,那麼派生類也會變爲抽象類,不能創建抽象類的對象

  • 虛函數的調用取決於指向或者引用的對象的類型,而不是指針或者引用自身的類型。

  • 默認參數是靜態綁定的,虛函數是動態綁定的。 默認參數的使用需要看指針或者引用本身的類型,而不是對象的類型。

  • 靜態函數不可以聲明爲虛函數,同時也不能被const 和 volatile關鍵字修飾

  • 爲什麼構造函數不可以爲虛函數?
    解:儘管虛函數表vtable是在編譯階段就已經建立的,但指向虛函數表的指針vptr是在運行階段實例化對象時才產生的。 如果類含有虛函數,編譯器會在構造函數中添加代碼來創建vptr。 問題來了,如果構造函數是虛的,那麼它需要vptr來訪問vtable,可這個時候vptr還沒產生。 因此,構造函數不可以爲虛函數。

  • 虛函數可以被私有化,但有一些細節需要注意。
    1 基類指針指向繼承類對象,則調用繼承類對象的函數;
    2 int main()必須聲明爲Base類的友元,否則編譯失敗。 編譯器報錯: ptr無法訪問私有函數。
    3 當然,把基類聲明爲public, 繼承類爲private,該問題就不存在了。

  • volatile 關鍵字聲明的變量,每次訪問時都必須從內存中取出值(沒有被 volatile 修飾的變量,可能由於編譯器的優化,從 CPU 寄存器中取值)

  • const 可以是 volatile (如只讀的狀態寄存器)

  • 指針可以是 volatile

  • 斷言,是宏,而非函數。assert 宏的原型定義在 (C)、(C++)中,其作用是如果它的條件返回錯誤,則終止程序執行。可以通過定義 NDEBUG 來關閉 assert,但是需要在源代碼的開頭,include 之前。

  • 斷言主要用於檢查邏輯上不可能的情況。

  • 它們可用於檢查代碼在開始運行之前所期望的狀態,或者在運行完成後檢查狀態。與正常的錯誤處理不同,斷言通常在運行時被禁用。

  • 忽略斷言:在代碼開頭加上:#define NDEBUG // 加上這行,則 assert 不可用

  • extern "C"全部都放在於cpp程序相關文件或其頭文件中。

  • 在C中struct只單純的用作數據的複合類型,也就是說,在結構體聲明中只能將數據成員放在裏面,而不能將函數放在裏面。

  • 在C結構體聲明中不能使用C++訪問修飾符,如:public、protected、private 而在C++中可以使用。

  • 在C中定義結構體變量,如果使用了下面定義必須加struct。

  • C的結構體不能繼承(沒有這一概念)。

  • 若結構體的名字與函數名相同,可以正常運行且正常的調用!例如:可以定義與 struct Base 不衝突的 void Base() {}。

  • 與C對比如下:
    1.C++結構體中不僅可以定義數據,還可以定義函數。
    2.C++結構體中可以使用訪問修飾符,如:public、protected、private 。
    3.C++結構體使用可以直接使用不帶struct。
    4.C++繼承若結構體的名字與函數名相同,可以正常運行且正常的調用!但是定義結構體變量時候只用帶struct的!

  • union
    1.默認訪問控制符爲 public
    2.可以含有構造函數、析構函數
    3.不能含有引用類型的成員
    4.不能繼承自其他類,不能作爲基類
    5.不能含有虛函數
    6.匿名 union 在定義所在作用域可直接訪問 union 成員
    7.匿名 union 不能包含 protected 成員或 private 成員
    8.全局匿名聯合必須是靜態(static)的

  • .C實現多態

  • 封裝
    C語言中是沒有class類這個概念的,但是有struct結構體,我們可以考慮使用struct來模擬;
    使用函數指針把屬性與方法封裝到結構體中。
  • 繼承
    結構體嵌套
  • 多態、
    類與子類方法的函數指針不同

在C語言的結構體內部是沒有成員函數的,如果實現這個父結構體和子結構體共有的函數呢?我們可以考慮使用函數指針來模擬。但是這樣處理存在一個缺陷就是:父子各自的函數指針之間指向的不是類似C++中維護的虛函數表而是一塊物理內存,如果模擬的函數過多的話就會不容易維護了。

模擬多態,必須保持函數指針變量對齊(在內容上完全一致,而且變量對齊上也完全一致)。否則父類指針指向子類對象,運行崩潰!

#include <stdio.h>

// 重定義一個函數指針類型
typedef void (*pf) ();

/**
   父類 
 */ 
typedef struct _A
{
    pf _f;
}A;


/**
   子類
 */
typedef struct _B
{ 
    A _b; // 在子類中定義一個基類的對象即可實現對父類的繼承。 
}B;

void FunA() 
{
    printf("%s\n","Base A::fun()");
}

void FunB() 
{
    printf("%s\n","Derived B::fun()");
}


int main() 
{
    A a;
    B b;

    a._f = FunA;
    b._b._f = FunB;

    A *pa = &a;
    pa->_f();
    pa = (A *)&b;   // 讓父類指針指向子類的對象,由於類型不匹配所以要進行強轉 
    pa->_f();
    return 0;
}
  • explicit 修飾構造函數時,可以防止隱式轉換和複製初始化
  • explicit 修飾轉換函數時,可以防止隱式轉換,但按語境轉換除外
#include <iostream>

using namespace std;

struct A
{
    A(int) { }
    operator bool() const { return true; }
};

struct B
{
    explicit B(int) {}
    explicit operator bool() const { return true; }
};

void doA(A a) {}

void doB(B b) {}

int main()
{
    A a1(1);        // OK:直接初始化
    A a2 = 1;        // OK:複製初始化
    A a3{ 1 };        // OK:直接列表初始化
    A a4 = { 1 };        // OK:複製列表初始化
    A a5 = (A)1;        // OK:允許 static_cast 的顯式轉換 
    doA(1);            // OK:允許從 int 到 A 的隱式轉換
    if (a1);        // OK:使用轉換函數 A::operator bool() 的從 A 到 bool 的隱式轉換
    bool a6(a1);        // OK:使用轉換函數 A::operator bool() 的從 A 到 bool 的隱式轉換
    bool a7 = a1;        // OK:使用轉換函數 A::operator bool() 的從 A 到 bool 的隱式轉換
    bool a8 = static_cast<bool>(a1);  // OK :static_cast 進行直接初始化

    B b1(1);        // OK:直接初始化
//    B b2 = 1;        // 錯誤:被 explicit 修飾構造函數的對象不可以複製初始化
    B b3{ 1 };        // OK:直接列表初始化
//    B b4 = { 1 };        // 錯誤:被 explicit 修飾構造函數的對象不可以複製列表初始化
    B b5 = (B)1;        // OK:允許 static_cast 的顯式轉換
//    doB(1);            // 錯誤:被 explicit 修飾構造函數的對象不可以從 int 到 B 的隱式轉換
    if (b1);        // OK:被 explicit 修飾轉換函數 B::operator bool() 的對象可以從 B 到 bool 的按語境轉換
    bool b6(b1);        // OK:被 explicit 修飾轉換函數 B::operator bool() 的對象可以從 B 到 bool 的按語境轉換
//    bool b7 = b1;        // 錯誤:被 explicit 修飾轉換函數 B::operator bool() 的對象不可以隱式轉換
    bool b8 = static_cast<bool>(b1);  // OK:static_cast 進行直接初始化

    return 0;
}
  • 友元提供了一種 普通函數或者類成員函數 訪問另一個類中的私有或保護成員 的機制。也就是說有兩種形式的友元:

(1)友元函數:普通函數對一個訪問某個類中的私有或保護成員。

(2)友元類:類A中的成員函數訪問類B中的私有或保護成員

優點:提高了程序的運行效率。

缺點:破壞了類的封裝性和數據的透明性。

總結: - 能訪問私有成員 - 破壞封裝性 - 友元關係不可傳遞 - 友元關係的單向性 - 友元聲明的形式及數量不受限制

在類聲明的任何區域中聲明,而定義則在類的外部。
friend <類型><友元函數名>(<參數表>);
注意,友元函數只是一個普通函數,並不是該類的類成員函數,它可以在任何地方調用,友元函數中通過對象名來訪問該類的私有或保護成員。

#include <iostream>

using namespace std;

class A
{
public:
    A(int _a):a(_a){};
    friend int geta(A &ca);  ///< 友元函數
private:
    int a;
};

int geta(A &ca) 
{
    return ca.a;
}

int main()
{
    A a(3);    
    cout<<geta(a)<<endl;

    return 0;
}

友元類的聲明在該類的聲明中,而實現在該類外。
friend class <友元類名>;
類B是類A的友元,那麼類B可以直接訪問A的私有成員。

#include <iostream>

using namespace std;

class A
{
public:
    A(int _a):a(_a){};
    friend class B;
private:
    int a;
};

class B
{
public:
    int getb(A ca) {
        return  ca.a; 
    };
};

int main() 
{
    A a(3);
    B b;
    cout<<b.getb(a)<<endl;
    return 0;
}

友元關係沒有繼承性 假如類B是類A的友元,類C繼承於類A,那麼友元類B是沒辦法直接訪問類C的私有或保護成員。

友元關係沒有傳遞性 假如類B是類A的友元,類C是類B的友元,那麼友元類C是沒辦法直接訪問類A的私有或保護成員,也就是不存在“友元的友元”這種關係。

  • using
#include <iostream>
#define isNs1 1
//#define isGlobal 2
using namespace std;
void func() 
{
    cout<<"::func"<<endl;
}

namespace ns1 {
    void func()
    {
        cout<<"ns1::func"<<endl; 
    }
}

namespace ns2 {
#ifdef isNs1 
    using ns1::func;    /// ns1中的函數
#elif isGlobal
    using ::func; /// 全局中的函數
#else
    void func() 
    {
        cout<<"other::func"<<endl; 
    }
#endif
}

int main() 
{
    /**
     * 這就是爲什麼在c++中使用了cmath而不是math.h頭文件
     */
    ns2::func(); // 會根據當前環境定義宏的不同來調用不同命名空間下的func()函數
    return 0;
}
class Base{
public:
 std::size_t size() const { return n;  }
protected:
 std::size_t n;
};
class Derived : private Base {
public:
 using Base::size;
protected:
 using Base::n;
};

在繼承過程中,派生類可以覆蓋重載函數的0個或多個實例,一旦定義了一個重載版本,那麼其他的重載版本都會變爲不可見。

如果對於基類的重載函數,我們需要在派生類中修改一個,又要讓其他的保持可見,必須要重載所有版本,這樣十分的繁瑣。

#include <iostream>
using namespace std;

class Base{
    public:
        void f(){ cout<<"f()"<<endl;
        }
        void f(int n){
            cout<<"Base::f(int)"<<endl;
        }
};

class Derived : private Base {
    public:
        using Base::f;
        void f(int n){
            cout<<"Derived::f(int)"<<endl;
        }
};

int main()
{
    Base b;
    Derived d;
    d.f();
    d.f(1);
    return 0;
}

如上代碼中,在派生類中使用using聲明語句指定一個名字而不指定形參列表,所以一條基類成員函數的using聲明語句就可以把該函數的所有重載實例添加到派生類的作用域中。此時,派生類只需要定義其特有的函數就行了,而無需爲繼承而來的其他函數重新定義。

C中常用typedef A B這樣的語法,將B定義爲A類型,也就是給A類型一個別名B

對應typedef A B,使用using B=A可以進行同樣的操作。

typedef vector<int> V1; 
using V2 = vector<int>;
  • 全局作用域符(::name):用於類型名稱(類、類成員、成員函數、變量等)前,表示作用域爲全局命名空間

  • 類作用域符(class::name):用於表示指定類型的作用域範圍是具體某個類的

  • 命名空間作用域符(namespace::name):用於表示指定類型的作用域範圍是具體某個命名空間的

  • enum:
    傳統問題:

    • 作用域不受限,會容易引起命名衝突。例如下面無法編譯通過的:
#include <iostream>
using namespace std;

enum Color {RED,BLUE};
enum Feeling {EXCITED,BLUE};

int main() 
{
    return 0;
}
  • 會隱式轉換爲int
  • 用來表徵枚舉變量的實際類型不能明確指定,從而無法支持枚舉類型的前向聲明。

解決作用域不受限帶來的命名衝突問題的一個簡單方法是,給枚舉變量命名時加前綴,如上面例子改成 COLOR_BLUE 以及 FEELING_BLUE。

namespace Color 
{
    enum Type
    {
        RED=15,
        YELLOW,
        BLUE
    };
};

這樣之後就可以用 Color::Type c = Color::RED; 來定義新的枚舉變量了。如果 using namespace Color 後,前綴還可以省去,使得代碼簡化。不過,因爲命名空間是可以隨後被擴充內容的,所以它提供的作用域封閉性不高。在大項目中,還是有可能不同人給不同的東西起同樣的枚舉類型名。

更“有效”的辦法是用一個類或結構體來限定其作用域,例如:定義新變量的方法和上面命名空間的相同。不過這樣就不用擔心類在別處被修改內容。這裏用結構體而非類,一是因爲本身希望這些常量可以公開訪問,二是因爲它只包含數據沒有成員函數。

struct Color1
{
    enum Type
    {
        RED=102,
        YELLOW,
        BLUE
    };
};

C++11 標準中引入了“枚舉類”(enum class),可以較好地解決上述問題。

  • 新的enum的作用域不在是全局的
  • 不能隱式轉換成其他類型
/**
 * @brief C++11的枚舉類
 * 下面等價於enum class Color2:int
 */
enum class Color2
{
    RED=2,
    YELLOW,
    BLUE
};
r2 c2 = Color2::RED;
cout << static_cast<int>(c2) << endl; //必須轉!
  • 可以指定用特定的類型來存儲enum
enum class Color3:char;  // 前向聲明

// 定義
enum class Color3:char 
{
    RED='r',
    BLUE
};
char c3 = static_cast<char>(Color3::RED);
class Person{
public:
    typedef enum {
        BOY = 0,
        GIRL
    }SexType;
};
//訪問的時候通過,Person::BOY或者Person::GIRL來進行訪問。
  • 枚舉常量不會佔用對象的存儲空間,它們在編譯時被全部求值。

  • 枚舉常量的缺點是:它的隱含數據類型是整數,其最大值有限,且不能表示浮點。

  • 這裏的括號是必不可少的,decltype的作用是“查詢表達式的類型”,因此,上面語句的效果是,返回 expression 表達式的類型。注意,decltype 僅僅“查詢”表達式的類型,並不會對錶達式進行“求值”。
decltype (expression)
int i = 4;
decltype(i) a; //推導結果爲int。a的類型爲int。
using size_t = decltype(sizeof(0));//sizeof(a)的返回值爲size_t類型
using ptrdiff_t = decltype((int*)0 - (int*)0);
using nullptr_t = decltype(nullptr);
vector<int >vec;
typedef decltype(vec.begin()) vectype;
for (vectype i = vec.begin; i != vec.end(); i++)
{
//...
}
//在C++中,我們有時候會遇上一些匿名類型,如:
struct 
{
    int d ;
    doubel b;
}anon_s;
//而藉助decltype,我們可以重新使用這個匿名的結構體:

decltype(anon_s) as ;//定義了一個上面匿名的結構體
泛型編程中結合auto,用於追蹤函數的返回值類型

這也是decltype最大的用途了。
template <typename T>
auto multiply(T x, T y)->decltype(x*y)
{
    return x*y;
}

對於decltype(e)而言,其判別結果受以下條件的影響:

如果e是一個沒有帶括號的標記符表達式或者類成員訪問表達式,那麼的decltype(e)就是e所命名的實體的類型。此外,如果e是一個被重載的函數,則會導致編譯錯誤。 否則 ,假設e的類型是T,如果e是一個將亡值,那麼decltype(e)爲T&& 否則,假設e的類型是T,如果e是一個左值,那麼decltype(e)爲T&。 否則,假設e的類型是T,則decltype(e)爲T。

標記符指的是除去關鍵字、字面量等編譯器需要使用的標記之外的程序員自己定義的標記,而單個標記符對應的表達式即爲標記符表達式。例如:
int arr[4]
則arr爲一個標記符表達式,而arr[3]+0不是。

int i = 4;
int arr[5] = { 0 };
int *ptr = arr;
struct S{ double d; }s ;
void Overloaded(int);
void Overloaded(char);//重載的函數
int && RvalRef();
const bool Func(int);

//規則一:推導爲其類型
decltype (arr) var1; //int 標記符表達式

decltype (ptr) var2;//int *  標記符表達式

decltype(s.d) var3;//doubel 成員訪問表達式

//decltype(Overloaded) var4;//重載函數。編譯錯誤。

//規則二:將亡值。推導爲類型的右值引用。

decltype (RvalRef()) var5 = 1;

//規則三:左值,推導爲類型的引用。

decltype ((i))var6 = i;     //int&

decltype (true ? i : i) var7 = i; //int&  條件表達式返回左值。

decltype (++i) var8 = i; //int&  ++i返回i的左值。

decltype(arr[5]) var9 = i;//int&. []操作返回左值

decltype(*ptr)var10 = i;//int& *操作返回左值

decltype("hello")var11 = "hello"; //const char(&)[9]  字符串字面常量爲左值,且爲const左值。


//規則四:以上都不是,則推導爲本類型

decltype(1) var12;//const int

decltype(Func(1)) var13=true;//const bool

decltype(i++) var14 = i;//int i++返回右值
  • 右值引用可實現轉移語義(Move Sementics)和精確傳遞(Perfect Forwarding),它的主要目的有兩個方面:

    1.消除兩個對象交互時不必要的對象拷貝,節省運算存儲資源,提高效率。
    2.能夠更簡潔明確地定義泛型函數。

引用摺疊
  • X& &X& &&X&& & 可摺疊成 X&
  • X&& && 可摺疊成 X&&
  • C++的引用在減少了程序員自由度的同時提升了內存操作的安全性和語義的優美性。

  • private 是完全私有的,只有當前類中的成員能訪問到.

  • protected 是受保護的,只有當前類的成員與繼承該類的類才能訪問.

  • 在使用new的時候做了兩件事:

1、調用operator new分配空間

2、調用構造函數初始化對象

  • 在使用delete的時候也做了兩件事:

1、調用析構函數清理對象

2、調用operator delete函數釋放空間

  • 在使用new[N]的時候也做了兩件事:

1、調用operator new分配空間

2、調用N次構造函數初始化N個對象

  • 在使用delete[]的時候也做了兩件事:

1、調用N次析構函數清理N個對象

2、調用operator delete函數釋放空間

  • 1.1 字符串化操作符(#)

在一個宏中的參數前面使用一個#,預處理器會把這個參數轉換爲一個字符數組,換言之就是:#是“字符串化”的意思,出現在宏定義中的#是把跟在後面的參數轉換成一個字符串

  • 1.2 符號連接操作符(##)

“##”是一種分隔連接方式,它的作用是先分隔,然後進行強制連接。將宏定義的多個形參轉換成一個實際參數名。

注意事項:

(1)當用##連接形參時,##前後的空格可有可無。
(2)連接後的實際參數名,必須爲實際存在的參數名或是編譯器已知的宏定義。
(3)如果##後的參數本身也是一個宏的話,##會阻止這個宏的展開。

  • 1.3 續行操作符(\

當定義的宏不能用一行表達完整時,可以用”\”表示下一行繼續此宏的定義。
注意 \ 前留空格。

template<typename ...Args>
class A {
private:
    int size = 0;    // c++11 支持類內初始化
public:
    A() {
        size = sizeof...(Args);
        cout << size << endl;
    }
};

 A<int, string, vector<int>> a;    // 類型任意

 // Tuple就是利用這個特性(變長參數模板)
tuple<int, string> t = make_tuple(1, "hha");

單例模式

//簡單實現,只適用於單線程下。
//懶漢 線程不安全
class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    if (p == nullptr)
        p = new singleton();
    return p;
}
//餓漢 這個是線程安全的
class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = new singleton();
singleton* singleton::instance() {
    return p;
}

//多線程下單例模式
class singleton {
private:
    singleton() {}
    static singleton *p;
    static mutex lock_;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    lock_guard<mutex> guard(lock_);
    if (p == nullptr)
        p = new singleton();
    return p;
}
//雙重檢查鎖+自動回收
class singleton {
private:
    singleton() {}

    static singleton *p;
    static mutex lock_;
public:
    singleton *instance();

    // 實現一個內嵌垃圾回收類
    class CGarbo
    {
    public:
        ~CGarbo()
        {
            if(singleton::p)
                delete singleton::p;
        }
    };
    static CGarbo Garbo; // 定義一個靜態成員變量,程序結束時,系統會自動調用它的析構函數從而釋放單例對象
};

singleton *singleton::p = nullptr;
singleton::CGarbo Garbo;

singleton* singleton::instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr)
            p = new singleton();
    }
    return p;
}

5.memory barrier指令

DCLP問題在C++11中,這個問題得到了解決。

因爲新的C++11規定了新的內存模型,保證了執行上述3個步驟的時候不會發生線程切換,相當這個初始化過程是“原子性”的的操作,DCL又可以正確使用了,不過在C++11下卻有更簡潔的多線程singleton寫法了,這個留在後面再介紹。

C++11之前解決方法是barrier指令。要使其正確執行的話,就得在步驟2、3直接加上一道memory barrier。強迫CPU執行的時候按照1、2、3的步驟來運行。

第一種實現:

基於operator new+placement new,遵循1,2,3執行順序依次編寫代碼。

// method 1 operator new + placement new
singleton *instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr) {
            singleton *tmp = static_cast<singleton *>(operator new(sizeof(singleton)));
            new(p)singleton();
            p = tmp;
        }
    }
    return p;
}

**第二種實現:**

基於直接嵌入ASM彙編指令mfence,uninx的barrier宏也是通過該指令實現的。

#define barrier() __asm__ volatile ("lwsync")
singleton *singleton::instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        barrier();
        if (p == nullptr) {
            p = new singleton();
        }
    }
    return p;
}

通常情況下是調用cpu提供的一條指令,這條指令的作用是會阻止cpu將該指令之前的指令交換到該指令之後,這條指令也通常被叫做barrier。 上面代碼中的asm表示這個是一條彙編指令,volatile是可選的,如果用了它,則表示向編譯器聲明不允許對該彙編指令進行優化。lwsync是POWERPC提供的barrier指令。

  • Scott Meyer在《Effective C++》中提出了一種簡潔的singleton寫法
singleton *singleton::instance() {
    static singleton p;
    return &p;
}
mutex singleton::lock_;
atomic<singleton *> singleton::p;

/*
* std::atomic_thread_fence(std::memory_order_acquire); 
* std::atomic_thread_fence(std::memory_order_release);
* 這兩句話可以保證他們之間的語句不會發生亂序執行。
*/
singleton *singleton::instance() {
    singleton *tmp = p.load(memory_order_relaxed);
    atomic_thread_fence(memory_order_acquire);
    if (tmp == nullptr) {
        lock_guard<mutex> guard(lock_);
        tmp = p.load(memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new singleton();
            atomic_thread_fence(memory_order_release);
            p.store(tmp, memory_order_relaxed);
        }
    }
    return p;
}

值得注意的是,上述代碼使用兩個比較關鍵的術語,獲得與釋放:

  • 獲得是一個對內存的讀操作,當前線程的任何後面的讀寫操作都不允許重排到這個操作的前面去。
  • 釋放是一個對內存的寫操作,當前線程的任何前面的讀寫操作都不允許重排到這個操作的後面去。

如果是在unix平臺的話,除了使用atomic operation外,在不適用C++11的情況下,還可以通過pthread_once來實現Singleton。

原型如下:
int pthread_once(pthread_once_t once_control, void (init_routine) (void));

class singleton {
private:
    singleton(); //私有構造函數,不允許使用者自己生成對象
    singleton(const singleton &other);

    //要寫成靜態方法的原因:類成員函數隱含傳遞this指針(第一個參數)
    static void init() {
        p = new singleton();
    }

    static pthread_once_t ponce_;
    static singleton *p; //靜態成員變量 
public:
    singleton *instance() {
        // init函數只會執行一次
        pthread_once(&ponce_, &singleton::init);
        return p;
    }
};
7898366-48fceda3b5d668d1
image
  • C++ tr1全稱Technical Report 1,是針對C++標準庫的第一次擴展。即將到來的下一個版本的C++標準c++0x會包括它,以及一些語言本身的擴充。tr1包括大家期待已久的smart pointer,正則表達式以及其他一些支持範型編程的內容。草案階段,新增的類和模板的名字空間是std::tr1。
#include <tr1/array>
std::tr1::array<int ,10> a;//第一個類型,第二個大小
  • deque與vector最大的差異就是:

    1.deque允許於常數時間內對頭端進行插入或刪除元素;

    2.deque是分段連續線性空間,隨時可以增加一段新的空間;

  • 用戶看起來deque使用的是連續空間,實際上是分段連續線性空間。爲了管理分段空間deque容器引入了map,稱之爲中控器,map是一塊連續的空間,其中每個元素是指向緩衝區的指針,緩衝區纔是deque存儲數據的主體。


    7898366-a3ee7b72ae446273.png
    image

    在上圖中,buffer稱爲緩衝區,顯示map size的一段連續空間就是中控器。
    中控器包含了map size,指向buffer的指針,deque的開始迭代器與結尾迭代器。

_Tp     **_M_map;
size_t      _M_map_size;
iterator    _M_start;
iterator    _M_finish;

deque是使用基類_Deque_base來完成內存管理與中控器管理

  • 在stack的源碼中我們關注兩點: - 默認_Sequence爲deque - 內部函數實現是調用_Sequence對應容器的函數。

  • 對於stack來說,底層容器可以是vector、deque、list,但不可以是map、set。 由於編譯器不會做全面性檢查,當調用函數不存在的時候,就編譯不通過,所以對於像set雖然不能作爲底層容器,但如果具有某些函數,調用仍然是成功的,直到調用的函數不存在。

  • 在queue的源碼中我們關注兩點: - 默認_Sequence爲deque - 內部函數實現是調用_Sequence對應容器的函數。

  • 優先隊列則是使用vector作爲默認容器。

  • 對於queue底層容器可以是deque,也可以是list,但不能是vector,map,set,使用默認的deque效率在插入方面比其他容器作爲底層要快!

  • 對於優先隊列來說,測試結果發現,採用deque要比默認的vector插入速度快! 底層支持vector、deque容器,但不支持list、map、set。

  • stack、queue、priority_queue不被稱爲容器, 把它稱爲容器配接器。

  • vector的數據安排以及操作方式,與array非常相似。兩者的唯一差別在於空間的運用的靈活性,array是靜態的,一旦配置了就不能改變,而 vector是動態空間,隨着元素的加入,它的內部機制會自行擴充空間以容納新元素。

  • 類作用域

在類外部訪問類中的名稱時,可以使用類作用域操作符,形如MyClass::name的調用通常存在三種:靜態數據成員、靜態成員函數和嵌套類型

struct MyClass {
    static int A; //靜態成員
    static int B(){cout<<"B()"<<endl; return 100;} //靜態函數
    typedef int C;  //嵌套類型
    struct A1 { //嵌套類型
        static int s;
    };
};
7898366-c2c2f658ea74679b.png
image
  • 一種能夠順序訪問容器中每個元素的方法,使用該方法不能暴露容器內部的表達方式。而類型萃取技術就是爲了要解決和 iterator 有關的問題的。

  • 總結:通過定義內嵌類型,我們獲得了知曉 iterator 所指元素類型的方法,通過 traits 技法,我們將函數模板對於原生指針和自定義 iterator 的定義都統一起來,我們使用 traits 技法主要是爲了解決原生指針和自定義 iterator 之間的不同所造成的代碼冗餘,這就是 traits 技法的妙處所在。

  • 因爲空類同樣可以被實例化,每個實例在內存中都有一個獨一無二的地址,爲了達到這個目的,編譯器往往會給一個空類隱含的加一個字節,這樣空類在實例化後在內存得到了獨一無二的地址.所以上述大小爲1.

  • 兩個不同對象的地址不同。

  • 基類爲空,通過繼承方式來獲得基類的功能,並沒有產生額外大小的優化稱之爲EBO(空基類優化)。

  • 第一種方式的內存管理:嵌入一個內存管理類

template<class T, class Allocator>
class MyContainerNotEBO {
    T *data_ = nullptr;
    std::size_t capacity_;
    Allocator allocator_;   // 嵌入一個MyAllocator
public:
    MyContainerNotEBO(std::size_t capacity)
            : capacity_(capacity), allocator_(), data_(nullptr) {
        std::cout << "alloc malloc" << std::endl;
        data_ = reinterpret_cast<T *>(allocator_.allocate(capacity * sizeof(T))); // 分配內存
    }

    ~MyContainerNotEBO() {
        std::cout << "MyContainerNotEBO free malloc" << std::endl;
        allocator_.deallocate(data_);
    }
};
  • 第二種方式:採用空基類優化,繼承來獲得內存管理功能
template<class T, class Allocator>
class MyContainerEBO
        : public Allocator {    // 繼承一個EBO
    T *data_ = nullptr;
    std::size_t capacity_;
public:
    MyContainerEBO(std::size_t capacity)
            : capacity_(capacity), data_(nullptr) {
        std::cout << "alloc malloc" << std::endl;
        data_ = reinterpret_cast<T *>(this->allocate(capacity * sizeof(T)));
    }

    ~MyContainerEBO() {
        std::cout << "MyContainerEBO free malloc" << std::endl;
        this->deallocate(data_);
    }
};

int main() {
    MyContainerNotEBO<int, MyAllocator> notEbo = MyContainerNotEBO<int, MyAllocator>(0);
    std::cout << "Using Not EBO Test sizeof is " << sizeof(notEbo) << std::endl;
    MyContainerEBO<int, MyAllocator> ebo = MyContainerEBO<int, MyAllocator>(0);
    std::cout << "Using EBO Test sizeof is " << sizeof(ebo) << std::endl;

    return 0;
}

//結果
alloc malloc
Using Not EBO Test sizeof is 24
alloc malloc
Using EBO Test sizeof is 16
MyContainerEBO free malloc
MyContainerNotEBO free malloc
  • 採用EBO的設計確實比嵌入設計好很多。

❑ 二分查找的速度比簡單查找快得多。
❑ O(log n)比O(n)快。需要搜索的元素越多,前者比後者就快得越多。
❑ 算法運行時間並不以秒爲單位。
❑ 算法運行時間是從其增速的角度度量的。
❑ 算法運行時間用大O表示法表示。

  • set/multiset以rb_tree爲底層結構,因此有元素自動排序特性。排序的依據是key,而set/multiset元素的value和key合二爲一:value就是key。

  • 第一個問題:key是value,value也是key。

  • 第二個問題:無法使用迭代器改變元素值。

  • 第三個問題:插入是唯一的key。

cout<<"flag: "<<itree._M_insert_unique(5).second<<endl;  // 學習返回值
typedef pair<int ,bool> _Res;    // 也來用一下typedef後的pair
cout<<_Res(1,true).first<<endl;  // 直接包裹
_Res r=make_pair(2,false);    // 定義新對象
cout<<r.first<<endl;   // 輸出結果
  • map的key爲key,value爲key+data,與set是不同的,set是key就是value,value就是key。
  • map的key不可修改,map與multimap的插入調用函數不同,影響了其key是否對應value。
  • initializer_list使用
  • map有[]操作符,而multimap沒有[]操作符。
insert的幾種方法:

(1) 插入 pair


std::pair<iterator, bool> insert(const value_type& __x)
{ return _M_t._M_insert_unique(__x); }
map裏面

(2) 在指定位置,插入pair


iterator insert(iterator __position, const value_type& __x)
{ return _M_t._M_insert_equal_(__position, __x); }
(3) 從一個範圍進行插入


template<typename _InputIterator>
void
insert(_InputIterator __first, _InputIterator __last)
{ _M_t._M_insert_equal(__first, __last); }
(4)從list中插入


void
insert(initializer_list<value_type> __l)
{ this->insert(__l.begin(), __l.end()); }
針對最後一個insert,裏面有個initializer_list,舉個例子大家就知道了。
  • 結論1:undered_map與undered_set不允許key重複,而帶multi的則允許key重複;
  • 結論2:undered_map與undered_multimap採用的迭代器是iterator,而undered_set與undered_multiset採用的迭代器是const_iterator。
  • 結論3:undered_map與undered_multimap的key是key,value是key+value;而undered_set與undered_multiset的key是Value,Value也是Key。

五種創建線程的方式

  • 函數指針
  • Lambda函數吧
  • Functor(仿函數)
  • 非靜態成員函數
  • 靜態成員函數

2.1 函數指針

// 1.函數指針
void fun(int x) {
    while (x-- > 0) {
        cout << x << endl;
    }
}
// 調用
std::thread t1(fun, 10);
t1.join();

2.2 Lambda函數

// 注意:如果我們創建多線程 並不會保證哪一個先開始
int main() {
    // 2.Lambda函數
    auto fun = [](int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    };
//    std::1.thread t1(fun, 10);
    // 也可以寫成下面:
    std::thread t1_1([](int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }, 11);
//    std::1.thread t2(fun, 10);
//    t1.join();
    t1_1.join();
//    t2.join();
    return 0;
}

2.3 仿函數

// 3.functor (Funciton Object)
class Base {
public:
    void operator()(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 調用
thread t(Base(), 10);
t.join();

2.4 非靜態成員函數

// 4.Non-static member function
class Base {
public:
    void fun(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 調用
thread t(&Base::fun,&b, 10);
t.join();

2.5 靜態成員函數

// 4.Non-static member function
class Base {
public:
    static void fun(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 調用
thread t(&Base::fun, 10);
t.join();

join

  • 一旦線程開始,我們要想等待線程完成,需要在該對象上調用join()
  • 雙重join將導致程序終止
  • 在join之前我們應該檢查顯示是否可以被join,通過使用joinable()
void run(int count) {
    while (count-- > 0) {
        cout << count << endl;
    }
    std::this_thread::sleep_for(chrono::seconds(3));
}

int main() {
    thread t1(run, 10);
    cout << "main()" << endl;
    t1.join();
    if (t1.joinable()) {
        t1.join();
    }
    cout << "main() after" << endl;
    return 0;
}

detach

  • 這用於從父線程分離新創建的線程
  • 在分離線程之前,請務必檢查它是否可以joinable,否則可能會導致兩次分離,並且雙重detach()將導致程序終止
  • 如果我們有分離的線程並且main函數正在返回,那麼分離的線程執行將被掛起
void run(int count) {
    while (count-- > 0) {
        cout << count << endl;
    }
    std::this_thread::sleep_for(chrono::seconds(3));
}

int main() {
    thread t1(run, 10);
    cout << "main()" << endl;
    t1.detach();
    if(t1.joinable())
        t1.detach();
    cout << "main() after" << endl;
    return 0;
  • 增加變量(i ++)的過程分三個步驟:

      1.將內存內容複製到CPU寄存器。 load
      2.在CPU中增加該值。 increment
      3.將新值存儲在內存中。 store
    
  • 如果只能通過一個線程訪問該內存位置(例如下面的變量i),則不會出現爭用情況,也沒有與i關聯的臨界區。 但是sum變量是一個全局變量,可以通過兩個線程進行訪問。 兩個線程可能會嘗試同時增加變量。
#include <iostream>
#include <mutex>
#include <thread>

using namespace std;

int sum = 0; //shared

mutex m;

void *countgold() {
    int i; //local to each thread
    for (i = 0; i < 10000000; i++) {
        sum += 1;
    }
    return NULL;
}

int main() {
    thread t1(countgold);
    thread t2(countgold);

    //Wait for both threads to finish
    t1.join();
    t2.join();

    cout << "sum = " << sum << endl;
    return 0;
}

初始化列表與賦值

  • const成員的初始化只能在構造函數初始化列表中進行
  • 引用成員的初始化也只能在構造函數初始化列表中進行
  • 對象成員(對象成員所對應的類沒有默認構造函數)的初始化,也只能在構造函數初始化列表中進行

類之間嵌套

第一種: 使用初始化列表。

class Animal {
public:
    Animal() {
        std::cout << "Animal() is called" << std::endl;
    }

    Animal(const Animal &) {
        std::cout << "Animal (const Animal &) is called" << std::endl;
    }

    Animal &operator=(const Animal &) {
        std::cout << "Animal & operator=(const Animal &) is called" << std::endl;
        return *this;
    }

    ~Animal() {
        std::cout << "~Animal() is called" << std::endl;
    }
};

class Dog {
public:
    Dog(const Animal &animal) : __animal(animal) {
        std::cout << "Dog(const Animal &animal) is called" << std::endl;
    }

    ~Dog() {
        std::cout << "~Dog() is called" << std::endl;
    }

private:
    Animal __animal;
};

int main() {
    Animal animal;
    std::cout << std::endl;
    Dog d(animal);
    std::cout << std::endl;
    return 0;
}

第二種:構造函數賦值來初始化對象。

Dog(const Animal &animal) {
    __animal = animal;
    std::cout << "Dog(const Animal &animal) is called" << std::endl;
}
  • 類中包含其他自定義的class或者struct,採用初始化列表,實際上就是創建對象同時並初始化
  • 而採用類中賦值方式,等價於先定義對象,再進行賦值,一般會先調用默認構造,在調用=操作符重載函數。
無默認構造函數的繼承關係中
class Animal {
public:
    Animal(int age) {
        std::cout << "Animal(int age) is called" << std::endl;
    }

    Animal(const Animal & animal) {
        std::cout << "Animal (const Animal &) is called" << std::endl;
    }

    Animal &operator=(const Animal & amimal) {
        std::cout << "Animal & operator=(const Animal &) is called" << std::endl;
        return *this;
    }

    ~Animal() {
        std::cout << "~Animal() is called" << std::endl;
    }
};

class Dog : Animal {
public:
    Dog(int age) : Animal(age) {
        std::cout << "Dog(int age) is called" << std::endl;
    }

    ~Dog() {
        std::cout << "~Dog() is called" << std::endl;
    }

};
  • 由於在Animal中沒有默認構造函數,所以報錯,遇到這種問題屬於災難性的,我們應該儘量避免,可以通過初始化列表給基類的構造初始化。

類中const數據成員、引用數據成員

  • 特別是引用數據成員,必須用初始化列表初始化,而不能通過賦值初始化!
class Animal {
public:
    Animal(int age, std::string name) : age_(age), name_(name) {
    std::cout << "Animal(int age) is called" << std::endl;
}
private:
    int &age_;
    const std::string name_;
};
// enum class
enum class EntityType {
    Ground = 0,
    Human,
    Aerial,
    Total
};

void foo(EntityType entityType)
{
    if (entityType == EntityType::Ground) {
        /*code*/
    }
}
  • 可以指定對象具有構造函數和析構函數,這些構造函數和析構函數在適當的時候由
  • 編譯器自動調用,這爲管理給定對象的內存提供了更爲方便的方法。
    • 資源在析構函數中被釋放
    • 該類的實例是堆棧分配的
    • 資源是在構造函數中獲取的。

RAII代表“資源獲取是初始化”。常見的例子有:

  • 文件操作

  • 智能指針

  • 互斥量

  • 爲了使用copy-swap,我們需要三件事:
    1.一個有效的拷貝構造函數
    2.一個有效的析構函數(兩者都是任何包裝程序的基礎,因此無論如何都應完整)以及交換功能。

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