1. C++基礎(chǔ)知識點
1.1 有符號類型和無符號類型
- 當(dāng)我們賦給無符號類型一個超出它表示范圍的值時,結(jié)果是初始值對無符號類型表示數(shù)值總數(shù)取模之后的余數(shù)。當(dāng)我們賦給帶符號類型一個超出它表示范圍的值時,結(jié)果是未定義的;此時,程序可能繼續(xù)工作、可能崩潰。也可能生成垃圾數(shù)據(jù)。
- 如果表達式中既有帶符號類型由于無符號類型那個,當(dāng)帶符號類型取值為負時會出現(xiàn)異常結(jié)果,這是因為帶符號數(shù)會自動轉(zhuǎn)換成無符號數(shù)。
int a = 1;unsigned int b = -2;
cout<<a+b<<endl; // 輸出4294967295
int c = a + b; //c=-1;
a = 3;b = -2;
cout<<a+b<<endl; // 輸出1
1.2 引用與指針
引用并非對象,它只是為一個已經(jīng)存在的對象起的一個別名。在定義引用時,程序把引用和它的初始值綁定在一起,而不是將初始值拷貝給引用。一旦初始化完成,應(yīng)用將和它的初始值綁定在一起。以為無法令引用重新綁定到另外一個對象,因此引用必須初始化。
指針是指向另外一種類型的符合類型。與引用類似,指針也實現(xiàn)了對其他對象的簡介訪問。然而指針與引用相比又有許多不同點:
- 指針本身就是一個對象,允許對指針賦值和拷貝。而且在指針的生命周期內(nèi)它可以先后指向幾個不同的對象。引用不是對象,所以也不能定義指向引用的指針。
- 指針無須在定義時賦值。
void*是一種特殊的指針類型,可以存放任意對象的地址。但我們對該地址中存放的是什么類型的對象并不了解,所以也不能直接操作void*指針所指的對象。
1.3 static關(guān)鍵字
- 申明為static的局部變量,存儲在靜態(tài)存儲區(qū),其生存期不再局限于當(dāng)前作用域,而是整個程序的生存期。
- 對于全局變量而言, 普通的全局變量和函數(shù),其作用域為整個程序或項目,外部文件(其它cpp文件)可以通過extern關(guān)鍵字訪問該變量和函數(shù);static全局變量和函數(shù),其作用域為當(dāng)前cpp文件,其它的cpp文件不能訪問該變量和函數(shù)。
- 當(dāng)使用static修飾成員變量和成員函數(shù)時,表示該變量或函數(shù)屬于一個類,而不是該類的某個實例化對象。
1.4 const限定符
const的作用
- 在定義常變量時必須同時對它初始化,此后它的值不能再改變。常變量不能出現(xiàn)在賦值號的左邊(不為“左值”);
- 對指針來說,可以指定指針本身為const,也可以指定指針所指的數(shù)據(jù)為const,或二者同時指定為const;
- 在一個函數(shù)聲明中,const可以修飾形參,表明它是一個輸入?yún)?shù),在函數(shù)內(nèi)部不能改變其值;
- 對于類的成員函數(shù),若指定其為const類型,則表明其是一個常函數(shù),不能修改類的成員變量;
- 對于類的成員函數(shù),有時候必須指定其返回值為const類型,以使得其返回值不為"左值"。例如:
//operator*的返回結(jié)果必須是一個const對象,否則下列代碼編譯出錯
const classA operator*(const classA& a1,const classA& a2);
classA a, b, c;
(a*b) = c; //對a*b的結(jié)果賦值。操作(a*b) = c顯然不符合編程者的初衷,也沒有任何意義
用const修飾的符號常量的區(qū)別:const位于(*)的左邊,表示被指物是常量;const位于(*)的右邊,表示指針自身是常量(常量指針)。(口訣:左定值,右定向)
const char *p; //指向const對象的指針,指針可以被修改,但指向的對象不能被修改。
char const *p; //同上
char * const p; //指向char類型的常量指針,指針不能被修改,但指向的對象可以被修改。
const char * const p; //指針及指向?qū)ο蠖疾荒苄薷摹?
const與#define的區(qū)別
- const常量有數(shù)據(jù)類型,而宏常量沒有數(shù)據(jù)類型。編譯器可以對前者進行類型安全檢查。而對后者只進行字符替換,沒有類型安全檢查,并且在字符替換可能會產(chǎn)生意料不到的錯誤(邊際效應(yīng))。
- 有些集成化的調(diào)試工具可以對const常量進行調(diào)試,但是不能對宏常量進行調(diào)試。
- 在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
1.5 數(shù)組與指針的區(qū)別
- 數(shù)組要么在靜態(tài)存儲區(qū)被創(chuàng)建(如全局數(shù)組),要么在棧上被創(chuàng)建。指針可以隨時指向任意類型的內(nèi)存塊。
- 用運算符sizeof可以計算出數(shù)組的容量(字節(jié)數(shù))。sizeof(p),p為指針得到的是一個指針變量的字節(jié)數(shù),而不是p所指的內(nèi)存容量。C/C++語言沒有辦法知道指針所指的內(nèi)存容量,除非在申請內(nèi)存時記住它。
- C++編譯系統(tǒng)將形參數(shù)組名一律作為指針變量來處理。實際上在函數(shù)調(diào)用時并不存在一個占有存儲空間的形參數(shù)組,只有指針變量。
實參數(shù)組名a代表一個固定的地址,或者說是指針型常量,因此要改變a的值是不可能的。例如:a++;是錯誤的。形參數(shù)組名array是指針變量,并不是一個固定的地址值。它的值是可以改變的。例如:array++;是合法的。
為了節(jié)省內(nèi)存,C/C++把常量字符串放到單獨的一個內(nèi)存區(qū)域。當(dāng)幾個指針賦值給相同的常量字符串時,它們實際上會指向相同的內(nèi)存地址。但用常量內(nèi)存初始化數(shù)組時,情況卻有所不同。
char str1[] = “Hello World”;
char str2[] = “Hello World”;
char *str3[] = “Hello World”;
char *str4[] = “Hello World”;
其中,str1和str2會為它們分配兩個長度為12個字節(jié)的空間,并把“Hello World”的內(nèi)容分別復(fù)制到數(shù)組中去,這是兩個初始地址不同的數(shù)組。str3和str4是兩個指針,我們無須為它們分配內(nèi)存以存儲字符串的內(nèi)容,而只需要把它們指向“Hello World”在內(nèi)存中的地址就可以了。由于“Hello World”是常量字符串,它在內(nèi)存中只有一個拷貝,因此str3和str4指向的是同一個地址。
1.6 sizeof運算符
sizeof是C語言的一種單目操作符,它并不是函數(shù)。操作數(shù)可以是一個表達式或類型名。數(shù)據(jù)類型必須用括號括住,sizeof(int);變量名可以不用括號括住。
int a[50]; //sizeof(a)=200
int *a=new int[50]; //sizeof(a)=4;
Class Test{int a; static double c}; //sizeof(Test)=4
Test *s; //sizeof(s)=4
Class Test{ }; //sizeof(Test)=1
int func(char s[5]); //sizeof(s)=4;
操作數(shù)不同時注意事項:
- 數(shù)組類型,其結(jié)果是數(shù)組的總字節(jié)數(shù);指向數(shù)組的指針,其結(jié)果是該指針的字節(jié)數(shù)。
- 函數(shù)中的數(shù)組形參或函數(shù)類型的形參,其結(jié)果是指針的字節(jié)數(shù)。
- 聯(lián)合類型,其結(jié)果采用成員最大長度對齊。
- 結(jié)構(gòu)類型或類類型,其結(jié)果是這種類型對象的總字節(jié)數(shù),包括任何填充在內(nèi)。
- 類中的靜態(tài)成員不對結(jié)果產(chǎn)生影響,因為靜態(tài)變量的存儲位置與結(jié)構(gòu)或者類的實例地址無關(guān);
- 沒有成員變量的類的大小為1,因為必須保證類的每一個實例在內(nèi)存中都有唯一的地址;
- 有虛函數(shù)的類都會建立一張?zhí)摵瘮?shù)表,表中存放的是虛函數(shù)的函數(shù)指針,這個表的地址存放在類中,所以不管有幾個虛函數(shù),都只占據(jù)一個指針大小。
例題:
1、下列聯(lián)合體的sizeof(sampleUnion)的值為多少。
union{
char flag[3];
short value;
} sampleUnion;
答案:4。聯(lián)合體占用大小采用成員最大長度的對齊,最大長度是short的2字節(jié)。但char flag[3]需要3個字節(jié),所以sizeof(sampleUnion) = 2*(2字節(jié))= 4。注意對齊有兩層含義,一個是按本身的字節(jié)大小數(shù)對齊,一個是整體按照最大的字節(jié)數(shù)對齊。
2、在32位系統(tǒng)中:
char arr[] = {4, 3, 9, 9, 2, 0, 1, 5};
char *str = arr;
sizeof(arr) = 8;
sizeof(str) = 4;
strlen(str) = 5;
答案:8,4,5。注意strlen函數(shù)求取字符串長度以ASCII值為0為止。
3、定義一個空的類型,里面沒有任何成員變量和成員函數(shù)。
問題:對該類型求sizeof,得到的結(jié)果是什么?
答案:1。
問題:為什么不是0?
答案:當(dāng)我們聲明該類型的實例的時候,它必須在內(nèi)存中占有一定的空間,否則無法使用這些實例。至于占用多少內(nèi)存,由編譯器決定。Visual Studio中每個空類型的實例占用1字節(jié)的空間。
問題:如果在該類型中添加一個構(gòu)造函數(shù)和析構(gòu)函數(shù),結(jié)果又是什么?
答案:還是1。調(diào)用構(gòu)造函數(shù)和析構(gòu)函數(shù)只需要知道函數(shù)的地址即可,而這些函數(shù)的地址只與類型相關(guān),而與類型的實例無關(guān)。
問題:那如果把析構(gòu)函數(shù)標記為虛函數(shù)呢?
答案:C++的編譯器一旦發(fā)現(xiàn)一個類型中有虛函數(shù),就會為該類型生成虛函數(shù)表,并在該類型的每一個實例中添加一個指向虛函數(shù)表的指針。在32位的機器上,指針占用4字節(jié),因此求sizeof得到4;如果是64位機器,將得到8。
1.7 四個強制類型轉(zhuǎn)換
C++中有以下四種命名的強制類型轉(zhuǎn)換:
- static_cast:任何具有明確定義的類型轉(zhuǎn)換,只要不包含底層const,都可以使用static_cast。
- const_cast:去const屬性,只能改變運算對象的底層const。常用于有函數(shù)重載的上下文中。
- reninterpret_cast:通常為運算對象的位模式提供較低層次的重新解釋,本質(zhì)依賴與機器。
- dynamic_cast:主要用來執(zhí)行“安全向下轉(zhuǎn)型”,也就是用來決定某對象是否歸屬繼承體系中的某個類型。主要用于多態(tài)類之間的轉(zhuǎn)換
一般來說,如果編譯器發(fā)現(xiàn)一個較大的算術(shù)類型試圖賦值給較小的類型,就會給出警告;但是當(dāng)我們執(zhí)行了顯式的類型轉(zhuǎn)換之后,警告信息就被關(guān)閉了。
//進行強制類型轉(zhuǎn)換以便執(zhí)行浮點數(shù)除法
int j = 1,i = 2;
double slope = static_cast<double>(j)/i;
//任何非常量對象的地址都能存入void*,通過static_cast可以將指針轉(zhuǎn)換會初始的指針類型
void* p = &slope;
double *dp = static_cast<double*>(p);
只有const_cast能夠改變表達式的常量屬性,其他形式的強制類型轉(zhuǎn)換改變表達式的常量屬性都將引發(fā)編譯器錯誤。
//利用const_cast去除底層const
const char c = 'a';
const char *pc = &c;
char* cp = const_cast<char*>(pc);
*cp = 'c';
reinterpret_cast常用于函數(shù)指針類型之間進行轉(zhuǎn)換。
int doSomething(){return0;};
typedef void(*FuncPtr)(); //FuncPtr是一個指向函數(shù)的指針,該函數(shù)沒有參數(shù),返回值類型為void
FuncPtr funcPtrArray[10]; //假設(shè)你希望把一個指向下面函數(shù)的指針存入funcPtrArray數(shù)組:
funcPtrArray[0] =&doSomething;// 編譯錯誤!類型不匹配
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); //不同函數(shù)指針類型之間進行轉(zhuǎn)換
dynamic_cast
有條件轉(zhuǎn)換,動態(tài)類型轉(zhuǎn)換,運行時類型安全檢查(轉(zhuǎn)換失敗返回NULL):
- 安全的基類和子類之間轉(zhuǎn)換。
- 必須要有虛函數(shù)。
- 相同基類不同子類之間的交叉轉(zhuǎn)換。但結(jié)果是NULL。
class Base {
public:
int m_iNum;
virtualvoid foo(){}; //基類必須有虛函數(shù)。保持多態(tài)特性才能使用dynamic_cast
};
class Derive: public Base {
public:
char*m_szName[100];
void bar(){};
};
Base* pb =new Derive();
Derive *pd1 = static_cast<Derive *>(pb); //子類->父類,靜態(tài)類型轉(zhuǎn)換,正確但不推薦
Derive *pd2 = dynamic_cast<Derive *>(pb); //子類->父類,動態(tài)類型轉(zhuǎn)換,正確
Base* pb2 =new Base();
Derive *pd21 = static_cast<Derive *>(pb2); //父類->子類,靜態(tài)類型轉(zhuǎn)換,危險!訪問子類m_szName成員越界
Derive *pd22 = dynamic_cast<Derive *>(pb2); //父類->子類,動態(tài)類型轉(zhuǎn)換,安全的。結(jié)果是NULL
1.8 結(jié)構(gòu)體的內(nèi)存對齊
內(nèi)存對齊規(guī)則
- 每個成員相對于這個結(jié)構(gòu)體變量地址的偏移量正好是該成員類型所占字節(jié)的整數(shù)倍。為了對齊數(shù)據(jù),可能必須在上一個數(shù)據(jù)結(jié)束和下一個數(shù)據(jù)開始的地方插入一些沒有用處字節(jié)。
- 且最終占用字節(jié)數(shù)為成員類型中最大占用字節(jié)數(shù)的整數(shù)倍。
struct AlignData1
{
char c;
short b;
int i;
char d;
}Node;
這個結(jié)構(gòu)體在編譯以后,為了字節(jié)對齊,會被整理成這個樣子:
struct AlignData1
{
char c;
char padding[1];
short b;
int i;
char d;
char padding[3];
}Node;
所以編譯前總的結(jié)構(gòu)體大小為:8個字節(jié)。編譯以后字節(jié)大小變?yōu)椋?2個字節(jié)。
但是,如果調(diào)整順序:
struct AlignData2
{
char c;
char d;
short b;
int i;
}Node;
那么這個結(jié)構(gòu)體在編譯前后的大小都是8個字節(jié)。
那么編譯后不用填充字節(jié)就能保持所有的成員都按各自默認的地址對齊。這樣可以節(jié)約不少內(nèi)存!一般的結(jié)構(gòu)體成員按照默認對齊字節(jié)數(shù)遞增或是遞減的順序排放,會使總的填充字節(jié)數(shù)最少。
1.9 malloc/free 與 new/delete的區(qū)別
- malloc與free是C++/C語言的標準庫函數(shù),new/delete是C++的運算符。它們都可用于申請和釋放動態(tài)內(nèi)存。
- 對于非內(nèi)部數(shù)據(jù)類型的對象而言,用maloc/free無法滿足動態(tài)對象的要求。對象在創(chuàng)建的同時要自動執(zhí)行構(gòu)造函數(shù),對象在消亡之前要自動執(zhí)行析構(gòu)函數(shù)。由malloc/free是庫函數(shù)而不是運算符,不在編譯器控制權(quán)限之內(nèi),不能夠把執(zhí)行構(gòu)造函數(shù)和析構(gòu)函數(shù)的任務(wù)強加于malloc/free,因此C++語言需要一個能完成動態(tài)內(nèi)存分配和初始化工作的運算符new,和一個能完成清理與釋放內(nèi)存工作的運算符delete。
- new可以認為是malloc加構(gòu)造函數(shù)的執(zhí)行。new出來的指針是直接帶類型信息的。而malloc返回的都是void*指針。newdelete在實現(xiàn)上其實調(diào)用了malloc,free函數(shù)。
- new建立的是一個對象;malloc分配的是一塊內(nèi)存。
2. 面對對象編程
2.1 String類的實現(xiàn)
class MyString
{
public:
MyString();
MyString(const MyString &);
MyString(const char *);
MyString(const size_t,const char);
~MyString();
size_t length();// 字符串長度
bool isEmpty();// 返回字符串是否為空
const char* c_str();// 返回c風(fēng)格的trr的指針
friend ostream& operator<< (ostream&, const MyString&);
friend istream& operator>> (istream&, MyString&);
//add operation
friend MyString operator+(const MyString&,const MyString&);
// compare operations
friend bool operator==(const MyString&,const MyString&);
friend bool operator!=(const MyString&,const MyString&);
friend bool operator<=(const MyString&,const MyString&);
friend bool operator>=(const MyString&,const MyString&);
// 成員函數(shù)實現(xiàn)運算符重載,其實一般需要返回自身對象的,成員函數(shù)運算符重載會好一些
char& operator[](const size_t);
const char& operator[](const size_t)const;
MyString& operator=(const MyString&);
MyString& operator+=(const MyString&);
// 成員操作函數(shù)
MyString substr(size_t pos,const size_t n);
MyString& append(const MyString&);
MyString& insert(size_t,const MyString&);
MyString& erase(size_t,size_t);
int find(const char* str,size_t index=0);
private:
char *p_str;
size_t strLength;
};
2.2 派生類中構(gòu)造函數(shù)與析構(gòu)函數(shù),調(diào)用順序
構(gòu)造函數(shù)的調(diào)用順序總是如下:
- 基類構(gòu)造函數(shù)。如果有多個基類,則構(gòu)造函數(shù)的調(diào)用順序是某類在類派生表中出現(xiàn)的順序,而不是它們在成員初始化表中的順序。
- 成員類對象構(gòu)造函數(shù)。如果有多個成員類對象則構(gòu)造函數(shù)的調(diào)用順序是對象在類中被聲明的順序,而不是它們出現(xiàn)在成員初始化表中的順序。如果有的成員不是類對象,而是基本類型,則初始化順序按照聲明的順序來確定,而不是在初始化列表中的順序。
- 派生類構(gòu)造函數(shù)。
析構(gòu)函數(shù)正好和構(gòu)造函數(shù)相反。
2.3 虛函數(shù)的實現(xiàn)原理
虛函數(shù)表:
編譯器會為每個有虛函數(shù)的類創(chuàng)建一個虛函數(shù)表,該虛函數(shù)表將被該類的所有對象共享。類的虛函數(shù)表是一塊連續(xù)的內(nèi)存,每個內(nèi)存單元中記錄一個JMP指令的地址。類的每個虛函數(shù)占據(jù)虛函數(shù)表中的一塊,如果類中有N個虛函數(shù),那么其虛函數(shù)表將有4N字節(jié)的大小。
編譯器在有虛函數(shù)的類的實例中創(chuàng)建了一個指向這個表的指針,該指針通常存在于對象實例中最前面的位置(這是為了保證取到虛函數(shù)表的有最高的性能)。這意味著可以通過對象實例的地址得到這張?zhí)摵瘮?shù)表,然后就可以遍歷其中函數(shù)指針,并調(diào)用相應(yīng)的函數(shù)。
有虛函數(shù)或虛繼承的類實例化后的對象大小至少為4字節(jié)(確切的說是一個指針的字節(jié)數(shù);說至少是因為還要加上其他非靜態(tài)數(shù)據(jù)成員,還要考慮對齊問題);沒有虛函數(shù)和虛繼承的類實例化后的對象大小至少為1字節(jié)(沒有非靜態(tài)數(shù)據(jù)成員的情況下也要有1個字節(jié)來記錄它的地址)。
哪些函數(shù)適合聲明為虛函數(shù),哪些不能?
- 當(dāng)存在類繼承并且析構(gòu)函數(shù)中有必須要進行的操作時(如需要釋放某些資源,或執(zhí)行特定的函數(shù))析構(gòu)函數(shù)需要是虛函數(shù),否則若使用父類指針指向子類對象,在delete時只會調(diào)用父類的析構(gòu)函數(shù),而不能調(diào)用子類的析構(gòu)函數(shù),從而造成內(nèi)存泄露或達不到預(yù)期結(jié)果;
- 內(nèi)聯(lián)函數(shù)不能為虛函數(shù):內(nèi)聯(lián)函數(shù)需要在編譯階段展開,而虛函數(shù)是運行時動態(tài)綁定的,編譯時無法展開;
- 構(gòu)造函數(shù)不能為虛函數(shù):構(gòu)造函數(shù)在進行調(diào)用時還不存在父類和子類的概念,父類只會調(diào)用父類的構(gòu)造函數(shù),子類調(diào)用子類的,因此不存在動態(tài)綁定的概念;但是構(gòu)造函數(shù)中可以調(diào)用虛函數(shù),不過并沒有動態(tài)效果,只會調(diào)用本類中的對應(yīng)函數(shù);
- 靜態(tài)成員函數(shù)不能為虛函數(shù):靜態(tài)成員函數(shù)是以類為單位的函數(shù),與具體對象無關(guān),虛函數(shù)是與對象動態(tài)綁定的。
2.4 虛繼承的實現(xiàn)原理
為了解決從不同途徑繼承來的同名的數(shù)據(jù)成員在內(nèi)存中有不同的拷貝造成數(shù)據(jù)不一致問題,將共同基類設(shè)置為虛基類。這時從不同的路徑繼承過來的同名數(shù)據(jù)成員在內(nèi)存中就只有一個拷貝,同一個函數(shù)名也只有一個映射。這樣不僅就解決了二義性問題,也節(jié)省了內(nèi)存,避免了數(shù)據(jù)不一致的問題。
構(gòu)造函數(shù)和析構(gòu)函數(shù)的順序:虛基類總是先于非虛基類構(gòu)造,與它們在集成體系中的次序和位置無關(guān)。如果有多個虛基類,則按它們在派生列表中出現(xiàn)的順序從左到右依次構(gòu)造。
#include <iostream>
using namespace std;
class zooAnimal
{
public: zooAnimal(){cout<<"zooAnimal construct"<<endl;}
};
class bear : virtual public zooAnimal
{
public: bear(){cout<<"bear construct"<<endl;}
};
class toyAnimal
{
public: toyAnimal(){cout<<"toyAnimal construct"<<endl;}
};
class character
{
public: character(){cout<<"character construct"<<endl;}
};
class bookCharacter : public character
{
public: bookCharacter(){cout<<"bookCharacter construct"<<endl;}
};
class teddyBear : public bookCharacter, public bear, virtual public toyAnimal
{
public: teddyBear(){cout<<"teddyBear construct"<<endl;}
};
int main()
{
teddyBear();
}
編譯器按照直接基類的聲明順序依次檢查,以確定其中是否含有虛基類。如果有,則先構(gòu)造虛基類,然后按照聲明順序依次構(gòu)造其他非虛基類。構(gòu)造函數(shù)的順序是:zooAnimal, toyAnimal, character, bookCharacter, bear, teddyBear。析構(gòu)過程與構(gòu)造過程正好相反。
3. 內(nèi)存管理
3.1 程序加載時的內(nèi)存分布
在多任務(wù)操作系統(tǒng)中,每個進程都運行在一個屬于自己的虛擬內(nèi)存中,而虛擬內(nèi)存被分為許多頁,并映射到物理內(nèi)存中,被加載到物理內(nèi)存中的文件才能夠被執(zhí)行。這里我們主要關(guān)注程序被裝載后的內(nèi)存布局,其可執(zhí)行文件包含了代碼段,數(shù)據(jù)段,BSS段,堆,棧等部分,其分布如下圖所示。

- 代碼段(.text):用來存放可執(zhí)行文件的機器指令。存放在只讀區(qū)域,以防止被修改。
- 只讀數(shù)據(jù)段(.rodata):用來存放常量存放在只讀區(qū)域,如字符串常量、全局const變量等。
- 可讀寫數(shù)據(jù)段(.data):用來存放可執(zhí)行文件中已初始化全局變量,即靜態(tài)分配的變量和全局變量。
- BSS段(.bss):未初始化的全局變量和局部靜態(tài)變量一般放在.bss的段里,以節(jié)省內(nèi)存空間。
- 堆:用來容納應(yīng)用程序動態(tài)分配的內(nèi)存區(qū)域。當(dāng)程序使用malloc或new分配內(nèi)存時,得到的內(nèi)存來自堆。堆通常位于棧的下方。
- 棧:用于維護函數(shù)調(diào)用的上下文。棧通常分配在用戶空間的最高地址處分配。
- 動態(tài)鏈接庫映射區(qū):如果程序調(diào)用了動態(tài)鏈接庫,則會有這一部分。該區(qū)域是用于映射裝載的動態(tài)鏈接庫。
- 保留區(qū):內(nèi)存中受到保護而禁止訪問的內(nèi)存區(qū)域。
3.2 堆與棧的區(qū)別
1. 申請管理方式
(1)棧:由編譯器自動管理,無需我們手工控制。
(2)堆:堆的申請和釋放工作由程序員控制,容易產(chǎn)生內(nèi)存泄漏。
2. 申請后系統(tǒng)的響應(yīng)
(1)棧:只要棧的剩余空間大于所申請空間,系統(tǒng)將為程序提供內(nèi)存,否則將報異常提示棧溢出。
(2)堆:首先應(yīng)該知道操作系統(tǒng)有一個記錄空閑內(nèi)存地址的鏈表,當(dāng)系統(tǒng)收到程序的申請時,會遍歷該鏈表,尋找第一個空間大于所申請空間的堆結(jié)點,然后將該結(jié)點從空閑結(jié)點鏈表中刪除,并將該結(jié)點的空間分配給程序,另外,對于大多數(shù)系統(tǒng),會在這塊內(nèi)存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內(nèi)存空間。另外,由于找到的堆結(jié)點的大小不一定正好等于申請的大小,系統(tǒng)會自動的將多余的那部分重新放入空閑鏈表中。
3、申請大小的限制
(1)棧:在Windows下,棧是向低地址擴展的數(shù)據(jù)結(jié)構(gòu),是一塊連續(xù)的內(nèi)存的區(qū)域。這句話的意思是棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的,在WINDOWS下,棧的大小是1M(可修改),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。
(2)堆:堆是向高地址擴展的數(shù)據(jù)結(jié)構(gòu),是不連續(xù)的內(nèi)存區(qū)域。這是由于系統(tǒng)是用鏈表來存儲的空閑內(nèi)存地址的,自然是不連續(xù)的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計算機系統(tǒng)中有效的虛擬內(nèi)存。由此可見,堆獲得的空間比較靈活,也比較大。
4、申請效率的比較
(1)棧由系統(tǒng)自動分配,速度較快。但程序員是無法控制的。
(2)堆是由new分配的內(nèi)存,一般速度比較慢,而且容易產(chǎn)生內(nèi)存碎片,不過用起來最方便。另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內(nèi)存,他不是在堆,也不是在棧是直接在進程的地址空間中保留一塊內(nèi)存,雖然用起來最不方便。但是速度快,也最靈活。
5、棧和堆中的存儲內(nèi)容
(1)棧:在函數(shù)調(diào)用時,第一個進棧的是主函數(shù)中后的下一條指令(函數(shù)調(diào)用語句的下一條可執(zhí)行語句)的地址,然后是函數(shù)的各個參數(shù),在大多數(shù)的C編譯器中,參數(shù)是由右往左入棧的,然后是函數(shù)中的局部變量。注意靜態(tài)變量是不入棧的。當(dāng)本次函數(shù)調(diào)用結(jié)束后,局部變量先出棧,然后是參數(shù),最后棧頂指針指向最開始存的地址,也就是主函數(shù)中的下一條指令,程序由該點繼續(xù)運行。
(2)堆:一般是在堆的頭部用一個字節(jié)存放堆的大小。堆中的具體內(nèi)容由程序員安排。
總結(jié):堆和棧相比,由于大量new/delete的使用,容易造成大量的內(nèi)存碎片;并且可能引發(fā)用戶態(tài)和核心態(tài)的切換,內(nèi)存的申請,代價變得更加昂貴。所以棧在程序中是應(yīng)用最廣泛的,就算是函數(shù)的調(diào)用也利用棧去完成,函數(shù)調(diào)用過程中的參數(shù),返回地址,ebp和局部變 量都采用棧的方式存放。所以,推薦大家盡量用棧,而不是用堆。雖然棧有如此眾多的好處,但是向堆申請內(nèi)存更加靈活,有時候分配大量的內(nèi)存空間,還是用堆好一些。
3.3 常見的內(nèi)存錯誤及其對策
內(nèi)存分配未成功,卻使用了它,因為沒有意識到內(nèi)存分配會不成功。
解決辦法:在使用內(nèi)存之前檢查指針是否為NULL。如果指針p是函數(shù)的參數(shù),那么在函數(shù)的入口處用assert(p!=NULL)進行檢查。如果是用malloc或new來申請內(nèi)存,應(yīng)該用if(p==NULL) 或if(p!=NULL)進行防錯處理。內(nèi)存分配雖然成功,但是尚未初始化就引用它。犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為內(nèi)存的缺省初值全為零,導(dǎo)致引用初值錯誤(例如數(shù)組)。
解決方法:不要忘記為數(shù)組和動態(tài)內(nèi)存賦初值,即便是賦零值也不可省略。防止將未被初始化的內(nèi)存作為右值使用。內(nèi)存分配成功并且已經(jīng)初始化,但操作越過了內(nèi)存的邊界。例如在使用數(shù)組時經(jīng)常發(fā)生下標“多1”或者“少1”的操作。特別是在for循環(huán)語句中,循環(huán)次數(shù)很容易搞錯,導(dǎo)致數(shù)組操作越界。
解決方法:避免數(shù)組或指針的下標越界,特別要當(dāng)心發(fā)生“多1”或者“少1”操作。忘記了釋放內(nèi)存,造成內(nèi)存泄露。含有這種錯誤的函數(shù)每被調(diào)用一次就丟失一塊內(nèi)存。剛開始時系統(tǒng)的內(nèi)存充足,你看不到錯誤。終有一次程序突然死掉,系統(tǒng)出現(xiàn)提示:內(nèi)存耗盡。
解決方法:動態(tài)內(nèi)存的申請與釋放必須配對,程序中malloc與free的使用次數(shù)一定要相同,否則肯定有錯誤(new/delete同理)。釋放了內(nèi)存卻繼續(xù)使用它。有三種情況:(1)程序中的對象調(diào)用關(guān)系過于復(fù)雜,實在難以搞清楚某個對象究竟是否已經(jīng)釋放了內(nèi)存,此時應(yīng)該重新設(shè)計數(shù)據(jù)結(jié)構(gòu),從根本上解決對象管理的混亂局面。(2)函數(shù)的return語句寫錯了,注意不要返回指向“棧內(nèi)存”的“指針”或者“引用”,因為該內(nèi)存在函數(shù)體結(jié)束時被自動銷毀。(3)使用free或delete釋放了內(nèi)存后,沒有將指針設(shè)置為NULL。導(dǎo)致產(chǎn)生“野指針”。
解決方法:用free或delete釋放了內(nèi)存之后,立即將指針設(shè)置為NULL,防止產(chǎn)生“野指針”。
3.4 智能指針
智能指針是在 <memory> 標頭文件中的std命名空間中定義的,該指針用于確保程序不存在內(nèi)存和資源泄漏且是異常安全的。它們對RAII“獲取資源即初始化”編程至關(guān)重要,RAII的主要原則是為將任何堆分配資源(如動態(tài)分配內(nèi)存或系統(tǒng)對象句柄)的所有權(quán)提供給其析構(gòu)函數(shù)包含用于刪除或釋放資源的代碼以及任何相關(guān)清理代碼的堆棧分配對象。大多數(shù)情況下,當(dāng)初始化原始指針或資源句柄以指向?qū)嶋H資源時,會立即將指針傳遞給智能指針。在C++11中,定義了3種智能指針(unique_ptr、shared_ptr、weak_ptr),并刪除了C++98中的auto_ptr。
智能指針的設(shè)計思想:將基本類型指針封裝為類對象指針(這個類肯定是個模板,以適應(yīng)不同基本類型的需求),并在析構(gòu)函數(shù)里編寫delete語句刪除指針指向的內(nèi)存空間。
unique_ptr 只允許基礎(chǔ)指針的一個所有者。unique_ptr小巧高效;大小等同于一個指針且支持rvalue引用,從而可實現(xiàn)快速插入和對STL集合的檢索。
shared_ptr采用引用計數(shù)的智能指針,主要用于要將一個原始指針分配給多個所有者(例如,從容器返回了指針副本又想保留原始指針時)的情況。當(dāng)所有的shared_ptr所有者超出了范圍或放棄所有權(quán),才會刪除原始指針。大小為兩個指針;一個用于對象,另一個用于包含引用計數(shù)的共享控制塊。最安全的分配和使用動態(tài)內(nèi)存的方法是調(diào)用make_shared標準庫函數(shù),此函數(shù)在動態(tài)分配內(nèi)存中分配一個對象并初始化它,返回對象的shared_ptr。
智能指針支持的操作
- 使用重載的
->和*運算符訪問對象。 - 使用get成員函數(shù)獲取原始指針,提供對原始指針的直接訪問。你可以使用智能指針管理你自己的代碼中的內(nèi)存,還能將原始指針傳遞給不支持智能指針的代碼。
- 使用刪除器定義自己的釋放操作。
- 使用release成員函數(shù)的作用是放棄智能指針對指針的控制權(quán),將智能指針置空,并返回原始指針。(只支持unique_ptr)
- 使用reset釋放智能指針對對象的所有權(quán)。
智能指針的使用示例:
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class base
{
public:
base(int _a): a(_a) {cout<<"構(gòu)造函數(shù)"<<endl;}
~base() {cout<<"析構(gòu)函數(shù)"<<endl;}
int a;
};
int main()
{
unique_ptr<base> up1(new base(2));
// unique_ptr<base> up2 = up1; //編譯器提示未定義
unique_ptr<base> up2 = move(up1); //轉(zhuǎn)移對象的所有權(quán)
// cout<<up1->a<<endl; //運行時錯誤
cout<<up2->a<<endl; //通過解引用運算符獲取封裝的原始指針
up2.reset(); // 顯式釋放內(nèi)存
shared_ptr<base> sp1(new base(3));
shared_ptr<base> sp2 = sp1; //增加引用計數(shù)
cout<<"共享智能指針的數(shù)量:"<<sp2.use_count()<<endl; //2
sp1.reset(); //
cout<<"共享智能指針的數(shù)量:"<<sp2.use_count()<<endl; //1
cout<<sp2->a<<endl;
auto sp3 = make_shared<base>(4);//利用make_shared函數(shù)動態(tài)分配內(nèi)存
}
4. C++對象內(nèi)存模型
在C++中有兩種類的數(shù)據(jù)成員:static和nonstatic,以及三種類的成員函數(shù):static、nonstatic和virtual。在C++對象模型中,非靜態(tài)數(shù)據(jù)成員被配置于每一個類的對象之中,靜態(tài)數(shù)據(jù)成員則被存放在所有的類對象之外;靜態(tài)及非靜態(tài)成員函數(shù)也被放在類對象之外,虛函數(shù)則通過以下兩個步驟支持:
- 每一個類產(chǎn)生出一堆指向虛函數(shù)的指針,放在表格之中,這個表格被稱為虛函數(shù)表(virtual table, vtbl)。
- 每一個類對象被添加了一個指針,指向相關(guān)的虛函數(shù)表,通常這個指針被稱為vptr。vptr的設(shè)定和重置都由每一個類的構(gòu)造函數(shù)、析構(gòu)函數(shù)和拷貝賦值運算符自動完成。另外,虛函數(shù)表地址的前面設(shè)置了一個指向type_info的指針,RTTI(Run Time Type Identification)運行時類型識別是由編譯器在編譯器生成的特殊類型信息,包括對象繼承關(guān)系,對象本身的描述,RTTI是為多態(tài)而生成的信息,所以只有具有虛函數(shù)的對象在會生成。
4.1 繼承下的對象內(nèi)存模型
C++支持單一繼承、多重繼承和虛繼承。在虛繼承的情況下,虛基類不管在繼承鏈中被派生多少次,永遠只會存在一個實體。
單一繼承,繼承關(guān)系為class Derived : public Base。其對象的內(nèi)存布局為:虛函數(shù)表指針、Base類的非static成員變量、Derived類的非static成員變量。
多重繼承,繼承關(guān)系為class Derived : public Base1, public Base2。其對象的內(nèi)存布局為:基類Base1子對象和基類Base2子對象及Derived類的非static成員變量組成。基類子對象包括其虛函數(shù)表指針和其非static的成員變量。
重復(fù)繼承,繼承關(guān)系如下。Derived類的對象的內(nèi)存布局與多繼承相似,但是可以看到基類Base的子對象在Derived類的對象的內(nèi)存中存在一份拷貝。這樣直接使用Derived中基類Base的相關(guān)成員時,就會引發(fā)歧義,可使用多重虛擬繼承消除之。
class Base1 : public Base
class Base2: public Base
class Derived : public Base1, public Base2
虛繼承,繼承關(guān)系如下。其對象的內(nèi)存布局與重復(fù)繼承的類的對象的內(nèi)存分布類似,但是基類Base的子對象沒有拷貝一份,在對象的內(nèi)存中僅存在在一個Base類的子對象。但是它的非static成員變量放置在對象的末尾處。
class Base1 : virtual public Base
class Base2: virtual public Base
class Derived : public Base1, public Base2

5. 常見的設(shè)計模式
5.1 單例模式
當(dāng)僅允許類的一個實例在應(yīng)用中被創(chuàng)建的時候,我們使用單例模式(Singleton Pattern)。它保護類的創(chuàng)建過程來確保只有一個實例被創(chuàng)建,它通過設(shè)置類的構(gòu)造方法為私有(private)來實現(xiàn)。要獲得類的實例,單例類可以提供一個方法,如GetInstance(),來返回類的實例。該方法是唯一可以訪問類來創(chuàng)建實例的方法。
優(yōu)點:(1)由于單例模式在內(nèi)存中只有一個實例,減少了內(nèi)存開支,特別是一個對象需要頻繁地創(chuàng)建、銷毀時,而且創(chuàng)建或銷毀時性能又無法優(yōu)化,單例模式的優(yōu)勢就非常明顯。(2)減少了系統(tǒng)的性能開銷,當(dāng)一個對象的產(chǎn)生需要比較多的資源時,如讀取配置、產(chǎn)生其他依賴對象時,則可以通過在應(yīng)用啟動時直接產(chǎn)生一個單例對象,然后永久駐留內(nèi)存的方式來解決。(3)避免對資源的多重占用。如避免對同一個資源文件的同時寫操作。(4)單例模式可以在系統(tǒng)設(shè)置全局的訪問點,優(yōu)化和共享資源訪問。
缺點:單例模式一般沒有接口,擴展困難。不利于測試。
使用場景:(1)在整個項目中需要一個共享訪問點或共享數(shù)據(jù)。(2)創(chuàng)建一個對象需要消耗的資源過多,如要訪問IO和數(shù)據(jù)庫等資源。(3)需要定義大量的靜態(tài)常量和靜態(tài)方法的環(huán)境。
實現(xiàn):懶漢實現(xiàn)與餓漢實現(xiàn)
懶漢實現(xiàn),即實例化在對象首次被訪問時進行??梢允褂妙惖乃接徐o態(tài)指針變量指向類的唯一實例,并用一個公有的靜態(tài)方法獲取該實例。同時需將默認構(gòu)造函數(shù)聲明為private,防止用戶調(diào)用默認構(gòu)造函數(shù)創(chuàng)建對象。
//Singleton.h
class Singleton
{
public:
static Singleton* GetInstance();
private:
Singleton() {}
static Singleton *m_pInstance;
};
//Singleton.cpp
Singleton* Singleton::m_pInstance = NULL;
Singleton* Singleton::GetInstance()
{
if (m_Instance == NULL)
{
Lock();
if (m_Instance == NULL)
{
m_Instance = new Singleton();
}
UnLock();
}
return m_pInstance;
}
該類有以下特征:
- 它的構(gòu)造函數(shù)是私有的,這樣就不能從別處創(chuàng)建該類的實例。
- 它有一個唯一實例的靜態(tài)指針m_pInstance,且是私有的。
- 它有一個公有的函數(shù),可以獲取這個唯一的實例,并在需要的時候創(chuàng)建該實例。
此處進行了兩次m_Instance == NULL的判斷,是借鑒了Java的單例模式實現(xiàn)時,使用的所謂的“雙檢鎖”機制。因為進行一次加鎖和解鎖是需要付出對應(yīng)的代價的,而進行兩次判斷,就可以避免多次加鎖與解鎖操作,同時也保證了線程安全。
上面的實現(xiàn)存在一個問題,就是沒有提供刪除對象的方法。一個妥善的方法是讓這個類自己知道在合適的時候把自己刪除。程序在結(jié)束的時候,系統(tǒng)會自動析構(gòu)所有的全局變量。事實上,系統(tǒng)也會析構(gòu)所有的類的靜態(tài)成員變量,就像這些靜態(tài)成員也是全局變量一樣。利用這個特征,我們可以在單例類中定義一個這樣的靜態(tài)成員變量,而它的唯一工作就是在析構(gòu)函數(shù)中刪除單例類的實例。如下面的代碼中的CGarbo類(Garbo意為垃圾工人):
class Singleton
{
public:
static Singleton* GetInstance() {}
private:
Singleton() {};
static Singleton *m_pInstance;
//CGarbo類的唯一工作就是在析構(gòu)函數(shù)中刪除CSingleton的實例
class CGarbo
{
public:
~CGarbo()
{
if (Singleton::m_pInstance != NULL)
delete Singleton::m_pInstance;
}
};
//定義一個靜態(tài)成員,在程序結(jié)束時,系統(tǒng)會調(diào)用它的析構(gòu)函數(shù)
static CGarbo Garbo;
};
類CGarbo被定義為Singleton的私有內(nèi)嵌類,以防該類被在其他地方濫用。程序運行結(jié)束時,系統(tǒng)會調(diào)用Singleton的靜態(tài)成員Garbo的析構(gòu)函數(shù),該析構(gòu)函數(shù)會刪除單例的唯一實例。
餓漢實現(xiàn)方法:在程序開始時就自行創(chuàng)建實例。如果說懶漢實現(xiàn)是“時間換空間”,那么餓漢實現(xiàn)就是“空間換時間”,因為一開始就創(chuàng)建了實例,所以每次用到的之后直接返回就好了。
//Singleton.h
class Singleton
{
public:
static Singleton* GetInstance();
private:
Singleton() {}
static Singleton *m_pInstance;
class CGarbo
{
public:
~CGarbo()
{
if (Singleton::m_pInstance != NULL)
delete Singleton::m_pInstance;
}
};
static CGarbo garbo;
};
//Singleton.cpp
Singleton* Singleton::m_pInstance = new Singleton();
Singleton* Singleton::GetInstance()
{
return m_pInstance;
}
5.2 簡單工廠模式
簡單工廠模式的主要特點是需要在工廠類中做判斷,從而創(chuàng)造相應(yīng)的產(chǎn)品。當(dāng)增加新的產(chǎn)品時,就需要修改工廠類。

例子:有一家生產(chǎn)處理器核的廠家,它只有一個工廠,能夠生產(chǎn)兩種型號的處理器核。客戶需要什么樣的處理器核,一定要顯式地告訴生產(chǎn)工廠。
enum CTYPE {COREA, COREB};
class SingleCore
{
public:
virtual void Show() = 0;
};
//單核A
class SingleCoreA: public SingleCore
{
public:
void Show() { cout<<"SingleCore A"<<endl; }
};
//單核B
class SingleCoreB: public SingleCore
{
public:
void Show() { cout<<"SingleCore B"<<endl; }
};
//唯一的工廠,可以生產(chǎn)兩種型號的處理器核,在內(nèi)部判斷
class Factory
{
public:
SingleCore* CreateSingleCore(enum CTYPE ctype)
{
if (ctype == COREA) //工廠內(nèi)部判斷
return new SingleCoreA(); //生產(chǎn)核A
else if (ctype == COREB)
return new SingleCoreB(); //生產(chǎn)核B
else
return NULL;
}
};
這樣設(shè)計的主要缺點之前也提到過,就是要增加新的核類型時,就需要修改工廠類。這就違反了開放封閉原則:軟件實體(類、模塊、函數(shù))可以擴展,但是不可修改。于是,工廠方法模式出現(xiàn)了。
5.3 工廠方法模式
工廠方法模式是指定義一個用于創(chuàng)建對象的接口,讓子類決定實例化哪一個類。工廠方法模式使一個類的實例化延遲到其子類。

例子:這家生產(chǎn)處理器核的廠家賺了不少錢,于是決定再開設(shè)一個工廠專門用來生產(chǎn)B型號的單核,而原來的工廠專門用來生產(chǎn)A型號的單核。這時,客戶要做的是找好工廠,比如要A型號的核,就找A工廠要;否則找B工廠要,不再需要告訴工廠具體要什么型號的處理器核了。
class SingleCore
{
public:
virtual void Show() = 0;
};
//單核A
class SingleCoreA: public SingleCore
{
public:
void Show() { cout<<"SingleCore A"<<endl; }
};
//單核B
class SingleCoreB: public SingleCore
{
public:
void Show() { cout<<"SingleCore B"<<endl; }
};
class Factory
{
public:
virtual SingleCore* CreateSingleCore() = 0;
};
//生產(chǎn)A核的工廠
class FactoryA: public Factory
{
public:
SingleCoreA* CreateSingleCore() { return new SingleCoreA(); }
};
//生產(chǎn)B核的工廠
class FactoryB: public Factory
{
public:
SingleCoreB* CreateSingleCore() { return new SingleCoreB(); }
};
工廠方法模式也有缺點,每增加一種產(chǎn)品,就需要增加一個對象的工廠。如果這家公司發(fā)展迅速,推出了很多新的處理器核,那么就要開設(shè)相應(yīng)的新工廠。在C++實現(xiàn)中,就是要定義一個個的工廠類。顯然,相比簡單工廠模式,工廠方法模式需要更多的類定義。
5.4 抽象工廠模式
抽象工廠模式的定義為提供一個創(chuàng)建一系列相關(guān)或相互依賴對象的接口,而無需指定它們具體的類。

例子:這家公司的技術(shù)不斷進步,不僅可以生產(chǎn)單核處理器,也能生產(chǎn)多核處理器?,F(xiàn)在簡單工廠模式和工廠方法模式都鞭長莫及。這家公司還是開設(shè)兩個工廠,一個專門用來生產(chǎn)A型號的單核多核處理器,而另一個工廠專門用來生產(chǎn)B型號的單核多核處理器。
//單核
class SingleCore
{
public:
virtual void Show() = 0;
};
class SingleCoreA: public SingleCore
{
public:
void Show() { cout<<"Single Core A"<<endl; }
};
class SingleCoreB :public SingleCore
{
public:
void Show() { cout<<"Single Core B"<<endl; }
};
//多核
class MultiCore
{
public:
virtual void Show() = 0;
};
class MultiCoreA : public MultiCore
{
public:
void Show() { cout<<"Multi Core A"<<endl; }
};
class MultiCoreB : public MultiCore
{
public:
void Show() { cout<<"Multi Core B"<<endl; }
};
//工廠
class CoreFactory
{
public:
virtual SingleCore* CreateSingleCore() = 0;
virtual MultiCore* CreateMultiCore() = 0;
};
//工廠A,專門用來生產(chǎn)A型號的處理器
class FactoryA :public CoreFactory
{
public:
SingleCore* CreateSingleCore() { return new SingleCoreA(); }
MultiCore* CreateMultiCore() { return new MultiCoreA(); }
};
//工廠B,專門用來生產(chǎn)B型號的處理器
class FactoryB : public CoreFactory
{
public:
SingleCore* CreateSingleCore() { return new SingleCoreB(); }
MultiCore* CreateMultiCore() { return new MultiCoreB(); }
};