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

第二周講解的是仍然是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)做事情。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末辕坝,一起剝皮案震驚了整個(gè)濱河市窍奋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌圣勒,老刑警劉巖费变,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異圣贸,居然都是意外死亡挚歧,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門吁峻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)滑负,“玉大人,你說(shuō)我怎么就攤上這事用含“剑” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵啄骇,是天一觀的道長(zhǎng)痴鳄。 經(jīng)常有香客問我,道長(zhǎng)缸夹,這世上最難降的妖魔是什么痪寻? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮虽惭,結(jié)果婚禮上橡类,老公的妹妹穿的比我還像新娘。我一直安慰自己芽唇,他們只是感情好顾画,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般研侣。 火紅的嫁衣襯著肌膚如雪谱邪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天庶诡,我揣著相機(jī)與錄音虾标,去河邊找鬼。 笑死灌砖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的傀蚌。 我是一名探鬼主播基显,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼善炫!你這毒婦竟也來(lái)了撩幽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤箩艺,失蹤者是張志新(化名)和其女友劉穎窜醉,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體艺谆,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡榨惰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了静汤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片琅催。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖虫给,靈堂內(nèi)的尸體忽然破棺而出藤抡,到底是詐尸還是另有隱情,我是刑警寧澤抹估,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布缠黍,位于F島的核電站,受9級(jí)特大地震影響药蜻,放射性物質(zhì)發(fā)生泄漏瓷式。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一谷暮、第九天 我趴在偏房一處隱蔽的房頂上張望蒿往。 院中可真熱鬧,春花似錦湿弦、人聲如沸瓤漏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蔬充。三九已至蝶俱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饥漫,已是汗流浹背榨呆。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庸队,地道東北人积蜻。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像彻消,于是被迫代替她去往敵國(guó)和親竿拆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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