因?yàn)镃++語(yǔ)言沒(méi)有內(nèi)嵌的GC機(jī)制,C++程序員不得不操心動(dòng)態(tài)內(nèi)存管理的問(wèn)題。而工程中很多內(nèi)存管理的問(wèn)題都是由于違背了rule of three的原則惜互。
按照rule of three
的要求,如果一個(gè)類(lèi)顯式地定義下列其中一個(gè)成員函數(shù),那么其他兩個(gè)成員函數(shù)也應(yīng)該一起被定義橄维。也就是說(shuō)這三個(gè)函數(shù)要么都不定義,要么都要定義拴曲。
- 析構(gòu)函數(shù)(
destructor
) - 復(fù)制構(gòu)造函數(shù)(
copy constructor
) - 復(fù)制賦值運(yùn)算符(
copy assignment operator
)
下面通過(guò)一個(gè)簡(jiǎn)單的例子說(shuō)明違反這個(gè)原則所帶來(lái)的潛在的內(nèi)存問(wèn)題争舞。
學(xué)生管理系統(tǒng)
學(xué)生類(lèi)的實(shí)現(xiàn)
Student
類(lèi),用來(lái)存儲(chǔ)學(xué)生的姓名及年齡澈灼。在構(gòu)造函數(shù)中根據(jù)name
所指向的字符串的長(zhǎng)度按需分配內(nèi)存竞川,在析構(gòu)中釋放分配的內(nèi)存以防止內(nèi)存泄露。
struct Student {
Student(const char * theName, int theAge)
: name(createName(theName))
, age(theAge) {
std::cout << "construct Student" << std::endl;
}
~Student() {
std::cout << "destruct Student" << std::endl;
if (name != NULL) {
delete[] name;
name = NULL;
}
}
private:
static char * createName(const char * theName) {
if (theName == NULL) {
return NULL;
}
char * ptr = new(std::nothrow) char[strlen(theName) + 1];
if (ptr == NULL) {
return NULL;
}
strcpy(ptr, theName);
return ptr;
}
private:
char * name;
int age;
};
學(xué)校類(lèi)的實(shí)現(xiàn)
學(xué)校類(lèi)用來(lái)存儲(chǔ)學(xué)校里所有的學(xué)生信息
struct School {
void addStudent(const Student & student) {
students.push_back(student);
}
private:
std::vector<Student> students;
};
在main
函數(shù)中我們構(gòu)造alice
與tom
兩名學(xué)生叁熔,并把他們加入到學(xué)校中委乌。
int main() {
Student tom("Tom", 17);
Student alice("Alice", 16);
School school;
school.addStudent(tom);
school.addStudent(alice);
return 0;
}
結(jié)果分析
編譯運(yùn)行這段代碼,發(fā)現(xiàn)程序運(yùn)行出現(xiàn)了內(nèi)存錯(cuò)誤:
從運(yùn)行結(jié)果上看荣回,
Student
的構(gòu)造函數(shù)被執(zhí)行了兩次遭贸,析構(gòu)函數(shù)里的log
輸出了三次。我們顯式構(gòu)造了tom
和alice
兩個(gè)實(shí)例心软,這兩個(gè)對(duì)象在程序退出時(shí)會(huì)分別分別調(diào)用一次析構(gòu)函數(shù)革砸。那么,多出來(lái)的哪次析構(gòu)函數(shù)調(diào)用是哪兒觸發(fā)的呢糯累?答案就在School
類(lèi)中算利。School
類(lèi)的成員變量std::vector<Student> students;
用來(lái)存儲(chǔ)所有學(xué)生的信息,在students.push_back
函數(shù)中泳姐,會(huì)調(diào)用Student
類(lèi)的拷貝構(gòu)造函數(shù)來(lái)構(gòu)造一個(gè)Student
對(duì)象效拭,插入到vector
的尾部,程序退出時(shí),變量school
被析構(gòu)缎患,school
的成員變量students
以及students
里的每一個(gè)Student
實(shí)例都會(huì)會(huì)被析構(gòu)慕的。可是
Students
類(lèi)中并沒(méi)有實(shí)現(xiàn)拷貝構(gòu)造函數(shù),為什么能夠編譯通過(guò)呢挤渔?原來(lái)是編譯器自動(dòng)幫我們實(shí)現(xiàn)了它肮街。
special member functions
C++標(biāo)準(zhǔn)中規(guī)定(參考c++98
的標(biāo)準(zhǔn)中Special member functions
一節(jié))
如果一個(gè)類(lèi)沒(méi)有顯式地聲明以下四個(gè)函數(shù),編譯器將自動(dòng)生成默認(rèn)版本判导。
- 構(gòu)造函數(shù)(
constructor
) - 拷貝構(gòu)造函數(shù)(
copy constructor
) - 拷貝賦值操作符(
copy assignment
) - 析構(gòu)函數(shù)(
destructor
)
由于Student的實(shí)現(xiàn)中并沒(méi)有顯式的聲明拷貝構(gòu)造函數(shù)嫉父,編譯器將會(huì)自動(dòng)為Student
類(lèi)創(chuàng)建一個(gè)拷貝構(gòu)造函數(shù)。而編譯器創(chuàng)建的版本只是單純地將源對(duì)象的每個(gè)非static
的成員變量拷貝到目標(biāo)對(duì)象眼刃。對(duì)于Student
類(lèi)绕辖,編譯器自動(dòng)生成的拷貝構(gòu)造函數(shù)實(shí)現(xiàn)如下:
Student::Student(const Student & rhs) {
this->name = rhs.name;
this->age = rhs.age;
}
可以看到,在編譯器自動(dòng)生成的這個(gè)版本中擂红,rhs.name
所指向的這段內(nèi)存的指針被賦給了this->name
仪际,在這兩個(gè)對(duì)象的析構(gòu)函數(shù)里,都會(huì)嘗試刪除這段內(nèi)存昵骤,從而導(dǎo)致了double free
的內(nèi)存錯(cuò)誤树碱。
copy assignment
Student tom("Tom", 17);
Student clone("clone", 10);
clone = tom;
閱讀上面的代碼,思考在函數(shù)退出時(shí)會(huì)發(fā)生啥变秦?
在這段代碼中成榜,我們通過(guò)copy assignment
來(lái)clone
了一個(gè)tom
。由于Student
類(lèi)中沒(méi)有顯式地聲明copy assignment
伴栓,編譯器為我們自動(dòng)生成了copy assignment
的默認(rèn)實(shí)現(xiàn)伦连。同拷貝構(gòu)造函數(shù)一樣雨饺,這個(gè)函數(shù)也只是簡(jiǎn)單地把源對(duì)象中的每一個(gè)non-static
的成員變量拷貝到目標(biāo)對(duì)象钳垮。對(duì)于Student
類(lèi),編譯器自動(dòng)生成的copy assignment
函數(shù)的如下:
Student & Student::operator = (const Student & rhs) {
this->name = rhs.name;
this->age = rhs.age;
return *this;
}
在執(zhí)行clone = tom
時(shí)额港,clone
對(duì)象的copy assignment
被調(diào)用饺窿。clone
對(duì)象里的name
指針指向了tom
對(duì)象的name
所指向的指針。在程序退出時(shí)移斩,保存字符原先clone.name
所指向的內(nèi)存不會(huì)被釋放肚医,而tom.name
所指向的內(nèi)存卻被釋放了兩次。(一次是在tom
的析構(gòu)函數(shù)中向瓷,一次是在clone
的析構(gòu)函數(shù)中)
rule of three
上面例子中出現(xiàn)的兩個(gè)內(nèi)存問(wèn)題肠套,都是因?yàn)闆](méi)有遵循role of three
。Student
類(lèi)聲明并實(shí)現(xiàn)了析構(gòu)函數(shù)猖任,卻沒(méi)有同時(shí)實(shí)現(xiàn)copy constructor
與copy assignment operator
你稚。
這三個(gè)成員函數(shù)屬于Special member functions
,如果沒(méi)有被顯式的定義或禁止,編譯器會(huì)自動(dòng)創(chuàng)建它們刁赖。如果一個(gè)類(lèi)顯式地定義了其中的一個(gè)函數(shù)搁痛,最可能的原因是這個(gè)類(lèi)牽扯到資源的管理,編譯器自動(dòng)生成的版本滿足不了類(lèi)資源管理的需求宇弛,因此需要重新實(shí)現(xiàn)鸡典。那么另外兩個(gè)函數(shù),也理應(yīng)做相應(yīng)的資源相關(guān)的操作枪芒。
而如果一個(gè)類(lèi)管理的資源不支持copy
與共享彻况,就應(yīng)該明確地拒絕,顯式的禁止copy constructor
與copy assignment operator
病苗,以防止因?qū)ο罂截惗鴰?lái)的資源使用錯(cuò)誤疗垛。
Student的參考實(shí)現(xiàn)1
struct Student {
Student(const char * theName, int theAge)
: name(createName(theName))
, age(theAge) {
std::cout << "construct Student" << std::endl;
}
~Student() {
std::cout << "destruct Student" << std::endl;
if (name != NULL) {
delete[] name;
name = NULL;
}
}
Student(const Student & rhs)
: name(createName(rhs.name))
, age(rhs.age) {
}
Student & operator = (const Student & rhs) {
if (this->name != NULL) {
delete [] this->name;
this->name = NULL;
}
if (rhs.name != NULL) {
this->name = createName(rhs.name);
}
this->age = rhs.age;
return *this;
}
private:
static char * createName(const char * theName) {
if (theName == NULL) {
return NULL;
}
char * ptr = new(std::nothrow) char[strlen(theName) + 1];
if (ptr == NULL) {
return NULL;
}
strcpy(ptr, theName);
return ptr;
}
private:
char * name;
int age;
};
Student的參考實(shí)現(xiàn)2
在克隆人技術(shù)實(shí)現(xiàn)之前,copy
一個(gè)人沒(méi)有意義硫朦,因此需要顯式地禁止copy constructor
與copy assignment operator
(將這兩個(gè)函數(shù)聲明為私有而不提供實(shí)現(xiàn))贷腕。
struct Student {
Student(const char * theName, int theAge)
: name(createName(theName))
, age(theAge) {
std::cout << "construct Student" << std::endl;
}
~Student() {
std::cout << "destruct Student" << std::endl;
if (name != NULL) {
delete[] name;
name = NULL;
}
}
private:
static char * createName(const char * theName) {
if (theName == NULL) {
return NULL;
}
char * ptr = new(std::nothrow) char[strlen(theName) + 1];
if (ptr == NULL) {
return NULL;
}
strcpy(ptr, theName);
return ptr;
}
private:
Student(const Student & rhs);
Student & operator = (const Student & rhs);
private:
char * name;
int age;
};
School的實(shí)現(xiàn)
由于Student類(lèi)禁止了拷貝構(gòu)造函數(shù),而vector
在push_back
的時(shí)候需要調(diào)用到對(duì)象的拷貝構(gòu)造函數(shù)咬展,因此先前的代碼會(huì)編譯不過(guò)泽裳,為了解決這個(gè)問(wèn)題,可以在vector
中存儲(chǔ)Student
的指針破婆。在容器中存儲(chǔ)指針涮总,消除了對(duì)象的構(gòu)造拷貝,提高了運(yùn)行效率祷舀,但引入了另外一個(gè)復(fù)雜度瀑梗,即對(duì)象創(chuàng)建與刪除職責(zé)的管理。
rule of five(C++11)
如果一個(gè)類(lèi)顯式定義了destructor
裳扯、copy constructor
抛丽、copy assignment operator
三個(gè)函數(shù)中的任意一個(gè)函數(shù),編譯器不再自動(dòng)生成move constructor
及move assignment operator
的默認(rèn)實(shí)現(xiàn)饰豺。如果一個(gè)類(lèi)支持移動(dòng)語(yǔ)義(move semantics
)亿鲜,需要顯式地聲明并實(shí)現(xiàn)這兩個(gè)函數(shù)。這樣冤吨,對(duì)于支持移動(dòng)語(yǔ)義的類(lèi)蒿柳,這五個(gè)函數(shù)應(yīng)該同時(shí)出現(xiàn)。
參考資料
- 《effective C++》條款05漩蟆,了解C++默默編寫(xiě)并調(diào)用哪些函數(shù)
- 《effective C++》條款06垒探,若不想使用編譯器自動(dòng)生成的函數(shù),就該明確拒絕
- 《C++編程規(guī)范》第52條怠李,一致地進(jìn)行復(fù)制和銷(xiāo)毀
后記
文中例子的運(yùn)行結(jié)果基于mac os + clang
圾叼,clang版本信息如下:
- Apple LLVM version 7.0.2 (clang-700.1.81)
- Target: x86_64-apple-darwin14.5.0
- Thread model: posix
感謝M23指正仔引,在Linux + gcc
的環(huán)境下,double free
的例子中褐奥,析構(gòu)函數(shù)里的log輸出了四次咖耘。在mac os + clang
環(huán)境下log輸出了三次并不因?yàn)槲鰳?gòu)函數(shù)應(yīng)該被調(diào)用三次,而是因?yàn)閮?nèi)存異常終止了log的輸出撬码。
至于Linux + gcc
與mac os + clang
異常處理上的不同是另外一個(gè)話題儿倒,在此不做討論。