【極客班】《c++面向?qū)ο蟾呒壘幊躺系诙堋穼W習筆記

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

包含指針成員的類需要自己實現(xiàn)三個特殊函數(shù)(稱為Big Three,在維基百科上被稱為Rule of Three?):

1)拷貝構造函數(shù)(copy constructor) 2)拷貝賦值操作符函數(shù)(copy assignment operator) 3)析構函數(shù)(destructor)

本周中給出的函數(shù)原型如下:

class String

{

public:

String(const char* cstr=0); //構造函數(shù)

String(const String& str); //拷貝構造函數(shù)

String& operator=(const String& str); //拷貝賦值操作符

~String(); //析構函數(shù)

char* get_c_str() const { return m_data; }

private:

char* m_data;

};

對于不包含指針成員的類,通常不需要編寫B(tài)ig Three,編譯器會自動生成這三個函數(shù),這些自動生成的函數(shù)會將源對象成員一一拷貝(淺拷貝,對于指針僅拷貝指針的值,不會拷貝所指向內(nèi)容)到目標對象。

如果使用默認的拷貝構造函數(shù)和拷貝賦值操作操作符函數(shù),那么執(zhí)行拷貝后,兩個String對象的指針可能指向同樣的內(nèi)容,另外一個對象指向的內(nèi)存可能泄露。

String的構造函數(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';

}

}

構造函數(shù)中會判斷傳入字符串是否為空,不空則分配新的空間(大小為給定參數(shù)長度+1)給m_data,然后將傳入字符串考拷貝給m_data.否則,只需要分配一個字節(jié)長度給m_data并初始化成'\0'.

而拷貝構造函數(shù)更加特殊,它的用法如下:

String s1("hello");

String s2(s1);

拷貝構造函數(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ù)就是分配合適大小空間并將實參str的m_data賦給它的m_data。

由于同一個類的多個對象之間互為友元,所以可以直接訪問str實參中的m_data.

拷貝賦值操作符函數(shù)用法如下:

String s1("hello");

String s2 = s1;

實現(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;

}

拷貝復制操作符函數(shù)中,最前面兩句代碼判斷是否對自己賦值,如果對自己賦值,那么可以直接返回自己,否則,將自己的m_data釋放,然后重新分配新空間給自己的m_data并將參數(shù)中存儲的字符串信息拷貝給自己的m_data.

這個函數(shù)中必須考慮自我賦值,最前面兩行代碼,那么釋放m_data后之后這個對象的m_data中就沒有有效數(shù)據(jù),后面執(zhí)行再執(zhí)行strlen就沒法得到正確的結果。

在c++,對象可能在分配于不同的區(qū)域,例如棧(stack)或者堆(heap)上.堆是操作系統(tǒng)中提供的一塊空間,程序可動態(tài)分配(通過malloc或者new)從中獲取若干空間。普通函數(shù)內(nèi)定義的局部變量通常是stack object(通常稱為auto object),在作用域結束后會被自動清理。而棧上可以分配static 對象,棧上的對象在調(diào)用該函數(shù)時才會被創(chuàng)建,在程序結束時才會被清理掉。在所有函數(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個描述其結構體大小的字段,注意結構體大小需要是4個字節(jié)的倍數(shù),所以可能還需要適當?shù)膒adding.

并且大小51h的最后一位用于區(qū)分是創(chuàng)建或者釋放對象,最后一位為1時表示分配對象,最后一位為0是表示釋放對象。

下圖給出Complex和String使用VC進行棧分配的的結構。

從上面的圖可以知道,array new(即分配數(shù)組對象)一定要搭配array delete,如下圖所示:

對于如果用new分配多個String對象,但是在釋放時使用delete p,那么只會調(diào)用一次String的析構函數(shù),另外兩個String對象的m_data成員指向的內(nèi)存就被泄露。而對于沒有包含指針成員的對象,如果使用new分配多個對象,但是不用array delete來清理指針,那么空間也不會泄露,但是這樣不是好的做法,使用array new時一定要搭配array delete.


從同一個類創(chuàng)建出不同對象有不同副本的數(shù)據(jù)成員成員,而所有函數(shù)都只有一個副本。事實上,數(shù)據(jù)成員和成員函數(shù)也可以定義成static,這是所有該類型的對象都只有一個副本。static數(shù)據(jù)成員需要在類外定義成相應的初始值才能起效。static函數(shù)跟普通成員函數(shù)的區(qū)別在于static成員函數(shù)沒有this指針。調(diào)用static函數(shù)可以用直接用對象或者類名來調(diào)用。


上面以及第一周所講解內(nèi)容都是關于object-based programming(即單個類的設計),而OOP(object-oriented programming)主要包含三個概念:

繼承(Inheritance)、復合(Composition)、委托(Delegation).

復合(composition)表示兩個類有has-a的關系,其中一個類是另一個類的一部分,比如我們可以說手是身體的一部分。

復合下的構造和析構函數(shù)執(zhí)行順序如下:

1)構造函數(shù)執(zhí)行從內(nèi)到外,即先調(diào)用作為部分(component)的構造函數(shù),然后調(diào)用自身的構造函數(shù)

2)析構函數(shù)執(zhí)行從外到內(nèi),即先執(zhí)行自身的西溝函數(shù),然后調(diào)用部分(component)的析構函數(shù)

委托(Delegation)類似于復合,只是包含指向component的指針(不像復合中包含的是component對象)。

繼承(Inheritance)表示兩個類是is-a的關系,其構造函數(shù)和析構函數(shù)的執(zhí)行順序如下:

1)構造函數(shù)從內(nèi)到外,即先執(zhí)行基類的構造函數(shù),后執(zhí)行自身的構造函數(shù)

2)析構函數(shù)從外到內(nèi),即先執(zhí)行自身的析構函數(shù),然后執(zhí)行子類的析構函數(shù)。

繼承關系下函數(shù)可以根據(jù)virtual函數(shù)的類型分成三類:

1)non virtual function:不希望派生類(derived class)重新定義(override)這個函數(shù)

2)virtual function 希望派生類重新定義(override)它,并且已有默認定義

3)希望派生類一定要重新定義(override)它,并且沒有默認定義。

virtual函數(shù)特別適用于c++應用框架中,開發(fā)者根據(jù)自己需要重寫virual function來完成定制功能。

另外還有繼承和復合結合,又分成兩種情況:

1)復合類位于基類中,然后基類產(chǎn)生派生類,其構造和析構函數(shù)的執(zhí)行順序如下;

a.構造函數(shù)執(zhí)行順序是先component,后基類,最后是派生類

b.析構函數(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;

}

編譯鏈接后輸出結果如下:

component construction

base constructor

derived constructor

derived destructor

base destructor

component destruction

2)基類產(chǎn)生派生類,然后復合類位于派生類中,其構造和析構函數(shù)的執(zhí)行順序如下;

a)構造函數(shù)執(zhí)行順序是先基類,后compnent,最后是派生類

b)析構函數(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

委托和繼承結合可以用觀察者(observer)模式來說明。

在review其它同學的學習筆記時,有同學給出跟下面類似的函數(shù)返回對象時調(diào)用拷貝構造函數(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來進行初始化,這兩種方式在執(zhí)行時會有完全不同的效果。

如果像上面的代碼直接定義局部變量,那么上面程序執(zhí)行結果如下:

constructor

before return

這時候,局部變量定義的對象其實相當于被返回值給替代了來進行操作,只需要在創(chuàng)建新對象時調(diào)用一次構造函數(shù)即可。

但是如果f函數(shù)中定義指針使用new來初始化(將代碼中的if 0改成if 1即可,注意這樣子代碼有bug,因為可能產(chǎn)生內(nèi)存泄露),那么編譯執(zhí)行后結果如下:

constructor

before return

copy constructor

這時候在函數(shù)返回前會調(diào)用拷貝構造函數(shù),這時候編譯器會添加一個隱含的引用類似的參數(shù),并且在return語句執(zhí)行拷貝構造將局部指針指向?qū)ο罂截惤o返回的對象。

這兩種不同的處理方式在<深度探索C++對象模型>第二章有詳細的說明。


另外,在習題中也遇到兩個有趣的問題。

第一個是在派生類拷貝構造函數(shù)(如果是自己編寫)如果沒有顯式調(diào)用基類的拷貝構造函數(shù),那么此時調(diào)用的是基類的默認構造函數(shù)(可能是自己編寫或者系統(tǒng)生成)。但是如果是派生類構造函數(shù)是系統(tǒng)生成的,那么會自動調(diào)用基類的拷貝構造函數(shù)。這個講解來自于stackoverflow?.

寫了個小程序來進行驗證:

#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í)行結果是:

test part 1

create object a

base contructor

derived contructor

use copy constructor to copy a to another object

base contructor

賦值操作函數(shù)與拷貝構造函數(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í)行結果如下:

test part 1

create object a

base contructor

derived contructor

base contructor

derived contructor

use copy assignment to another object

derived copy assignment operator


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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容