- Visual C++實(shí)用教程
- 鄭阿奇編著
- 7887字
- 2018-12-30 12:04:34
2.1 類(lèi)和對(duì)象
類(lèi)是面向?qū)ο蟪绦蛟O(shè)計(jì)的核心,它實(shí)際上是一種新的數(shù)據(jù)類(lèi)型,也是實(shí)現(xiàn)抽象類(lèi)型的工具,因?yàn)轭?lèi)是通過(guò)抽象數(shù)據(jù)類(lèi)型的方法來(lái)實(shí)現(xiàn)的一種數(shù)據(jù)類(lèi)型。類(lèi)是對(duì)某一類(lèi)對(duì)象的抽象;而對(duì)象是某一種類(lèi)的實(shí)例,因此,類(lèi)和對(duì)象是密切相關(guān)的。
2.1.1 類(lèi)的定義
類(lèi)的定義一般分為聲明部分和實(shí)現(xiàn)部分。聲明部分用來(lái)聲明該類(lèi)中的成員,包含數(shù)據(jù)成員(或稱(chēng)“成員變量”)的聲明和成員函數(shù)的聲明。成員函數(shù)是用來(lái)對(duì)數(shù)據(jù)成員進(jìn)行操作的,又稱(chēng)方法。實(shí)現(xiàn)部分用來(lái)對(duì)成員函數(shù)進(jìn)行定義。概括地說(shuō),聲明部分是告訴使用者“干什么”,而實(shí)現(xiàn)部分是告訴使用者“怎么干”。
C++中定義類(lèi)的一般格式如下:
class <類(lèi)名> { private: [<私有型數(shù)據(jù)和函數(shù)>] public: [<公有型數(shù)據(jù)和函數(shù)>] protected: [<保護(hù)型數(shù)據(jù)和函數(shù)>] }; <各個(gè)成員函數(shù)的實(shí)現(xiàn)>
其中,class是定義類(lèi)的關(guān)鍵字,class的后面是用戶(hù)定義的類(lèi)名(它通常用大寫(xiě)的C字母開(kāi)始的標(biāo)識(shí)符來(lái)描述,C表示Class,以與對(duì)象、函數(shù)及其他數(shù)據(jù)類(lèi)型名相區(qū)別)。類(lèi)中的數(shù)據(jù)和函數(shù)是類(lèi)的成員,分別稱(chēng)為數(shù)據(jù)成員和成員函數(shù)。由一對(duì)花括號(hào)構(gòu)成的是類(lèi)體。注意,類(lèi)體中最后一個(gè)花括號(hào)后面的分號(hào)“;”不能省略。
類(lèi)中的關(guān)鍵字public、private和protected聲明了類(lèi)中的成員與程序其他部分(或類(lèi)外)之間的關(guān)系,稱(chēng)為訪問(wèn)權(quán)限。對(duì)于public成員來(lái)說(shuō),它們是公有的,能被外面的程序訪問(wèn);對(duì)于private成員來(lái)說(shuō),它們是私有的,不能被外面的程序所訪問(wèn)。數(shù)據(jù)成員只能由類(lèi)中的函數(shù)所使用,成員函數(shù)只允許在類(lèi)中調(diào)用。而對(duì)于protected成員來(lái)說(shuō),它們是受保護(hù)的,具有半公開(kāi)性質(zhì),可在類(lèi)中或其子類(lèi)中訪問(wèn)(以后還會(huì)討論)。
<各個(gè)成員函數(shù)的實(shí)現(xiàn)>是類(lèi)定義中的實(shí)現(xiàn)部分,這部分包含所有在類(lèi)體中聲明的函數(shù)的定義(即對(duì)成員函數(shù)的實(shí)現(xiàn))。如果一個(gè)成員函數(shù)在類(lèi)體中定義,則其實(shí)現(xiàn)部分將不需要。如果所有的成員函數(shù)都在類(lèi)體中定義,則實(shí)現(xiàn)部分可以省略。需要說(shuō)明的是,當(dāng)類(lèi)的成員函數(shù)的函數(shù)體在類(lèi)的外部定義時(shí),必須由作用域運(yùn)算符“::”來(lái)通知編譯系統(tǒng)該函數(shù)所屬的類(lèi)。例如:
class CMeter { public: double m_nPercent; // 聲明一個(gè)公有數(shù)據(jù)成員 void StepIt(); // 聲明一個(gè)公有成員函數(shù) void SetPos(int nPos); // 聲明一個(gè)公有成員函數(shù) int GetPos() { return m_nPos; } // 聲明一個(gè)公有成員函數(shù)并定義 private: int m_nPos; // 聲明一個(gè)私有數(shù)據(jù)成員 }; // 注意分號(hào)不能省略 void CMeter::StepIt() { m_nPos++; } void CMeter::SetPos(int nPos) { m_nPos = nPos; }
類(lèi)CMeter中,成員函數(shù)GetPos是在類(lèi)體中定義的,而StepIt和SetPos是在類(lèi)的外部定義的,注意兩者的區(qū)別。另外,定義類(lèi)時(shí)還應(yīng)注意:
(1)類(lèi)中的數(shù)據(jù)成員的類(lèi)型可以是任意的,包含整型、浮點(diǎn)型、字符型、數(shù)組、指針和引用等,也可以是另一個(gè)類(lèi)的對(duì)象,但不允許對(duì)所定義的數(shù)據(jù)成員進(jìn)行初始化,也不能指定除static之外的任何存儲(chǔ)類(lèi)型。例如,類(lèi)CMeter中,下面的定義是錯(cuò)誤的:
class CMeter { //… private: int m_nPos=10; // 錯(cuò)誤:不能直接對(duì)數(shù)據(jù)成員進(jìn)行初始化 auto int n; // 錯(cuò)誤:不合法的存儲(chǔ)類(lèi)型
//… };
(2)在“public:”、“protected:”或“private:”后面定義的所有成員都是公有、保護(hù)或私有的,直到下一個(gè)“public:”、“protected:”或“private:”出現(xiàn)為止。若成員前面沒(méi)有任何訪問(wèn)權(quán)限的指定,則所定義的成員是private(私有),這是類(lèi)的默認(rèn)設(shè)置。事實(shí)上,結(jié)構(gòu)也可看成類(lèi)的一種簡(jiǎn)單形式,只是其成員的默認(rèn)訪問(wèn)權(quán)限是公有的。一般來(lái)說(shuō),當(dāng)只需要描述數(shù)據(jù)結(jié)構(gòu)而不想在結(jié)構(gòu)中進(jìn)行數(shù)據(jù)操作時(shí),則用結(jié)構(gòu)較好。而若既要描述數(shù)據(jù)又要描述對(duì)數(shù)據(jù)的處理方法時(shí),則用類(lèi)為好。
(3)關(guān)鍵字public、protected和private可以在類(lèi)中出現(xiàn)多次,且前后的順序沒(méi)有關(guān)系;但最好先聲明公有成員,后聲明私有成員,因?yàn)閜ublic成員是用戶(hù)最關(guān)心的。每個(gè)訪問(wèn)權(quán)限關(guān)鍵詞為類(lèi)成員所確定的訪問(wèn)權(quán)限是從該關(guān)鍵詞開(kāi)始到下一個(gè)關(guān)鍵詞為止的。
(4)在進(jìn)行類(lèi)設(shè)計(jì)時(shí),通常將數(shù)據(jù)成員聲明為私有的,而將大多數(shù)成員函數(shù)聲明成公有的。這樣,類(lèi)以外的代碼就不能直接訪問(wèn)類(lèi)的訪問(wèn)權(quán)限私有數(shù)據(jù),從而實(shí)現(xiàn)了數(shù)據(jù)的封裝。而公有成員函數(shù)可為內(nèi)部的私有數(shù)據(jù)成員提供外部接口,但接口實(shí)現(xiàn)的細(xì)節(jié)在類(lèi)外又是不可見(jiàn)的,這就是C++類(lèi)的優(yōu)點(diǎn)之一。
(5)盡量將類(lèi)單獨(dú)存放在一個(gè)文件中,或?qū)㈩?lèi)的聲明放在.h文件中,而將成員函數(shù)的實(shí)現(xiàn)放在與.h文件同名的.cpp文件中。以后將會(huì)看到,Visual C++ 6.0為用戶(hù)創(chuàng)建的應(yīng)用程序框架中都是將各個(gè)類(lèi)以.h和同名的.cpp文件組織的。
2.1.2 對(duì)象的定義
作為一種復(fù)雜的數(shù)據(jù)構(gòu)造類(lèi)型,類(lèi)聲明后,就可以定義該類(lèi)的對(duì)象。與結(jié)構(gòu)類(lèi)型一樣,它也有三種定義方式:聲明之后定義、聲明之時(shí)定義和一次性定義。但由于“類(lèi)”比任何數(shù)據(jù)類(lèi)型都要復(fù)雜得多,為了提高程序的可讀性,真正將“類(lèi)”當(dāng)成一個(gè)密閉、“封裝”的盒子(接口),在程序中應(yīng)盡量在對(duì)象的聲明之后定義方式,并按下列格式進(jìn)行:
<類(lèi)名> <對(duì)象名表>
其中,類(lèi)名是用戶(hù)已定義過(guò)的類(lèi)的標(biāo)識(shí)符,對(duì)象名可以有一個(gè)或多個(gè),多個(gè)時(shí)要用逗號(hào)分隔。被定義的對(duì)象既可以是一個(gè)普通對(duì)象,也可以是一個(gè)數(shù)組對(duì)象或指針對(duì)象。例如:
CMeter myMeter,*Meter,Meters[2];
這時(shí),myMeter是類(lèi)CMeter的一個(gè)普通對(duì)象,Meter和Meters分別是該類(lèi)的一個(gè)指針對(duì)象和對(duì)象數(shù)組。
一個(gè)對(duì)象的成員就是該對(duì)象的類(lèi)所定義的數(shù)據(jù)成員(成員變量)和成員函數(shù)。訪問(wèn)對(duì)象的成員變量和成員函數(shù)和訪問(wèn)變量和函數(shù)的方法是一樣的,只不過(guò)要在成員前面加上對(duì)象名和成員運(yùn)算符“.”,其表示方式如下:
<對(duì)象名>.<成員變量> <對(duì)象名>.<成員函數(shù)>(<參數(shù)表>)
例如:
myMeter.m_nPercent, myMeter.SetPos(2), Meters[0].StepIt();
需要說(shuō)明的是,一個(gè)類(lèi)對(duì)象只能訪問(wèn)該類(lèi)的公有型成員,而對(duì)于私有型成員則不能訪問(wèn)。上述成員m_nPercent、SetPos、StepIt都是public訪問(wèn)類(lèi)型。
若對(duì)象是一個(gè)指針,則對(duì)象的成員訪問(wèn)形式如下:
<對(duì)象指針名>-><成員變量> <對(duì)象指針名>-><成員函數(shù)>(<參數(shù)表>)
“->”是一個(gè)表示成員的運(yùn)算符,它與“.”運(yùn)算符的區(qū)別是:“->”用來(lái)表示指向?qū)ο蟮闹羔樀某蓡T,而“.”用來(lái)表示一般對(duì)象的成員。
需要說(shuō)明的是,下面的兩種表示是等價(jià)的:
<對(duì)象指針名>-><成員變量> (*<對(duì)象指針名>).<成員變量>
這對(duì)于成員函數(shù)也適用。另外,對(duì)于引用類(lèi)型對(duì)象,其成員訪問(wèn)形式與一般對(duì)象的成員訪問(wèn)形式相同。例如:
CMeter &other=one; cout<< other.GetPos()<<endl;
2.1.3 類(lèi)作用域和成員訪問(wèn)權(quán)限
類(lèi)的作用域是指在類(lèi)的定義中由一對(duì)花括號(hào)所括起來(lái)的部分。從類(lèi)的定義可知,類(lèi)作用域中可以定義變量,也可以定義函數(shù)。從這一點(diǎn)上看,類(lèi)作用域與文件作用域很相似。但是,類(lèi)作用域又不同于文件作用域,在類(lèi)作用域中定義的變量不能使用auto、register和extern等修飾符,只能用static修飾符,而定義的函數(shù)也不能用extern修飾符。另外,在類(lèi)作用域中的靜態(tài)成員和成員函數(shù)還具有類(lèi)外的連接屬性(以后會(huì)討論)。文件作用域中可以包含類(lèi)作用域,顯然,類(lèi)作用域小于文件作用域。一般地,類(lèi)作用域中可包含成員函數(shù)的作用域。
1.類(lèi)名的作用域
如果在類(lèi)聲明時(shí)指定了類(lèi)名,則類(lèi)名的作用范圍是從類(lèi)名指定的位置開(kāi)始一直到文件結(jié)尾,即類(lèi)名是具有文件作用域的標(biāo)識(shí)符。若類(lèi)的聲明是放在頭文件中的,則類(lèi)名在程序文件中的作用范圍是從包含預(yù)處理指令位置處開(kāi)始一直到文件結(jié)尾。
需要說(shuō)明的是,如果在類(lèi)聲明之前就需要使用該類(lèi)名定義對(duì)象,則必須用下列格式在使用前進(jìn)行提前聲明(注意,類(lèi)的這種形式的聲明可以在相同作用域中出現(xiàn)多次):
class <類(lèi)名>;
例如:
class COne; // 將類(lèi)COne提前聲明 class COne; // 可以聲明多次 class CTwo { //… private: COne a; // 數(shù)據(jù)成員a是已定義的COne類(lèi)對(duì)象 }; class COne { //… };
2.類(lèi)中成員的可見(jiàn)性
(1)在類(lèi)中使用成員時(shí),成員聲明的前后不會(huì)影響該成員在類(lèi)中的使用,這是類(lèi)作用域的特殊性。例如:
class A { void f1() { f2(); // 調(diào)用類(lèi)中的成員函數(shù)f2 cout<<a<<endl; // 使用類(lèi)中的成員變量a } void f2(){} int a; };
(2)由于類(lèi)的成員函數(shù)可以在類(lèi)體外定義,因而此時(shí)由“類(lèi)名::”指定開(kāi)始一直到函數(shù)體最后一個(gè)花括號(hào)為止的范圍也是該類(lèi)作用域的范圍。例如:
class A { void f1(); //… }; void A::f1() { //… }
則從A::開(kāi)始一直到f1函數(shù)體最后一個(gè)花括號(hào)為止的范圍都是屬于類(lèi)A的作用域。
(3)在同一個(gè)類(lèi)的作用域中,不管成員具有怎樣的訪問(wèn)權(quán)限,都可在類(lèi)作用域中使用,而在類(lèi)作用域外卻不可使用。例如:
class A { public: int a; //… }; a=10; // 錯(cuò)誤,不能在A作用域外直接使用類(lèi)中的成員
3.類(lèi)外對(duì)象成員的可見(jiàn)性
對(duì)于訪問(wèn)權(quán)限public、private和protected來(lái)說(shuō),只有在子類(lèi)中或用對(duì)象來(lái)訪問(wèn)成員時(shí),它們才會(huì)起作用。在用類(lèi)外對(duì)象來(lái)訪問(wèn)成員時(shí),只能訪問(wèn)public成員,而對(duì)private和protected均不能訪問(wèn)。
2.1.4 構(gòu)造函數(shù)和析構(gòu)函數(shù)
事實(shí)上,一個(gè)類(lèi)總有兩種特殊的成員函數(shù):構(gòu)造函數(shù)和析構(gòu)函數(shù)。構(gòu)造函數(shù)的功能是在創(chuàng)建對(duì)象時(shí),使用給定的值將對(duì)象初始化。析構(gòu)函數(shù)的功能是用來(lái)釋放一個(gè)對(duì)象,在對(duì)象刪除前,用它來(lái)做一些內(nèi)存釋放等清理工作,它與構(gòu)造函數(shù)的功能正好相反。
1.構(gòu)造函數(shù)
前面已提及,在類(lèi)的定義中是不能對(duì)數(shù)據(jù)成員進(jìn)行初始化的。為了能給數(shù)據(jù)成員設(shè)置某些初值,就要使用類(lèi)的特殊成員函數(shù)——構(gòu)造函數(shù)。構(gòu)造函數(shù)的最大特點(diǎn)是在對(duì)象建立時(shí)它會(huì)被自動(dòng)執(zhí)行,因此用于變量、對(duì)象的初始化代碼一般都放在構(gòu)造函數(shù)中。
C++規(guī)定,一個(gè)類(lèi)的構(gòu)造函數(shù)必須與相應(yīng)的類(lèi)同名,它可以帶參數(shù),也可以不帶參數(shù),與一般的成員函數(shù)定義相同,可以重載,也可以有默認(rèn)的形參值。例如:
class CMeter { public: CMeter(int nPos) // 帶參數(shù)的構(gòu)造函數(shù) { m_nPos = nPos; } //… }
這樣若有:
CMeter oMeter(10), oTick(20);
則會(huì)自動(dòng)調(diào)用構(gòu)造函數(shù)CMeter(int nPos),從而使得對(duì)象oMeter中的私有成員m_nPos的值為10,使得對(duì)象oTick中的私有成員m_nPos的值為20。
2.對(duì)構(gòu)造函數(shù)的幾點(diǎn)說(shuō)明
雖然構(gòu)造函數(shù)的定義方式與一般成員函數(shù)沒(méi)有什么區(qū)別,但要注意:
(1)構(gòu)造函數(shù)的約定使系統(tǒng)在生成類(lèi)的對(duì)象時(shí)自動(dòng)調(diào)用。同時(shí),指定對(duì)象括號(hào)里的參數(shù)就是構(gòu)造函數(shù)的實(shí)參,例如,oMeter(10)就是oMeter. CMeter(10)。故當(dāng)構(gòu)造函數(shù)重載及設(shè)定構(gòu)造函數(shù)默認(rèn)形參值時(shí),要避免出現(xiàn)二義。
CPerson(char*str,float h=170,float w=130) //A { strcpy(name, str); height = h; weight = w; } CPerson(char*str) //B { strcpy(name, str); }
則當(dāng)執(zhí)行“CPerson other("DING");”時(shí),即“other.CPerson("DING");”,因編譯無(wú)法確定是上述哪一個(gè)構(gòu)造函數(shù)的調(diào)用,從而出現(xiàn)編譯錯(cuò)誤。
(2)定義的構(gòu)造函數(shù)不能指定其返回值的類(lèi)型,也不能指定為void類(lèi)型。事實(shí)上,由于構(gòu)造函數(shù)主要用于對(duì)象數(shù)據(jù)成員的初始化,因而無(wú)須返回函數(shù)值,也就無(wú)須有返回類(lèi)型。
(3)若要用類(lèi)定義對(duì)象,則構(gòu)造函數(shù)必須是公有型成員函數(shù),否則類(lèi)無(wú)法實(shí)例化(即無(wú)法定義對(duì)象)。若類(lèi)僅用于派生其他類(lèi),則構(gòu)造函數(shù)可定義為保護(hù)型成員函數(shù)。
3.默認(rèn)構(gòu)造函數(shù)
實(shí)際上,在類(lèi)定義時(shí),如果沒(méi)有定義任何構(gòu)造函數(shù),則編譯自動(dòng)為類(lèi)隱式生成一個(gè)不帶任何參數(shù)的默認(rèn)構(gòu)造函數(shù),由于函數(shù)體是空塊,因此默認(rèn)構(gòu)造函數(shù)不進(jìn)行任何操作,僅僅為了滿(mǎn)足對(duì)象創(chuàng)建時(shí)的語(yǔ)法需要。其形式如下:
<類(lèi)名>() {}
例如,對(duì)于CMeter類(lèi)來(lái)說(shuō),默認(rèn)構(gòu)造函數(shù)的形式如下:
CMeter() // 默認(rèn)構(gòu)造函數(shù)的形式 {}
默認(rèn)構(gòu)造函數(shù)的目的是使下列對(duì)象定義形式合法:
CMeter one; //one.CMeter(); 會(huì)自動(dòng)調(diào)用默認(rèn)構(gòu)造函數(shù)
此時(shí),由于對(duì)象one沒(méi)指定任何初值,因而編譯會(huì)自動(dòng)調(diào)用類(lèi)中隱式生成的默認(rèn)構(gòu)造函數(shù)對(duì)其初始化。需要說(shuō)明的是:
(1)默認(rèn)構(gòu)造函數(shù)對(duì)數(shù)據(jù)成員初值的初始化還取決于對(duì)象的存儲(chǔ)類(lèi)型。例如:
CMeter one; // 自動(dòng)存儲(chǔ)類(lèi)型,數(shù)據(jù)成員的初值為無(wú)效值 static CMeter one; // 靜態(tài)存儲(chǔ)類(lèi)型,數(shù)據(jù)成員的初值為空值或0
(2)若類(lèi)定義中指定了構(gòu)造函數(shù),則隱式的默認(rèn)構(gòu)造函數(shù)不再存在,因此,對(duì)于前面定義的CMeter類(lèi)來(lái)說(shuō),若有:
CMeter four; // 錯(cuò)誤
則因?yàn)檎也坏侥J(rèn)構(gòu)造函數(shù)而出現(xiàn)編譯錯(cuò)誤。此時(shí),在類(lèi)中還要給出默認(rèn)構(gòu)造函數(shù)的具體定義,即定義一個(gè)不帶任何參數(shù)的構(gòu)造函數(shù),稱(chēng)為顯式的默認(rèn)構(gòu)造函數(shù),這樣才能對(duì)four進(jìn)行定義并初始化。
(3)在定義對(duì)象時(shí),不能寫(xiě)成“CMeter four();”,因?yàn)檫@是一個(gè)函數(shù)的聲明。
4.析構(gòu)函數(shù)
與構(gòu)造函數(shù)相對(duì)應(yīng)的是析構(gòu)函數(shù)。析構(gòu)函數(shù)是另一個(gè)特殊的C++成員函數(shù),它只是在類(lèi)名稱(chēng)前面加上一個(gè)“~”符號(hào)(邏輯非),以示與構(gòu)造函數(shù)功能相反。每一個(gè)類(lèi)只有一個(gè)析構(gòu)函數(shù),沒(méi)有任何參數(shù),也不返回任何值。例如:
class CMeter { public: //… ~CMeter() // 析構(gòu)函數(shù) { } //… }
析構(gòu)函數(shù)只有在下列兩種情況下才會(huì)被自動(dòng)調(diào)用:
(1)當(dāng)對(duì)象定義在一個(gè)函數(shù)體中,該函數(shù)調(diào)用結(jié)束后,析構(gòu)函數(shù)被自動(dòng)調(diào)用。
(2)用new為對(duì)象分配動(dòng)態(tài)內(nèi)存,當(dāng)使用delete釋放對(duì)象時(shí),析構(gòu)函數(shù)被自動(dòng)調(diào)用。
與默認(rèn)構(gòu)造函數(shù)類(lèi)似,若類(lèi)的聲明中沒(méi)有定義析構(gòu)函數(shù),則編譯也會(huì)自動(dòng)生成一個(gè)隱式的不做任何操作的默認(rèn)析構(gòu)函數(shù)。
5.應(yīng)用示例
類(lèi)的構(gòu)造函數(shù)和析構(gòu)函數(shù)的一個(gè)典型應(yīng)用是在構(gòu)造函數(shù)中用new為指針成員開(kāi)辟獨(dú)立的動(dòng)態(tài)內(nèi)存空間,而在析構(gòu)函數(shù)中用delete釋放它們。
【例Ex_Name】 使用構(gòu)造函數(shù)和析構(gòu)函數(shù)
#include <iostream.h> #include <string.h> class CName { public: CName() //A:顯式默認(rèn)構(gòu)造函數(shù) { strName=NULL; // 空值 } CName(char*str) //B { strName = str; } ~CName() {} // 顯式默認(rèn)析構(gòu)函數(shù) char*getName() // 獲取字符串 { return strName; } private: char *strName; // 字符指針,名稱(chēng) }; int main() { char*p=new char[5]; // 為p開(kāi)辟內(nèi)存空間 strcpy(p,"DING"); // p指向的內(nèi)存空間的值為"DING" CName one(p); // 對(duì)象初始化 delete[]p; // 釋放p的內(nèi)存空間 cout<<one.getName()<<endl; return 0; }
由于“CName one(p);”調(diào)用的是B重載構(gòu)造函數(shù),從而使得私有指針成員strName的指向等于p的指向。而p指向new開(kāi)辟的內(nèi)存空間,其內(nèi)容為“DING”,一旦p指向的內(nèi)存空間刪除后,p的指向就變得不確定了,此時(shí)strName指向也不確定,所以此時(shí)運(yùn)行結(jié)果為:
m葺葺葺葺?
顯然,輸出的是一個(gè)無(wú)效的字符串。因此,為了保證類(lèi)的封裝性,類(lèi)中的指針成員所指向的內(nèi)存空間必須在類(lèi)中自行獨(dú)立開(kāi)辟和釋放。因此,類(lèi)CName應(yīng)改成下列代碼:
class CName { public: CName() //A:顯式默認(rèn)構(gòu)造函數(shù) { strName=NULL; // 空值 } CName(char*str) //B { strName = (char *)new char[strlen(str)+1]; // 因字符串后面還有一個(gè)結(jié)束符,因此內(nèi)存空間的大小要多開(kāi)辟1個(gè)內(nèi)存單元 strcpy(strName,str); // 復(fù)制內(nèi)容 } ~CName() { if(strName) delete[]strName; strName=NULL; // 一個(gè)好習(xí)慣 } char *getName() { return strName; } private: char *strName; // 字符指針,名稱(chēng) };
這樣,主函數(shù)中的代碼才會(huì)有正確的運(yùn)行結(jié)果:
DING
總之,為了使類(lèi)具有通用性,通常將類(lèi)中的字符串?dāng)?shù)據(jù)用char指針來(lái)描述,如char *strName,但此時(shí)應(yīng)將strName在構(gòu)造函數(shù)中另開(kāi)辟獨(dú)立的內(nèi)存空間來(lái)存儲(chǔ)字符串,然后在析構(gòu)函數(shù)中釋放,以保證成員數(shù)據(jù)在類(lèi)中的封裝性。若在構(gòu)造函數(shù)中將指針成員直接指向字符串或指向外部的存儲(chǔ)字符串的內(nèi)存空間,則會(huì)出現(xiàn)潛在的危險(xiǎn)。
1.賦值
在C++中,一個(gè)類(lèi)的對(duì)象的初值設(shè)定可以有多種形式。例如,對(duì)于前面的類(lèi)CName來(lái)說(shuō),則可有下列對(duì)象的定義方式:
CName o1; // 通過(guò)A顯式默認(rèn)構(gòu)造函數(shù)設(shè)定初值 CName o2("DING"); // 通過(guò)B重載構(gòu)造函數(shù)設(shè)定初值
2.1.5 對(duì)象賦值和拷貝
等都是合法有效的。但是若有:
o1=o2; // 通過(guò)賦值語(yǔ)句設(shè)定初值
則雖合法,但因?yàn)橥?lèi)型的變量可以直接用“=”賦值,運(yùn)行后卻會(huì)出現(xiàn)程序終止的情況,這是為什么呢?這是因?yàn)閷?duì)于“CName o1;”這種定義方式,編譯會(huì)自動(dòng)調(diào)用相應(yīng)的默認(rèn)構(gòu)造函數(shù),此時(shí)顯式的默認(rèn)構(gòu)造函數(shù)使私有指針成員strName為空值;而“o1 = o2;”中,C++賦值運(yùn)算符的操作是將右操作對(duì)象的內(nèi)容拷貝(復(fù)制,余同)到左操作對(duì)象的內(nèi)存空間,由于左操作對(duì)象o1中的strName沒(méi)有指向任何內(nèi)存空間,因此試圖將數(shù)據(jù)復(fù)制到一個(gè)不存在的內(nèi)存空間中,程序必然異常終止。所以“o1 = o2;”看上去合法,但實(shí)際上是不可行的。
C++還常用下列形式的初始化來(lái)將另一個(gè)對(duì)象作為對(duì)象的初值:
<類(lèi)名> <對(duì)象名1>(<對(duì)象名2>)
例如:
CName o2("DING"); //A:通過(guò)構(gòu)造函數(shù)設(shè)定初值 CName o3(o2); //B:通過(guò)指定對(duì)象設(shè)定初值
B語(yǔ)句是將o2作為o3的初值,與o2一樣,o3這種初始化形式要調(diào)用相應(yīng)的構(gòu)造函數(shù),但此時(shí)找不到相匹配的構(gòu)造函數(shù),因?yàn)镃Name類(lèi)沒(méi)有任何構(gòu)造函數(shù)的形參是CName類(lèi)對(duì)象。事實(shí)上,CName還隱含一個(gè)特殊的默認(rèn)構(gòu)造函數(shù),其原型為CName(const CName &),這種特殊的默認(rèn)構(gòu)造函數(shù)稱(chēng)為默認(rèn)拷貝構(gòu)造函數(shù)。在C++中,每一個(gè)類(lèi)總有一個(gè)默認(rèn)拷貝構(gòu)造函數(shù),其目的是保證B語(yǔ)句中對(duì)象初始化形式的合法性,其功能就等價(jià)于“CName o3 = o2;”。但語(yǔ)句“CName o3(o2);”與語(yǔ)句“o1 = o2;”一樣,也會(huì)出現(xiàn)程序終止的情況,其原因和“o1 = o2;”的原因一樣。但是,若有類(lèi)CData:
class CData { public: CData( int data = 0) {
m_nData = data; } ~CData() {} int getData() { return m_nData; } private: int m_nData; };
則下列初始化形式卻都是合法有效的:
CData a(3); // 通過(guò)重載構(gòu)造函數(shù)設(shè)定初值 CData b(a); // 通過(guò)默認(rèn)拷貝構(gòu)造函數(shù)設(shè)定初值 // 等價(jià)于 CData b=a; cout<<a.getData()<<endl; // 輸出 3 cout<<b.getData()<<endl; // 輸出 3
可見(jiàn),與變量一樣,在C++中類(lèi)對(duì)象的初始化也可以有兩種方式:賦值方式和默認(rèn)拷貝方式。這兩種方式是等價(jià)的,例如,CData b(a);和CData b = a;是等價(jià)的。
為什么CData對(duì)象的賦值和默認(rèn)拷貝初始化是可行的,而CName對(duì)象的賦值和默認(rèn)拷貝初始化卻是不行的呢?問(wèn)題就出在其數(shù)據(jù)成員的內(nèi)存空間上。
CName的數(shù)據(jù)成員strName是一個(gè)“char *”指針,由于其自身的內(nèi)存空間是用來(lái)存放指針的地址,因而其數(shù)據(jù)的存儲(chǔ)還需另辟一個(gè)不依附外部的獨(dú)立的內(nèi)存空間。而CData的數(shù)據(jù)成員m_nData自身的內(nèi)存空間就是用來(lái)存儲(chǔ)數(shù)據(jù)的,因此CData對(duì)象初始化所進(jìn)行的數(shù)值拷貝是有效的。
解決CName對(duì)象初始化的內(nèi)容拷貝問(wèn)題,在C++中有兩種手段,一是給“=”運(yùn)算符賦予新的操作,稱(chēng)為運(yùn)算符重載(以后會(huì)討論);二是重新定義或重載默認(rèn)拷貝構(gòu)造函數(shù)。
2.淺拷貝和深拷貝
前面已說(shuō)過(guò),每一個(gè)C++類(lèi)都有一個(gè)隱式的默認(rèn)拷貝構(gòu)造函數(shù),其目的是保證對(duì)象初始化方式的合法性,其功能是將一個(gè)已定義的對(duì)象所在的內(nèi)存空間的內(nèi)容依次拷貝到被初始化對(duì)象的內(nèi)存空間中。這種僅僅將內(nèi)存空間的內(nèi)容拷貝的方式稱(chēng)為淺拷貝。也就是說(shuō),默認(rèn)拷貝構(gòu)造函數(shù)是淺拷貝方式。
事實(shí)上,對(duì)于數(shù)據(jù)成員有指針類(lèi)型的類(lèi)來(lái)說(shuō),均會(huì)出現(xiàn)如CName類(lèi)的問(wèn)題,由于默認(rèn)拷貝構(gòu)造函數(shù)無(wú)法解決,因此必須自己定義一個(gè)拷貝構(gòu)造函數(shù),在進(jìn)行數(shù)值拷貝之前,為指針類(lèi)型的數(shù)據(jù)成員另辟一個(gè)獨(dú)立的內(nèi)存空間。由于這種拷貝還需另辟內(nèi)存空間,因而稱(chēng)其為深拷貝。
3.深拷貝構(gòu)造函數(shù)
拷貝構(gòu)造函數(shù)是一種比較特殊的構(gòu)造函數(shù),除遵循構(gòu)造函數(shù)的聲明和實(shí)現(xiàn)規(guī)則外,還應(yīng)按下列格式進(jìn)行定義。
<類(lèi)名>(參數(shù)表) {}
可見(jiàn),拷貝構(gòu)造函數(shù)的格式就是帶參數(shù)的構(gòu)造函數(shù)。由于拷貝操作的實(shí)質(zhì)是類(lèi)對(duì)象空間的引用,因此C++規(guī)定,拷貝構(gòu)造函數(shù)的參數(shù)個(gè)數(shù)可以是1個(gè)或多個(gè),但左起的第1個(gè)參數(shù)必須是類(lèi)的引用對(duì)象,它可以是“類(lèi)名 &對(duì)象”或是“const類(lèi)名 &對(duì)象”形式,其中“類(lèi)名”是拷貝構(gòu)造函數(shù)所在類(lèi)的類(lèi)名。也就是說(shuō),對(duì)于CName的拷貝構(gòu)造函數(shù),可有下列合法的函數(shù)原型:
CName(CName&x); //x為合法的對(duì)象標(biāo)識(shí)符 CName( const CName &x ); CName(CName&x,…); // “…”表示還有其他參數(shù) CName( const CName &x,…);
需要說(shuō)明的是,一旦在類(lèi)中定義了拷貝構(gòu)造函數(shù),則隱式的默認(rèn)拷貝構(gòu)造函數(shù)和隱式的默認(rèn)構(gòu)造函數(shù)就不再有效了。
【例Ex_CopyCon】 使用拷貝構(gòu)造函數(shù)
#include <iostream.h> #include <string.h> class CName { public: CName() { strName = NULL; } CName( char *str ) { strName = (char *)new char[strlen(str)+1]; strcpy(strName,str); // 復(fù)制內(nèi)容 } CName(CName&one) // A:顯式的默認(rèn)拷貝構(gòu)造函數(shù) { // 為strName開(kāi)辟獨(dú)立的內(nèi)存空間 strName = (char *)new char[strlen(one.strName)+1]; strcpy(strName,one.strName); // 復(fù)制內(nèi)容 } CName(CName&one,char*add) // B:帶其他參數(shù)的拷貝構(gòu)造函數(shù) { // 為strName開(kāi)辟獨(dú)立的內(nèi)存空間 strName = (char *)new char[strlen(one.strName) + strlen(add) +1]; strcpy(strName,one.strName); // 復(fù)制內(nèi)容 strcat(strName,add); // 連接到strName中 } ~CName() { if(strName) delete[]strName; strName=NULL; // 一個(gè)好習(xí)慣 } char *getName() { return strName; } private: char *strName; // 字符指針,名稱(chēng) }; int main() { CName o1("DING"); // 通過(guò)構(gòu)造函數(shù)初始化 CName o2(o1); // 通過(guò)顯式的默認(rèn)拷貝構(gòu)造函數(shù)來(lái)初始化 cout<<o2.getName()<<endl; CName o3(o1,"YOU HE"); // 通過(guò)帶其他參數(shù)的拷貝構(gòu)造函數(shù)來(lái)初始化 cout<<o3.getName()<<endl; return 0; }
代碼中,類(lèi)CName定義了兩個(gè)拷貝構(gòu)造函數(shù)A和B,其中A稱(chēng)為顯式的默認(rèn)拷貝構(gòu)造函數(shù), B稱(chēng)為重載拷貝構(gòu)造函數(shù),它還帶有字符指針參數(shù),用來(lái)將新對(duì)象的數(shù)據(jù)成員字符指針strName指向一個(gè)開(kāi)辟的動(dòng)態(tài)內(nèi)存空間,然后將另一個(gè)對(duì)象one的內(nèi)容復(fù)制到strName中,最后調(diào)用cstring頭文件定義的庫(kù)函數(shù)strcat,將字符指針參數(shù)add指向的字符串連接到strName中。
程序運(yùn)行結(jié)果如下:
DING
DING YOU HE
2.1.6 對(duì)象成員的初始化
在實(shí)際應(yīng)用中,一個(gè)類(lèi)的數(shù)據(jù)成員除了普通數(shù)據(jù)類(lèi)型變量外,還往往是其他已定義的類(lèi)的對(duì)象,這樣的成員就稱(chēng)為對(duì)象成員,擁有對(duì)象成員的類(lèi)常稱(chēng)為組合類(lèi)。此時(shí),為提高對(duì)象初始化效率,增強(qiáng)程序的可讀性,C++允許在構(gòu)造函數(shù)的函數(shù)頭后面跟一個(gè)由冒號(hào)“:”來(lái)引導(dǎo)的對(duì)象成員初始化列表,列表中包含類(lèi)中對(duì)象成員或數(shù)據(jù)成員的拷貝初始化代碼,各對(duì)象初始化之間用逗號(hào)分隔,如下列格式:

以前已討論過(guò),數(shù)據(jù)成員的初始化是通過(guò)構(gòu)造函數(shù)來(lái)進(jìn)行的。這就意味著,類(lèi)的對(duì)象成員也可在類(lèi)的構(gòu)造函數(shù)體中進(jìn)行初始化。這樣一來(lái),類(lèi)的對(duì)象成員的初始化就可以有兩種方式:一是在構(gòu)造函數(shù)體中進(jìn)行,稱(chēng)為函數(shù)構(gòu)造方式;二是使用由冒號(hào)“:”來(lái)引導(dǎo)的對(duì)象成員初始化列表的形式,稱(chēng)為對(duì)象成員列表方式。
先來(lái)看看第一種方式(函數(shù)構(gòu)造方式),例如:
class CPoint { public: CPoint( int x, int y) { xPos=x; yPos=y; } private: int xPos, yPos; }; class CRect { public: CRect( int x1, int y1, int x2, int y2)
{ {L-End} m_ptLT =CPoint(x1,y1); {L-End} m_ptRB =CPoint(x2,y2); } private: {L-End} CPoint m_ptLT, m_ptRB; }; int main() { CRect rc(10, 100, 80, 250); return 0; }
雖然,類(lèi)CRect中的對(duì)象成員m_ptLT和m_ptRB的初值的設(shè)定是在構(gòu)造函數(shù)中完成的,但此時(shí)編譯卻出現(xiàn)“找不到匹配的CPoint默認(rèn)構(gòu)造函數(shù)”的編譯錯(cuò)誤。這是因?yàn)楫?dāng)主函數(shù)main中定義并初始化CRect對(duì)象rc時(shí),它首先為類(lèi)CRect的數(shù)據(jù)成員m_ptLT和m_ptRB作定義并分配內(nèi)存空間,由于m_ptLT和m_ptRB是CPoint對(duì)象,因而編譯會(huì)查找其構(gòu)造函數(shù)進(jìn)行初始化,此時(shí)m_ptLT和m_ptRB不帶任何初值,故需要調(diào)用CPoint類(lèi)的默認(rèn)構(gòu)造函數(shù),而CPoint類(lèi)已定義了帶參數(shù)的構(gòu)造函數(shù),因此CPoint類(lèi)的默認(rèn)構(gòu)造函數(shù)不再存在,所以會(huì)出現(xiàn)編譯錯(cuò)誤。
但若使用第二種方式(對(duì)象成員列表方式),即使用由冒號(hào)“:”來(lái)引導(dǎo)的對(duì)象成員初始化列表的形式,如下面的代碼:
class CPoint
{//…
};
class CRect
{
public:
CRect( int x1, int y1, int x2, int y2)
{L-End}
: m_ptLT(x1, y1), m_ptRB(x2, y2)
{}
private:
CPoint m_ptLT, m_ptRB;
};
int main()
{
CRect rc(10, 100, 80, 250);
return 0;
}
則編譯會(huì)順利通過(guò)。這是因?yàn)榈诙N由冒號(hào)“:”來(lái)引導(dǎo)的對(duì)象成員初始化列表的形式實(shí)際上是將對(duì)象成員的定義和初始化同時(shí)進(jìn)行。當(dāng)在main函數(shù)中定義了CRect對(duì)象rc時(shí),編譯首先根據(jù)類(lèi)中聲明的數(shù)據(jù)成員次序,為成員分配內(nèi)存空間,然后從對(duì)象初始化列表中尋找其初始化代碼,若查找不到,則調(diào)用相應(yīng)的構(gòu)造函數(shù)進(jìn)行初始化,若可查找到,則根據(jù)對(duì)象成員的初始化形式調(diào)用相應(yīng)的構(gòu)造函數(shù)進(jìn)行初始化。顯然,在對(duì)象成員初始化列表中由于存在m_ptLT(x1, y1)和m_ptRB(x2, y2)對(duì)象初始化代碼,因此成員m_ptLT和m_ptRB構(gòu)造時(shí)調(diào)用的是CPoint( int , int)形式的構(gòu)造函數(shù),而類(lèi)CPoint剛好有此形式的構(gòu)造函數(shù)定義,故編譯能通過(guò)。可見(jiàn):
(1)函數(shù)構(gòu)造方式實(shí)際上是將對(duì)象成員進(jìn)行了兩次初始化:第一次是在對(duì)象成員聲明的同時(shí)自動(dòng)調(diào)用默認(rèn)構(gòu)造函數(shù)進(jìn)行的,而第二次是在構(gòu)造函數(shù)體中執(zhí)行的初始化代碼。
(2)對(duì)象成員列表方式雖是將對(duì)象成員的定義和初始化代碼分在兩個(gè)地方書(shū)寫(xiě),但卻是同時(shí)運(yùn)行。對(duì)比函數(shù)構(gòu)造方式可以看出,由冒號(hào)“:”來(lái)引導(dǎo)的對(duì)象初始化列表的形式能簡(jiǎn)化對(duì)象初始化操作,提高對(duì)象初始化效率。
(3)在對(duì)象成員列表方式下,成員初始化的順序是按成員的聲明次序進(jìn)行的,而與成員在由冒號(hào)“:”來(lái)引導(dǎo)的對(duì)象初始化列表中的次序無(wú)關(guān)。
- Python快樂(lè)編程:人工智能深度學(xué)習(xí)基礎(chǔ)
- 無(wú)代碼編程:用云表搭建企業(yè)數(shù)字化管理平臺(tái)
- Web Scraping with Python
- Practical Internet of Things Security
- 深入淺出Windows API程序設(shè)計(jì):編程基礎(chǔ)篇
- Expert Android Programming
- 程序員修煉之道:通向務(wù)實(shí)的最高境界(第2版)
- Hands-On Full Stack Development with Go
- Corona SDK Mobile Game Development:Beginner's Guide(Second Edition)
- Java面向?qū)ο蟪绦蛟O(shè)計(jì)
- 機(jī)器學(xué)習(xí)微積分一本通(Python版)
- 網(wǎng)絡(luò)數(shù)據(jù)采集技術(shù):Java網(wǎng)絡(luò)爬蟲(chóng)實(shí)戰(zhàn)
- PostgreSQL Developer's Guide
- 面向?qū)ο蟪绦蛟O(shè)計(jì)及C++(第3版)
- Android智能手機(jī)APP界面設(shè)計(jì)實(shí)戰(zhàn)教程