『C++』基礎知識點


一、基礎知識

1、C++編譯流程

以Unix系統編譯中間文件為說明:

.cpp—(編譯預處理)—>.ii—(編譯)—>.s—(匯編)—>.o—(ld,連接)—>.out

2、#include

作用於編譯預處理階段,將被include文件抄送在include所在位置,並會在相應位置寫出調用棧,生成中間文件.ii,該中間文件可讀

include文件加引號表示先從當前目錄尋找索引,加尖括號表示從編譯器指定根目錄索引,Unix默認為"~//usr/include"目錄

3、定義、聲明、頭文件

.h頭文件中只應存放三種代碼:

函數聲明:沒有大括號,形如void fun()

變量聲明:extern 變量名

class、結構體定義

extern表示聲明一個全局變量

聲明只是提示編譯器,存在這個東西,並沒有定義出實體,不定義直接調用會報錯。

4、標准頭文件結構

#ifndef HEADER_FLAG

#define HEADER_FLAG

/*頭文件*/

#endif

這是為了防止多次include同一個頭文件時,每次都抄送到預編譯文件中,造成文件過大、循環導入或者結構體定義重復以致報錯(聲明重復問題不大)。

5、默認參數

在聲明中寫默認參數,不在定義中給默認參數。

6、調用函數過程

本地變量進入堆棧(未必初始化)

函數參數進入堆棧

返回地址計入堆棧

返回值進入寄存器(運行函數)

pop掉參數

返回值進入堆棧(返回地址,所以要pop掉參數,堆棧先進后出)

7、內聯函數

在編譯階段優化,省略上一小結中復雜的堆棧操作,效果如下,

匯編(偽)優化如下,

注意,inline 函數名實際是一個聲明,而非定義所以不需要額外聲明。實際上以空間換時間(編譯會將函數插入調用位置),編譯器如果發現函數遞歸或者過於巨大,可能會拒絕inline操作,函數較小可能被自動inline,建議就是小函數inline(2-3行),超過20行的就不要inline了。

相比於宏,inline可以做類型檢查,給出debug提示,下圖中C++會提示double的f(a)和%d不匹配,C會直接給出一個奇怪的值,

 8、const

初始化之后不可修改,值得注意的是下圖這種,指針和const,到底是地址(指針)還是地址中的內容(對象)是const

重點在於const和*的位置順序,下述代碼中2、3兩句等價,

且const變量不能傳給其他非const的指針(因為這樣有可能造成修改),

函數和const

函數虛參加const表示函數內部不可修改該變量,對輸入無要求

return const 對接收函數返回的類型無要求

class和const

const 對象,此時我們不能保證class方法是否修改成員變量,又不能限制函數不能使用(class就沒有意義了)

const 對象 or 成員變量是const,要求成員變量必須有初始值,因為事后無法賦值

main文件(編譯時可以感知類聲明文件),類聲明文件,類定義文件:聲明、定義(兩個位置都需要添加)函數時后面添加const關鍵字如,int fun() const。

下圖運行結果為"f() const",

實際構成了重載,

void f(A* this)

void f(const A* this)

 9、字符串

char *s = "Hello World";  // 將代碼段的字符串地址直接付給指針,所以后面嘗試修改會報錯(代碼段不可修改),

             // 應該在開頭改寫為const char *s

char s[] = "Hello World";  // 數組被寫入堆棧,將代碼段的字符串拷貝到堆棧

10、引用

char& r = c;  // 引用可以做左值

相當於給c取了一個別名,此時c、r綁定到同一實體。

int x;int y;

int& a = x;

int& b = y;

a = b;  // 等價x=y

注意,引用無法取地址,即 int&* r 的寫法是錯誤的,不過相對的,int*& p 是沒問題的,指針可以被引用。

class的成員變量是引用時

此時只能使用initializer list的方式初始化引用對應的變量,如果在{}中使用m_y=a則表示將a復制給m_y對應的變量。

函數返回引用時

return一個全局變量,

這個引用表示變量,不表示值,所以最后一句表示賦值。

11、中間結果

相當於Python中的“_”,i*3這樣的結果會作為const int類型臨時保存。

二、class入門

1、變量

Field,成員變量,作用域為class的對象,類的函數中可以直接使用;class本身不能擁有變量,理解為聲明一個變量(函數和變量不同,函數屬於class而不是對象);

parameters,函數參數;

local variables,本地變量,作用域為本函數;

 后兩者完全相同,本地存儲,出來作用域則不存在該變量。

關鍵字 this:一個指針,為當前對象的指針(指該次調用成員函數的對象的指針),

經由指針this區分調用成員函數的不同實例,其原理如下:使用'實例對象.成員函數'來調用等價於直接調用該函數並將對象指針作為首個參數輸入,即:成員函數(this),原理和python一致,成員函數實際上有一個默認存在的參數輸入,接收實例指針

2、構造和析構

在C++中,class實例化時成員變量不會初始化,僅僅尋找到一塊足夠大的地址(java會清空地址內數據)。VS會在debug時為未初始化空間填充0xcd,用於排查(0xcd0xcd在國標碼中為‘燙’)。

constructor:構造函數,初始化對象時自動執行(相當於python中的__init__)

函數名和class名相同

沒有返回類型

destructor:析構函數,退出對象所在scope時自動執行

函數名為'~'加構造函數名

沒有返回類型

不可以有參數

有關‘{}’,表示scope,如下代碼中,進入‘{’后會執行Tree的構造函數,退出‘}’前,本scope內資源回收,會自動執行析構函數

數組、結構體、使用構建函數的class初始化方式對比:

Y經由構建函數Y()間接將f、i賦值,順便一提,數組b后面未指定元素會被初始化為0

default constructor:無參數構建函數,見下右的第二行會報錯,因為構建y2有兩個元素,而第二個元素會調用default constructor,但實際上constructor需要參數,所以會報錯:

:

3、scope和存儲空間

編譯器在‘{}’開始的位置會分配好空間,而在運行到相關定義時才會真正的運行構造函數。

如下圖,某些情況下編譯會出錯,因為一旦goto成功,則x1不會被構建,相應的退出‘{}’時,析構函數執行會失敗。

4、動態分配空間

new:制造對象,類似malloc;分配空間、調用構造函數(對於class),返回地址

使用一張表,記錄下每次申請的內存大小和對應的地址

delete:收回空間,類似free;析構對象(對於class)、回收空間;它有兩種用法,如下:

delete p :普通用法

delete[] p :一般來說new p[]時,需要使用這個,會將所有對象的析構函數分別調用,否則回收內存正常,但只調用指針直接指向的對象的析構

5、訪問控制

public:任何人可以訪問

private:成員函數可以訪問 ,注意對class來講,同一個class不同對象可以互相訪問私有變量,如下代碼,p[0]是可以訪問b的私有變量的

friends:聲名一個函數/class等,使之可以訪問自己(本class的任何實例)的私有變量

下面代碼涉及兩個知識點:1、friends聲明在class內部;2、結構體可以前向聲明(開頭的X),用於在結構體Y定義中占位。

 

protected:自己及子類可以訪問

6、struct vs class

未指定訪問控制屬性的變量、函數,class默認為private,struct默認public

7、初始化list

初始化后才執行構造函數(大括號中語句)

在大括號中賦值的話會先默認初始化變量,然后賦值;初始化list的方式直接用目標值初始化變量

8、成員函數和inline

在class內部給出了body的成員函數,視為內聯函數。

三、父類子類

1、組合和繼承

組合:已有類作為新的類的成員

繼承:改造類,class B: public A {},意為B類為A類子類 

     父類的private,在子類中存在,但是不能直接訪問(需要使用父類的public方法),需要使用protected聲明。

另一點值得注意的是,由於構造函數不可以直接調用, 調用父類的構造函數方式需要使用初始化list方法,而且必須最先構造父類(如果父類構造函數有參數),構造先父后子,析構先子后父:

2、覆蓋(override)、重載(overload)、隱藏

overload

在同一作用域中,函數名相同,參數列表不同,返回值可同可不同的函數,編譯器會根據傳入參數決定調用哪個函數,注意僅返回值不同不能構成overload關系。

override

又叫覆蓋,是指不在同一個作用域中(分別在父類和子類中),函數名,參數個數,參數類型,返回值類型都相同,並且父類函數必須有virtual關鍵字的函數,就構成了重寫(協變除外)。協變:協變也是一種重寫,只是父類和子類中的函數的返回值不同,父類的函數返回父類的指針或者引用,子類函數返回子類的指針或者引用。

virtual:子類的同名同參函數之間有聯系(繼承樹中某一個函數是virtual的,子類的該方法都是virtual的)。

重定義

又叫隱藏,是指在不同的作用域中(分別在父類和子類中),函數名相同,不能構成重寫的都是重定義(重定義的不光是函數,還可以是成員變量),隱藏和覆蓋不同,被隱藏的父類成員可以通過子類.父類::成員的方式調用。

3、向上造型upcasting

子類對象可以被傳給父類對象指針,如下圖所示,

這是由於C++的class類似於C的結構體,實際上是一個指針指向一塊有特定內容排列順序的內存,子類只會在父類的內存規划上向后擴充,不會更改父類已經規划好的部分。如果有子類方法隱藏了父類方法,向上造型后會隱藏失效,此時的對象指針僅能識別父類原有的模塊。

類似地,也有向下造型,不過可能會出錯。

 

Employee是Manager的父類

4、多態

本小節摘抄自文章:C++ 多態的實現及原理

想要理解多態,需要區分函數和虛函數的區別(內存上的位置差異),並要理解向上造型的概念,了解了前面兩點,就了解了動態綁定、靜態綁定的區別,對於多態產生的種種現象就能夠從機理上給出自己的解釋。

virtual虛函數內存機制

上面提到過,virtual是讓子類與父類之間的同名函數有聯系,這就是多態性,實現動態綁定。

任何類若是有虛函數就會比正常類大一點,所有有virtual的類的對象里面最頭上會自動加上一個隱藏的,不讓我知道的指針,它指向一張表,注意,該表對於同一個class的不同對象是同一個,不同class(指父類子類)的表不同。這張表叫做vtable(虛表),vtable里是所有virtual函數的地址,對於下面代碼,

class Shape {
public:
    Shape();
    virtual  ~Shape();
    virtual void render();
    void move(const pos&);
    virtual void resize();
protected:
    pos center;
};

 其內存分布如下:

我們看一下其子類的內存分布:

class Ellipse : public Shape{
public:
    Ellipse (float majr, float minr);
    virtual void render();

protected:
    float major_axis;
    float minor_axis;
};

 

這里的resize沿用了shape的成員函數。

多態實現邏輯

看如下代碼,

#include "stdafx.h"
#include <iostream> 
#include <stdlib.h>
using namespace std; 

class Father
{
public:
    void Face()
    {
        cout << "Father's face" << endl;
    }

    void Say()
    {
        cout << "Father say hello" << endl;
    }
};


class Son:public Father
{
public:     
    void Say()
    {
        cout << "Son say hello" << endl;
    }
};

void main()
{
    Son son;
    Father *pFather=&son; // 隱式類型轉換
    pFather->Say();
}

輸出的結果為:

我們在main()函數中首先定義了一個Son類的對象son,接着定義了一個指向Father類的指針變量pFather,然后利用該變量調用pFather->Say().估計很多人往往將這種情況和c++的多態性搞混淆,認為son實際上是Son類的對象,應該是調用Son類的Say,輸出"Son say hello",然而結果卻不是.

從編譯的角度來看:

c++編譯器在編譯的時候,要確定每個對象調用的函數(非虛函數)的地址,這稱為早期綁定,當我們將Son類的對象son的地址賦給pFather時,c++編譯器進行了類型轉換,此時c++編譯器認為變量pFather保存的就是Father對象的地址,當在main函數中執行pFather->Say(),調用的當然就是Father對象的Say函數

從內存角度看:

    

Son類對象的內存模型如上圖

我們構造Son類的對象時,首先要調用Father類的構造函數去構造Father類的對象,然后才調用Son類的構造函數完成自身部分的構造,從而拼接出一個完整的Son類對象。當我們將Son類對象轉換為Father類型時,該對象就被認為是原對象整個內存模型的上半部分,也就是上圖中“Father的對象所占內存”,那么當我們利用類型轉換后的對象指針去調用它的方法時,當然也就是調用它所在的內存中的方法,因此,輸出“Father Say hello”,也就順理成章了。

正如很多人那么認為,在上面的代碼中,我們知道pFather實際上指向的是Son類的對象,我們希望輸出的結果是son類的Say方法,那么想到達到這種結果,就要用到虛函數了。

前面輸出的結果是因為編譯器在編譯的時候,就已經確定了對象調用的函數的地址,要解決這個問題就要使用晚綁定,當編譯器使用晚綁定時候,就會在運行時再去確定對象的類型以及正確的調用函數,而要讓編譯器采用晚綁定,就要在基類中聲明函數時使用virtual關鍵字,這樣的函數我們就稱之為虛函數,一旦某個函數在基類中聲明為virtual,那么在所有的派生類中該函數都是virtual,而不需要再顯式地聲明為virtual。

代碼稍微改動一下,看一下運行結果

#include "stdafx.h"
#include <iostream> 
#include <stdlib.h>
using namespace std; 

class Father
{
public:
    void Face()
    {
        cout << "Father's face" << endl;
    }

    virtual void Say()
    {
        cout << "Father say hello" << endl;
    }
};


class Son:public Father
{
public:     
    void Say()
    {
        cout << "Son say hello" << endl;
    }
};

void main()
{
    Son son;
    Father *pFather=&son; // 隱式類型轉換
    pFather->Say();
}

我們發現結果是"Son say hello"也就是根據對象的類型調用了正確的函數,那么當我們將Say()聲明為virtual時,背后發生了什么。

編譯器在編譯的時候,發現Father類中有虛函數,此時編譯器會為每個包含虛函數的類創建一個虛表(即 vtable),該表是一個一維數組,在這個數組中存放每個虛函數的地址,

那么如何定位虛表呢?編譯器另外還為每個對象提供了一個虛表指針(即vptr),這個指針指向了對象所屬類的虛表,在程序運行時,根據對象的類型去初始化vptr,從而讓vptr正確的指向了所屬類的虛表,從而在調用虛函數的時候,能夠找到正確的函數,對於第二段代碼程序,由於pFather實際指向的對象類型是Son,因此vptr指向的Son類的vtable,當調用pFather->Son()時,根據虛表中的函數地址找到的就是Son類的Say()函數.

正是由於每個對象調用的虛函數都是通過虛表指針來索引的,也就決定了虛表指針的正確初始化是非常重要的,換句話說,在虛表指針沒有正確初始化之前,我們不能夠去調用虛函數,那么虛表指針是在什么時候,或者什么地方初始化呢?

答案是在構造函數中進行虛表的創建和虛表指針的初始化,在構造子類對象時,要先調用父類的構造函數,此時編譯器只“看到了”父類,並不知道后面是否還有繼承者,它初始化父類對象的虛表指針,該虛表指針指向父類的虛表,當執行子類的構造函數時,子類對象的虛表指針被初始化,指向自身的虛表。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM