《求職》第一部分 - 語言篇 - C++常見面試題

1.基礎

1.1static關鍵字的作用

1.全局靜態變量

在全局變量前加上關鍵字static,全局變量就定義成一個全局靜態變量.

靜態存儲區,在整個程序運行期間一直存在。

初始化:未經初始化的全局靜態變量會被自動初始化爲0(自動對象的值是任意的,除非他被顯式初始化);

作用域:全局靜態變量在聲明他的文件之外是不可見的,準確地說是從定義之處開始,到文件結尾。

2.局部靜態變量

在局部變量之前加上關鍵字static,局部變量就成爲一個局部靜態變量。

內存中的位置:靜態存儲區

初始化:未經初始化的全局靜態變量會被自動初始化爲0(自動對象的值是任意的,除非他被顯式初始化);

作用域:作用域仍爲局部作用域,當定義它的函數或者語句塊結束的時候,作用域結束。但是當局部靜態變量離開作用域後,並沒有銷燬,而是仍然駐留在內存當中,只不過我們不能再對它進行訪問,直到該函數再次被調用,並且值不變。

3.靜態函數

在函數返回類型前加static,函數就定義爲靜態函數。函數的定義和聲明在默認情況下都是extern的,但靜態函數只是在聲明他的文件當中可見,不能被其他文件所用

函數的實現使用static修飾,那麼這個函數只可在本cpp內使用,不會同其他cpp中的同名函數引起衝突

warning:不要再頭文件中聲明static的全局函數,不要在cpp內聲明非static的全局函數,如果你要在多個cpp中複用該函數,就把它的聲明提到頭文件裏去,否則cpp內部聲明需加上static修飾。

4.類的靜態成員

在類中,靜態成員可以實現多個對象之間的數據共享,並且使用靜態數據成員還不會破壞隱藏的原則,即保證了安全性。因此,靜態成員是類的所有對象中共享的成員,而不是某個對象的成員。對多個對象來說,靜態數據成員只存儲一處,供所有對象共用

5.類的靜態函數

靜態成員函數和靜態數據成員一樣,它們都屬於類的靜態成員,它們都不是對象成員。因此,對靜態成員的引用不需要用對象名。

在靜態成員函數的實現中不能直接引用類中說明的非靜態成員,可以引用類中說明的靜態成員(這點非常重要)。如果靜態成員函數中要引用非靜態成員時,可通過對象來引用。從中可看出,調用靜態成員函數使用如下格式:<類名>::<靜態成員函數名>(<參數表>);

1.2C++和C的區別

設計思想上:

C++是面向對象的語言,而C是面向過程的結構化編程語言

語法上:

C++具有封裝、繼承和多態三種特性;

C++相比C,增加多許多類型安全的功能,比如強制類型轉換;

C++支持範式編程,比如模板類、函數模板等。

1.3assert()

斷言,是宏,而非函數。assert 宏的原型定義在<assert.h>(C)、<cassert>(C++)中,其作用是如果它的條件返回錯誤,則終止程序執行。如:

assert( p != NULL );

1.4#pragma pack(n)

設定結構體、聯合以及類成員變量以 n 字節方式對齊

#pragma pack(n) 使用
#pragma pack(push)  // 保存對齊狀態
#pragma pack(4)     // 設定爲 4 字節對齊

struct test
{
    char m1;
    double m4;
    int m3;
};

#pragma pack(pop)   // 恢復對齊狀態

1.5位域

Bit mode: 2;    // mode 佔 2 位

類可以將其(非靜態)數據成員定義爲位域(bit-field),在一個位域中含有一定數量的二進制位。當一個程序需要向其他程序或硬件設備傳遞二進制數據時,通常會用到位域。

  • 位域在內存中的佈局是與機器有關的
  • 位域的類型必須是整型或枚舉類型,帶符號類型中的位域的行爲將因具體實現而定
  • 取地址運算符(&)不能作用於位域,任何指針都無法指向類的位域

1.6volatile

volatile int i = 10; 
  • volatile 關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素(操作系統、硬件、其它線程等)更改。所以使用 volatile 告訴編譯器不應對這樣的對象進行優化。
  • volatile 關鍵字聲明的變量,每次訪問時都必須從內存中取出值(沒有被 volatile 修飾的變量,可能由於編譯器的優化,從 CPU 寄存器中取值)
  • const 可以是 volatile (如只讀的狀態寄存器)
  • 指針可以是 volatile

1.7指針和引用

1.7.1引用

C++是C語言的繼承,它可進行過程化程序設計,又可以進行以抽象數據類型爲特點的基於對象的程序設計,還可以進行以繼承和多態爲特點的面向對象的程序設計。引用就是C++對C語言的重要擴充。引用就是某一變量的一個別名,對引用的操作與對變量直接操作完全一樣。引用的聲明方法:類型標識符 &引用名=目標變量名;引用引入了對象的一個同義詞。定義引用的表示方法與定義指針相似,只是用&代替了*。

左值引用

常規引用,一般表示對象的身份。

右值引用

右值引用就是必須綁定到右值(一個臨時對象、將要銷燬的對象)的引用,一般表示對象的值。

右值引用可實現轉移語義(Move Sementics)和精確傳遞(Perfect Forwarding),它的主要目的有兩個方面:

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

引用摺疊

  • X& &X& &&X&& & 可摺疊成 X&
  • X&& && 可摺疊成 X&&

從彙編層去解釋一下引用

  1. 9: int x = 1;

  2. 00401048 mov dword ptr [ebp-4],1

  3. 10: int &b = x;

  4. 0040104F lea eax,[ebp-4]

  5. 00401052 mov dword ptr [ebp-8],eax

x的地址爲ebp-4,b的地址爲ebp-8,因爲棧內的變量內存是從高往低進行分配的。所以b的地址比x的低。lea eax,[ebp-4] 這條語句將x的地址ebp-4放入eax寄存器mov dword ptr [ebp-8],eax 這條語句將eax的值放入b的地址ebp-8中上面兩條彙編的作用即:將x的地址存入變量b中,這不和將某個變量的地址存入指針變量是一樣的嗎?所以從彙編層次來看,的確引用是通過指針來實現的。

1.7.2指針

指針利用地址,它的值直接指向存在電腦存儲器中另一個地方的值。由於通過地址能找到所需的變量單元,可以說,地址指向該變量單元。因此,將地址形象化的稱爲“指針”。意思是通過它能找到以它爲地址的內存單元。

懸空指針和野指針有什麼區別?

  1. 野指針:野指針指,訪問一個已刪除或訪問受限的內存區域的指針,野指針不能判斷是否爲NULL來避免。指針沒有初始化,釋放後沒有置空,越界

  2. 懸空指針:一個指針的指向對象已被刪除,那麼就成了懸空指針。野指針是那些未初始化的指針。

野指針是什麼?如何檢測內存泄漏?

  1. 野指針:指向內存被釋放的內存或者沒有訪問權限的內存的指針。

  2. “野指針”的成因主要有3種:

① 指針變量沒有被初始化。任何指針變量剛被創建時不會自動成爲NULL指針,它的缺省值是隨機的,它會亂指一氣。所以,指針變量在創建的同時應當被初始化,要麼將指針設置爲NULL,要麼讓它指向合法的內存。例如
char *p = NULL;
char *str = new char(100);

② 指針p被free或者delete之後,沒有置爲NULL;

③ 指針操作超越了變量的作用範圍。

  1. 如何避免野指針:

① 對指針進行初始化

  • 將指針初始化爲NULL。

char * p = NULL;

  • 用malloc分配內存

char * p = (char * )malloc(sizeof(char));

  • 用已有合法的可訪問的內存地址對指針初始化

char num[ 30] = {0};

char *p = num;

② 指針用完後釋放內存,將指針賦NULL。

delete§;

p = NULL;

1.7.3區別

1.指針是一個實體,需要分配內存空間。引用只是變量的別名,不需要分配內存空間。

2.使用sizeof看一個指針的大小是4,而引用則是被引用對象的大小。

3.指針可以被初始化爲NULL,指針在定義的時候不一定要初始化,並且指向的空間可變。而引用必須被初始化且必須是一個已有對象的引用;(注:不能有引用的值不能爲NULL)

4.作爲參數傳遞時,指針需要被解引用纔可以對對象進行操作,而直接對引用的修改都會改變引用所指向的對象; 傳指針的實質是傳值,傳遞的值是指針的地址;傳引用的實質是傳地址,傳遞的是變量的地址。

5.可以有const指針,但是沒有const引用;

6.指針在使用中可以指向其它對象,但是引用只能是一個對象的引用,不能被改變;

7.指針可以有多級指針(**p),而引用只有一級;

8.指針和引用使用++運算符的意義不一樣(指針是指向下一個空間,引用時引用的變量值加1)。

9.如果返回動態內存分配的對象或者內存,必須使用指針,引用可能引起內存泄露。

10.引用訪問一個變量是直接訪問,而指針訪問一個變量是間接訪問。

11.引用底層是通過指針實現的;

12.使用指針前最好做類型檢查,防止野指針的出現;

13.引用必須被初始化,指針不必。

14.引用初始化後不能被改變,指針可以改變所指對象。

15.不存在指向空的引用,但存在指向空的指針。

1.8判斷點P是否在ABC內

根據面積法,如果P在三角形ABC內,那麼三角形ABP的面積+三角形BCP的面積+三角形ACP的面積應該等於三角形ABC的面積。算法如下:

#include <iostream>
#include <math.h>

using namespace std;

#define ABS_FLOAT_0 0.0001

struct point_float
{
	float x;
	float y;
};

/** 
  * @brief 計算三角形面積 
  */ 
float GetTriangleSquar(const point_float pt0, const point_float pt1, const point_float pt2)
{
	point_float AB,   BC;
	AB.x = pt1.x - pt0.x;
	AB.y = pt1.y - pt0.y;
	BC.x = pt2.x - pt1.x;
	BC.y = pt2.y - pt1.y;
	
	return fabs((AB.x * BC.y - AB.y * BC.x)) / 2.0f;
}

/** 
  * @brief 判斷給定一點是否在三角形內或邊上 
  */ 
bool IsInTriangle(const point_float A, const point_float B, const point_float C, const` point_float D)
{
	float SABC, SADB, SBDC, SADC;
	SABC = GetTriangleSquar(A, B, C);``
	SADB = GetTriangleSquar(A, D, B);``
	SBDC = GetTriangleSquar(B, D, C);``
	SADC = GetTriangleSquar(A, D, C);``
	
	float` `SumSuqar = SADB + SBDC + SADC;``
	if((-ABS_FLOAT_0 < (SABC - SumSuqar)) && ((SABC - SumSuqar) < ABS_FLOAT_0))``
	{
		return` `true;
	 }
	 else
	 {
	 	return` `false;
	 }
}

1.9智能指針

智能指針主要用於管理在堆上分配的內存,它將普通的指針封裝爲一個棧對象。當棧對象的生存週期結束後,會在析構函數中釋放掉申請的內存,從而防止內存泄漏。C++11中最常用的智能指針類型爲shared_ptr,它採用引用計數的方法,記錄當前內存資源被多少個智能指針引用。該引用計數的內存在堆上分配。當新增一個時引用計數加1,當過期時引用計數減一。只有引用計數爲0時,智能指針纔會自動釋放引用的內存資源。對shared_ptr進行初始化時不能將一個普通指針直接賦值給智能指針,因爲一個是指針,一個是類。可以通過make_shared函數或者通過構造函數傳入普通指針。並可以通過get函數獲得普通指針。

C++裏面的四個智能指針: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中後三個是c++11支持,並且第一個已經被11棄用。

爲什麼要使用智能指針:

智能指針的作用是管理一個指針,因爲存在以下這種情況:申請的空間在函數結束時忘記釋放,造成內存泄漏。使用智能指針可以很大程度上的避免這個問題,因爲智能指針就是一個類,當超出了類的作用域是,類會自動調用析構函數,析構函數會自動釋放資源。所以智能指針的作用原理就是在函數結束時自動釋放內存空間,不需要手動釋放內存空間。

1.auto_ptr(c++98的方案,cpp11已經拋棄)

採用所有權模式。

auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不會報錯.

此時不會報錯,p2剝奪了p1的所有權,但是當程序運行時訪問p1將會報錯。所以auto_ptr的缺點是:存在潛在的內存崩潰問題!

2.unique_ptr(替換auto_ptr)

unique_ptr實現獨佔式擁有或嚴格擁有概念,保證同一時間內只有一個智能指針可以指向該對象。它對於避免資源泄露(例如“以new創建對象後因爲發生異常而忘記調用delete”)特別有用。

採用所有權模式,還是上面那個例子

unique_ptr<string> p3 (``new` `string (``"auto"``));   //#4``unique_ptr<string> p4;                       ``//#5``p4 = p3;``//此時會報錯!!`

編譯器認爲p4=p3非法,避免了p3不再指向有效數據的問題。因此,unique_ptr比auto_ptr更安全。

另外unique_ptr還有更聰明的地方:當程序試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這麼做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這麼做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下懸掛的unique_ptr(pu1),這可能導致危害。而#2不會留下懸掛的unique_ptr,因爲它調用 unique_ptr 的構造函數,該構造函數創建的臨時對象在其所有權讓給 pu3 後就會被銷燬。這種隨情況而已的行爲表明,unique_ptr 優於允許兩種賦值的auto_ptr 。

注:如果確實想執行類似與#1的操作,要安全的重用這種指針,可給它賦新值。C++有一個標準庫函數std::move(),讓你能夠將一個unique_ptr賦給另一個。例如:

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

3.shared_ptr

shared_ptr實現共享式擁有概念。多個智能指針可以指向相同對象,該對象和其相關資源會在“最後一個引用被銷燬”時候釋放。從名字share就可以看出了資源可以被多個指針共享,它使用計數機制來表明資源被幾個指針共享。可以通過成員函數use_count()來查看資源的所有者個數。除了可以通過new來構造,還可以通過傳入auto_ptr, unique_ptr,weak_ptr來構造。當我們調用release()時,當前指針會釋放資源所有權,計數減一。當計數等於0時,資源會被釋放。

shared_ptr 是爲了解決 auto_ptr 在對象所有權上的侷限性(auto_ptr 是獨佔的), 在使用引用計數的機制上提供了可以共享所有權的智能指針。

成員函數:

use_count 返回引用計數的個數

unique 返回是否是獨佔所有權( use_count 爲 1)

swap 交換兩個 shared_ptr 對象(即交換所擁有的對象)

reset 放棄內部對象的所有權或擁有對象的變更, 會引起原有對象的引用計數的減少

get 返回內部對象(指針), 由於已經重載了()方法, 因此和直接使用對象是一樣的.如 shared_ptr< int > sp(new int(1)); sp 與 sp.get()是等價的

4.weak_ptr

weak_ptr 是一種不控制對象生命週期的智能指針, 它指向一個 shared_ptr 管理的對象. 進行該對象的內存管理的是那個強引用的 shared_ptr. weak_ptr只是提供了對管理對象的一個訪問手段。weak_ptr 設計的目的是爲配合 shared_ptr 而引入的一種智能指針來協助 shared_ptr 工作, 它只可以從一個 shared_ptr 或另一個 weak_ptr 對象構造, 它的構造和析構不會引起引用記數的增加或減少。weak_ptr是用來解決shared_ptr相互引用時的死鎖問題,如果說兩個shared_ptr相互引用,那麼這兩個指針的引用計數永遠不可能下降爲0,資源永遠不會釋放。它是對對象的一種弱引用,不會增加對象的引用計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過調用lock函數來獲得shared_ptr。

class B;
class A
{
public:
    shared_ptr<B> pb_;
    ~A()
    {
    cout<<"A delete\n";
    }
};
class B
{
public:
	shared_ptr<A> pa_;
	~B()
	{
		cout<<"B delete\n";
	}
};
void fun()
{
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
}
int main()
{
    fun();
    return 0;
}

可以看到fun函數中pa ,pb之間互相引用,兩個資源的引用計數爲2,當要跳出函數時,智能指針pa,pb析構時兩個資源引用計數會減一,但是兩者引用計數還是爲1,導致跳出函數時資源沒有被釋放(A B的析構函數沒有被調用),如果把其中一個改爲weak_ptr就可以了,我們把類A裏面的shared_ptr pb_; 改爲weak_ptr pb_; 運行結果如下,這樣的話,資源B的引用開始就只有1,當pb析構時,B的計數變爲0,B得到釋放,B釋放的同時也會使A的計數減一,同時pa析構時使A的計數減一,那麼A的計數爲0,A得到釋放。

注意的是我們不能通過weak_ptr直接訪問對象的方法,比如B對象中有一個方法print(),我們不能這樣訪問,pa->pb_->print(); 英文pb_是一個weak_ptr,應該先把它轉化爲shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

智能指針的內存泄漏如何解決?

爲了解決循環引用導致的內存泄漏,引入了weak_ptr弱指針,weak_ptr的構造函數不會修改引用計數的值,從而不會對對象的內存進行管理,其類似一個普通指針,但不指向引用計數的共享內存,但是其可以檢測到所管理的對象是否已經被釋放,從而避免非法訪問。

1.10數組和指針的區別

指針和數組的主要區別如下:

指針 數組
保存數據的地址 保存數據
間接訪問數據,首先獲得指針的內容,然後將其作爲地址,從該地址中提取數據 直接訪問數據,
通常用於動態的數據結構 通常用於固定數目且數據類型相同的元素
通過Malloc分配內存,free釋放內存 隱式的分配和刪除
通常指向匿名數據,操作匿名函數 自身即爲數據名

1.11 inline 內聯函數

特徵

  • 相當於把內聯函數裏面的內容寫在調用內聯函數處;
  • 相當於不用執行進入函數的步驟,直接執行函數體;
  • 相當於宏,卻比宏多了類型檢查,真正具有函數特性;
  • 不能包含循環、遞歸、switch 等複雜操作;
  • 類中除了虛函數的其他函數都會自動隱式地當成內聯函數。

使用

inline 使用
// 聲明1(加 inline,建議使用)
inline int functionName(int first, int secend,...);

// 聲明2(不加 inline)
int functionName(int first, int secend,...);

// 定義
inline int functionName(int first, int secend,...) {/****/};

編譯器對 inline 函數的處理步驟

  1. 將 inline 函數體複製到 inline 函數調用點處;
  2. 爲所用 inline 函數中的局部變量分配內存空間;
  3. 將 inline 函數的的輸入參數和返回值映射到調用方法的局部變量空間中;
  4. 如果 inline 函數有多個返回點,將其轉變爲 inline 函數代碼塊末尾的分支(使用 GOTO)。

優缺點

優點

  1. 內聯函數同宏函數一樣將在被調用處進行代碼展開,省去了參數壓棧、棧幀開闢與回收,結果返回等,從而提高程序運行速度
  2. 內聯函數相比宏函數來說,在代碼展開時,會做安全檢查或自動類型轉換(同普通函數),而宏定義則不會。
  3. 在類中聲明同時定義的成員函數,自動轉化爲內聯函數,因此內聯函數可以訪問類的成員變量,宏定義則不能。
  4. 內聯函數在運行時可調試,而宏定義不可以。

缺點

  1. 代碼膨脹。內聯是以代碼膨脹(複製)爲代價,消除函數調用帶來的開銷。如果執行函數體內代碼的時間,相比於函數調用的開銷較大,那麼效率的收穫會很少。另一方面,每一處內聯函數的調用都要複製代碼,將使程序的總代碼量增大,消耗更多的內存空間。
  2. inline 函數無法隨着函數庫升級而升級。inline函數的改變需要重新編譯,不像 non-inline 可以直接鏈接。
  3. 是否內聯,程序員不可控。內聯函數只是對編譯器的建議,是否對函數內聯,決定權在於編譯器。

虛函數(virtual)可以是內聯函數(inline)嗎?

Are “inline virtual” member functions ever actually “inlined”?

  • 虛函數可以是內聯函數,內聯是可以修飾虛函數的,但是當虛函數表現多態性的時候不能內聯。
  • 內聯是在編譯器建議編譯器內聯,而虛函數的多態性在運行期,編譯器無法知道運行期調用哪個代碼,因此虛函數表現爲多態性時(運行期)不可以內聯。
  • inline virtual 唯一可以內聯的時候是:編譯器知道所調用的對象是哪個類(如 Base::who()),這隻有在編譯器具有實際對象而不是對象的指針或引用時纔會發生。
虛函數內聯使用
#include <iostream>  
using namespace std;
class Base
{
public:
	inline virtual void who()
	{
		cout << "I am Base\n";
	}
	virtual ~Base() {}
};
class Derived : public Base
{
public:
	inline void who()  // 不寫inline時隱式內聯
	{
		cout << "I am Derived\n";
	}
};

int main()
{
	// 此處的虛函數 who(),是通過類(Base)的具體對象(b)來調用的,編譯期間就能確定了,所以它可以是內聯的,但最終是否內聯取決於編譯器。 
	Base b;
	b.who();

	// 此處的虛函數是通過指針調用的,呈現多態性,需要在運行時期間才能確定,所以不能爲內聯。  
	Base *ptr = new Derived();
	ptr->who();

	// 因爲Base有虛析構函數(virtual ~Base() {}),所以 delete 時,會先調用派生類(Derived)析構函數,再調用基類(Base)析構函數,防止內存泄漏。
	delete ptr;
	ptr = nullptr;

	system("pause");
	return 0;
} 

1.12函數指針

1、定義

函數指針是指向函數的指針變量。

函數指針本身首先是一個指針變量,該指針變量指向一個具體的函數。這正如用指針變量可指向整型變量、字符型、數組一樣,這裏是指向函數。

C在編譯時,每一個函數都有一個入口地址,該入口地址就是函數指針所指向的地址。有了指向函數的指針變量後,可用該指針變量調用函數,就如同用指針變量可引用其他類型變量一樣,在這些概念上是大體一致的。

2、用途

調用函數和做函數的參數,比如回調函數。

3、示例

char * fun(char * p) {…} // 函數fun

char * (*pf)(char * p); // 函數指針pf

pf = fun; // 函數指針pf指向函數fun

pf§; // 通過函數指針pf調用函數fun

1.13 fork函數

Fork:創建一個和當前進程映像一樣的進程可以通過fork( )系統調用:

#include <sys/types.h> 
#include <unistd.h> 
pid_t fork(void); 

成功調用fork( )會創建一個新的進程,它幾乎與調用fork( )的進程一模一樣,這兩個進程都會繼續運行。在子進程中,成功的fork( )調用會返回0。在父進程中fork( )返回子進程的pid。如果出現錯誤,fork( )返回一個負值。

最常見的fork( )用法是創建一個新的進程,然後使用exec( )載入二進制映像,替換當前進程的映像。這種情況下,派生(fork)了新的進程,而這個子進程會執行一個新的二進制可執行文件的映像。這種“派生加執行”的方式是很常見的。

在早期的Unix系統中,創建進程比較原始。當調用fork時,內核會把所有的內部數據結構複製一份,複製進程的頁表項,然後把父進程的地址空間中的內容逐頁的複製到子進程的地址空間中。但從內核角度來說,逐頁的複製方式是十分耗時的。現代的Unix系統採取了更多的優化,例如Linux,採用了寫時複製的方法,而不是對父進程空間進程整體複製。

1.14extern 用法

  1. extern修飾變量的聲明

如果文件a.c需要引用b.c中變量int v,就可以在a.c中聲明extern int v,然後就可以引用變量v。

  1. extern修飾函數的聲明

如果文件a.c需要引用b.c中的函數,比如在b.c中原型是int fun(int mu),那麼就可以在a.c中聲明extern int fun(int mu),然後就能使用fun來做任何事情。就像變量的聲明一樣,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的範圍中。

  1. extern修飾符可用於指示C或者C++函數的調用規範。

比如在C++中調用C庫函數,就需要在C++程序中用extern “C”聲明要引用的函數。這是給鏈接器用的,告訴鏈接器在鏈接的時候用C函數規範來鏈接。主要原因是C++和C程序編譯完成後在目標代碼中命名規則不同。

  • 被 extern 限定的函數或變量是 extern 類型的
  • extern "C" 修飾的變量和函數是按照 C 語言方式編譯和連接的

extern "C" 的作用是讓 C++ 編譯器將 extern "C" 聲明的代碼當作 C 語言代碼處理,可以避免 C++ 因符號修飾導致代碼不能和C語言庫中的符號進行鏈接的問題。

extern "C" 使用
#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int, size_t);

#ifdef __cplusplus
}
#endif

1.15enum 枚舉類型

限定作用域的枚舉類型

enum class open_modes { input, output, append };

不限定作用域的枚舉類型

enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10 };

1.16 static關鍵字

1.先來介紹它的第一條也是最重要的一條:隱藏。(static函數,static變量均可)

當同時編譯多個文件時,所有未加static前綴的全局變量和函數都具有全局可見性。

2.static的第二個作用是保持變量內容的持久。(static變量中的記憶功能和全局生存期)存儲在靜態數據區的變量會在程序剛開始運行時就完成初始化,也是唯一的一次初始化。共有兩種變量存儲在靜態存儲區:全局變量和static變量,只不過和全局變量比起來,static可以控制變量的可見範圍,說到底static還是用來隱藏的。

3.static的第三個作用是默認初始化爲0(static變量)

其實全局變量也具備這一屬性,因爲全局變量也存儲在靜態數據區。在靜態數據區,內存中所有的字節默認值都是0x00,某些時候這一特點可以減少程序員的工作量。

4.static的第四個作用:C++中的類成員聲明static

  1. 函數體內static變量的作用範圍爲該函數體,不同於auto變量,該變量的內存只被分配一次,因此其值在下次調用時仍維持上次的值;

  2. 在模塊內的static全局變量可以被模塊內所用函數訪問,但不能被模塊外其它函數訪問;

  3. 在模塊內的static函數只可被這一模塊內的其它函數調用,這個函數的使用範圍被限制在聲明它的模塊內;

  4. 在類中的static成員變量屬於整個類所擁有,對類的所有對象只有一份拷貝;

  5. 在類中的static成員函數屬於整個類所擁有,這個函數不接收this指針,因而只能訪問類的static成員變量。

類內:

  1. static類對象必須要在類外進行初始化,static修飾的變量先於對象存在,所以static修飾的變量要在類外初始化;

  2. 由於static修飾的類成員屬於類,不屬於對象,因此static類成員函數是沒有this指針的,this指針是指向本對象的指針。正因爲沒有this指針,所以static類成員函數不能訪問非static的類成員,只能訪問 static修飾的類成員;

  3. static成員函數不能被virtual修飾,static成員不屬於任何對象或實例,所以加上virtual沒有任何實際意義;靜態成員函數沒有this指針,虛函數的實現是爲每一個對象分配一個vptr指針,而vptr是通過this指針調用的,所以不能爲virtual;虛函數的調用關係,this->vptr->ctable->virtual function

1.加了static關鍵字的全局變量只能在本文件中使用。例如在a.c中定義了static int a=10;那麼在b.c中用extern int a是拿不到a的值得,a的作用域只在a.c中。

2.static定義的靜態局部變量分配在數據段上,普通的局部變量分配在棧上,會因爲函數棧幀的釋放而被釋放掉。

3.對一個類中成員變量和成員函數來說,加了static關鍵字,則此變量/函數就沒有了this指針了,必須通過類名才能訪問。

靜態變量什麼時候初始化

  1. 初始化只有一次,但是可以多次賦值,在主程序之前,編譯器已經爲其分配好了內存。

  2. 靜態局部變量和全局變量一樣,數據都存放在全局區域,所以在主程序之前,編譯器已經爲其分配好了內存,但在C和C++中靜態局部變量的初始化節點又有點不太一樣。在C中,初始化發生在代碼執行之前,編譯階段分配好內存之後,就會進行初始化,所以我們看到在C語言中無法使用變量對靜態局部變量進行初始化,在程序運行結束,變量所處的全局內存會被全部回收。

  3. 而在C++中,初始化時在執行相關代碼時纔會進行初始化,主要是由於C++引入對象後,要進行初始化必須執行相應構造函數和析構函數,在構造函數或析構函數中經常會需要進行某些程序中需要進行的特定操作,並非簡單地分配內存。所以C++標準定爲全局或靜態對象是有首次用到時纔會進行構造,並通過atexit()來管理。在程序結束,按照構造順序反方向進行逐個析構。所以在C++中是可以使用變量對靜態局部變量進行初始化的。

1.17 struct 和 typedef struct

C 中

// c
typedef struct Student {
    int age; 
} S;

等價於

// c
struct Student { 
    int age; 
};

typedef struct Student S;

此時 S 等價於 struct Student,但兩個標識符名稱空間不相同。

另外還可以定義與 struct Student 不衝突的 void Student() {}

C++ 中

由於編譯器定位符號的規則(搜索規則)改變,導致不同於C語言。

一、如果在類標識符空間定義了 struct Student {...};,使用 Student me; 時,編譯器將搜索全局標識符表,Student 未找到,則在類標識符內搜索。

即表現爲可以使用 Student 也可以使用 struct Student,如下:

// cpp
struct Student { 
    int age; 
};

void f( Student me );       // 正確,"struct" 關鍵字可省略

二、若定義了與 Student 同名函數之後,則 Student 只代表函數,不代表結構體,如下:

typedef struct Student { 
    int age; 
} S;

void Student() {}           // 正確,定義後 "Student" 只代表此函數

//void S() {}               // 錯誤,符號 "S" 已經被定義爲一個 "struct Student" 的別名

int main() {
    Student(); 
    struct Student me;      // 或者 "S me";
    return 0;
}

1.18 decltype

decltype 關鍵字用於檢查實體的聲明類型或表達式的類型及值分類。語法:

decltype ( expression )
decltype 使用 ```cpp // 尾置返回允許我們在參數列表之後聲明返回類型 template auto fcn(It beg, It end) -> decltype(*beg) { // 處理序列 return *beg; // 返回序列中一個元素的引用 } // 爲了使用模板參數成員,必須用 typename template auto fcn2(It beg, It end) -> typename remove_reference

1.19++i和i++

++i和i++的區別

++i先自增1,再返回,i++先返回i,再自增1

++i和i++的實現

  1. ++i 實現:
int&  int::operator++()
{
	*this +=1return *this}
  1. i++ 實現:
const int  int::operatorint)``
{
	int oldValue = *this;``
	++*this);
	
	return oldValue;``
}`

1.20類型轉換

隱式類型轉換

首先,對於內置類型,低精度的變量給高精度變量賦值會發生隱式類型轉換,其次,對於只存在單個參數的構造函數的對象構造來說,函數調用可以直接使用該參數傳入,編譯器會自動調用其構造函數生成臨時對象。

類型轉換

reinterpret_cast:可以用於任意類型的指針之間的轉換,對轉換的結果不做任何保證

dynamic_cast:這種其實也是不被推薦使用的,更多使用static_cast,dynamic本身只能用於存在虛函數的父子關係的強制類型轉換,對於指針,轉換失敗則返回nullptr,對於引用,轉換失敗會拋出異常。

const_cast:對於未定義const版本的成員函數,我們通常需要使用const_cast來去除const引用對象的const,完成函數調用。另外一種使用方式,結合static_cast,可以在非const版本的成員函數內添加const,調用完const版本的成員函數後,再使用const_cast去除const限定。

static_cast:完成基礎數據類型;同一個繼承體系中類型的轉換;任意類型與空指針類型void* 之間的轉換。

1.21形參與實參的區別

  1. 形參變量只有在被調用時才分配內存單元,在調用結束時, 即刻釋放所分配的內存單元。因此,形參只有在函數內部有效。 函數調用結束返回主調函數後則不能再使用該形參變量。

  2. 實參可以是常量、變量、表達式、函數等, 無論實參是何種類型的量,在進行函數調用時,它們都必須具有確定的值, 以便把這些值傳送給形參。 因此應預先用賦值,輸入等辦法使實參獲得確定值,會產生一個臨時變量。

  3. 實參和形參在數量上,類型上,順序上應嚴格一致, 否則會發生“類型不匹配”的錯誤。

  4. 函數調用中發生的數據傳送是單向的。 即只能把實參的值傳送給形參,而不能把形參的值反向地傳送給實參。 因此在函數調用過程中,形參的值發生改變,而實參中的值不會變化。

  5. 當形參和實參不是指針類型時,在該函數運行時,形參和實參是不同的變量,他們在內存中位於不同的位置,形參將實參的內容複製一份,在該函數運行結束的時候形參被釋放,而實參內容不會改變。

函數傳參

  1. 值傳遞:有一個形參向函數所屬的棧拷貝數據的過程,如果值傳遞的對象是類對象或是大的結構體對象,將耗費一定的時間和空間。(傳值)

  2. 指針傳遞:同樣有一個形參向函數所屬的棧拷貝數據的過程,但拷貝的數據是一個固定爲4字節的地址。(傳值,傳遞的是地址值)

  3. 引用傳遞:同樣有上述的數據拷貝過程,但其是針對地址的,相當於爲該數據所在的地址起了一個別名。(傳地址)

  4. 效率上講,指針傳遞和引用傳遞比值傳遞效率高。一般主張使用引用傳遞,代碼邏輯上更加緊湊、清晰。

1.22const

const 使用 1. 修飾變量,說明該變量不可以被改變; 2. 修飾指針,分爲指向常量的指針和指針常量; 3. 常量引用,經常用於形參類型,即避免了拷貝,又避免了函數對值的修改; 4. 修飾成員函數,說明該成員函數內不能修改成員變量。
// 類
class A
{
private:
    const int a;                // 常對象成員,只能在初始化列表賦值

public:
    // 構造函數
    A() { };
    A(int x) : a(x) { };        // 初始化列表

    // const可用於對重載函數的區分
    int getValue();             // 普通成員函數
    int getValue() const;       // 常成員函數,不得修改類中的任何數據成員的值
};

void function()
{
    // 對象
    A b;                        // 普通對象,可以調用全部成員函數
    const A a;                  // 常對象,只能調用常成員函數、更新常成員變量
    const A *p = &a;            // 常指針
    const A &q = a;             // 常引用

    // 指針
    char greeting[] = "Hello";
    char* p1 = greeting;                // 指針變量,指向字符數組變量
    const char* p2 = greeting;          // 指針變量,指向字符數組常量
    char* const p3 = greeting;          // 常指針,指向字符數組變量
    const char* const p4 = greeting;    // 常指針,指向字符數組常量
}

// 函數
void function1(const int Var);           // 傳遞過來的參數在函數內不可變
void function2(const char* Var);         // 參數指針所指內容爲常量
void function3(char* const Var);         // 參數指針爲常指針
void function4(const int& Var);          // 引用參數在函數內爲常量

// 函數返回值
const int function5();      // 返回一個常數
const int* function6();     // 返回一個指向常量的指針變量,使用:const int *p = function6();
int* const function7();     // 返回一個指向變量的常指針,使用:int* const p = function7();

  1. 阻止一個變量被改變,可以使用const關鍵字。在定義該const變量時,通常需要對它進行初始化,因爲以後就沒有機會再去改變它了;

  2. 對指針來說,可以指定指針本身爲const,也可以指定指針所指的數據爲const,或二者同時指定爲const;

  3. 在一個函數聲明中,const可以修飾形參,表明它是一個輸入參數,在函數內部不能改變其值;

  4. 對於類的成員函數,若指定其爲const類型,則表明其是一個常函數,不能修改類的成員變量,類的常對象只能訪問類的常成員函數;

  5. 對於類的成員函數,有時候必須指定其返回值爲const類型,以使得其返回值不爲“左值”。

  6. const成員函數可以訪問非const對象的非const數據成員、const數據成員,也可以訪問const對象內的所有數據成員;

  7. 非const成員函數可以訪問非const對象的非const數據成員、const數據成員,但不可以訪問const對象的任意數據成員;

  8. 一個沒有明確聲明爲const的成員函數被看作是將要修改對象中數據成員的函數,而且編譯器不允許它爲一個const對象所調用。因此const對象只能調用const成員函數。

  9. const類型變量可以通過類型轉換符const_cast將const類型轉換爲非const類型;

  10. const類型變量必須定義的時候進行初始化,因此也導致如果類的成員變量有const類型的變量,那麼該變量必須在類的初始化列表中進行初始化;

  11. 對於函數值傳遞的情況,因爲參數傳遞是通過複製實參創建一個臨時變量傳遞進函數的,函數內只能改變臨時變量,但無法改變實參。則這個時候無論加不加const對實參不會產生任何影響。但是在引用或指針傳遞函數調用中,因爲傳進去的是一個引用或指針,這樣函數內部可以改變引用或指針所指向的變量,這時const 纔是實實在在地保護了實參所指向的變量。因爲在編譯階段編譯器對調用函數的選擇是根據實參進行的,所以,只有引用傳遞和指針傳遞可以用是否加const來重載。一個擁有頂層const的形參無法和另一個沒有頂層const的形參區分開來。

const成員函數的理解和應用

① const Stock & Stock::topval (②const Stock & s) ③const

①處const:確保返回的Stock對象在以後的使用中不能被修改 。

②處const:確保此方法不修改傳遞的參數 S。

③處const:保證此方法不修改調用它的對象,const對象只能調用const成員函數,不能調用非const函數。

mutable

  1. 如果需要在const成員方法中修改一個成員變量的值,那麼需要將這個成員變量修飾爲mutable。即用mutable修飾的成員變量不受const成員方法的限制;

  2. 可以認爲mutable的變量是類的輔助狀態,但是隻是起到類的一些方面表述的功能,修改他的內容我們可以認爲對象的狀態本身並沒有改變的。實際上由於const_cast的存在,這個概念很多時候用處不是很到了。

1.23this 指針

  1. this 指針是一個隱含於每一個非靜態成員函數中的特殊指針。它指向正在被該成員函數操作的那個對象。
  2. 當對一個對象調用成員函數時,編譯程序先將對象的地址賦給 this 指針,然後調用成員函數,每次成員函數存取數據成員時,由隱含使用 this 指針。
  3. 當一個成員函數被調用時,自動向它傳遞一個隱含的參數,該參數是一個指向這個成員函數所在的對象的指針。
  4. this 指針被隱含地聲明爲: ClassName *const this,這意味着不能給 this 指針賦值;在 ClassName 類的 const 成員函數中,this 指針的類型爲:const ClassName* const,這說明不能對 this 指針所指向的這種對象是不可修改的(即不能對這種對象的數據成員進行賦值操作);
  5. this 並不是一個常規變量,而是個右值,所以不能取得 this 的地址(不能 &this)。
  6. 在以下場景中,經常需要顯式引用 this 指針:
    1. 爲實現對象的鏈式引用;
    2. 爲避免對同一對象進行賦值操作;
    3. 在實現一些數據結構時,如 list

1.24C++ 中 struct 和 class

總的來說,struct 更適合看成是一個數據結構的實現體,class 更適合看成是一個對象的實現體。

區別

  • 最本質的一個區別就是默認的訪問控制
    1. 默認的繼承訪問權限。struct 是 public 的,class 是 private 的。
    2. struct 作爲數據結構的實現體,它默認的數據訪問控制是 public 的,而 class 作爲對象的實現體,它默認的成員變量訪問控制是 private 的。

1.25union 聯合

聯合(union)是一種節省空間的特殊的類,一個 union 可以有多個數據成員,但是在任意時刻只有一個數據成員可以有值。當某個成員被賦值後其他成員變爲未定義狀態。聯合有如下特點:

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

union UnionTest {
    UnionTest() : i(10) {};
    int i;
    double d;
};

static union {
    int i;
    double d;
};

int main() {
    UnionTest u;

    union {
        int i;
        double d;
    };

    std::cout << u.i << std::endl;  // 輸出 UnionTest 聯合的 10

    ::i = 20;
    std::cout << ::i << std::endl;  // 輸出全局靜態匿名聯合的 20

    i = 30;
    std::cout << i << std::endl;    // 輸出局部匿名聯合的 30

    return 0;
}

1.26C 實現 C++ 類

C 語言實現封裝、繼承和多態

1.27explicit(顯式)構造函數

explicit 修飾的構造函數可用來防止隱式轉換

explicit 使用
class Test1
{
public:
    Test1(int n)            // 普通構造函數
    {
        num=n;
    }
private:
    int num;
};

class Test2
{
public:
    explicit Test2(int n)   // explicit(顯式)構造函數
    {
        num=n;
    }
private:
    int num;
};

int main()
{
    Test1 t1=12;            // 隱式調用其構造函數,成功
    Test2 t2=12;            // 編譯錯誤,不能隱式調用其構造函數
    Test2 t2(12);           // 顯式調用成功
    return 0;
}

1.28 friend 友元類和友元函數

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

1.29 using

using 聲明

一條 using 聲明 語句一次只引入命名空間的一個成員。它使得我們可以清楚知道程序中所引用的到底是哪個名字。如:

using namespace_name::name;

構造函數的 using 聲明【C++11】

在 C++11 中,派生類能夠重用其直接基類定義的構造函數。

class Derived : Base {
public:
    using Base::Base;
    /* ... */
};

如上 using 聲明,對於基類的每個構造函數,編譯器都生成一個與之對應(形參列表完全相同)的派生類構造函數。生成如下類型構造函數:

derived(parms) : base(args) { }

using 指示

using 指示 使得某個特定命名空間中所有名字都可見,這樣我們就無需再爲它們添加任何前綴限定符了。如:

using namespace_name name;

儘量少使用 using 指示 污染命名空間

一般說來,使用 using 命令比使用 using 編譯命令更安全,這是由於它只導入了制定的名稱。如果該名稱與局部名稱發生衝突,編譯器將發出指示。using編譯命令導入所有的名稱,包括可能並不需要的名稱。如果與局部名稱發生衝突,則局部名稱將覆蓋名稱空間版本,而編譯器並不會發出警告。另外,名稱空間的開放性意味着名稱空間的名稱可能分散在多個地方,這使得難以準確知道添加了哪些名稱。

using 使用 儘量少使用 `using 指示`
using namespace std;

應該多使用 using 聲明

int x;
std::cin >> x ;
std::cout << x << std::endl;

或者

using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;

1.30:: 範圍解析運算符

分類

  1. 全局作用域符(::name):用於類型名稱(類、類成員、成員函數、變量等)前,表示作用域爲全局命名空間
  2. 類作用域符(class::name):用於表示指定類型的作用域範圍是具體某個類的
  3. 命名空間作用域符(namespace::name):用於表示指定類型的作用域範圍是具體某個命名空間的
:: 使用
int count = 0;        // 全局(::)的 count

class A {
public:
    static int count; // 類 A 的 count(A::count)
};

int main() {
    ::count = 1;      // 設置全局的 count 的值爲 1

    A::count = 2;     // 設置類 A 的 count 爲 2

    int count = 0;    // 局部的 count
    count = 3;        // 設置局部的 count 的值爲 3

    return 0;
}

1.31成員初始化列表

好處

  • 更高效:少了一次調用默認構造函數的過程。
  • 有些場合必須要用初始化列表:
    1. 常量成員,因爲常量只能初始化不能賦值,所以必須放在初始化列表裏面
    2. 引用類型,引用必須在定義的時候初始化,並且不能重新賦值,所以也要寫在初始化列表裏面
    3. 沒有默認構造函數的類類型,因爲使用初始化列表可以不必調用默認構造函數來初始化,而是直接調用拷貝構造函數初始化。

1.32 initializer_list 列表初始化【C++11】

用花括號初始化器列表列表初始化一個對象,其中對應構造函數接受一個 std::initializer_list 參數.

initializer_list 使用
#include <iostream>
#include <vector>
#include <initializer_list>
 
template <class T>
struct S {
    std::vector<T> v;
    S(std::initializer_list<T> l) : v(l) {
         std::cout << "constructed with a " << l.size() << "-element list\n";
    }
    void append(std::initializer_list<T> l) {
        v.insert(v.end(), l.begin(), l.end());
    }
    std::pair<const T*, std::size_t> c_arr() const {
        return {&v[0], v.size()};  // 在 return 語句中複製列表初始化
                                   // 這不使用 std::initializer_list
    }
};
 
template <typename T>
void templated_fn(T) {}
 
int main()
{
    S<int> s = {1, 2, 3, 4, 5}; // 複製初始化
    s.append({6, 7, 8});      // 函數調用中的列表初始化
 
    std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";
 
    for (auto n : s.v)
        std::cout << n << ' ';
    std::cout << '\n';
 
    std::cout << "Range-for over brace-init-list: \n";
 
    for (int x : {-1, -2, -3}) // auto 的規則令此帶範圍 for 工作
        std::cout << x << ' ';
    std::cout << '\n';
 
    auto al = {10, 11, 12};   // auto 的特殊規則
 
    std::cout << "The list bound to auto has size() = " << al.size() << '\n';
 
//    templated_fn({1, 2, 3}); // 編譯錯誤!“ {1, 2, 3} ”不是表達式,
                             // 它無類型,故 T 無法推導
    templated_fn<std::initializer_list<int>>({1, 2, 3}); // OK
    templated_fn<std::vector<int>>({1, 2, 3});           // 也 OK
}

2.面向對象

2.1面向對象三大特徵

封裝

  • 把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
  • 關鍵字:public, protected, friendly, private。不寫默認爲 friendly。
關鍵字 當前類 包內 子孫類 包外
public
protected ×
friendly × ×
private × × ×

繼承

  • 基類(父類)——> 派生類(子類)
  1. 類與類之間的關係

has-A包含關係,用以描述一個類由多個部件類構成,實現has-A關係用類的成員屬性表示,即一個類的成員屬性是另一個已經定義好的類;

use-A,一個類使用另一個類,通過類之間的成員函數相互聯繫,定義友元或者通過傳遞參數的方式來實現;

is-A,繼承關係,關係具有傳遞性;

  1. 繼承的相關概念

所謂的繼承就是一個類繼承了另一個類的屬性和方法,這個新的類包含了上一個類的屬性和方法,被稱爲子類或者派生類,被繼承的類稱爲父類或者基類;

  1. 繼承的特點

子類擁有父類的所有屬性和方法,子類可以擁有父類沒有的屬性和方法,子類對象可以當做父類對象使用;

  1. 繼承中的訪問控制

public、protected、private

  1. 繼承中的構造和析構函數

  2. 繼承中的兼容性原則

多態

  • 多態,即多種狀態,在面嚮對象語言中,接口的多種不同的實現方式即爲多態。
  • C++ 多態有兩種:靜態多態(早綁定)、動態多態(晚綁定)。靜態多態是通過函數重載實現的;動態多態是通過虛函數實現的。
  • 多態是以封裝和繼承爲基礎的。

靜態多態(早綁定)

函數重載

class A
{
public:
    void do(int a);
    void do(int a, int b);
};

動態多態(晚綁定)

  • 虛函數:用 virtual 修飾成員函數,使其成爲虛函數

注意:

  • 普通函數(非類成員函數)不能是虛函數
  • 靜態函數(static)不能是虛函數
  • 構造函數不能是虛函數(因爲在調用構造函數時,虛表指針並沒有在對象的內存空間中,必須要構造函數調用完成後纔會形成虛表指針)
  • 內聯函數不能是表現多態性時的虛函數,解釋見:虛函數(virtual)可以是內聯函數(inline)嗎?
動態多態使用
class Shape                     // 形狀類
{
public:
    virtual double calcArea()
    {
        ...
    }
    virtual ~Shape();
};
class Circle : public Shape     // 圓形類
{
public:
    virtual double calcArea();
    ...
};
class Rect : public Shape       // 矩形類
{
public:
    virtual double calcArea();
    ...
};
int main()
{
    Shape * shape1 = new Circle(4.0);
    Shape * shape2 = new Rect(5.0, 6.0);
    shape1->calcArea();         // 調用圓形類裏面的方法
    shape2->calcArea();         // 調用矩形類裏面的方法
    delete shape1;
    shape1 = nullptr;
    delete shape2;
    shape2 = nullptr;
    return 0;
}

介紹一下C++裏面的多態?

(1)靜態多態(重載,模板)

是在編譯的時候,就確定調用函數的類型。

(2)動態多態(覆蓋,虛函數實現)

在運行的時候,才確定調用的是哪個函數,動態綁定。運行基類指針指向派生類的對象,並調用派生類的函數。

虛函數實現原理:虛函數表和虛函數指針。

純虛函數: virtual int fun() = 0;

函數的運行版本由實參決定,在運行時選擇函數的版本,所以動態綁定又稱爲運行時綁定。

當編譯器遇到一個模板定義時,它並不生成代碼。只有當實例化出模板的一個特定版本時,編譯器纔會生成代碼。

類什麼時候會析構?

  1. 對象生命週期結束,被銷燬時;

  2. delete指向對象的指針時,或delete指向對象的基類類型指針,而其基類虛構函數是虛函數時;

  3. 對象i是對象o的成員,o的析構函數被調用時,對象i的析構函數也被調用。

爲什麼友元函數必須在類內部聲明?

  1. 因爲編譯器必須能夠讀取這個結構的聲明以理解這個數據類型的大、行爲等方面的所有規則。有一條規則在任何關係中都很重要,那就是誰可以訪問我的私有部分。

2.2虛析構函數

虛析構函數是爲了解決基類的指針指向派生類對象,並用基類的指針刪除派生類對象。

虛析構函數使用 ```cpp class Shape { public: Shape(); // 構造函數不能是虛函數 virtual double calcArea(); virtual ~Shape(); // 虛析構函數 }; class Circle : public Shape // 圓形類 { public: virtual double calcArea(); ... }; int main() { Shape * shape1 = new Circle(4.0); shape1->calcArea(); delete shape1; // 因爲Shape有虛析構函數,所以delete釋放內存時,先調用子類析構函數,再調用基類析構函數,防止內存泄漏。 shape1 = NULL; return 0; } ```

2.3虛函數與純虛函數

純虛函數

純虛函數是一種特殊的虛函數,在基類中不能對虛函數給出有意義的實現,而把它聲明爲純虛函數,它的實現留給該基類的派生類去做。

virtual int A() = 0;

虛函數與純虛函數區別

CSDN . C++ 中的虛函數、純虛函數區別和聯繫

  • 類裏如果聲明瞭虛函數,這個函數是實現的,哪怕是空實現,它的作用就是爲了能讓這個函數在它的子類裏面可以被覆蓋,這樣的話,這樣編譯器就可以使用後期綁定來達到多態了。純虛函數只是一個接口,是個函數的聲明而已,它要留到子類裏去實現。
  • 虛函數在子類裏面也可以不重載的;但純虛函數必須在子類去實現。
  • 虛函數的類用於 “實作繼承”,繼承接口的同時也繼承了父類的實現。當然大家也可以完成自己的實現。純虛函數關注的是接口的統一性,實現由子類完成。
  • 帶純虛函數的類叫虛基類,這種基類不能直接生成對象,而只有被繼承,並重寫其虛函數後,才能使用。這樣的類也叫抽象類。抽象類和大家口頭常說的虛基類還是有區別的,在 C# 中用 abstract 定義抽象類,而在 C++ 中有抽象類的概念,但是沒有這個關鍵字。抽象類被繼承後,子類可以繼續是抽象類,也可以是普通類,而虛基類,是含有純虛函數的類,它如果被繼承,那麼子類就必須實現虛基類裏面的所有純虛函數,其子類不能是抽象類。

虛函數指針、虛函數表

  • 虛函數指針:在含有虛函數類的對象中,指向虛函數表,在運行時確定。
  • 虛函數表:在程序只讀數據段(.rodata section,見:目標文件存儲結構),存放虛函數指針,如果派生類實現了基類的某個虛函數,則在虛表中覆蓋原本基類的那個虛函數指針,在編譯時根據類的聲明創建。

虛繼承

虛繼承用於解決多繼承條件下的菱形繼承問題(浪費存儲空間、存在二義性)。

底層實現原理與編譯器相關,一般通過虛基類指針虛基類表實現,每個虛繼承的子類都有一個虛基類指針(佔用一個指針的存儲空間,4字節)和虛基類表(不佔用類對象的存儲空間)(需要強調的是,虛基類依舊會在子類裏面存在拷貝,只是僅僅最多存在一份而已,並不是不在子類裏面了);當虛繼承的子類被當做父類繼承時,虛基類指針也會被繼承。

實際上,vbptr 指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;通過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持着公共基類(虛基類)的兩份同樣的拷貝,節省了存儲空間。

虛繼承、虛函數

  • 相同之處:都利用了虛指針(均佔用類的存儲空間)和虛表(均不佔用類的存儲空間)
  • 不同之處:
    • 虛繼承
      • 虛基類依舊存在繼承類中,只佔用存儲空間
      • 虛基類表存儲的是虛基類相對直接繼承類的偏移
    • 虛函數
      • 虛函數不佔用存儲空間
      • 虛函數表存儲的是虛函數地址

模板類、成員模板、虛函數

  • 模板類中可以使用虛函數
  • 一個類(無論是普通類還是類模板)的成員模板(本身是模板的成員函數)不能是虛函數

抽象類、接口類、聚合類

  • 抽象類:含有純虛函數的類
  • 接口類:僅含有純虛函數的抽象類
  • 聚合類:用戶可以直接訪問其成員,並且具有特殊的初始化語法形式。滿足如下特點:
    • 所有成員都是 public
    • 沒有有定於任何構造函數
    • 沒有類內初始化
    • 沒有基類,也沒有 virtual 函數

虛函數可以聲明爲inline嗎?

  1. 虛函數用於實現運行時的多態,或者稱爲晚綁定或動態綁定。而內聯函數用於提高效率。內聯函數的原理是,在編譯期間,對調用內聯函數的地方的代碼替換成函數代碼。內聯函數對於程序中需要頻繁使用和調用的小函數非常有用。

  2. 虛函數要求在運行時進行類型確定,而內斂函數要求在編譯期完成相關的函數替換;

構造函數爲什麼不能爲虛函數?析構函數爲什麼要虛函數?

1.從存儲空間角度,虛函數相應一個指向vtable虛函數表的指針,這大家都知道,但是這個指向vtable的指針事實上是存儲在對象的內存空間的。問題出來了,假設構造函數是虛的,就須要通過 vtable來調用,但是對象還沒有實例化,也就是內存空間還沒有,怎麼找vtable呢?所以構造函數不能是虛函數。

2.從使用角度,虛函數主要用於在信息不全的情況下,能使重載的函數得到相應的調用。構造函數本身就是要初始化實例,那使用虛函數也沒有實際意義呀。所以構造函數沒有必要是虛函數。虛函數的作用在於通過父類的指針或者引用來調用它的時候可以變成調用子類的那個成員函數。而構造函數是在創建對象時自己主動調用的,不可能通過父類的指針或者引用去調用,因此也就規定構造函數不能是虛函數。

3.構造函數不須要是虛函數,也不同意是虛函數,由於創建一個對象時我們總是要明白指定對象的類型,雖然我們可能通過實驗室的基類的指針或引用去訪問它但析構卻不一定,我們往往通過基類的指針來銷燬對象。這時候假設析構函數不是虛函數,就不能正確識別對象類型從而不能正確調用析構函數。

4.從實現上看,vbtl在構造函數調用後才建立,因而構造函數不可能成爲虛函數從實際含義上看,在調用構造函數時還不能確定對象的真實類型(由於子類會調父類的構造函數);並且構造函數的作用是提供初始化,在對象生命期僅僅運行一次,不是對象的動態行爲,也沒有必要成爲虛函數。

5.當一個構造函數被調用時,它做的首要的事情之中的一個是初始化它的VPTR。因此,它僅僅能知道它是“當前”類的,而全然忽視這個對象後面是否還有繼承者。當編譯器爲這個構造函數產生代碼時,它是爲這個類的構造函數產生代碼——既不是爲基類,也不是爲它的派生類(由於類不知道誰繼承它)。所以它使用的VPTR必須是對於這個類的VTABLE。並且,僅僅要它是最後的構造函數調用,那麼在這個對象的生命期內,VPTR將保持被初始化爲指向這個VTABLE, 但假設接着另一個更晚派生的構造函數被調用,這個構造函數又將設置VPTR指向它的 VTABLE,等.直到最後的構造函數結束。VPTR的狀態是由被最後調用的構造函數確定的。這就是爲什麼構造函數調用是從基類到更加派生類順序的還有一個理由。可是,當這一系列構造函數調用正發生時,每一個構造函數都已經設置VPTR指向它自己的VTABLE。假設函數調用使用虛機制,它將僅僅產生通過它自己的VTABLE的調用,而不是最後的VTABLE(全部構造函數被調用後纔會有最後的VTABLE)。

因爲構造函數本來就是爲了明確初始化對象成員才產生的,然而virtual function主要是爲了再不完全瞭解細節的情況下也能正確處理對象。另外,virtual函數是在不同類型的對象產生不同的動作,現在對象還沒有產生,如何使用virtual函數來完成你想完成的動作。

直接的講,C++中基類採用virtual虛析構函數是爲了防止內存泄漏。具體地說,如果派生類中申請了內存空間,並在其析構函數中對這些內存空間進行釋放。假設基類中採用的是非虛析構函數,當刪除基類指針指向的派生類對象時就不會觸發動態綁定,因而只會調用基類的析構函數,而不會調用派生類的析構函數。那麼在這種情況下,派生類中申請的空間就得不到釋放從而產生內存泄漏。所以,爲了防止這種情況的發生,C++中基類的析構函數應採用virtual虛析構函數。

抽象基類爲什麼不能創建對象?

抽象類是一種特殊的類,它是爲了抽象和設計的目的爲建立的,它處於繼承層次結構的較上層。

(1)抽象類的定義:
稱帶有純虛函數的類爲抽象類。

(2)抽象類的作用:
抽象類的主要作用是將有關的操作作爲結果接口組織在一個繼承層次結構中,由它來爲派生類提供一個公共的根,派生類將具體實現在其基類中作爲接口的操作。所以派生類實際上刻畫了一組子類的操作接口的通用語義,這些語義也傳給子類,子類可以具體實現這些語義,也可以再將這些語義傳給自己的子類。

(3)使用抽象類時注意:
抽象類只能作爲基類來使用,其純虛函數的實現由派生類給出。如果派生類中沒有重新定義純虛函數,而只是繼承基類的純虛函數,則這個派生類仍然還是一個抽象類。如果派生類中給出了基類純虛函數的實現,則該派生類就不再是抽象類了,它是一個可以建立對象的具體的類。

抽象類是不能定義對象的。一個純虛函數不需要(但是可以)被定義。

一、純虛函數定義
純虛函數是一種特殊的虛函數,它的一般格式如下:
 class <類名>
 {
 virtual <類型><函數名>(<參數表>)=0;
 …
 };
 在許多情況下,在基類中不能對虛函數給出有意義的實現,而把它聲明爲純虛函數,它的實現留給該基類的派生類去做。這就是純虛函數的作用。
 純虛函數可以讓類先具有一個操作名稱,而沒有操作內容,讓派生類在繼承時再去具體地給出定義。凡是含有純虛函數的類叫做抽象類。這種類不能聲明對象,只是作爲基類爲派生類服務。除非在派生類中完全實現基類中所有的的純虛函數,否則,派生類也變成了抽象類,不能實例化對象。

二、純虛函數引入原因
1、爲了方便使用多態特性,我們常常需要在基類中定義虛擬函數。
2、在很多情況下,基類本身生成對象是不合情理的。例如,動物作爲一個基類可以派生出老虎、孔 雀等子類,但動物本身生成對象明顯不合常理。

爲了解決上述問題,引入了純虛函數的概念,將函數定義爲純虛函數(方法:virtual ReturnType Function()= 0;)。若要使派生類爲非抽象類,則編譯器要求在派生類中,必須對純虛函數予以重載以實現多態性。同時含有純虛函數的類稱爲抽象類,它不能生成對象。這樣就很好地解決了上述兩個問題。

例如,繪畫程序中,shape作爲一個基類可以派生出圓形、矩形、正方形、梯形等, 如果我要求面積總和的話,那麼會可以使用一個 shape * 的數組,只要依次調用派生類的area()函數了。如果不用接口就沒法定義成數組,因爲既可以是circle ,也可以是square ,而且以後還可能加上rectangle,等等.

三、相似概念
1、多態性

指相同對象收到不同消息或不同對象收到相同消息時產生不同的實現動作。C++支持兩種多態性:編譯時多態性,運行時多態性。
a.編譯時多態性:通過重載函數實現
b.運行時多態性:通過虛函數實現。
2、虛函數
虛函數是在基類中被聲明爲virtual,並在派生類中重新定義的成員函數,可實現成員函數的動態重載。
3、抽象類
包含純虛函數的類稱爲抽象類。由於抽象類包含了沒有定義的純虛函數,所以不能定義抽象類的對象。

2.4虛函數和多態

多態的實現主要分爲靜態多態和動態多態,靜態多態主要是重載,在編譯的時候就已經確定;動態多態是用虛函數機制實現的,在運行期間動態綁定。舉個例子:一個父類類型的指針指向一個子類對象時候,使用父類的指針去調用子類中重寫了的父類中的虛函數的時候,會調用子類重寫過後的函數,在父類中聲明爲加了virtual關鍵字的函數,在子類中重寫時候不需要加virtual也是虛函數。

虛函數的實現:在有虛函數的類中,類的最開始部分是一個虛函數表的指針,這個指針指向一個虛函數表,表中放了虛函數的地址,實際的虛函數在代碼段(.text)中。當子類繼承了父類的時候也會繼承其虛函數表,當子類重寫父類中虛函數時候,會將其繼承到的虛函數表中的地址替換爲重新寫的函數地址。使用了虛函數,會增加訪問內存開銷,降低效率。

2.5靜態函數和虛函數的區別

靜態函數在編譯的時候就已經確定運行時機,虛函數在運行的時候動態綁定。虛函數因爲用了虛函數表機制,調用的時候會增加一次內存開銷。

2.6單基繼承與多基繼承

什麼時候使用單基繼承

首先我們來了解什麼是單基繼承,單基繼承就是一個基類單方向的派生出另一個類,比如:基類A,派生類B,A—>B。所以當我們現在所寫的類中需要完整繼承一個類並且此類已經足夠完成我們代碼所需,就可以用單基,且單基是最常見的繼承方式了。當構造時先創建基類A,再創建B,析構相反,先析構B再析構A。

什麼時候使用多基繼承

多基繼承是指一個類同時繼承於兩個及以上的類,比如:有一個類A,有一個類B,現在創建一個類C,用C同時繼承A和B,這樣寫:class C:pubic A,public B 當構造時先創建基類A,再創建B,最後創建C,這裏顧慮是按照你寫的繼承順序,此處我是publicA,publicB;析構相反,C->B->A。

2.7繼承和組合的優缺點

一:繼承

繼承是Is a 的關係,比如說Student繼承Person,則說明Student is a Person。繼承的優點是子類可以重寫父類的方法來方便地實現對父類的擴展。

繼承的缺點有以下幾點:

①:父類的內部細節對子類是可見的。

②:子類從父類繼承的方法在編譯時就確定下來了,所以無法在運行期間改變從父類繼承的方法的行爲。

③:如果對父類的方法做了修改的話(比如增加了一個參數),則子類的方法必須做出相應的修改。所以說子類與父類是一種高耦合,違背了面向對象思想。

二:組合

組合也就是設計類的時候把要組合的類的對象加入到該類中作爲自己的成員變量。

組合的優點:

①:當前對象只能通過所包含的那個對象去調用其方法,所以所包含的對象的內部細節對當前對象時不可見的。

②:當前對象與包含的對象是一個低耦合關係,如果修改包含對象的類中代碼不需要修改當前對象類的代碼。

③:當前對象可以在運行時動態的綁定所包含的對象。可以通過set方法給所包含對象賦值。

組合的缺點:①:容易產生過多的對象。②:爲了能組合多個對象,必須仔細對接口進行定義。

2.8重載和覆蓋

重載:兩個函數名相同,但是參數列表不同(個數,類型),返回值類型沒有要求,在同一作用域中。

重寫:子類繼承了父類,父類中的函數是虛函數,在子類中重新定義了這個虛函數,這種情況是重寫。

2.9析構函數

析構函數的作用?

析構函數與構造函數對應,當對象結束其生命週期,如對象所在的函數已調用完畢時,系統會自動執行析構函數。

析構函數名也應與類名相同,只是在函數名前面加一個位取反符~,例如 ~stud( ),以區別於構造函數。它不能帶任何參數,也沒有返回值(包括void類型)。只能有一個析構函數,不能重載。

如果用戶沒有編寫析構函數,編譯系統會自動生成一個缺省的析構函數(即使自定義了析構函數,編譯器也總是會爲我們合成一個析構函數,並且如果自定義了析構函數,編譯器在執行時會先調用自定義的析構函數再調用合成的析構函數),它也不進行任何操作。所以許多簡單的類中沒有用顯式的析構函數。

如果一個類中有指針,且在使用的過程中動態的申請了內存,那麼最好顯示構造析構函數在銷燬類之前,釋放掉申請的內存空間,避免內存泄漏。

類析構順序:1)派生類本身的析構函數;2)對象成員析構函數;3)基類析構函數。

爲什麼析構函數必須是虛函數?爲什麼C++默認的析構函數不是虛函數?

將可能會被繼承的父類的析構函數設置爲虛函數,可以保證當我們new一個子類,然後使用基類指針指向該子類對象,釋放基類指針時可以釋放掉子類的空間,防止內存泄漏。

C++默認的析構函數不是虛函數是因爲虛函數需要額外的虛函數表和虛表指針,佔用額外的內存。而對於不會被繼承的類來說,其析構函數如果是虛函數,就會浪費內存。因此C++默認的析構函數不是虛函數,而是隻有當需要當作父類時,設置爲虛函數。

析構函數的作用,如何起作用?

  1. 構造函數只是起初始化值的作用,但實例化一個對象的時候,可以通過實例去傳遞參數,從主函數傳遞到其他的函數裏面,這樣就使其他的函數裏面有值了。規則,只要你一實例化對象,系統自動回調用一個構造函數,就是你不寫,編譯器也自動調用一次。

  2. 析構函數與構造函數的作用相反,用於撤銷對象的一些特殊任務處理,可以是釋放對象分配的內存空間;特點:析構函數與構造函數同名,但該函數前面加~。 析構函數沒有參數,也沒有返回值,而且不能重載,在一個類中只能有一個析構函數。 當撤銷對象時,編譯器也會自動調用析構函數。 每一個類必須有一個析構函數,用戶可以自定義析構函數,也可以是編譯器自動生成默認的析構函數。一般析構函數定義爲類的公有成員。

構造函數和析構函數可以調用虛函數嗎,爲什麼

  1. 在C++中,提倡不在構造函數和析構函數中調用虛函數;

  2. 構造函數和析構函數調用虛函數時都不使用動態聯編,如果在構造函數或析構函數中調用虛函數,則運行的是爲構造函數或析構函數自身類型定義的版本;

  3. 因爲父類對象會在子類之前進行構造,此時子類部分的數據成員還未初始化,因此調用子類的虛函數時不安全的,故而C++不會進行動態聯編;

  4. 析構函數是用來銷燬一個對象的,在銷燬一個對象時,先調用子類的析構函數,然後再調用基類的析構函數。所以在調用基類的析構函數時,派生類對象的數據成員已經銷燬,這個時候再調用子類的虛函數沒有任何意義。

=構造函數的執行順序?析構函數的執行順序?構造函數內部幹了啥?拷貝構造幹了啥?==

  1. 構造函數順序

① 基類構造函數。如果有多個基類,則構造函數的調用順序是某類在類派生表中出現的順序,而不是它們在成員初始化表中的順序。

② 成員類對象構造函數。如果有多個成員類對象則構造函數的調用順序是對象在類中被聲明的順序,而不是它們出現在成員初始化表中的順序。

③ 派生類構造函數。

  1. 析構函數順序

① 調用派生類的析構函數;

② 調用成員類對象的析構函數;

③ 調用基類的析構函數。

虛析構函數的作用,父類的析構函數是否要設置爲虛函數?

  1. C++中基類採用virtual虛析構函數是爲了防止內存泄漏。具體地說,如果派生類中申請了內存空間,並在其析構函數中對這些內存空間進行釋放。假設基類中採用的是非虛析構函數,當刪除基類指針指向的派生類對象時就不會觸發動態綁定,因而只會調用基類的析構函數,而不會調用派生類的析構函數。那麼在這種情況下,派生類中申請的空間就得不到釋放從而產生內存泄漏。所以,爲了防止這種情況的發生,C++中基類的析構函數應採用virtual虛析構函數。

  2. 純虛析構函數一定得定義,因爲每一個派生類析構函數會被編譯器加以擴張,以靜態調用的方式調用其每一個虛基類以及上一層基類的析構函數。因此,缺乏任何一個基類析構函數的定義,就會導致鏈接失敗。因此,最好不要把虛析構函數定義爲純虛析構函數。

構造函數析構函數可以調用虛函數嗎?

  1. 在構造函數和析構函數中最好不要調用虛函數;

  2. 構造函數或者析構函數調用虛函數並不會發揮虛函數動態綁定的特性,跟普通函數沒區別;

  3. 即使構造函數或者析構函數如果能成功調用虛函數, 程序的運行結果也是不可控的。

構造函數析構函數可否拋出異常

  1. C++只會析構已經完成的對象,對象只有在其構造函數執行完畢纔算是完全構造妥當。在構造函數中發生異常,控制權轉出構造函數之外。因此,在對象b的構造函數中發生異常,對象b的析構函數不會被調用。因此會造成內存泄漏。

  2. 用auto_ptr對象來取代指針類成員,便對構造函數做了強化,免除了拋出異常時發生資源泄漏的危機,不再需要在析構函數中手動釋放資源;

  3. 如果控制權基於異常的因素離開析構函數,而此時正有另一個異常處於作用狀態,C++會調用terminate函數讓程序結束;

  4. 如果異常從析構函數拋出,而且沒有在當地進行捕捉,那個析構函數便是執行不全的。如果析構函數執行不全,就是沒有完成他應該執行的每一件事情。

2.10強制類型轉換運算符

MSDN . 強制轉換運算符

static_cast

  • 用於非多態類型的轉換
  • 不執行運行時類型檢查(轉換安全性不如 dynamic_cast)
  • 通常用於轉換數值數據類型(如 float -> int)
  • 可以在整個類層次結構中移動指針,子類轉化爲父類安全(向上轉換),父類轉化爲子類不安全(因爲子類可能有不在父類的字段或方法)

向上轉換是一種隱式轉換。

dynamic_cast

  • 用於多態類型的轉換
  • 執行行運行時類型檢查
  • 只適用於指針或引用
  • 對不明確的指針的轉換將失敗(返回 nullptr),但不引發異常
  • 可以在整個類層次結構中移動指針,包括向上轉換、向下轉換

const_cast

  • 用於刪除 const、volatile 和 __unaligned 特性(如將 const int 類型轉換爲 int 類型 )

reinterpret_cast

  • 用於位的簡單重新解釋
  • 濫用 reinterpret_cast 運算符可能很容易帶來風險。 除非所需轉換本身是低級別的,否則應使用其他強制轉換運算符之一。
  • 允許將任何指針轉換爲任何其他指針類型(如 char*int*One_class*Unrelated_class* 之類的轉換,但其本身並不安全)
  • 也允許將任何整數類型轉換爲任何指針類型以及反向轉換。
  • reinterpret_cast 運算符不能丟掉 const、volatile 或 __unaligned 特性。
  • reinterpret_cast 的一個實際用途是在哈希函數中,即,通過讓兩個不同的值幾乎不以相同的索引結尾的方式將值映射到索引。

bad_cast

  • 由於強制轉換爲引用類型失敗,dynamic_cast 運算符引發 bad_cast 異常。
bad_cast 使用
try {  
    Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);   
}  
catch (bad_cast b) {  
    cout << "Caught: " << b.what();  
} 

2.11運行時類型信息 (RTTI)

dynamic_cast

  • 用於多態類型的轉換

typeid

  • typeid 運算符允許在運行時確定對象的類型
  • type_id 返回一個 type_info 對象的引用
  • 如果想通過基類的指針獲得派生類的數據類型,基類必須帶有虛函數
  • 只能獲取對象的實際類型

type_info

  • type_info 類描述編譯器在程序中生成的類型信息。 此類的對象可以有效存儲指向類型的名稱的指針。 type_info 類還可存儲適合比較兩個類型是否相等或比較其排列順序的編碼值。 類型的編碼規則和排列順序是未指定的,並且可能因程序而異。
  • 頭文件:typeinfo
typeid、type_info 使用
class Flyable                       // 能飛的
{
public:
    virtual void takeoff() = 0;     // 起飛
    virtual void land() = 0;        // 降落
};
class Bird : public Flyable         // 鳥
{
public:
    void foraging() {...}           // 覓食
    virtual void takeoff() {...}
    virtual void land() {...}
};
class Plane : public Flyable        // 飛機
{
public:
    void carry() {...}              // 運輸
    virtual void take off() {...}
    virtual void land() {...}
};

class type_info
{
public:
    const char* name() const;
    bool operator == (const type_info & rhs) const;
    bool operator != (const type_info & rhs) const;
    int before(const type_info & rhs) const;
    virtual ~type_info();
private:
    ...
};

class doSomething(Flyable *obj)                 // 做些事情
{
    obj->takeoff();

    cout << typeid(*obj).name() << endl;        // 輸出傳入對象類型("class Bird" or "class Plane")

    if(typeid(*obj) == typeid(Bird))            // 判斷對象類型
    {
        Bird *bird = dynamic_cast<Bird *>(obj); // 對象轉化
        bird->foraging();
    }

    obj->land();
};

2.12組合

  1. 一個類裏面的數據成員是另一個類的對象,即內嵌其他類的對象作爲自己的成員;創建組合類的對象:首先創建各個內嵌對象,難點在於構造函數的設計。創建對象時既要對基本類型的成員進行初始化,又要對內嵌對象進行初始化。

  2. 創建組合類對象,構造函數的執行順序:先調用內嵌對象的構造函數,然後按照內嵌對象成員在組合類中的定義順序,與組合類構造函數的初始化列表順序無關。然後執行組合類構造函數的函數體,析構函數調用順序相反。

3.容器和算法

STL 方法含義

3.1map和set的區別

map和set都是C++的關聯容器,其底層實現都是紅黑樹(RB-Tree)。由於 map 和set所開放的各種操作接口,RB-tree 也都提供了,所以幾乎所有的 map 和set的操作行爲,都只是轉調 RB-tree 的操作行爲。

map和set區別在於:

(1)map中的元素是key-value(關鍵字—值)對:關鍵字起到索引的作用,值則表示與索引相關聯的數據;Set與之相對就是關鍵字的簡單集合,set中每個元素只包含一個關鍵字。

(2)set的迭代器是const的,不允許修改元素的值;map允許修改value,但不允許修改key。其原因是因爲map和set是根據關鍵字排序來保證其有序性的,如果允許修改key的話,那麼首先需要刪除該鍵,然後調節平衡,再插入修改後的鍵值,調節平衡,如此一來,嚴重破壞了map和set的結構,導致iterator失效,不知道應該指向改變前的位置,還是指向改變後的位置。所以STL中將set的迭代器設置成const,不允許修改迭代器的值;而map的迭代器則不允許修改key值,允許修改value值。

(3)map支持下標操作,set不支持下標操作。map可以用key做下標,map的下標運算符[ ]將關鍵碼作爲下標去執行查找,如果關鍵碼不存在,則插入一個具有該關鍵碼和mapped_type類型默認值的元素至map中,因此下標運算符[ ]在map應用中需要慎用,const_map不能用,只希望確定某一個關鍵值是否存在而不希望插入元素時也不應該使用,mapped_type類型沒有默認值也不應該使用。如果find能解決需要,儘可能用find。

3.2迭代器

1、迭代器

Iterator(迭代器)模式又稱Cursor(遊標)模式,用於提供一種方法順序訪問一個聚合對象中各個元素, 而又不需暴露該對象的內部表示。或者這樣說可能更容易理解:Iterator模式是運用於聚合對象的一種模式,通過運用該模式,使得我們可以在不知道對象內部表示的情況下,按照一定順序(由iterator提供的方法)訪問聚合對象中的各個元素。

由於Iterator模式的以上特性:與聚合對象耦合,在一定程度上限制了它的廣泛運用,一般僅用於底層聚合支持類,如STL的list、vector、stack等容器類及ostream_iterator等擴展iterator。

2、迭代器和指針的區別

迭代器不是指針,是類模板,表現的像指針。他只是模擬了指針的一些功能,通過重載了指針的一些操作符,->、*、++、–等。迭代器封裝了指針,是一個“可遍歷STL( Standard Template Library)容器內全部或部分元素”的對象, 本質是封裝了原生指針,是指針概念的一種提升(lift),提供了比指針更高級的行爲,相當於一種智能指針,他可以根據不同類型的數據結構來實現不同的++,–等操作。

迭代器返回的是對象引用而不是對象的值,所以cout只能輸出迭代器使用*取值後的值而不能直接輸出其自身。

3、迭代器產生原因*

Iterator類的訪問方式就是把不同集合類的訪問邏輯抽象出來,使得不用暴露集合內部的結構而達到循環遍歷集合的效果。

3.3vector和list的區別

1、概念
1)Vector

連續存儲的容器,動態數組,在堆上分配空間

底層實現:數組

兩倍容量增長:

vector 增加(插入)新元素時,如果未超過當時的容量,則還有剩餘空間,那麼直接添加到最後(插入指定位置),然後調整迭代器。 如果沒有剩餘空間了,則會重新配置原有元素個數的兩倍空間,然後將原空間元素通過複製的方式初始化新空間,再向新空間增加元素,最後析構並釋放原空間,之前的迭代器會失效。

性能:

訪問:O(1)

插入:在最後插入(空間夠):很快

在最後插入(空間不夠):需要內存申請和釋放,以及對之前數據進行拷貝。

在中間插入(空間夠):內存拷貝

在中間插入(空間不夠):需要內存申請和釋放,以及對之前數據進行拷貝。

刪除:在最後刪除:很快

在中間刪除:內存拷貝

適用場景:經常隨機訪問,且不經常對非尾節點進行插入刪除。

2)List

動態鏈表,在堆上分配空間,每插入一個元數都會分配空間,每刪除一個元素都會釋放空間。

底層:雙向鏈表

性能:

訪問:隨機訪問性能很差,只能快速訪問頭尾節點。

插入:很快,一般是常數開銷

刪除:很快,一般是常數開銷

適用場景:經常插入刪除大量數據

2、區別

1)vector底層實現是數組;list是雙向 鏈表。

2)vector支持隨機訪問,list不支持。

3)vector是順序內存,list不是。

4)vector在中間節點進行插入刪除會導致內存拷貝,list不會。

5)vector一次性分配好內存,不夠時才進行2倍擴容;list每次插入新節點都會進行內存申請。

6)vector隨機訪問性能好,插入刪除性能差;list隨機訪問性能差,插入刪除性能好。

3、應用

vector擁有一段連續的內存空間,因此支持隨機訪問,如果需要高效的隨即訪問,而不在乎插入和刪除的效率,使用vector。

list擁有一段不連續的內存空間,如果需要高效的插入和刪除,而不關心隨機訪問,則應使用list。

vector與list的區別與應用?怎麼找某vector或者list的倒數第二個元素

  1. vector數據結構
    vector和數組類似,擁有一段連續的內存空間,並且起始地址不變。因此能高效的進行隨機存取,時間複雜度爲o(1);但因爲內存空間是連續的,所以在進行插入和刪除操作時,會造成內存塊的拷貝,時間複雜度爲o(n)。另外,當數組中內存空間不夠時,會重新申請一塊內存空間並進行內存拷貝。連續存儲結構:vector是可以實現動態增長的對象數組,支持對數組高效率的訪問和在數組尾端的刪除和插入操作,在中間和頭部刪除和插入相對不易,需要挪動大量的數據。它與數組最大的區別就是vector不需程序員自己去考慮容量問題,庫裏面本身已經實現了容量的動態增長,而數組需要程序員手動寫入擴容函數進形擴容。

  2. list數據結構
    list是由雙向鏈表實現的,因此內存空間是不連續的。只能通過指針訪問數據,所以list的隨機存取非常沒有效率,時間複雜度爲o(n);但由於鏈表的特點,能高效地進行插入和刪除。非連續存儲結構:list是一個雙鏈表結構,支持對鏈表的雙向遍歷。每個節點包括三個信息:元素本身,指向前一個元素的節點(prev)和指向下一個元素的節點(next)。因此list可以高效率的對數據元素任意位置進行訪問和插入刪除等操作。由於涉及對額外指針的維護,所以開銷比較大。

區別:

vector的隨機訪問效率高,但在插入和刪除時(不包括尾部)需要挪動數據,不易操作。list的訪問要遍歷整個鏈表,它的隨機訪問效率低。但對數據的插入和刪除操作等都比較方便,改變指針的指向即可。list是單向的,vector是雙向的。vector中的迭代器在使用後就失效了,而list的迭代器在使用之後還可以繼續使用。

int mySize = vec.size();vec.at(mySize -2);

list不提供隨機訪問,所以不能用下標直接訪問到某個位置的元素,要訪問list裏的元素只能遍歷,不過你要是隻需要訪問list的最後N個元素的話,可以用反向迭代器來遍歷:

3.4STL迭代器如何實現

1.迭代器是一種抽象的設計理念,通過迭代器可以在不瞭解容器內部原理的情況下遍歷容器,除此之外,STL中迭代器一個最重要的作用就是作爲容器與STL算法的粘合劑。

2.迭代器的作用就是提供一個遍歷容器內部所有元素的接口,因此迭代器內部必須保存一個與容器相關聯的指針,然後重載各種運算操作來遍歷,其中最重要的是*運算符與->運算符,以及++、–等可能需要重載的運算符重載。這和C++中的智能指針很像,智能指針也是將一個指針封裝,然後通過引用計數或是其他方法完成自動釋放內存的功能。

3.最常用的迭代器的相應型別有五種:value type、difference type、pointer、reference、iterator catagoly;

3.5set與hash_set的區別

1.set底層是以RB-Tree實現,hash_set底層是以hash_table實現的;

2.RB-Tree有自動排序功能,而hash_table不具有自動排序功能;

3.set和hash_set元素的鍵值就是實值;

4.hash_table有一些無法處理的型別;

3.6容器內部刪除一個元素

  1. 順序容器

erase迭代器不僅使所指向被刪除的迭代器失效,而且使被刪元素之後的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一個有效迭代器;

It = c.erase(it);

  1. 關聯容器

erase迭代器只是被刪除元素的迭代器失效,但是返回值是void,所以要採用erase(it++)的方式刪除迭代器;

c.erase(it++)

hashmap與map的區別

1.底層實現不同;

2.map具有自動排序的功能,hash_map不具有自動排序的功能;

3.hashtable有一些無法處理的型別;

map、set是怎麼實現的,紅黑樹是怎麼能夠同時實現這兩種容器? 爲什麼使用紅黑樹?

  1. 他們的底層都是以紅黑樹的結構實現,因此插入刪除等操作都在O(logn)時間內完成,因此可以完成高效的插入刪除;

  2. 在這裏我們定義了一個模版參數,如果它是key那麼它就是set,如果它是map,那麼它就是map;底層是紅黑樹,實現map的紅黑樹的節點數據類型是key+value,而實現set的節點數據類型是value

  3. 因爲map和set要求是自動排序的,紅黑樹能夠實現這一功能,而且時間複雜度比較低。

如何在共享內存上使用stl標準庫?

  1. 想像一下把STL容器,例如map, vector, list等等,放入共享內存中,IPC一旦有了這些強大的通用數據結構做輔助,無疑進程間通信的能力一下子強大了很多。我們沒必要再爲共享內存設計其他額外的數據結構,另外,STL的高度可擴展性將爲IPC所驅使。STL容器被良好的封裝,默認情況下有它們自己的內存管理方案。當一個元素被插入到一個STL列表(list)中時,列表容器自動爲其分配內存,保存數據。考慮到要將STL容器放到共享內存中,而容器卻自己在堆上分配內存。一個最笨拙的辦法是在堆上構造STL容器,然後把容器複製到共享內存,並且確保所有容器的內部分配的內存指向共享內存中的相應區域,這基本是個不可能完成的任務。

  2. 假設進程A在共享內存中放入了數個容器,進程B如何找到這些容器呢?一個方法就是進程A把容器放在共享內存中的確定地址上(fixed offsets),則進程B可以從該已知地址上獲取容器。另外一個改進點的辦法是,進程A先在共享內存某塊確定地址上放置一個map容器,然後進程A再創建其他容器,然後給其取個名字和地址一併保存到這個map容器裏。進程B知道如何獲取該保存了地址映射的map容器,然後同樣再根據名字取得其他容器的地址。

map插入方式有幾種?

  1. 用insert函數插入pair數據,

mapStudent.insert(pair<int, string>(1,“student_one”));

  1. 用insert函數插入value_type數據

mapStudent.insert(map<int, string>::value_type(1,“student_one”));

  1. 在insert函數中使用make_pair()函數

mapStudent.insert(make_pair(1,“student_one”));

  1. 用數組方式插入數據

mapStudent[1] = “student_one”;

STL中unordered_map(hash_map)和map的區別,hash_map如何解決衝突以及擴容

  1. unordered_map和map類似,都是存儲的key-value的值,可以通過key快速索引到value。不同的是unordered_map不會根據key的大小進行排序,

  2. 存儲時是根據key的hash值判斷元素是否相同,即unordered_map內部元素是無序的,而map中的元素是按照二叉搜索樹存儲,進行中序遍歷會得到有序遍歷。

  3. 所以使用時map的key需要定義operator<。而unordered_map需要定義hash_value函數並且重載operator==。但是很多系統內置的數據類型都自帶這些,

  4. 那麼如果是自定義類型,那麼就需要自己重載operator<或者hash_value()了。

  5. 如果需要內部元素自動排序,使用map,不需要排序使用unordered_map

  6. unordered_map的底層實現是hash_table;

  7. hash_map底層使用的是hash_table,而hash_table使用的開鏈法進行衝突避免,所有hash_map採用開鏈法進行衝突解決。

  8. **什麼時候擴容:**當向容器添加元素的時候,會判斷當前容器的元素個數,如果大於等於閾值—即當前數組的長度乘以加載因子的值的時候,就要自動擴容啦。

  9. **擴容(resize)**就是重新計算容量,向HashMap對象裏不停的添加元素,而HashMap對象內部的數組無法裝載更多的元素時,對象就需要擴大數組的長度,以便能裝入更多的元素。

vector越界訪問下標,map越界訪問下標?vector刪除元素時會不會釋放空間?

  1. 通過下標訪問vector中的元素時不會做邊界檢查,即便下標越界。也就是說,下標與first迭代器相加的結果超過了finish迭代器的位置,程序也不會報錯,而是返回這個地址中存儲的值。如果想在訪問vector中的元素時首先進行邊界檢查,可以使用vector中的at函數。通過使用at函數不但可以通過下標訪問vector中的元素,而且在at函數內部會對下標進行邊界檢查。

  2. map的下標運算符[]的作用是:將key作爲下標去執行查找,並返回相應的值;如果不存在這個key,就將一個具有該key和value的某人值插入這個map。

  3. erase()函數,只能刪除內容,不能改變容量大小; erase成員函數,它刪除了itVect迭代器指向的元素,並且返回要被刪除的itVect之後的迭代器,迭代器相當於一個智能指針;clear()函數,只能清空內容,不能改變容量大小;如果要想在刪除內容的同時釋放內存,那麼你可以選擇deque容器。

map[]與find的區別?

  1. map的下標運算符[]的作用是:將關鍵碼作爲下標去執行查找,並返回對應的值;如果不存在這個關鍵碼,就將一個具有該關鍵碼和值類型的默認值的項插入這個map。

  2. map的find函數:用關鍵碼執行查找,找到了返回該位置的迭代器;如果不存在這個關鍵碼,就返回尾迭代器。

vector的增加刪除都是怎麼做的?爲什麼是1.5倍?

  1. 新增元素:vector通過一個連續的數組存放元素,如果集合已滿,在新增數據的時候,就要分配一塊更大的內存,將原來的數據複製過來,釋放之前的內存,在插入新增的元素;

  2. 對vector的任何操作,一旦引起空間重新配置,指向原vector的所有迭代器就都失效了 ;

  3. 初始時刻vector的capacity爲0,塞入第一個元素後capacity增加爲1;

  4. 不同的編譯器實現的擴容方式不一樣,VS2015中以1.5倍擴容,GCC以2倍擴容。

對比可以發現採用採用成倍方式擴容,可以保證常數的時間複雜度,而增加指定大小的容量只能達到O(n)的時間複雜度,因此,使用成倍的方式擴容。

  1. 考慮可能產生的堆空間浪費,成倍增長倍數不能太大,使用較爲廣泛的擴容方式有兩種,以2二倍的方式擴容,或者以1.5倍的方式擴容。

  2. 以2倍的方式擴容,導致下一次申請的內存必然大於之前分配內存的總和,導致之前分配的內存不能再被使用,所以最好倍增長因子設置爲(1,2)之間:

  3. 向量容器vector的成員函數pop_back()可以刪除最後一個元素.

  4. 而函數erase()可以刪除由一個iterator指出的元素,也可以刪除一個指定範圍的元素。

  5. 還可以採用通用算法remove()來刪除vector容器中的元素.

  6. 不同的是:採用remove一般情況下不會改變容器的大小,而pop_back()與erase()等成員函數會改變容器的大小。

STL中list與queue之間的區別

  1. list不再能夠像vector一樣以普通指針作爲迭代器,因爲其節點不保證在存儲空間中連續存在;

  2. list插入操作和結合才做都不會造成原有的list迭代器失效;

  3. list不僅是一個雙向鏈表,而且還是一個環狀雙向鏈表,所以它只需要一個指針;

  4. list不像vector那樣有可能在空間不足時做重新配置、數據移動的操作,所以插入前的所有迭代器在插入操作之後都仍然有效;

  5. deque是一種雙向開口的連續線性空間,所謂雙向開口,意思是可以在頭尾兩端分別做元素的插入和刪除操作;可以在頭尾兩端分別做元素的插入和刪除操作;

  6. deque和vector最大的差異,一在於deque允許常數時間內對起頭端進行元素的插入或移除操作,二在於deque沒有所謂容量概念,因爲它是動態地以分段連續空間組合而成,隨時可以增加一段新的空間並鏈接起來,deque沒有所謂的空間保留功能。

STL中的allocator,deallocator

  1. 第一級配置器直接使用malloc()、free()和relloc(),第二級配置器視情況採用不同的策略:當配置區塊超過128bytes時,視之爲足夠大,便調用第一級配置器;當配置器區塊小於128bytes時,爲了降低額外負擔,使用複雜的內存池整理方式,而不再用一級配置器;

  2. 第二級配置器主動將任何小額區塊的內存需求量上調至8的倍數,並維護16個free-list,各自管理大小爲8~128bytes的小額區塊;

  3. 空間配置函數allocate(),首先判斷區塊大小,大於128就直接調用第一級配置器,小於128時就檢查對應的free-list。如果free-list之內有可用區塊,就直接拿來用,如果沒有可用區塊,就將區塊大小調整至8的倍數,然後調用refill(),爲free-list重新分配空間;

  4. 空間釋放函數deallocate(),該函數首先判斷區塊大小,大於128bytes時,直接調用一級配置器,小於128bytes就找到對應的free-list然後釋放內存。

STL中hash_map擴容發生什麼?

  1. hash table表格內的元素稱爲桶(bucket),而由桶所鏈接的元素稱爲節點(node),其中存入桶元素的容器爲stl本身很重要的一種序列式容器——vector容器。之所以選擇vector爲存放桶元素的基礎容器,主要是因爲vector容器本身具有動態擴容能力,無需人工干預。

  2. 向前操作:首先嚐試從目前所指的節點出發,前進一個位置(節點),由於節點被安置於list內,所以利用節點的next指針即可輕易完成前進操作,如果目前正巧是list的尾端,就跳至下一個bucket身上,那正是指向下一個list的頭部節點。

4.編譯和底層

4.1C++程序編譯過程

對於C++源文件,從文本到可執行文件一般需要四個過程:

預處理階段:對源代碼文件中文件包含關係(頭文件)、預編譯語句(宏定義)進行分析和替換,生成預編譯文件。

編譯階段:將經過預處理後的預編譯文件轉換成特定彙編代碼,生成彙編文件

彙編階段:將編譯階段生成的彙編文件轉化成機器碼,生成可重定位目標文件

鏈接階段:將多個目標文件及所需要的庫連接成最終的可執行目標文件

4.2頭文件的引用

Include頭文件的順序:對於include的頭文件來說,如果在文件a.h中聲明一個在文件b.h中定義的變量,而不引用b.h。那麼要在a.c文件中引用b.h文件,並且要先引用b.h,後引用a.h,否則彙報變量類型未聲明錯誤。

雙引號和尖括號的區別:編譯器預處理階段查找頭文件的路徑不一樣。

對於使用雙引號包含的頭文件,查找頭文件路徑的順序爲:

當前頭文件目錄 ->編譯器設置的頭文件路徑(編譯器可使用-I顯式指定搜索路徑) ->系統變量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的頭文件路徑

對於使用尖括號包含的頭文件,查找頭文件的路徑順序爲:

編譯器設置的頭文件路徑(編譯器可使用-I顯式指定搜索路徑) ->系統變量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的頭文件路徑

4.3內存分配與釋放

malloc的原理

Malloc函數用於動態分配內存。爲了減少內存碎片和系統調用的開銷,malloc其採用內存池的方式,先申請大塊內存作爲堆區,然後將堆區分爲多個內存塊,以塊作爲內存管理的基本單位。當用戶申請內存時,直接從堆區分配一塊合適的空閒塊。Malloc採用隱式鏈表結構將堆區分成連續的、大小不一的塊,包含已分配塊和未分配塊;同時malloc採用顯示鏈表結構來管理所有的空閒塊,即使用一個雙向鏈表將空閒塊連接起來,每一個空閒塊記錄了一個連續的、未分配的地址。

當進行內存分配時,Malloc會通過隱式鏈表遍歷所有的空閒塊,選擇滿足要求的塊進行分配;當進行內存合併時,malloc採用邊界標記法,根據每個塊的前後塊是否已經分配來決定是否進行塊合併。

Malloc在申請內存時,一般會通過brk或者mmap系統調用進行申請。其中當申請內存小於128K時,會使用系統函數brk在堆區中分配;而當申請內存大於128K時,會使用系統函數mmap在映射區分配。

new和malloc的區別

1、new分配內存按照數據類型進行分配,malloc分配內存按照指定的大小分配;

2、new返回的是指定對象的指針,類型嚴格與對象匹配,無須進行類型轉換,故new是符合類型安全性的操作符。而malloc返回的是void*,因此malloc的返回值一般都需要進行類型轉化。

3、new不僅分配一段內存,而且會調用構造函數,malloc不會。

4、new分配的內存要用delete銷燬,malloc要用free來銷燬;delete銷燬的時候會調用對象的析構函數,而free則不會。

5、new是一個操作符可以重載,malloc是一個庫函數

6、malloc分配的內存不夠的時候,可以用realloc擴容。擴容的原理?new沒用這樣操作。

7、new如果分配失敗了會拋出bad_malloc的異常,而malloc失敗了會返回NULL。

8、申請數組時: new[]一次分配所有內存,多次調用構造函數,搭配使用delete[],delete[]多次調用析構函數,銷燬數組中的每個對象。而malloc則只能sizeof(int) * n。

9、new/delete是C++關鍵字,需要編譯器支持。malloc/free是庫函數,需要頭文件支持;

10、new會先調用operator new函數,申請足夠的內存(通常底層使用malloc實現)。然後調用類型的構造函數,初始化成員變量,最後返回自定義類型指針。delete先調用析構函數,然後調用operator delete函數釋放內存(通常底層使用free實現)。malloc/free是庫函數,只能動態的申請和釋放內存,無法強制要求其做自定義類型對象構造和析構工作。

delete p;與delete[]p,allocator

1、 動態數組管理new一個數組時,[]中必須是一個整數,但是不一定是常量整數,普通數組必須是一個常量整數;

2、 new動態數組返回的並不是數組類型,而是一個元素類型的指針;

3、 delete[]時,數組中的元素按逆序的順序進行銷燬;

4、 new在內存分配上面有一些侷限性,new的機制是將內存分配和對象構造組合在一起,同樣的,delete也是將對象析構和內存釋放組合在一起的。allocator將這兩部分分開進行,allocator申請一部分內存,不進行初始化對象,只有當需要的時候才進行初始化操作。

new和delete的實現原理, delete是如何知道釋放內存的大小的?

1、 new簡單類型直接調用operator new分配內存;而對於複雜結構,先調用operator new分配內存,然後在分配的內存上調用構造函數;對於簡單類型,new[]計算好大小後調用operator new;對於複雜數據結構,new[]先調用operator new[]分配內存,然後在p的前四個字節寫入數組大小n,然後調用n次構造函數,針對複雜類型,new[]會額外存儲數組大小;

① new表達式調用一個名爲operator new(operator new[])函數,分配一塊足夠大的、原始的、未命名的內存空間;

② 編譯器運行相應的構造函數以構造這些對象,併爲其傳入初始值;

③ 對象被分配了空間並構造完成,返回一個指向該對象的指針。

2、 delete簡單數據類型默認只是調用free函數;複雜數據類型先調用析構函數再調用operator delete;針對簡單類型,delete和delete[]等同。假設指針p指向new[]分配的內存。因爲要4字節存儲數組大小,實際分配的內存地址爲[p-4],系統記錄的也是這個地址。delete[]實際釋放的就是p-4指向的內存。而delete會直接釋放p指向的內存,這個內存根本沒有被系統記錄,所以會崩潰。

3、 需要在 new [] 一個對象數組時,需要保存數組的維度,C++ 的做法是在分配數組空間時多分配了 4 個字節的大小,專門保存數組的大小,在 delete [] 時就可以取出這個保存的數,就知道了需要調用析構函數多少次了。

malloc申請的存儲空間能用delete釋放嗎

不能,malloc /free主要爲了兼容C,new和delete 完全可以取代malloc /free的。malloc /free的操作對象都是必須明確大小的。而且不能用在動態類上。new 和delete會自動進行類型檢查和大小,malloc/free不能執行構造函數與析構函數,所以動態對象它是不行的。當然從理論上說使用malloc申請的內存是可以通過delete釋放的。不過一般不這樣寫的。而且也不能保證每個C++的運行時都能正常。

malloc與free的實現原理?

1、 在標準C庫中,提供了malloc/free函數分配釋放內存,這兩個函數底層是由brk、mmap、,munmap這些系統調用實現的;

2、 brk是將數據段(.data)的最高地址指針_edata往高地址推,mmap是在進程的虛擬地址空間中(堆和棧中間,稱爲文件映射區域的地方)找一塊空閒的虛擬內存。這兩種方式分配的都是虛擬內存,沒有分配物理內存。在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,操作系統負責分配物理內存,然後建立虛擬內存和物理內存之間的映射關係;

3、 malloc小於128k的內存,使用brk分配內存,將_edata往高地址推;malloc大於128k的內存,使用mmap分配內存,在堆和棧之間找一塊空閒內存分配;brk分配的內存需要等到高地址內存釋放以後才能釋放,而mmap分配的內存可以單獨釋放。當最高地址空間的空閒內存超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行內存緊縮操作(trim)。在上一個步驟free的時候,發現最高地址空閒內存超過128K,於是內存緊縮。

4、 malloc是從堆裏面申請內存,也就是說函數返回的指針是指向堆裏面的一塊內存。操作系統中有一個記錄空閒內存地址的鏈表。當操作系統收到程序的申請時,就會遍歷該鏈表,然後就尋找第一個空間大於所申請空間的堆結點,然後就將該結點從空閒結點鏈表中刪除,並將該結點的空間分配給程序。

malloc、realloc、calloc的區別

  1. malloc函數
    

void* malloc(unsigned int num_size);

int p = malloc(20sizeof(int));申請20個int類型的空間;

  1. calloc函數
    

void* calloc(size_t n,size_t size);

int *p = calloc(20, sizeof(int));

省去了人爲空間計算;malloc申請的空間的值是隨機初始化的,calloc申請的空間的值是初始化爲0的;

  1. realloc函數
    

void realloc(void *p, size_t new_size);

給動態分配的空間分配額外的空間,用於擴充容量。

111

malloc、calloc、realloc、alloca

  1. malloc:申請指定字節數的內存。申請到的內存中的初始值不確定。
  2. calloc:爲指定長度的對象,分配能容納其指定個數的內存。申請到的內存的每一位(bit)都初始化爲 0。
  3. realloc:更改以前分配的內存長度(增加或減少)。當增加長度時,可能需將以前分配區的內容移到另一個足夠大的區域,而新增區域內的初始值則不確定。
  4. alloca:在棧上申請內存。程序在出棧的時候,會自動釋放內存。但是需要注意的是,alloca 不具可移植性, 而且在沒有傳統堆棧的機器上很難實現。alloca 不宜使用在必須廣泛移植的程序中。C99 中支持變長數組 (VLA),可以用來替代 alloca。

malloc、free

用於分配、釋放內存

malloc、free 使用 申請內存,確認是否申請成功
char *str = (char*) malloc(100);
assert(str != nullptr);

釋放內存後指針置空

free(p); 
p = nullptr;

new、delete

  1. new / new[]:完成兩件事,先底層調用 malloc 分了配內存,然後調用構造函數(創建對象)。
  2. delete/delete[]:也完成兩件事,先調用析構函數(清理資源),然後底層調用 free 釋放空間。
  3. new 在申請內存時會自動計算所需字節數,而 malloc 則需我們自己輸入申請內存空間的字節數。
new、delete 使用 申請內存,確認是否申請成功
int main()
{
    T* t = new T();     // 先內存分配 ,再構造函數
    delete t;           // 先析構函數,再內存釋放
    return 0;
}

定位 new

定位 new(placement new)允許我們向 new 傳遞額外的參數。

new (palce_address) type
new (palce_address) type (initializers)
new (palce_address) type [size]
new (palce_address) type [size] { braced initializer list }
  • palce_address 是個指針
  • initializers 提供一個(可能爲空的)以逗號分隔的初始值列表

4.4 delete this 合法嗎

Is it legal (and moral) for a member function to say delete this?

合法,但:

  1. 必須保證 this 對象是通過 new(不是 new[]、不是 placement new、不是棧上、不是全局、不是其他對象成員)分配的
  2. 必須保證調用 delete this 的成員函數是最後一個調用 this 的成員函數
  3. 必須保證成員函數的 delete this 後面沒有調用 this 了
  4. 必須保證 delete this 後沒有人使用了

4.5內存管理

在C++中,虛擬內存分爲代碼段、數據段、BSS段、堆區、文件映射區以及棧區六部分。

代碼段:包括只讀存儲區和文本區,其中只讀存儲區存儲字符串常量,文本區存儲程序的機器代碼。

數據段:存儲程序中已初始化的全局變量和靜態變量

bss 段:存儲未初始化的全局變量和靜態變量(局部+全局),以及所有被初始化爲0的全局變量和靜態變量。

堆區:調用new/malloc函數時在堆區動態分配內存,同時需要調用delete/free來手動釋放申請的內存。

映射區:存儲動態鏈接庫以及調用mmap函數進行的文件映射

棧:使用棧空間存儲函數的返回地址、參數、局部變量、返回值

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Mq6gpFhb-1586226672180)(./images/1.png)]

32bitCPU可尋址4G線性空間,每個進程都有各自獨立的4G邏輯地址,其中0~ 3G是用戶態空間,3~4G是內核空間,不同進程相同的邏輯地址會映射到不同的物理地址中。其邏輯地址其劃分如下:

各個段說明如下:

3G用戶空間和1G內核空間

靜態區域:

text segment(代碼段):包括只讀存儲區和文本區,其中只讀存儲區存儲字符串常量,文本區存儲程序的機器代碼。

data segment(數據段):存儲程序中已初始化的全局變量和靜態變量

bss segment:存儲未初始化的全局變量和靜態變量(局部+全局),以及所有被初始化爲0的全局變量和靜態變量,對於未初始化的全局變量和靜態變量,程序運行main之前時會統一清零。即未初始化的全局變量編譯器會初始化爲0

動態區域:

heap(堆): 當進程未調用malloc時是沒有堆段的,只有調用malloc時採用分配一個堆,並且在程序運行過程中可以動態增加堆大小(移動break指針),從低地址向高地址增長。分配小內存時使用該區域。 堆的起始地址由mm_struct 結構體中的start_brk標識,結束地址由brk標識。

memory mapping segment(映射區):存儲動態鏈接庫等文件映射、申請大內存(malloc時調用mmap函數)

stack(棧):使用棧空間存儲函數的返回地址、參數、局部變量、返回值,從高地址向低地址增長。在創建進程時會有一個最大棧大小,Linux可以通過ulimit命令指定。

什麼時候會發生段錯誤

段錯誤通常發生在訪問非法內存地址的時候,具體來說分爲以下幾種情況:

  • 使用野指針

  • 試圖修改字符串常量的內容

什麼是memory leak,也就是內存泄漏?

內存泄漏(memory leak)是指由於疏忽或錯誤造成了程序未能釋放掉不再使用的內存的情況。內存泄漏並非指內存在物理上的消失,而是應用程序分配某段內存後,由於設計錯誤,失去了對該段內存的控制,因而造成了內存的浪費。

內存泄漏的分類:

1.堆內存泄漏 (Heap leak)。對內存指的是程序運行中根據需要分配通過malloc,realloc new等從堆中分配的一塊內存,再是完成後必須通過調用對應的 free或者delete 刪掉。如果程序的設計的錯誤導致這部分內存沒有被釋放,那麼此後這塊內存將不會被使用,就會產生Heap Leak.

2.系統資源泄露(Resource Leak)。主要指程序使用系統分配的資源比如 Bitmap,handle ,SOCKET等沒有使用相應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能降低,系統運行不穩定。

3.沒有將基類的析構函數定義爲虛函數。當基類指針指向子類對象時,如果基類的析構函數不是virtual,那麼子類的析構函數將不會被調用,子類的資源沒有正確是釋放,因此造成內存泄露。

內存泄漏

  1. 內存泄漏

內存泄漏是指由於疏忽或錯誤造成了程序未能釋放掉不再使用的內存的情況。內存泄漏並非指內存在物理上消失,而是應用程序分配某段內存後,由於設計錯誤,失去了對該段內存的控制;

  1. 後果

只發生一次小的內存泄漏可能不被注意,但泄漏大量內存的程序將會出現各種證照:性能下降到內存逐漸用完,導致另一個程序失敗;

  1. 如何排除

使用工具軟件BoundsChecker,BoundsChecker是一個運行時錯誤檢測工具,它主要定位程序運行時期發生的各種錯誤;

調試運行DEBUG版程序,運用以下技術:CRT(C run-time libraries)、運行時函數調用堆棧、內存泄漏時提示的內存分配序號(集成開發環境OUTPUT窗口),綜合分析內存泄漏的原因,排除內存泄漏。

  1. 解決方法

智能指針。

  1. 檢查、定位內存泄漏

檢查方法:在main函數最後面一行,加上一句_CrtDumpMemoryLeaks()。調試程序,自然關閉程序讓其退出,查看輸出:

輸出這樣的格式{453}normal block at 0x02432CA8,868 bytes long

被{}包圍的453就是我們需要的內存泄漏定位值,868 bytes long就是說這個地方有868比特內存沒有釋放。

定位代碼位置

在main函數第一行加上_CrtSetBreakAlloc(453);意思就是在申請453這塊內存的位置中斷。然後調試程序,程序中斷了,查看調用堆棧。加上頭文件#include <crtdbg.h>

如何判斷內存泄漏?

內存泄漏通常是由於調用了malloc/new等內存申請的操作,但是缺少了對應的free/delete。爲了判斷內存是否泄露,我們一方面可以使用linux環境下的內存泄漏檢查工具Valgrind,另一方面我們在寫代碼時可以添加內存申請和釋放的統計功能,統計當前申請和釋放的內存是否一致,以此來判斷內存是否泄露。

4.6 reactor模型組成

reactor模型要求主線程只負責監聽文件描述上是否有事件發生,有的話就立即將該事件通知工作線程,除此之外,主線程不做任何其他實質性的工作,讀寫數據、接受新的連接以及處理客戶請求均在工作線程中完成。其模型組成如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XQ5h8ixG-1586226672180)(./images/2.jpg)]

1)Handle:即操作系統中的句柄,是對資源在操作系統層面上的一種抽象,它可以是打開的文件、一個連接(Socket)、Timer等。由於Reactor模式一般使用在網絡編程中,因而這裏一般指Socket Handle,即一個網絡連接。

2)Synchronous Event Demultiplexer(同步事件複用器):阻塞等待一系列的Handle中的事件到來,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的執行返回的事件類型。這個模塊一般使用操作系統的select來實現。

3)Initiation Dispatcher:用於管理Event Handler,即EventHandler的容器,用以註冊、移除EventHandler等;另外,它還作爲Reactor模式的入口調用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,當阻塞等待返回時,根據事件發生的Handle將其分發給對應的Event Handler處理,即回調EventHandler中的handle_event()方法。

4)Event Handler:定義事件處理方法:handle_event(),以供InitiationDispatcher回調使用。

5)Concrete Event Handler:事件EventHandler接口,實現特定事件處理邏輯。

4.7提高單線程的高併發

在單線程模型中,可以採用I/O複用來提高單線程處理多個請求的能力,然後再採用事件驅動模型,基於異步回調來處理事件來。

4.8STL 的內存優化

1)二級配置器結構
STL內存管理使用二級內存配置器。
1、第一級配置器
第一級配置器以malloc(),free(),realloc()等C函數執行實際的內存配置、釋放、重新配置等操作,並且能在內存需求不被滿足的時候,調用一個指定的函數。
一級空間配置器分配的是大於128字節的空間
如果分配不成功,調用句柄釋放一部分內存
如果還不能分配成功,拋出異常
2、第二級配置器
在STL的第二級配置器中多了一些機制,避免太多小區塊造成的內存碎片,小額區塊帶來的不僅是內存碎片,配置時還有額外的負擔。區塊越小,額外負擔所佔比例就越大。
3、分配原則
如果要分配的區塊大於128bytes,則移交給第一級配置器處理。
如果要分配的區塊小於128bytes,則以內存池管理(memory pool),又稱之次層配置(sub-allocation):每次配置一大塊內存,並維護對應的16個空閒鏈表(free-list)。下次若有相同大小的內存需求,則直接從free-list中取。如果有小額區塊被釋放,則由配置器回收到free-list中。
當用戶申請的空間小於128字節時,將字節數擴展到8的倍數,然後在自由鏈表中查找對應大小的子鏈表
如果在自由鏈表查找不到或者塊數不夠,則向內存池進行申請,一般一次申請20塊
如果內存池空間足夠,則取出內存
如果不夠分配20塊,則分配最多的塊數給自由鏈表,並且更新每次申請的塊數
如果一塊都無法提供,則把剩餘的內存掛到自由鏈表,然後向系統heap申請空間,如果申請失敗,則看看自由鏈表還有沒有可用的塊,如果也沒有,則最後調用一級空間配置器

2)二級內存池
二級內存池採用了16個空閒鏈表,這裏的16個空閒鏈表分別管理大小爲8、16、24…120、128的數據塊。這裏空閒鏈表節點的設計十分巧妙,這裏用了一個聯合體既可以表示下一個空閒數據塊(存在於空閒鏈表中)的地址,也可以表示已經被用戶使用的數據塊(不存在空閒鏈表中)的地址。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hYvBfLkz-1586226672181)(./images/3.png)]

1、空間配置函數allocate
首先先要檢查申請空間的大小,如果大於128字節就調用第一級配置器,小於128字節就檢查對應的空閒鏈表,如果該空閒鏈表中有可用數據塊,則直接拿來用(拿取空閒鏈表中的第一個可用數據塊,然後把該空閒鏈表的地址設置爲該數據塊指向的下一個地址),如果沒有可用數據塊,則調用refill重新填充空間。
2、空間釋放函數deallocate
首先先要檢查釋放數據塊的大小,如果大於128字節就調用第一級配置器,小於128字節則根據數據塊的大小來判斷回收後的空間會被插入到哪個空閒鏈表。
3、重新填充空閒鏈表refill
在用allocate配置空間時,如果空閒鏈表中沒有可用數據塊,就會調用refill來重新填充空間,新的空間取自內存池。缺省取20個數據塊,如果內存池空間不足,那麼能取多少個節點就取多少個。
從內存池取空間給空閒鏈表用是chunk_alloc的工作,首先根據end_free-start_free來判斷內存池中的剩餘空間是否足以調出nobjs個大小爲size的數據塊出去,如果內存連一個數據塊的空間都無法供應,需要用malloc取堆中申請內存。
假如山窮水盡,整個系統的堆空間都不夠用了,malloc失敗,那麼chunk_alloc會從空閒鏈表中找是否有大的數據塊,然後將該數據塊的空間分給內存池(這個數據塊會從鏈表中去除)。

3)總結

1.使用allocate向內存池請求size大小的內存空間,如果需要請求的內存大小大於128bytes,直接使用malloc。

2.如果需要的內存大小小於128bytes,allocate根據size找到最適合的自由鏈表。
a. 如果鏈表不爲空,返回第一個node,鏈表頭改爲第二個node。
b. 如果鏈表爲空,使用blockAlloc請求分配node。
x. 如果內存池中有大於一個node的空間,分配竟可能多的node(但是最多20個),將一個node返回,其他的node添加到鏈表中。
y. 如果內存池只有一個node的空間,直接返回給用戶。
z. 若果如果連一個node都沒有,再次向操作系統請求分配內存。
①分配成功,再次進行b過程。
②分配失敗,循環各個自由鏈表,尋找空間。
I. 找到空間,再次進行過程b。
II. 找不到空間,拋出異常。

3.用戶調用deallocate釋放內存空間,如果要求釋放的內存空間大於128bytes,直接調用free。

4.否則按照其大小找到合適的自由鏈表,並將其插入。

4.9只在堆上(棧上)生成對象的類

如何定義一個只能在堆上(棧上)生成對象的類?

只能在堆上

方法:將析構函數設置爲私有

原因:C++ 是靜態綁定語言,編譯器管理棧上對象的生命週期,編譯器在爲類對象分配棧空間時,會先檢查類的析構函數的訪問性。若析構函數不可訪問,則不能在棧上創建對象。

只能在棧上

方法:將 new 和 delete 重載爲私有

原因:在堆上生成對象,使用 new 關鍵詞操作,其過程分爲兩階段:第一階段,使用 new 在堆上尋找可用內存,分配給對象;第二階段,調用構造函數生成對象。將 new 操作設置爲私有,那麼第一階段就無法完成,就不能夠在堆上生成對象。

5.C++11

5.1C++11新特性

C++11 最常用的新特性如下:

auto關鍵字:編譯器可以根據初始值自動推導出類型。但是不能用於函數傳參以及數組類型的推導

nullptr關鍵字:nullptr是一種特殊類型的字面值,它可以被轉換成任意其它的指針類型;而NULL一般被宏定義爲0,在遇到重載時可能會出現問題。

智能指針:C++11新增了std::shared_ptr、std::weak_ptr等類型的智能指針,用於解決內存管理的問題。

初始化列表:使用初始化列表來對類進行初始化

右值引用:基於右值引用可以實現移動語義和完美轉發,消除兩個對象交互時不必要的對象拷貝,節省運算存儲資源,提高效率

atomic原子操作用於多線程資源互斥操作

新增STL容器array以及tuple

5.2C++11的可變參數模板

C++11的可變參數模板,對參數進行了高度泛化,可以表示任意數目、任意類型的參數,其語法爲:在class或typename後面帶上省略號”。

例如:

template<class...T>
void func(T ... args)
{
    cout<<"num is "<< sizeof...(args)<<endl;
}

func();//args不含任何參數

func(1);//args包含一個int類型的實參

func(1,2.0)//args包含一個int一個double類型的實參

其中T叫做模板參數包,args叫做函數參數包

省略號作用如下:

1)聲明一個包含0到任意個模板參數的參數包

2)在模板定義得右邊,可以將參數包展成一個個獨立的參數

遞歸函數方式展開

C++11可以使用遞歸函數的方式展開參數包,獲得可變參數的每個值。通過遞歸函數展開參數包,需要提供一個參數包展開的函數和一個遞歸終止函數。例如:

// 最終遞歸函數
void print()
{
	cout << "empty" << endl;
}

// 展開函數
template<class T,class ...Args>
void print(T head,Args...args)
{
	cout << "parameter " << head << endl;
	print(args...);
}

int main()
{
	print(1, 2, 3, 4); 
    return 0;
}

參數包Args …在展開的過程中遞歸調用自己,沒調用一次參數包中的參數就會少一個,直到所有參數都展開爲止。當沒有參數時就會調用非模板函數printf終止遞歸過程。

逗號表達式展開
遞歸函數展開參數包是一種標準做法,也比較好理解,但也有一個缺點,就是必須要一個重載的遞歸終止函數,即必須要有一個同名的終止函數來終止遞歸,這樣可能會感覺稍有不便。有沒有一種更簡單的方式呢?其實還有一種方法可以不通過遞歸方式來展開參數包,這種方式需要藉助逗號表達式和初始化列表。比如前面print的例子可以改成這樣:

template <class T>
void print(T t)
{
   cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
   int arr[] = {(print(args), 0)...};
}

expand(1,2,3,4);
//用到數組的初始化列表,這個數組的目的純粹是爲了在數組構造的過程展開參數包。
//{(printarg(args), 0)...}將會展開成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0),  etc... ),
//最終會創建一個元素值都爲0的數組int arr[sizeof...(Args)]。printarg便會處理參數包中每一個參數。


支持lambda表達式
template<class F, class... Args>
void expand(const F& f, Args&&...args) 
{
    //這裏用到了完美轉發
    initializer_list<int>{(f(std::forward< Args>(args)),0)...};
}
expand([](int i){cout<<i<<endl;}, 1,2,3);

https://blog.csdn.net/tony__lin/article/details/84677316

5.3智能指針

C++ 標準庫(STL)中

頭文件:#include <memory>

C++ 98

std::auto_ptr<std::string> ps (new std::string(str))

C++ 11

  1. shared_ptr
  2. unique_ptr
  3. weak_ptr
  4. auto_ptr(被 C++11 棄用)
  • Class shared_ptr 實現共享式擁有(shared ownership)概念。多個智能指針指向相同對象,該對象和其相關資源會在 “最後一個 reference 被銷燬” 時被釋放。爲了在結構較複雜的情景中執行上述工作,標準庫提供 weak_ptr、bad_weak_ptr 和 enable_shared_from_this 等輔助類。
  • Class unique_ptr 實現獨佔式擁有(exclusive ownership)或嚴格擁有(strict ownership)概念,保證同一時間內只有一個智能指針可以指向該對象。你可以移交擁有權。它對於避免內存泄漏(resource leak)——如 new 後忘記 delete ——特別有用。
shared_ptr

多個智能指針可以共享同一個對象,對象的最末一個擁有着有責任銷燬對象,並清理與該對象相關的所有資源。

  • 支持定製型刪除器(custom deleter),可防範 Cross-DLL 問題(對象在動態鏈接庫(DLL)中被 new 創建,卻在另一個 DLL 內被 delete 銷燬)、自動解除互斥鎖
weak_ptr

weak_ptr 允許你共享但不擁有某對象,一旦最末一個擁有該對象的智能指針失去了所有權,任何 weak_ptr 都會自動成空(empty)。因此,在 default 和 copy 構造函數之外,weak_ptr 只提供 “接受一個 shared_ptr” 的構造函數。

  • 可打破環狀引用(cycles of references,兩個其實已經沒有被使用的對象彼此互指,使之看似還在 “被使用” 的狀態)的問題
unique_ptr

unique_ptr 是 C++11 纔開始提供的類型,是一種在異常時可以幫助避免資源泄漏的智能指針。採用獨佔式擁有,意味着可以確保一個對象和其相應的資源同一時間只被一個 pointer 擁有。一旦擁有着被銷燬或編程 empty,或開始擁有另一個對象,先前擁有的那個對象就會被銷燬,其任何相應資源亦會被釋放。

  • unique_ptr 用於取代 auto_ptr
auto_ptr

被 c++11 棄用,原因是缺乏語言特性如 “針對構造和賦值” 的 std::move 語義,以及其他瑕疵。

auto_ptr 與 unique_ptr 比較
  • auto_ptr 可以賦值拷貝,複製拷貝後所有權轉移;unqiue_ptr 無拷貝賦值語義,但實現了move 語義;
  • auto_ptr 對象不能管理數組(析構調用 delete),unique_ptr 可以管理數組(析構調用 delete[] );

6.Effective C++

  1. 視 C++ 爲一個語言聯邦(C、Object-Oriented C++、Template C++、STL)
  2. 儘量以 constenuminline 替換 #define(寧可以編譯器替換預處理器)
  3. 儘可能使用 const
  4. 確定對象被使用前已先被初始化(構造時賦值(copy 構造函數)比 default 構造後賦值(copy assignment)效率高)
  5. 瞭解 C++ 默默編寫並調用哪些函數(編譯器暗自爲 class 創建 default 構造函數、copy 構造函數、copy assignment 操作符、析構函數)
  6. 若不想使用編譯器自動生成的函數,就應該明確拒絕(將不想使用的成員函數聲明爲 private,並且不予實現)
  7. 爲多態基類聲明 virtual 析構函數(如果 class 帶有任何 virtual 函數,它就應該擁有一個 virtual 析構函數)
  8. 別讓異常逃離析構函數(析構函數應該吞下不傳播異常,或者結束程序,而不是吐出異常;如果要處理異常應該在非析構的普通函數處理)
  9. 絕不在構造和析構過程中調用 virtual 函數(因爲這類調用從不下降至 derived class)
  10. operator= 返回一個 reference to *this (用於連鎖賦值)
  11. operator= 中處理 “自我賦值”
  12. 賦值對象時應確保複製 “對象內的所有成員變量” 及 “所有 base class 成分”(調用基類複製構造函數)
  13. 以對象管理資源(資源在構造函數獲得,在析構函數釋放,建議使用智能指針,資源取得時機便是初始化時機(Resource Acquisition Is Initialization,RAII))
  14. 在資源管理類中小心 copying 行爲(普遍的 RAII class copying 行爲是:抑制 copying、引用計數、深度拷貝、轉移底部資源擁有權(類似 auto_ptr))
  15. 在資源管理類中提供對原始資源(raw resources)的訪問(對原始資源的訪問可能經過顯式轉換或隱式轉換,一般而言顯示轉換比較安全,隱式轉換對客戶比較方便)
  16. 成對使用 new 和 delete 時要採取相同形式(new 中使用 []delete []new 中不使用 []delete
  17. 以獨立語句將 newed 對象存儲於(置入)智能指針(如果不這樣做,可能會因爲編譯器優化,導致難以察覺的資源泄漏)
  18. 讓接口容易被正確使用,不易被誤用(促進正常使用的辦法:接口的一致性、內置類型的行爲兼容;阻止誤用的辦法:建立新類型,限制類型上的操作,約束對象值、消除客戶的資源管理責任)
  19. 設計 class 猶如設計 type,需要考慮對象創建、銷燬、初始化、賦值、值傳遞、合法值、繼承關係、轉換、一般化等等。
  20. 寧以 pass-by-reference-to-const 替換 pass-by-value (前者通常更高效、避免切割問題(slicing problem),但不適用於內置類型、STL迭代器、函數對象)
  21. 必須返回對象時,別妄想返回其 reference(絕不返回 pointer 或 reference 指向一個 local stack 對象,或返回 reference 指向一個 heap-allocated 對象,或返回 pointer 或 reference 指向一個 local static 對象而有可能同時需要多個這樣的對象。)
  22. 將成員變量聲明爲 private(爲了封裝、一致性、對其讀寫精確控制等)
  23. 寧以 non-member、non-friend 替換 member 函數(可增加封裝性、包裹彈性(packaging flexibility)、機能擴充性)
  24. 若所有參數(包括被this指針所指的那個隱喻參數)皆須要類型轉換,請爲此採用 non-member 函數
  25. 考慮寫一個不拋異常的 swap 函數
  26. 儘可能延後變量定義式的出現時間(可增加程序清晰度並改善程序效率)
  27. 儘量少做轉型動作(舊式:(T)expressionT(expression);新式:const_cast<T>(expression)dynamic_cast<T>(expression)reinterpret_cast<T>(expression)static_cast<T>(expression)、;儘量避免轉型、注重效率避免 dynamic_casts、儘量設計成無需轉型、可把轉型封裝成函數、寧可用新式轉型)
  28. 避免使用 handles(包括 引用、指針、迭代器)指向對象內部(以增加封裝性、使 const 成員函數的行爲更像 const、降低 “虛吊號碼牌”(dangling handles,如懸空指針等)的可能性)

7.Google C++編程規範

Google C++ Style Guide是一份不錯的C++編碼指南,我製作了一張比較全面的說明圖,可以在短時間內快速掌握規範的重點內容。不過規範畢竟是人定的,記得活學活用。看圖前別忘了閱讀下面的重要建議:

1 保持一致也非常重要,如果你在一個文件中新加的代碼和原有代碼風格相去甚遠的話,這就破壞了文件本身的整體美觀也影響閱讀,所以要儘量避免。

2 一些條目往往有例外,比如下面這些,所以本圖不能代替文檔,有時間還是把PDF認真閱讀一遍吧。

異常在測試框架中確實很好用
RTTI在某些單元測試中非常有用
在記錄日誌時可以使用流
操作符重載 不提倡使用,有些STL 算法確實需要重載operator==時可以這麼做。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章