弄清C 在幕後為你做的

2021-03-31 08:56:29 字數 3261 閱讀 8851

乙個空類什麼時候不是空類? ---- 當c++編譯器通過它的時候。如果你沒有宣告下列函式,體貼的編譯器會宣告它自己的版本。這些函式是:乙個拷貝建構函式,乙個賦值運算子,乙個析構函式,一對取址運算子。另外,如果你沒有宣告任何建構函式,它也將為你宣告乙個預設建構函式。所有這些函式都是公有的。換句話說,如果你這麼寫:

class empty{};

和你這麼寫是一樣的:

class empty ;

現在,如果需要,這些函式就會被生成,但你會很容易就需要它們。下面的**將使得每個函式被生成:

const empty e1;                     // 預設建構函式

// 析構函式

empty e2(e1);                       // 拷貝建構函式

e2 = e1;                            //  賦值運算子

empty *pe2 = &e2;                   // 取址運算子

// (非const)

const empty *pe1 = &e1;             //  取址運算子

// (const)

假設編譯器為你寫了函式,這些函式又做些什麼呢?是這樣的,預設建構函式和析構函式實際上什麼也不做,它們只是讓你能夠建立和銷毀類的物件(對編譯器來說,將一些 "幕後" 行為的**放在此處也很方便。注意,生成的析構函式一般是非虛擬的,除非它所在的類是從乙個宣告了虛析構函式的基類繼承而來。預設取址運算子只是返回物件的位址。這些函式實際上就如同下面所定義的那樣:

inline empty::empty() {}

inline empty::~empty() {}

inline empty * empty::operator&()

inline const empty * empty::operator&() const

至於拷貝建構函式和賦值運算子,官方的規則是:預設拷貝建構函式(賦值運算子)對類的非靜態資料成員進行 "以成員為單位的" 逐一拷貝構造(賦值)。即,如果m是類c中型別為t的非靜態資料成員,並且c沒有宣告拷貝建構函式(賦值運算子),m將會通過型別t的拷貝建構函式(賦值運算子)被拷貝構造(賦值)---- 如果t有拷貝建構函式(賦值運算子)的話。如果沒有,規則遞迴應用到m的資料成員,直至找到乙個拷貝建構函式(賦值運算子)或固定型別(例如,int,double,指標,等)為止。預設情況下,固定型別的物件拷貝構造(賦值)時是從源物件到目標物件的 "逐位" 拷貝。對於從別的類繼承而來的類來說,這條規則適用於繼承層次結構中的每一層,所以,使用者自定義的建構函式和賦值運算子無論在哪一層被宣告,都會被呼叫。

我希望這已經說得很清楚了。

但怕萬一沒說清楚,還是給個例子。看這樣乙個namedobject模板的定義,它的例項是可以將名字和物件聯絡起來的類:

template

class namedobject ;

因為namedobject類宣告了至少乙個建構函式,編譯器將不會生成預設建構函式;但因為沒有宣告拷貝建構函式和賦值運算子,編譯器將生成這些函式(如果需要的話)。

看下面對拷貝建構函式的呼叫:

namedobjectno1("**allest prime number", 2);

namedobjectno2(no1);      // 呼叫拷貝建構函式

編譯器生成的拷貝建構函式必須分別用no1.namevalue和no1.objectvalue來初始化no2.namevalue和no2.objectvalue。namevalue的型別是string,string有乙個拷貝建構函式(你可以在標準庫中檢視string來證實),所以no2.namevalue初始化時將呼叫string的拷貝建構函式,引數為no1.namevalue。另一方面,namedobject::objectvalue的型別是int(因為這個模板例項中,t是int),int沒有定義拷貝建構函式,所以no2.objectvalue是通過從no1.objectvalue拷貝每乙個位元(bit)而被初始化的。

編譯器為namedobject生成的賦值運算子也以同樣的方式工作,但通常,編譯器生成的賦值運算子要想如上面所描述的那樣工作,與此相關的所有**必須合法且行為上要合理。如果這兩個條件中有乙個不成立,編譯器將拒絕為你的類生成operator=,你就會在編譯時收到一些診斷資訊。

例如,假設namedobject象這樣定義,namevalue是乙個string的引用,objectvalue是乙個const t:

template

class namedobject ;

現在看看下面將會發生什麼:

string newdog("persephone");

string olddog("satch");

namedobjectp(newdog, 2);      // 正在我寫本書時,我們的

// 愛犬persephone即將過

// 她的第二個生日

namedobjects(olddog, 29);     // 家犬satch如果還活著,

// 會有29歲了(從我童年時算起)

p = s;                              // p中的資料成員將會發生

// 些什麼呢?

賦值之前,p.namevalue指向某個string物件,s.namevalue也指向乙個string,但並非同乙個。賦值會給p.namevalue帶來怎樣的影響呢?賦值之後,p.namevalue應該指向 "被s.namevalue所指向的string" 嗎,即,引用本身應該被修改嗎?如果是這樣,那太陽從西邊出來了,因為c++沒有辦法讓乙個引用指向另乙個不同的物件。或者,p.namevalue所指的string物件應該被修改嗎? 這樣的話,含有 "指向那個string的指標或引用" 的其它物件也會受影響,也就是說,和賦值沒有直接關係的其它物件也會受影響。這是編譯器生成的賦值運算子應該做的嗎?

面對這樣的難題,c++拒絕編譯這段**。如果想讓乙個包含引用成員的類支援賦值,你就得自己定義賦值運算子。對於包含const成員的類(例如上面被修改的類中的objectvalue)來說,編譯器的處理也相似;因為修改const成員是不合法的,所以編譯器在隱式生成賦值函式時也會不知道怎麼辦。還有,如果派生類的基類將標準賦值運算子宣告為private,  編譯器也將拒絕為這個派生類生成賦值運算子。因為,編譯器為派生類生成的賦值運算子也應該處理基類部分,但這樣做的話,就得呼叫對派生類來說無權訪問的基類成員函式,這當然是不可能的。

以上關於編譯器生成函式的討論引發了這樣的問題:如果想禁止使用這些函式,那該怎麼辦呢?也就是說,假如你永遠不想讓類的物件進行賦值,所以有意不宣告operator=,那該怎麼做呢?指標成員和編譯器生成的拷貝建構函式及賦值運算子之間的相互影響經常被人忽視,

弄清C 在幕後為你所寫 所呼叫的函式

乙個空類什麼時候不是空類?當c 編譯器通過它的時候。如果你沒有宣告下列函式,體貼的編譯器會宣告它自己的版本。這些函式是 乙個拷貝建構函式,乙個賦值運算子,乙個析構函式,一對取址運算子。另外,如果你沒有宣告任何建構函式,它也將為你宣告乙個預設建構函式。所有這些函式都是公有的。換句話說,如果你這麼寫 c...

弄清C 在幕後為你所寫 所呼叫的函式

乙個空類什麼時候不是空類?當c 編譯器通過它的時候。如果你沒有宣告下列函式,體貼的編譯器會宣告它自己的版本。這些函式是 乙個拷貝建構函式,乙個賦值運算子,乙個析構函式,一對取址運算子。另外,如果你沒有宣告任何建構函式,它也將為你宣告乙個預設建構函式。所有這些函式都是公有的。換句話說,如果你這麼寫 c...

條款45 弄清C 在幕後為你所寫 所呼叫的函式

如果你沒有宣告下列函式,體貼的編譯器會宣告它自己的版本。這些函式是 乙個拷貝建構函式,乙個賦值運算子,乙個析構函式,一對取址運算子。另外,如果你沒有宣告任何建構函式,它也將為你宣告乙個預設建構函式。所有這些函式都是公有的。換句話說,如果你這麼寫 class empty 和你這麼寫是一樣的 class...