1. 程式人生 > >C++複習大全(各種知識點)

C++複習大全(各種知識點)

前言

這篇部落格是我之前的一個禮拜覆習總結的各種知識點,可能有些多,其中的一些觀點是來自於《Effective C++》和《C++程式設計思想》,這兩本書中的知識給了我很多啟發,也讓我懂得了許多不一樣的知識點,我連帶我的認識以及理解整理起來,希望會對你們有所幫助。

資源就是一旦被使用,將來必須要返還給系統。在c++中最常使用的資源就是動態分配記憶體(如果分配了記憶體卻從來不歸還它,會導致記憶體洩漏

其他的常見資源還有 檔案描述器,互斥鎖,圖形介面中的字型和筆刷,資料庫連線,以及網路 sockets

條款 13 以物件管理資源
  • 資源的分配可能會來不及釋放,比如說下面這種情況
void f()
{
    Investment* pInv = createInvestment();
    .......  //這裡看起來沒有任何問題,但是如果有一個return
語句在這裡提前中斷,控制流就無法接觸到delete語句 delete pInv; } -2 類似的情況還可能發生在迴圈內,如果迴圈內有一個 continue或者 goto 語句導致過早退出 -3 還有一種情況就是可能在 ... 中可能丟擲異常, 無論delete是如何被忽略掉的,我們洩漏的不只是內含投資物件的那塊記憶體,還包括那些投資物件所儲存的任何資源

為了確保物件返回的資源總是可以被釋放掉,我們必須把資源放進物件內,當控制流離開函式 f()時,該物件的解構函式會自動的釋放那些資源

  • 智慧指標 auto_ptr
    智慧指標的功能就是其解構函式自動對齊所指物件呼叫 delete
void f()
{
     std::auto_ptr<Investment>pInv(createInvestment());
                             //呼叫factory函式
                             //一如以往地使用pInv
                             //經由 auto_ptr 的解構函式自動刪除pInv
}
  • 獲得資源後立刻放進管理物件
  • 管理物件運用解構函式確保資源被釋放
  • (如果在釋放資源過程中丟擲異常,那麼可以在解構函式內部實現資源釋放)
  • 需要注意的一點是,由於 auto_ptr 被消耗時會自動刪除它所指的物件,所以一定要注意別讓多個 auto_ptr 指向同一個物件,這樣可能會造成記憶體洩漏,因此只能有一個指標具有管理權
std::auto_ptr<Investment> 
  pInv1(createInvestment()); //pInv1 指向返回物

  std::auto_ptr<Investment> pInv2(pInv1); //pInv2指向物件,pInv1 設為NULL

  pInv1 = pInv2; //pInv1 指向物件,pInv2 設為NULL
  • auto_ptr 的代替方案 是增加了引用計數的智慧指標,用來持續追蹤共有多少個物件指向某筆資源,並在無人指向它時自動刪除該資源 。RCSP 提供的行為類似於垃圾回收,但是不同的是RCSP無法打破環狀引用,比如兩個已經沒有被使用的物件彼此互指,因而好像還處在被使用狀態。(這塊需要深入瞭解)

boost::scoped_array 和 boost::shared_array classes

條款 14 在資源管理類中小心copying 行為
  • 並非所有的資源都是heap-based ,對那種資源而言,像 auto_ptr 和tr1::shared_ptr 這樣的智慧指標往往不適合作為資源掌管者,因此有時候我們需要自己建立資源管理類
  • 假設我們使用 C API 函式處理型別為 Mutex 的互斥器物件,共有lock 和unlock兩種函式可用
void lock(Mutex* pm); //鎖定pm所指向的互斥器
void unlock(Mutex* pm)//將互斥器解除鎖定

-建立一個類來管理機鎖 (資源在構造期間獲得,字析構期間釋放)

class Lock{
    public:
    explicit Lock(Mutex* pm)
    :mutexPtr(pm)
    {
          lock(mutxPtr);
    }
    ~Lock()
    {
        unlock(mutexPtr);
    }
    private:
    Mutex *mutexPtr;
};
  • 當一個RAII物件被複制,會發生什麼?
    (1)禁止複製 把拷貝函式定義為私有的
    (2)對底層資源祭出“引用計數法”
    (3)複製底部資源(深拷貝)在你不再使用時記得釋放
    ($)轉移底部資源的所有權

請記住

複製RAII物件必須一併複製它所管理的資源,所以資源的拷貝行為決定了RAII物件的拷貝行為
普遍兒常見的RAII class copying 行為是:抑制拷貝行為,施行引用計數法

條款 16 : 成對使用 new 和delete 時要採取相同形式

std::string * stringArray = new std::string[100];

delete stirngArray;

stringArray所包含的一百個物件中99個沒有被解構函式釋放掉
  • 當你使用new 時,會發生兩件事 1.記憶體被分配 2.針對此記憶體會有一個或者多個建構函式被呼叫

  • delete必須知道記憶體中有多少個物件,才能去相應的呼叫多少次解構函式

單一物件和陣列的記憶體佈局決定了析構方式的差別(具體看圖)
  • 如果你使用delete時加上[],delete便認定指標指向一個數組,否則它就認定指標指向一個單一物件。
  • 基於這個原因,我們必須要將 new 和delete 匹配起來使用,這樣才能避免記憶體洩漏

寧以pass-by-reference-to-const替換pass-by-value

在預設的情況下,C++是以傳值的方式傳遞物件到函式,除非我們自己指定,否則函式引數都是以實際引數的一份拷貝作為初值。這些拷貝的復件都是通過呼叫拷貝建構函式產生的,這樣做使得傳值呼叫的方式帶來了很大的時間開銷和記憶體開銷(當然,這是基於一個比較大的物件來說的)

- 每次給函式傳參,都會呼叫一次拷貝建構函式,同樣的,也會呼叫一次解構函式

class Person
{
    public:
    Person();
    virtual ~Peson;

    private:
    std::string name;
    std::string address;
};

class Student:public Person{
    public:
       Student();
       ~Student();

    private:
       std::string schoolName;
       std::string schoolAddress;
};

(1)Student plato;
(2)Student s(plato);

現在我們來分析一下這段程式碼,先提前說一下,這段程式碼的拷貝建構函式呼叫次數一定會讓你感到很震驚。

  • 單就拿程式碼(2)來說吧。已經存在一個 Student 類物件plato,然後我們要拷貝構造一個 Studnet 類物件 s。由於s是拷貝構造 plato 的,那就會呼叫一次派生類拷貝構造,而派生類又會呼叫基類的拷貝構造,這就已經是兩次了,對應的,既然呼叫了兩次拷貝構造,那就會呼叫兩次解構函式。
  • 然後再分析,派生類中的物件繼承了基類的物件,所以總共有四個string 物件,那麼又會呼叫四次建構函式,對應四次解構函式
  • 最後得出結論,程式碼(2)總共呼叫了六次建構函式和六次解構函式

  • 但是,如果傳遞的是引用的話,那就可以直接對物件進行操作,避免呼叫建構函式和解構函式。因為沒有任何新的物件被建立,以引用傳遞也可以避免物件切割問題,當一個派生類以值傳遞的方式將會被宣告為基類物件,基類的拷貝建構函式被呼叫,造成派生類的特化性質全被切割

  • 為了解決切割問題,我們可以給函式的引數傳入一個 const 的引用

  • 引用的底層實際上也就是個指標
  • 但是對於STL的迭代器和函式物件以及內建型別,傳值呼叫更加適合

條款21 必須返回物件時,別妄想返回引用

class rational{
    public:
    Rational(int numrator =0,
             int denominator = 1);
    private:
       int n,d;
       friend const Rational operator* (const Rational &lhs,
                                        const Rational &rhs);
    };
}

我們需要搞清楚的一個問題是,引用既然是變數的別名,那就必須有一個變數存在,這個時候,如果我們的函式沒有定義變數而直接就用引用,那麼這個引用一定是存在問題的。這個時候,我們或許可以想到使用在函式中直接定義一個區域性變數,然後有一個引用作為他的別名。但是我們需要考慮的問題是,當函式的生命週期結束,這個開闢在棧上的區域性變數一定是要被銷燬的。這個時候,我們的引用仍然指向這塊變數,殊不知,這塊變數早已經消失了,那麼引用也就失去了它的價值。

  • 基於以上觀點,我們一定要注意,不要對一個區域性變數宣告一個引用,這樣一定會引發問題。

-為了解決這個問題,我們只能採取另一種方案,即直接在堆上動態開闢記憶體空間給物件。這樣做是可以避免函式棧楨自動銷燬的問題,但是,還有另一個問題有待解決,這是什麼呢?看一段程式碼

const Rational& operator* (const Rational& lhs,
                           const Rational& rhs)
    {
        Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
        return *result;
    }
    即使如此,我們還是需要付出代價,因未分配所得的記憶體將會以一個適當的建構函式進行初始化,既然 new 了一個物件,那麼誰來對它進行 delete呢?

//這種情況下,如何 delete?
Rational w,x,y,z;
w = x*y*z;   //與operator*(operator*(x,y),z) 相同

在上面的程式碼中,同一個語句呼叫了兩次 operator* 因而使用了兩次 new ,那麼相對的也就需要兩次 delete.但卻沒有合理的辦法讓 operator* 使用者進行那些delete呼叫,因為沒有合理的辦法讓他們取得 operator* 返回的引用背後隱藏的那個指標,這樣做絕對會導致資源洩漏。

請記住

  • 絕對不要返回一個指標或者引用指向一個 local static 物件而有可能同時需要多個這樣的物件。

條款 23:寧以 non-member,non-friend 替換 member 函式

  • 面向物件守則要求,資料以及資料操作的那些函式應該被捆綁在一起,這意味著它建議member函式是較好的選擇,但是很不幸的是這個建議不正確。

  • C++ 引用簡介

  • 引用就是變數的別名,引用的基本用法和指標是相同的,但是帶引用的函式呼叫比帶指標的函式呼叫在語法構成上更加清晰。
  • 引用看起來像是按值傳遞,實際上是按地址傳遞。
  • 如果指標宣告為 void* ,它意味著任何型別的地址都可以間接引用那個指標
int main()
{
    void* vp;
    char c;
    int i;
    float f;
    double d;

    vp = &c;
    vp = &i;
    vp = &f;
    vp = &d;
}

- 但是一旦我們間接引用一個 void*,就會丟失關於型別的資訊,這意味著在使用前必須轉化為正確的型別

int main()
{
    int i=99;
    void* vp =&i;
    必須先轉換型別
    *((int*)vp) = 3;
}
//但是這樣做也會存在一個問題,既然可以轉化為 int 型別,那麼同樣的也就可以轉化為 char ,double,這將改變已經分配給int 的儲存空間大小,可能會引起程式崩潰

作用域

  • 作用域就是告訴我們一個變數的有效範圍,它在哪裡建立,在哪裡銷燬。
  • 變數的有效作用域從它的定義點開始,到和定義變數之前最鄰近的的開括號配對的第一個閉括號
  • C語言強制在作用域的開始處就定義所有的變數,以便編譯器建立一個塊時,能給所有這些變數分配空間。
  • C++允許在作用域內的任意地方定義變數,所以可以在正好使用它之前定義,可以在定義變數時對它初始化以防止某種型別的錯誤

指定儲存空間分配

1.全域性變數

全域性變數是在所有的函式體的外部定義的,程式的所有部分(甚至其他檔案中的程式碼)都可以使用
- 使用extern 可以進行外部連結,使得另一個程式碼可以使用本程式碼中的變數。

2.區域性變數

區域性變數經常被稱為自動變數。因為他們在進入作用域時自動生成,離開作用域時自動消失。區域性變數預設為auto,沒必要顯式宣告。

  • 暫存器變數
    暫存器變數也是一個區域性變數,關鍵字是 register ,它告訴編譯器要儘快訪問這個變數,但是這個動作通常是編譯器做的,現在許多的編譯器會對經常訪問的變數直接放在暫存器中,我們如果強制這樣宣告並沒有什麼好處。
  • 使用 register 變數,我們不能得到或者計算 register 的地址,register 變數只能在一個塊中宣告,暫存器變數可以做函式形參

  • 靜態變數
    通常情況下,我們如果在函式中定義了一個區域性變數,會在函式作用域結束時自動消失,當我們下一次呼叫這個函式時,會重新建立該變數的儲存空間,它的值也會被重新初始化。如果想要時區域性變數的值在整個程式都會存在,我們可以定義函式的區域性變數為static,並給他一個初值

#include<isotream>
using namespace std;

void func() {
    static int i = 0;
    cout<<"i = "<<++i<<endl;
    }

int main()
{
    for(int x=0;x<10;x++)
    {
        func();
    }
}
}
//如果宣告的不是static,那麼每次都會打印出來1
static變數在作用域外不可用
3.7 轉換運算子

簡單的型別轉換確實很奏效,但是有時候卻會佔用更大的記憶體空間,這可能會破壞其他的資料,因為它強迫編譯器把一個數據看做是一個比它實際上更大的型別。這種強制型別轉換通常用在指標的型別轉換上,因為指標的大小在系統下都是固定的,但是有時候也會存在問題。

  • 把一個整型指標強轉為一個long型別的指標,那麼編譯器就會預設指標指向的是一塊long型別的地址,這可能會造成資料冗餘,改為short則有可能會造成資料丟失。
C++的顯示型別轉換
  • static_cast //用於良性和適度良性轉換,包括不用強制轉換(自動型別轉換)- -
  • const——cast //對“const” 和/或“volatile“進行轉換
  • reinterpret_cast //轉換為完全不同的意思,為了安全使用它,關鍵必須轉化為原來的型別。轉換成的型別一般只能用於位操作,否則就是為了其他隱藏的目的。這是所有轉化中最危險的。
  • dynamic_cast //用於型別安全的向下轉換
靜態轉換(static_cast)

轉換型別包括典型的非強制轉換,窄化(有資訊丟失)變換,使用void*的強制變換,隱式型別轉換和類層次的靜態定位

    -
void func(int)
{}
int main(){   //使用static_cast 提升資料型別或者降低資料型別都是可以的
    int i = 0x7fff;  //但是一定要注意資料丟失
    long l;
    float f;
    l=i;
    f=i;
    l = static_cast<long>(i);
    f = static_cast<float>(i);
}
常量轉換(const_cast)

如果從const 轉化為非 const 或從 volatile 轉換為非 volatile ,可以使用 const_cast ,這是const_cast 唯一允許的轉換

int main()
{
    const int i = 0;
    int *j = (int*)& i;
    j = const_cast<int*>(& i);

    volatile int k = 0;
    int* u = const_cast<int*>(& k);

記住,如果取得了const 的地址,就可以生成一個指向 const 的指標,不用轉換是不能將它賦給非 const指標的。
}
重解釋轉換 (reinterpret_cast) [最不安全的型別轉換]

建立複合型別

用typedef命名別名
typedef 原型別名  別名

typedef unsigned long ulong 
  • 在一些重要的場合,編譯器必須知道我們正在將名字當做型別處理,所以typedef 起了關鍵的作用

- typedef 經常用到的地方是指標型別

int* x,y;

typedef int* IntPtr
IntPtr x,y; //生成兩個指標

結構體

typedef struct Structure3{
    char c;
    int i;
    float f;
}Struture3;

int main()
{
    Structure3 s1,s2;
    Structure3 *p = &s1;
    p-> c ='a';
}
列舉,列舉本質上就是一個整數,但是他又不完全等價於一個整數。比如一個color的列舉型別,編譯器是這樣做的

enum color{
a++; //本質上這樣是不對的
}; //必須加上;

  • 1.將列舉的值隱式地從 color 強制轉化為 int,然後遞增該值,再把int強制轉化回 color。如果相對color進行增量運算,應該宣告一個類。

用union節省空間

  • union 把所有的資料放在一個單獨的空間內,他計算出放在union中的最大項所必需的空間數,並生成union的大小
  • 我們建立的是一個能容納任何union變數的超變數。所有的union變數的地址都是一樣的。

- 每次只能取到一個變數的值,因為他們是共用這塊空間

陣列
void func1(int a[],int size);
void func2(int *a ,int size);
atoi() atol() atof()


int main(int argc,char* argv[])
{
    for(int i=1;i<argc;i++)
    {
        cout<<atoi(argv[i])<<endl;

    }
}

把變數和表示式轉化為字串

define PR(x) cout<< #x “=” << x <<”\n”;

3.10 函式地址

  • 一旦函式被編譯並載入計算機中執行,它就會佔用一塊記憶體,這塊記憶體有一個地址,因此函式也有地址。
  • 可以通過指標使用函式地址,就像可以使用變數的地址一樣。
定義函式指標
要定義一個無參無返回值的函式
void (*funcptr)(); //記得函式指標的分辨
如果這樣宣告,就不一樣了
void *funcPtr(); //不加()就會看成是一個返回值為 void* 的函式
複雜的宣告和定義
void*(*(*fp1)(int))[10]; // fp1 是一個指向函式的指標,該函式接受一個整形引數並返回一個指向 10 個void  指標陣列的指標

float(*(*fp2)(int,int,float))(int); //fp2是一個指向函式的指標,該函式接收三個引數且返回一個指向函式的指標,該函式

typedef double (*(*(*fp3)())[10])();

int (*(*f4())[10])();

12.6 動態特性

  • 在多數情況下,程式的功能是在編譯時就確定下來的,我們稱為靜態特性,如果程式的功能是在執行時才確定下來的,則稱為動態特性。
  • C++虛擬函式,抽象基類,動態繫結和多型構成了出色的動態特性。

虛擬函式

如果一個類的一個函式被宣告為虛擬函式,那麼其派生類的對應函式也自動成為虛擬函式,這樣一級級傳遞下去。雖然預設是虛擬函式,但是我們最好還是顯式地宣告一下,方便我們理解。

class Shape{   //虛擬函式的重寫
    public:
    virtual void Draw(void);
    };

class Rectangle;public Shape{
    public:
    virtual void Draw(void);
}

抽象基類

不能例項化出物件的類稱為抽象類(那些把所有的建構函式都宣告為private的類也是不能例項化的類)
- 抽象類的唯一目的就是讓其派生類繼承並實現它的介面方法。
- 如果該基類的虛擬函式宣告為純虛擬函式,那麼該類就被定義為抽象基類。純函式虛擬函式是在宣告時將其初始化為0的函式

class Shape{
    public:
       virtual void Draw(void) = 0;
};

分析:函式名就是函式的地址,將一個函式初始化為0意味著函式的地址將為0,這就是在告訴編譯器,不要為該函式編地址,從而阻止該類的例項化行為。
- 抽象基類的主要用途是:(介面與實現分離):不僅要把資料成員隱藏起來,而且還要把實現完全隱藏起來,只留一些介面給外部呼叫。即使將來實現改變了,介面仍然可以保持不變。
- 一般的資訊隱藏就是把類的所有資料成員宣告為private 或者protected,並提供相應的get 和set來訪問物件的資料。抽象基類則進一步,它把資料和函式實現都隱藏在實現類中,而在抽象基類中提供豐富的介面函式供呼叫,這些函式都是public的純虛擬函式,這樣的抽象基類叫做介面類。

class IRectangle
{
    virtual ~IRectangle(){}
    virtual float Getlength()const  = 0;
    virtual void Setlength(float newLength) = 0;
    virtual float Getwidth()const = 0;
    virtual void Setwidth(float newWidth) = 0;
    static IRectangle*_stdcall CreateRectangle(); //入口函式
    void Destroy(){ delete this;}
};

class RectangleImp:public TRectangle{
    public:
         RectangleImp():m_length(1),m_width(1),m_color(0x00FFEC4D){}
         virtual ~RectangleImp(){}
         virtual float Getlength()const  {  return m_length;}
         virtual void Setlength(float newLength) {  m_length = newLength; }
    private:
        float m_length;
        float m_width;
        RGB   m_color;
};
  • 由於抽象基類不能被例項化,並且實現類被完全隱藏,所以必須以其他的途徑使使用者能夠獲得實現類的物件,比如提供入口函式來動態建立實現類的物件。入口函式可以使全域性函式,但最好是靜態函式。
IRectangle* _stdcall IRectangle::CreateRectangle()
{
    return new(nothrow)RectangleImp;
}
void main()
{
    IRectangle *pRect = IRectangle::CreateRectangle();

}
動態繫結
  • C++ 的動態繫結機制是如何實現的?
  • 程式之所以能夠在執行時選擇正確的虛擬函式,必定隱藏了一段執行時進行物件型別判斷或是函式定址的程式碼。
  • C++編譯器必須為每一個多型類至少建立一個虛擬函式表,它其實就是一個函式指標陣列,其中存放著這個類所有的虛擬函式的地址及該類的型別資訊,其中也包括那些繼承但未改寫的虛擬函式。
  • 同類物件的型別資訊完全相同,所以只需要在該類的vtable中保留一份就足夠了。每一個多型物件都有一個隱含的指標成員,它指向所屬的通過基類指標或引用對虛擬函式的呼叫語句都會被編譯器改寫成下面這種形式
(*(p->vptr[slotNum]))(p,arg-list); //指標當做陣列來用,最後改寫為指標運算
派生類定義中的名字(物件或函式名)將義無反顧地遮蔽(隱藏)基類中任何同名的物件或函式
  • 函式原型完全相同,當返回型別不同時稱為協變

執行時多型

當許多的派生類因為繼承了共同的基類而建立 is -a 關係時,沒一個派生類的物件都可以被當成基類的物件來使用,這些派生類物件能對同一個函式呼叫做出不同的反應,這就是執行時多型。

void Draw(Shape *pShape)
{
    pShape->Draw();
}
main()
{
    Circle aCircle;
    Cube   aCube;
    Sphere aSphere;
    ::Draw(&aCircle);
    ::Draw(&aCube);
    ::Draw(&aSphere);
}

關於多型的總結

  • 經過隱含的轉型操作,令一個public多型基類的指標或者引用指向它的一個派生類的物件。
  • 通過這個指標或者引用呼叫基類的虛擬函式,包括通過指標的反引用呼叫虛擬函式,因為反引用一個指標將返回所指物件的引用
  • 使用dynamic_cast<>和typeid運算子

優點

  • 應用程式不必為每一個派生類編寫功能呼叫,只需要對基類的虛擬函式進行改寫或擴充套件即可。可以大大提高程式的可複用性和可擴充套件性。
  • 派生類的功能可以被基類指標引用,這叫向後相容。以前寫的程式可以被將來寫的程式呼叫,這不足為奇,但是將來寫的程式可以被以前寫的程式呼叫那就很了不起了。

多型陣列

在基類物件陣列中存放派生類物件

Shape      a(Point(1,1));
Circle     b(Point(2,2),5);
Rectangle  c(Point(3,3),Point(4,4));
Shape   myShapes[3];
myShapes[0] = a;
myShapes[1] = b;
myShapes[2] = c;

for(int i=0;i<3;i++)
  myShapes[i].Draw();

C++物件模型
- 非靜態資料成員被放在每一個物件體內作為物件專有的資料成員
- 靜態資料成員被提取出來放在程式的靜態資料區內為該類所有物件共享,因此僅存在一份。
- 靜態和非靜態成員函式最終都被提取出來放在程式的程式碼段中併為該類的所有物件共享,因此每一個成員函式也只存在一份程式碼實體。

因此,構成物件本身的只有資料,任何成員函式都不隸屬於任何一個物件,非靜態成員函式與物件的關係就是繫結,繫結的中介就是this指標。
增加了繼承和虛擬函式的類的物件模型變得更加複雜,規則如下:
  • 為每一個多型類建立一個虛擬函式指標陣列vtable,該類的所有虛擬函式(繼承自基類或者新增的)的地址都儲存在這張表中。
  • 如果基類已經插入了vfptr,則派生類將繼承和重用該vfptr
  • 如果派生類從多個基類繼承或者有多個繼承分支,而其中若干個繼承分支上出現了多型類,則派生類將從這些分支中的每個分支上繼承一個vfptr,編譯器也將為它生成多個vtable
  • vfptr在派生類物件中的相對位置不會隨著繼承層次的逐漸加深而改變,現在的編譯器一般都將vfptr放在所有資料成員的最前面。
  • 只有虛擬函式訪問需要經過vfptr的間接定址,增加了一層間接性,因此帶來了一些額外的執行時開銷

隱含成員

  • 一個C++的複合型別物件,其可能的隱含成員包含:若干vfptr,預設建構函式,預設拷貝建構函式,解構函式和預設拷貝賦值函式
  • 該類含義虛擬函式,無論是自己定義的還是從基類繼承下來的。
  • 該類的繼承鏈中至少有一個基類是多型類
  • 該類至少有一個虛基類
  • 該類包含了多型的成員物件,但是該類不一定是多型類
顯然,當建立一個物件的時候,其隱含的成員vfptr必須被初始化為指向正確的vtable,而且這個初始化工作只能在執行時完成,所以這個任務自然就交給了建構函式。
C++物件模型要充分考慮物件資料成員的空間效率和訪問速度,以優化效能。另外,每一個物件必須佔據足夠大的記憶體空間以便容納其所有的非靜態資料成員。因此,物件的實際大小可能比簡單地把各個成員的大小加在一起的結果還要大。
造成這種結果的原因主要有兩條:
  • (1)由編譯器自動安插的額外隱含資料成員,以支援物件模型,入vfptr
  • (2)除去對存取效率的考慮而增加的填補位元組,以使物件的邊界能夠對齊到機器字長(WORD),即為WORD的整數倍
C++編譯器如何處理成員函式
  • 在編譯器眼中,同一個函式只存在一個實現,不管是全域性函式還是成員函式。對於在兩個編譯單元中分別定義的兩個完全相同的static全域性函式,由於編譯器認為它們是不同的函式,因此會分別為它們生成可執行程式碼。
  • C++通過命名技術把每一個成員函式都轉換成了名字唯一的全域性函式,並把通過物件,指標和引用對每一個成員函式的呼叫語句改寫成相應的全域性函式呼叫語句。
  • 需要了解的是,不同的C++編譯器對class的資料成員,成員函式和全域性函式等的命名方案是不同的,這是造成不同編譯器之間存在二進位制連線相容性的主要原因之一
C++如何處理靜態成員
  • 在C++中,凡是使用static關鍵字宣告和定義的程式元素,不論其作用域是檔案,函式或是類,都將具有static儲存型別,並且其生存期限為永久,即在程式開始執行時建立,在程式結束時銷燬。因此,類的靜態成員在本質上就是一種全域性變數或函式。
  • 類的靜態資料成員可以在class的定義中直接初始化,但是要清楚:這只是宣告並給它提供一個初值而已,還必須在某一個編譯單元把它定義一次(分配記憶體)
  • 靜態成員函式像其他成員函式一樣,也要經過名字修飾處理並被提出到class之外,但是不同的是它們不需要this指標引數
  • 基類的靜態成員也會被派生類繼承,但這種繼承並不是繼承它們的實體,而是使得它們能在派生類中直接訪問。
  • 靜態成員的最大特點是沒有this指標,因此可以通過作用域解析運算子直接引用。

型別

  • C++ 強制型別轉換相比較C語言能夠更好一點,它對使用者進行的操作提醒,有可能產生什麼樣的後果,但是C語言就是一把轉,不太適合

相近型別支援隱式型別轉換
不相關型別一定是強制型別轉換

static_cast 型別轉換

對相關型別或者相近型別隱式型別轉換

double d = static_cast<int>(i);

int *p = &i;
int j = reinterptret

int j = reinterpret_cast<int>(p)  //不相關型別的轉換,部分強制型別的轉換

const int*p1 = p; //提醒把 const 屬性去掉了
int* p2 =const_cast<int*>(p1)//去const屬性--部分強制型別轉換
typedef void (*Func)()

int Dosomething(int i)
{
    cout<<"Do something"<<endl;
    }
void Test()
{
    Func f =reinterpret_cast<Fun>(Dosomething);
}
volatile  const int a = 1; //通過檢視彙編程式碼可以確定 a 就是 1 ,無法被修改,放在程式碼段也就是常量區,這樣是一種優化,預設不會被修改,也就不會再記憶體中去找變數值。

class A{

}

dynamic_cast 只能用於含有虛擬函式的類轉換(把父類指標轉化為子類指標)

B* p1 = (B*)p;
B* p2 = dynamic_cast<B*>(p);
  • 可以讓我們識別指向的是父類還是指向的子類

如果是父類指標指向子類,那麼訪問子類元素就存在越界,但是如果是子類指標的,就可以訪問到。

  • 可以實現安全的轉換,如果是父類要轉換為子類不安全,就會返回 0
隱式型別轉換-具有單引數建構函式的型別
  • exciplit
class A{
    public:
      A(int a);
        :_a(a)
        {
            cout<<"build"<<endl;
        }
}

int main()
{
     A a(10);
     a b = 20; //建構函式和拷貝建構函式同時使用時,可以生成一箇中間臨時變數直接優化程式碼(生成匿名物件)在一個表示式裡面就會優化

}

-在C++中,初始化和清楚地概念是簡化庫的使用的關鍵之處,並可以減少那些在客戶程式設計師忘記去完成這些操作時會引發的細微錯誤

用建構函式確保初始化

如果一個類有建構函式,那麼編譯器在建立物件時就自動呼叫這個函式。
- 成員函式預設傳的第一個引數是 this 指標,所以建構函式傳入的第一個引數是 this 指標,也就是呼叫這一函式的物件的地址,對建構函式來說,this 指標指向一個沒有被初始化的記憶體塊,建構函式的作用就是正確的初始化該記憶體塊。
- 建構函式也可以像普通函式一樣傳遞引數,指定物件該如何建立或設定物件初始值

用解構函式確保清除
  • 當物件超出他的作用域時,編譯器將自動呼叫解構函式
清除定義塊

出去安全性的考慮,應該儘可能在靠近變數的使用點處定義變數,並在定義時就初始化,通過減少變數在塊中的生命週期,就可以減少該變數在塊的其他地方被誤用的機會,另外,程式的可讀性也會增強,因為讀者不需要跳到塊的開頭去確定變數的型別

聚合初始化
  • 當產生一個聚合物件時,要做的只是指定初始值,然後初始化工作就由編譯器去承擔
struct X{
    int i;
    float f;
    char c;
}

X x1  ={1,2.2,'c'};

當必須指定建構函式呼叫時,最好這樣做
Y y1[] = {Y(1),Y(2),Y(3) };

預設建構函式

預設建構函式就是不帶任何引數的建構函式。當編譯器需要建立一個物件又不知道任何細節時,預設的建構函式就顯得非常重要
- 當有建構函式而沒有預設建構函式時,定義的變數就會出現一個編譯錯誤
- 因為由編譯器生成的建構函式應該可以做一些智慧化的初始化工作,比如把物件的所有記憶體置零。但是實際上編譯器並不會這樣做。因為這樣做會增加額外的負擔,而且使程式設計師無法控制。
- 解決辦法,如果我們還是想要把記憶體初始化為0,那就得顯式地編寫預設的預設建構函式。
- 建構函式的過載,當我們想要初始化物件中不同個數的資料時,我們就可以同時在類中宣告在類外定義多個建構函式。但是在進行建構函式過載時一定要注意一點:當有全部都有初始值得建構函式時就不要再定義其他的構造函數了,因為這樣做會導致建構函式呼叫不清晰。

第七章【 函式過載與預設引數 】

名字修飾

  • 函式過載是為了解決當多個函式實現的功能相同只是引數有所不同時的做法,這樣可以提高程式碼的複用性,同時也可以使得程式碼更加簡潔,增加了可讀性。
  • 函式過載的內部實現機制其實是編譯器對函式的名字進行了修飾:通過函式的引數不同而改變函式的名字,這樣就可以實現實際呼叫的是兩個不同的函式
  • 一個很有趣的結論->通過返回值過載
    猜想:既然可以通過引數或者範圍來實現過載,應該也就可以使用返回值進行過載。
int f();
void f();

當編譯器能夠從上下文中唯一確定函式的意思時,如 int x = f();,這樣當然是可以的,然而,在C語言中總是可以呼叫一個函式但忽略它的返回值,即呼叫了函式的副作用

型別安全連線
  • 對名字修飾還可以帶來一個好處。在C中如果使用者錯誤的聲明瞭一個函式,或者更糟糕地,一個函式還沒宣告就呼叫了,而編譯器則安函式被呼叫的方式去推斷函式的宣告。

聯合

一個聯合也可以帶有建構函式,解構函式,成員函式甚至訪問控制

union U {
    privateint i;
       float f;
    public:
       U(int a);
       U(float b);
       ~U();
       int read_int();
       float read_float();
};
U::U(int a){ i=a;}
U::U(float b) { f =b; }
U::~U() { cout<<"U::~U()\n" }
int U::read_int() { return i; }
float U::read_float() { return  f; }

int main()
{
    U X(12), Y(1.9F);
    cout<<X.read_int()<<endl;
    cout<<Y.read_float()<<endl;

}

常量

const 關鍵字現在用於各種場景,指標,函式變數,返回型別,類物件以及成員函式。

值替代

define BUFSIZE 100

BUFSIZE 是一個名字,它只是在預處理期間存在,因此它不佔用儲存空間且能放在一個頭檔案裡,目的是為使用它的所有編譯但願提供一個值。
const int bufsize = 100;
這樣就可以在編譯時編譯器需要知道這個值的任何地方使用bufsize,同時編譯器還可以執行常量摺疊

標頭檔案的const

通過包含標頭檔案,可把const定義單獨放在一個地方並把它分配給一個編譯單元,C++中的 const 預設為內部連線,const 僅在const被定義過的檔案裡才是可見的,而在連線時不能被其他編譯單元看到。
extern const int bufsize ;
編譯器並不會為const 建立儲存空間,相反它把這個定義儲存在符號表中。但是,extern 強制進行了儲存空間分配,由於 extern 意味著外部連線,因此必須分配儲存空間

常量摺疊

當眾多的const 在多個cpp 檔案中分配記憶體,容易引起連線錯誤,然而,const 預設內部連線,所以連線程式不會跨過編譯單元連線那些定義,因此不會有衝突。在大部分場合使用內建資料型別的情況,包括常量表達式,編譯都能執行常量摺疊

const 的安全性

如果不想讓一個值改變,就應該宣告成const,這不僅可以防止意外的更改提供安全措施,也消除了讀儲存器和讀記憶體操作,使編譯器產生的程式碼更有效。

C與C++中const的區別
  • 在C語言中,const只是被定義為一個不能被修改的普通變數,因此會分配儲存空間,而在C++中 const是被看做一個編譯時常量,不會分配記憶體
  • C語言中,const預設是外部連線,因此不存在內存摺疊的問題,在C++中,const 預設為內部連線,可以完成
指向const的指標

const int* u;
int const* u;
這倆其實是一樣的,都是一個指向常量的指標,因此指向的常量不能被修改

指向普通變數的常指標

int const* u;
指標的指向在它的生命週期裡不能被修改

指向const的常指標

const int const* u;
這種情況,不論是指向的變數還是指標本身的指向都不可以被修改。

const許可權問題
  • 可以把一個非const物件的地址賦給一個const指標,這樣可以達成的效果就是可以使得本來可以修改的變數強制不能被修改掉,這屬於許可權縮小。
  • 但是不可以把一個const物件賦給一個非const指標,這屬於許可權擴大,這種操作是不允許的。

關於C++的複習,我會一直堅持下去的,所以這個系列的複習筆記我會時常更新的。因為我是兩本書一塊看的,所以可能連貫性不是很大,過一段時間我會整理出一張思維導圖,讓你們對C++的複習有一個全面的概括。最後,感謝你們可以看到這裡,希望可以交個朋友,多交流經驗。