【極客班】《c++面向?qū)ο蟾呒?jí)編程上第二周》學(xué)習(xí)筆記

第二周講解的是仍然是object-based programming,以String類為例說(shuō)明包含指針成員的類的寫(xiě)法。

包含指針成員的類需要自己實(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ì)于不包含指針成員的類,通常不需要編寫(xiě)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ì)象之間互為友元,所以可以直接訪問(wèn)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中就沒(méi)有有效數(shù)據(jù),后面執(zhí)行再執(zhí)行strlen就沒(méi)法得到正確的結(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ù)之外定義的沒(méi)有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ì)于沒(méi)有包含指針成員的對(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ù)沒(méi)有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)它,并且沒(méi)有默認(rèn)定義。

virtual函數(shù)特別適用于c++應(yīng)用框架中,開(kāi)發(fā)者根據(jù)自己需要重寫(xiě)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.

寫(xiě)了個(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,最后是基類。

寫(xiě)了段小程序驗(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è)有趣的問(wèn)題。

第一個(gè)是在派生類拷貝構(gòu)造函數(shù)(如果是自己編寫(xiě))如果沒(méi)有顯式調(diào)用基類的拷貝構(gòu)造函數(shù),那么此時(shí)調(diào)用的是基類的默認(rèn)構(gòu)造函數(shù)(可能是自己編寫(xiě)或者系統(tǒng)生成)。但是如果是派生類構(gòu)造函數(shù)是系統(tǒng)生成的,那么會(huì)自動(dòng)調(diào)用基類的拷貝構(gòu)造函數(shù)。這個(gè)講解來(lái)自于stackoverflow?.

寫(xiě)了個(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ù)有差別,如果沒(méi)有在派生類賦值操作函數(shù)中調(diào)用基類的賦值操作函數(shù),那么最終只會(huì)調(diào)用派生類的賦值操作函數(shù)??聪旅胬樱?/p>

#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


總而言之,為了安全起見(jiàn),應(yīng)當(dāng)盡量顯式的調(diào)用在派生類的構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)、拷貝復(fù)制函數(shù)中調(diào)用基類的相應(yīng)函數(shù),避免用c++隱含規(guī)則來(lái)做事情。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容