第二周講解的是仍然是object-based programming,以String類為例說明包含指針成員的類的寫法。
包含指針成員的類需要自己實(shí)現(xiàn)三個特殊函數(shù)(稱為Big Three,在維基百科上被稱為Rule of Three?):
1)拷貝構(gòu)造函數(shù)(copy constructor) 2)拷貝賦值操作符函數(shù)(copy assignment operator) 3)析構(gòu)函數(shù)(destructor)
本周中給出的函數(shù)原型如下:
class String
{
public:
String(const char* cstr=0); //構(gòu)造函數(shù)
String(const String& str); //拷貝構(gòu)造函數(shù)
String& operator=(const String& str); //拷貝賦值操作符
~String(); //析構(gòu)函數(shù)
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
對于不包含指針成員的類,通常不需要編寫B(tài)ig Three,編譯器會自動生成這三個函數(shù),這些自動生成的函數(shù)會將源對象成員一一拷貝(淺拷貝,對于指針僅拷貝指針的值,不會拷貝所指向內(nèi)容)到目標(biāo)對象。
如果使用默認(rèn)的拷貝構(gòu)造函數(shù)和拷貝賦值操作操作符函數(shù),那么執(zhí)行拷貝后,兩個String對象的指針可能指向同樣的內(nèi)容,另外一個對象指向的內(nèi)存可能泄露。
String的構(gòu)造函數(shù)實(shí)現(xiàn)代碼如下:
inline
String::String(const char* cstr)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}
構(gòu)造函數(shù)中會判斷傳入字符串是否為空,不空則分配新的空間(大小為給定參數(shù)長度+1)給m_data,然后將傳入字符串考拷貝給m_data.否則,只需要分配一個字節(jié)長度給m_data并初始化成'\0'.
而拷貝構(gòu)造函數(shù)更加特殊,它的用法如下:
String s1("hello");
String s2(s1);
拷貝構(gòu)造函數(shù)語法給構(gòu)造函數(shù)類似,只是其參數(shù)是類類型的對象,本例中實(shí)現(xiàn)如下:
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
這個函數(shù)就是分配合適大小空間并將實(shí)參str的m_data賦給它的m_data。
由于同一個類的多個對象之間互為友元,所以可以直接訪問str實(shí)參中的m_data.
拷貝賦值操作符函數(shù)用法如下:
String s1("hello");
String s2 = s1;
實(shí)現(xiàn)如下:
inline
String& String::operator=(const String& str)
{
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
拷貝復(fù)制操作符函數(shù)中,最前面兩句代碼判斷是否對自己賦值,如果對自己賦值,那么可以直接返回自己,否則,將自己的m_data釋放,然后重新分配新空間給自己的m_data并將參數(shù)中存儲的字符串信息拷貝給自己的m_data.
這個函數(shù)中必須考慮自我賦值,最前面兩行代碼,那么釋放m_data后之后這個對象的m_data中就沒有有效數(shù)據(jù),后面執(zhí)行再執(zhí)行strlen就沒法得到正確的結(jié)果。
在c++,對象可能在分配于不同的區(qū)域,例如棧(stack)或者堆(heap)上.堆是操作系統(tǒng)中提供的一塊空間,程序可動態(tài)分配(通過malloc或者new)從中獲取若干空間。普通函數(shù)內(nèi)定義的局部變量通常是stack object(通常稱為auto object),在作用域結(jié)束后會被自動清理。而棧上可以分配static 對象,棧上的對象在調(diào)用該函數(shù)時才會被創(chuàng)建,在程序結(jié)束時才會被清理掉。在所有函數(shù)之外定義的沒有static聲明的變量被稱為全局變量,全局變量在程序執(zhí)行前會被創(chuàng)建出來,在程序退出前被釋放掉。
以上一周Complex類為例說明使用new創(chuàng)建新對象時編譯器所做的事情,例如我們使用下面的代碼:
Complex *pc = new Complex(1,2);
編譯器轉(zhuǎn)化成下面三條語句:
void *mem? = operator new(sizeof(Complex));
pc = static_cast<Complex*>(mem);
pc->Complex::Complex(1,2);
其中operator函數(shù)內(nèi)部調(diào)用了malloc函數(shù)。
在釋放pc指針的時候使用下面代碼:
delete pc;
編譯器會轉(zhuǎn)化成下面兩條語句:
Complex::~Complex(pc);
operator delete(pc);
其中operator delete函數(shù)內(nèi)部調(diào)用了free(pc)。
使用new動態(tài)分配內(nèi)存時,在VC下編譯器會多分配一些空間(下圖左邊是Complex在debug和release模式下分配堆空間,右邊是String對象在debug或release模式下分配的堆空間)
debug模式下會多處32個byte的debug header和4個字節(jié)的debug footer。在其前后還有2個描述其結(jié)構(gòu)體大小的字段,注意結(jié)構(gòu)體大小需要是4個字節(jié)的倍數(shù),所以可能還需要適當(dāng)?shù)膒adding.
并且大小51h的最后一位用于區(qū)分是創(chuàng)建或者釋放對象,最后一位為1時表示分配對象,最后一位為0是表示釋放對象。
下圖給出Complex和String使用VC進(jìn)行棧分配的的結(jié)構(gòu)。
從上面的圖可以知道,array new(即分配數(shù)組對象)一定要搭配array delete,如下圖所示:
對于如果用new分配多個String對象,但是在釋放時使用delete p,那么只會調(diào)用一次String的析構(gòu)函數(shù),另外兩個String對象的m_data成員指向的內(nèi)存就被泄露。而對于沒有包含指針成員的對象,如果使用new分配多個對象,但是不用array delete來清理指針,那么空間也不會泄露,但是這樣不是好的做法,使用array new時一定要搭配array delete.
從同一個類創(chuàng)建出不同對象有不同副本的數(shù)據(jù)成員成員,而所有函數(shù)都只有一個副本。事實(shí)上,數(shù)據(jù)成員和成員函數(shù)也可以定義成static,這是所有該類型的對象都只有一個副本。static數(shù)據(jù)成員需要在類外定義成相應(yīng)的初始值才能起效。static函數(shù)跟普通成員函數(shù)的區(qū)別在于static成員函數(shù)沒有this指針。調(diào)用static函數(shù)可以用直接用對象或者類名來調(diào)用。
上面以及第一周所講解內(nèi)容都是關(guān)于object-based programming(即單個類的設(shè)計),而OOP(object-oriented programming)主要包含三個概念:
繼承(Inheritance)、復(fù)合(Composition)、委托(Delegation).
復(fù)合(composition)表示兩個類有has-a的關(guān)系,其中一個類是另一個類的一部分,比如我們可以說手是身體的一部分。
復(fù)合下的構(gòu)造和析構(gòu)函數(shù)執(zhí)行順序如下:
1)構(gòu)造函數(shù)執(zhí)行從內(nèi)到外,即先調(diào)用作為部分(component)的構(gòu)造函數(shù),然后調(diào)用自身的構(gòu)造函數(shù)
2)析構(gòu)函數(shù)執(zhí)行從外到內(nèi),即先執(zhí)行自身的西溝函數(shù),然后調(diào)用部分(component)的析構(gòu)函數(shù)
委托(Delegation)類似于復(fù)合,只是包含指向component的指針(不像復(fù)合中包含的是component對象)。
繼承(Inheritance)表示兩個類是is-a的關(guān)系,其構(gòu)造函數(shù)和析構(gòu)函數(shù)的執(zhí)行順序如下:
1)構(gòu)造函數(shù)從內(nèi)到外,即先執(zhí)行基類的構(gòu)造函數(shù),后執(zhí)行自身的構(gòu)造函數(shù)
2)析構(gòu)函數(shù)從外到內(nèi),即先執(zhí)行自身的析構(gòu)函數(shù),然后執(zhí)行子類的析構(gòu)函數(shù)。
繼承關(guān)系下函數(shù)可以根據(jù)virtual函數(shù)的類型分成三類:
1)non virtual function:不希望派生類(derived class)重新定義(override)這個函數(shù)
2)virtual function 希望派生類重新定義(override)它,并且已有默認(rèn)定義
3)希望派生類一定要重新定義(override)它,并且沒有默認(rèn)定義。
virtual函數(shù)特別適用于c++應(yīng)用框架中,開發(fā)者根據(jù)自己需要重寫virual function來完成定制功能。
另外還有繼承和復(fù)合結(jié)合,又分成兩種情況:
1)復(fù)合類位于基類中,然后基類產(chǎn)生派生類,其構(gòu)造和析構(gòu)函數(shù)的執(zhí)行順序如下;
a.構(gòu)造函數(shù)執(zhí)行順序是先component,后基類,最后是派生類
b.析構(gòu)函數(shù)執(zhí)行順序是先派生類,后基類,最后是component.
寫了個小程序驗證,代碼如下:
#includeusing namespace std;
class Component{
public:
Component()
{
cout << "component construction" << endl;
}
~Component()
{
cout << "component destruction" << endl;
}
};
class Base{
public:
Base()
{
cout << "base constructor" << endl;
}
~Base()
{
cout << "base destructor" << endl;
}
private:
Component d;
};
class Derived:public Base{
public:
Derived()
{
cout << "derived constructor" << endl;
}
~Derived()
{
cout << "derived destructor" << endl;
}
};
int main(void)
{
Derived d;
return 0;
}
編譯鏈接后輸出結(jié)果如下:
component construction
base constructor
derived constructor
derived destructor
base destructor
component destruction
2)基類產(chǎn)生派生類,然后復(fù)合類位于派生類中,其構(gòu)造和析構(gòu)函數(shù)的執(zhí)行順序如下;
a)構(gòu)造函數(shù)執(zhí)行順序是先基類,后compnent,最后是派生類
b)析構(gòu)函數(shù)執(zhí)行順序是先派生類,后component,最后是基類。
寫了段小程序驗證:
#includeusing namespace std;
class Component{
public:
Component()
{
cout << "component construction" << endl;
}
~Component()
{
cout << "component destruction" << endl;
}
};
class Base{
public:
Base()
{
cout << "base constructor" << endl;
}
~Base()
{
cout << "base destructor" << endl;
}
};
class Derived:public Base{
public:
Derived()
{
cout << "derived constructor" << endl;
}
~Derived()
{
cout << "derived destructor" << endl;
}
private:
Component d;
};
int main(void)
{
Derived d;
return 0;
}
編譯鏈接后輸出信息如下:
base constructor
component construction
derived constructor
derived destructor
component destruction
base destructor
委托和繼承結(jié)合可以用觀察者(observer)模式來說明。
在review其它同學(xué)的學(xué)習(xí)筆記時,有同學(xué)給出跟下面類似的函數(shù)返回對象時調(diào)用拷貝構(gòu)造函數(shù)實(shí)例:
#includeusing namespace std;
class A{
public:
A() { cout << "constructor" << endl; }
A(const A& a)
{
cout << "copy constructor" << endl;
}
A& operator= (const A& a)
{
cout << "copy assignment operator" << endl;
return *this;
}
};
A f()
{
#if 0
A *p = new A;
cout << "before return" << endl;
return *p;
#else
A a;
cout << "before return" << endl;
return a;
#endif
}
int main(void)
{
f();
//A d = f();
//A d(f());
return 0;
}
在這部分代碼中,函數(shù)f內(nèi)可以定義局部變量或者定義指針并用new來進(jìn)行初始化,這兩種方式在執(zhí)行時會有完全不同的效果。
如果像上面的代碼直接定義局部變量,那么上面程序執(zhí)行結(jié)果如下:
constructor
before return
這時候,局部變量定義的對象其實(shí)相當(dāng)于被返回值給替代了來進(jìn)行操作,只需要在創(chuàng)建新對象時調(diào)用一次構(gòu)造函數(shù)即可。
但是如果f函數(shù)中定義指針使用new來初始化(將代碼中的if 0改成if 1即可,注意這樣子代碼有bug,因為可能產(chǎn)生內(nèi)存泄露),那么編譯執(zhí)行后結(jié)果如下:
constructor
before return
copy constructor
這時候在函數(shù)返回前會調(diào)用拷貝構(gòu)造函數(shù),這時候編譯器會添加一個隱含的引用類似的參數(shù),并且在return語句執(zhí)行拷貝構(gòu)造將局部指針指向?qū)ο罂截惤o返回的對象。
這兩種不同的處理方式在<深度探索C++對象模型>第二章有詳細(xì)的說明。
另外,在習(xí)題中也遇到兩個有趣的問題。
第一個是在派生類拷貝構(gòu)造函數(shù)(如果是自己編寫)如果沒有顯式調(diào)用基類的拷貝構(gòu)造函數(shù),那么此時調(diào)用的是基類的默認(rèn)構(gòu)造函數(shù)(可能是自己編寫或者系統(tǒng)生成)。但是如果是派生類構(gòu)造函數(shù)是系統(tǒng)生成的,那么會自動調(diào)用基類的拷貝構(gòu)造函數(shù)。這個講解來自于stackoverflow?.
寫了個小程序來進(jìn)行驗證:
#include <iostream>
using namespace std;
class Base{
public:
Base()
{
cout << "base contructor" << endl;
}
Base(const Base& other)
{
cout << "base copy constructor" << endl;
}
Base& operator=(const Base& other)
{
cout << "copy assignment operator" << endl;
return *this;
}
};
class Derived:public Base{
public:
Derived():Base()
{
cout << "derived contructor" << endl;
}
Derived(const Derived& other)
{
cout << "derived copy constructor" << endl;
}
Derived& operator=(const Derived& other)
{
cout << "copy assignment operator" << endl;
return *this;
}
};
int main(void)
{
cout << "test part 1" << endl;
cout << "create object a" << endl;
Derived a;
cout << "use copy constructor to copy a to another object" << endl;
Derived b(a);
return 0;
}
得到的執(zhí)行結(jié)果是:
test part 1
create object a
base contructor
derived contructor
use copy constructor to copy a to another object
base contructor
賦值操作函數(shù)與拷貝構(gòu)造函數(shù)有差別,如果沒有在派生類賦值操作函數(shù)中調(diào)用基類的賦值操作函數(shù),那么最終只會調(diào)用派生類的賦值操作函數(shù)。看下面例子:
#include <iostream>
using namespace std;
class Base{
public:
Base()
{
cout << "base contructor" << endl;
}
Base(const Base& other)
{
cout << "base copy constructor" << endl;
}
Base& operator=(const Base& other)
{
cout << "base copy assignment operator" << endl;
return *this;
}
};
class Derived:public Base{
public:
Derived():Base()
{
cout << "derived contructor" << endl;
}
Derived(const Derived& other)
{
cout << "derived copy constructor" << endl;
}
Derived& operator=(const Derived& other)
{
//Base::operator=(other);
cout << "derived copy assignment operator" << endl;
return *this;
}
};
int main(void)
{
cout << "test part 1" << endl;
cout << "create object a" << endl;
Derived a,b;
cout << "use copy assignment to another object" << endl;
b = a;
return 0;
}
得到執(zhí)行結(jié)果如下:
test part 1
create object a
base contructor
derived contructor
base contructor
derived contructor
use copy assignment to another object
derived copy assignment operator
總而言之,為了安全起見,應(yīng)當(dāng)盡量顯式的調(diào)用在派生類的構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)、拷貝復(fù)制函數(shù)中調(diào)用基類的相應(yīng)函數(shù),避免用c++隱含規(guī)則來做事情。