第二周講解的是仍然是object-based programming,以String類為例說(shuō)明包含指針成員的類的寫法变隔。
包含指針成員的類需要自己實(shí)現(xiàn)三個(gè)特殊函數(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;
};
對(duì)于不包含指針成員的類泥兰,通常不需要編寫B(tài)ig Three,編譯器會(huì)自動(dòng)生成這三個(gè)函數(shù)昙衅,這些自動(dòng)生成的函數(shù)會(huì)將源對(duì)象成員一一拷貝(淺拷貝战坤,對(duì)于指針僅拷貝指針的值车要,不會(huì)拷貝所指向內(nèi)容)到目標(biāo)對(duì)象食棕。
如果使用默認(rèn)的拷貝構(gòu)造函數(shù)和拷貝賦值操作操作符函數(shù)寿弱,那么執(zhí)行拷貝后渗磅,兩個(gè)String對(duì)象的指針可能指向同樣的內(nèi)容嚷硫,另外一個(gè)對(duì)象指向的內(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ù)中會(huì)判斷傳入字符串是否為空,不空則分配新的空間(大小為給定參數(shù)長(zhǎng)度+1)給m_data仔掸,然后將傳入字符串考拷貝給m_data.否則脆贵,只需要分配一個(gè)字節(jié)長(zhǎng)度給m_data并初始化成'\0'.
而拷貝構(gòu)造函數(shù)更加特殊,它的用法如下:
String s1("hello");
String s2(s1);
拷貝構(gòu)造函數(shù)語(yǔ)法給構(gòu)造函數(shù)類似起暮,只是其參數(shù)是類類型的對(duì)象卖氨,本例中實(shí)現(xiàn)如下:
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
這個(gè)函數(shù)就是分配合適大小空間并將實(shí)參str的m_data賦給它的m_data。
由于同一個(gè)類的多個(gè)對(duì)象之間互為友元负懦,所以可以直接訪問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ù)中筒捺,最前面兩句代碼判斷是否對(duì)自己賦值,如果對(duì)自己賦值纸厉,那么可以直接返回自己系吭,否則,將自己的m_data釋放颗品,然后重新分配新空間給自己的m_data并將參數(shù)中存儲(chǔ)的字符串信息拷貝給自己的m_data.
這個(gè)函數(shù)中必須考慮自我賦值肯尺,最前面兩行代碼,那么釋放m_data后之后這個(gè)對(duì)象的m_data中就沒有有效數(shù)據(jù)躯枢,后面執(zhí)行再執(zhí)行strlen就沒法得到正確的結(jié)果则吟。
在c++,對(duì)象可能在分配于不同的區(qū)域,例如棧(stack)或者堆(heap)上.堆是操作系統(tǒng)中提供的一塊空間闺金,程序可動(dòng)態(tài)分配(通過(guò)malloc或者new)從中獲取若干空間逾滥。普通函數(shù)內(nèi)定義的局部變量通常是stack object(通常稱為auto object),在作用域結(jié)束后會(huì)被自動(dòng)清理败匹。而棧上可以分配static 對(duì)象寨昙,棧上的對(duì)象在調(diào)用該函數(shù)時(shí)才會(huì)被創(chuàng)建,在程序結(jié)束時(shí)才會(huì)被清理掉掀亩。在所有函數(shù)之外定義的沒有static聲明的變量被稱為全局變量舔哪,全局變量在程序執(zhí)行前會(huì)被創(chuàng)建出來(lái),在程序退出前被釋放掉槽棍。
以上一周Complex類為例說(shuō)明使用new創(chuàng)建新對(duì)象時(shí)編譯器所做的事情捉蚤,例如我們使用下面的代碼:
Complex *pc = new Complex(1,2);
編譯器轉(zhuǎn)化成下面三條語(yǔ)句:
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指針的時(shí)候使用下面代碼:
delete pc;
編譯器會(huì)轉(zhuǎn)化成下面兩條語(yǔ)句:
Complex::~Complex(pc);
operator delete(pc);
其中operator delete函數(shù)內(nèi)部調(diào)用了free(pc)炼七。
使用new動(dòng)態(tài)分配內(nèi)存時(shí)缆巧,在VC下編譯器會(huì)多分配一些空間(下圖左邊是Complex在debug和release模式下分配堆空間,右邊是String對(duì)象在debug或release模式下分配的堆空間)
debug模式下會(huì)多處32個(gè)byte的debug header和4個(gè)字節(jié)的debug footer豌拙。在其前后還有2個(gè)描述其結(jié)構(gòu)體大小的字段陕悬,注意結(jié)構(gòu)體大小需要是4個(gè)字節(jié)的倍數(shù),所以可能還需要適當(dāng)?shù)膒adding.
并且大小51h的最后一位用于區(qū)分是創(chuàng)建或者釋放對(duì)象按傅,最后一位為1時(shí)表示分配對(duì)象捉超,最后一位為0是表示釋放對(duì)象胧卤。
下圖給出Complex和String使用VC進(jìn)行棧分配的的結(jié)構(gòu)。
從上面的圖可以知道拼岳,array new(即分配數(shù)組對(duì)象)一定要搭配array delete枝誊,如下圖所示:
對(duì)于如果用new分配多個(gè)String對(duì)象,但是在釋放時(shí)使用delete p,那么只會(huì)調(diào)用一次String的析構(gòu)函數(shù)惜纸,另外兩個(gè)String對(duì)象的m_data成員指向的內(nèi)存就被泄露叶撒。而對(duì)于沒有包含指針成員的對(duì)象,如果使用new分配多個(gè)對(duì)象堪簿,但是不用array delete來(lái)清理指針痊乾,那么空間也不會(huì)泄露,但是這樣不是好的做法椭更,使用array new時(shí)一定要搭配array delete.
從同一個(gè)類創(chuàng)建出不同對(duì)象有不同副本的數(shù)據(jù)成員成員哪审,而所有函數(shù)都只有一個(gè)副本。事實(shí)上虑瀑,數(shù)據(jù)成員和成員函數(shù)也可以定義成static湿滓,這是所有該類型的對(duì)象都只有一個(gè)副本。static數(shù)據(jù)成員需要在類外定義成相應(yīng)的初始值才能起效舌狗。static函數(shù)跟普通成員函數(shù)的區(qū)別在于static成員函數(shù)沒有this指針叽奥。調(diào)用static函數(shù)可以用直接用對(duì)象或者類名來(lái)調(diào)用。
上面以及第一周所講解內(nèi)容都是關(guān)于object-based programming(即單個(gè)類的設(shè)計(jì))痛侍,而OOP(object-oriented programming)主要包含三個(gè)概念:
繼承(Inheritance)朝氓、復(fù)合(Composition)、委托(Delegation).
復(fù)合(composition)表示兩個(gè)類有has-a的關(guān)系主届,其中一個(gè)類是另一個(gè)類的一部分赵哲,比如我們可以說(shuō)手是身體的一部分。
復(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對(duì)象)。
繼承(Inheritance)表示兩個(gè)類是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)這個(gè)函數(shù)
2)virtual function 希望派生類重新定義(override)它,并且已有默認(rèn)定義
3)希望派生類一定要重新定義(override)它再菊,并且沒有默認(rèn)定義。
virtual函數(shù)特別適用于c++應(yīng)用框架中颜曾,開發(fā)者根據(jù)自己需要重寫virual function來(lái)完成定制功能纠拔。
另外還有繼承和復(fù)合結(jié)合,又分成兩種情況:
1)復(fù)合類位于基類中泛豪,然后基類產(chǎn)生派生類稠诲,其構(gòu)造和析構(gòu)函數(shù)的執(zhí)行順序如下;
a.構(gòu)造函數(shù)執(zhí)行順序是先component,后基類诡曙,最后是派生類
b.析構(gòu)函數(shù)執(zhí)行順序是先派生類臀叙,后基類,最后是component.
寫了個(gè)小程序驗(yàn)證价卤,代碼如下:
#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,最后是基類厌处。
寫了段小程序驗(yàn)證:
#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)模式來(lái)說(shuō)明。
在review其它同學(xué)的學(xué)習(xí)筆記時(shí)岁疼,有同學(xué)給出跟下面類似的函數(shù)返回對(duì)象時(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來(lái)進(jìn)行初始化,這兩種方式在執(zhí)行時(shí)會(huì)有完全不同的效果捷绒。
如果像上面的代碼直接定義局部變量瑰排,那么上面程序執(zhí)行結(jié)果如下:
constructor
before return
這時(shí)候,局部變量定義的對(duì)象其實(shí)相當(dāng)于被返回值給替代了來(lái)進(jìn)行操作疙驾,只需要在創(chuàng)建新對(duì)象時(shí)調(diào)用一次構(gòu)造函數(shù)即可凶伙。
但是如果f函數(shù)中定義指針使用new來(lái)初始化(將代碼中的if 0改成if 1即可,注意這樣子代碼有bug它碎,因?yàn)榭赡墚a(chǎn)生內(nèi)存泄露)函荣,那么編譯執(zhí)行后結(jié)果如下:
constructor
before return
copy constructor
這時(shí)候在函數(shù)返回前會(huì)調(diào)用拷貝構(gòu)造函數(shù),這時(shí)候編譯器會(huì)添加一個(gè)隱含的引用類似的參數(shù)扳肛,并且在return語(yǔ)句執(zhí)行拷貝構(gòu)造將局部指針指向?qū)ο罂截惤o返回的對(duì)象傻挂。
這兩種不同的處理方式在<深度探索C++對(duì)象模型>第二章有詳細(xì)的說(shuō)明。
另外挖息,在習(xí)題中也遇到兩個(gè)有趣的問題金拒。
第一個(gè)是在派生類拷貝構(gòu)造函數(shù)(如果是自己編寫)如果沒有顯式調(diào)用基類的拷貝構(gòu)造函數(shù),那么此時(shí)調(diào)用的是基類的默認(rèn)構(gòu)造函數(shù)(可能是自己編寫或者系統(tǒng)生成)。但是如果是派生類構(gòu)造函數(shù)是系統(tǒng)生成的绪抛,那么會(huì)自動(dòng)調(diào)用基類的拷貝構(gòu)造函數(shù)资铡。這個(gè)講解來(lái)自于stackoverflow?.
寫了個(gè)小程序來(lái)進(jìn)行驗(yà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ù)幢码,那么最終只會(huì)調(diào)用派生類的賦值操作函數(shù)笤休≡U眨看下面例子:
#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ī)則來(lái)做事情。