構造函數語意學 筆記(二)

第二篇筆記拖了好長時間,因爲我自身也是一知半解。所幸在查閱許多資料後,略有所得,故與諸君共享。這篇可能理解起來比較晦澀,我儘可能做好鋪墊。

若存在錯誤 請指正 萬分感想

鋪墊:

    1.

     Default memberwise initialization and bitwise copy 概念的區分和理解。說實話,一開始我也不明白,後來翻閱了下資料,知道涉及到深淺拷貝。

    default memberwise initialization 同 user_defined initialization 對應。前者是從編譯器的角度來說,後者自然是程序員角度來說。

     這個地方的初始化行爲我是這樣理解的:初始化只是目標,需要一定的方式才能進行,至於具體的方式那麼就看底下的內容。

     2.

     bitwise copy 同 memberwise copy 對應。兩種不同的拷貝方式,分別是淺拷貝和深拷貝。通常編譯器爲了效率會選擇bitwise 拷貝。

我在看深度探索C++模型一書中,在拷貝構造函數那一塊是十分混亂的,書中有時提及memberwise initialization ,但是後面又講了bitwise copy ,令我一度不能完成此章節的學習,博文也就拖到了今天。這兩個概念在類的對象初始化和賦值的時候經常會一起出現。

      但是隻要把握住上面那句話,初始化只是目標,而拷貝卻是具體的實現方式。

      從整體對象的角度來說:類中對象的初始化和賦值操作通常是按成員進行的,這個地方不是指memberwise copy.

      這個地方描述的按成員的意思是對每一個成員進行某種操作。

     具體到每一個數據成員,編譯器通常採用的是bitwise copy 的方式實現。

     如果用過memcpy函數的話,那麼這個地方的bitwise copy 就是如此實現的。

     示例:

#include <iostream>
using namespace  std;
class Base{
private:
	int a;
	int b;
	int* ptr;
public:
	Base(){
		a = 0;
		b = 0;
		ptr = nullptr;
	}
	Base(int a_a, int b_b, int* p_p){
		a = a_a;
		b = b_b;
		ptr = p_p;
	}
};
int main(){
	int tmp = 33333;
	int* ptmp = &tmp;
	Base A(100, 25,ptmp);
	Base B = A;  //這個時候的A 和 B 的ptr 指針指向哪裏?
	system("pause");
	return 0;
<span style="color:#330099;">}</span>
用圖片解釋下:(詳細情況可以參看下維基百科)


   我就解釋下右邊這個P指針都指向一個一個位置時候的情況,那麼當一個對象被析構掉的時候,那麼另一個指針怎麼辦?

    這個時候你就需要按照左邊圖片的方式來解決問題。

      示例:

#include <iostream>
using namespace  std;
class Base{
private:
	int a;
	int b;
	int* ptr;
public:
	Base(){
		ptr = new int(100);
	}
	Base(int a_a, int b_b, int* p_p){
		a = a_a;
		b = b_b;
		ptr = p_p;
	}
	/*Base(const Base& p){
	a = p.a; b = p.b;
	ptr = p.ptr;//這個地方會發生什麼?直接進行地址的賦值。
	}*/
	~Base(){
		delete ptr;
	}
	Base(const Base& p){
		a = p.a; b = p.b;
		ptr = new int;  //這個地方分配空間了.
		if (!ptr){
			cout << "!!" << endl;
		}

		*ptr = *(p.ptr);
	}
	friend ostream& operator<<(ostream& os, const Base& p){
		os << p.a << "   " << p.b << "   " << *(p.ptr) << endl;
		return os;
	}
};
int main(){
	Base b1;
	Base b2( b1);  //這個時候的b1 和 b2 的ptr 指針指向哪裏?
	cout << b1 << endl;
	cout << "----------" << endl;
	cout << b2 << endl;
	system("pause");
	return 0;
}
   鋪墊知識就到這裏了,詳情可以翻閱MSDN,大牛博客等資料。

    重點是要記住一句話,初始化是目標,實現目標的方式卻是進行拷貝。拷貝的方式就是由你和編譯器一起決定的。

    關於深淺拷貝的詳細知識點我乎總結的,但是現在卻不是合適的時候,重點是要理解淺拷貝爲什麼出問題,以及如何解決這個問題。核心是對內存的操作。

正題:

 三個問題:

    1.

         拷貝構造函數是解決什麼問題的?一定要明確了,是用對象給對象進行初始化或者賦值的時候纔會產生。

    一開始我在讀書的時候經常會混亂,讀着讀着竟然想到了默認構造函數,後來我一想,是我自己根本沒搞清楚拷貝構造函數的產生是幹什麼,以及何時會遇到這種問題。

        這個問題的產生是源於對象到對象的初始化。

     2.

         是否對象到對象的初始化一定要用到構造函數呢?答案是否定的。

     3.

         成員初始化只是目標,而不是具體的實現方式,實現方式你可以通過bitwise copy ,拷貝構造函數。


  三種情況:

     1.用拷貝構造函數初始化對象(這個地方我可沒說是同類對象之間)

     2.按值傳遞對象

     3.按值返回一個對象的副本。

     具體的你可以參見前面的臨時對象相關博文。

     大部分情況一下,以同類對象進行初始化,會調用相應的拷貝構造函數(前提是你已經顯示定義了一個,如果你不顯式的定義一個,那麼編譯器會合成嘛?)

Default memberwise initialization

   首先回憶上面的話,這個是目標,不然你可能會像我一樣進入一個怪圈。

    按照書中所描述,若類中沒用顯式的拷貝構造函數,當用同類對象進行初始化的時候,是按照default memberwise initialization 的方式完成初始化。

   看好了,這個地方只是簡單的initialization,並不是memberwise copy(深拷貝行爲)。但是若類中竟然包含了一個member class object 時,那麼對這個會對類中包含的成員對象進行遞歸併且是按照default memberwise initialization 的方式完成初始化。

  示例:

#include <iostream>
using namespace  std;
class String{
private:
	int len;
	char* str;
public:
	//沒用顯式的拷貝構造函數。
	String(){
		str = new char(100);
	}
	String(char* p){
		strcpy(str, p);
	}
};
int main(){
	String noun("book");
	String verb = noun;
	/*內部的操作類似:
	verb.len=noun.len;
	verb.str=noun.str
	暫時迴避淺拷貝的問題*/
}
   如果類裏面是含有對象成員的例子我就不舉了。大致的操作是類似的。

   上面我已經提到了,這個只是我們的目標而已,具體的實現方式是如何實現的呢?

   是編譯器合成的拷貝構造函數實現的嘛?

   這裏的理解可以參考默認構造函數的合成:C++標準是如此解釋的,只有在必要的時候纔會合成。關鍵是還是要理解什麼時候是必要的,這個把握住了,拷貝構造函數的合成時機也就能把握住了。

  合成的構造函數是trival 或者是nontrival 是依據一個標準進行劃分的。

Bitwise copy semantics

    當用同類對象進行初始化的時候,如果類中展現出了bitwise copy semantics ,那麼是不需要合成拷貝構造函數的,僅僅通過bitwise copy 就可以實現對象成員之間的初始化工作,也就是上面提到的default memberwise initialization 行爲。

    示例:

#include"word.h"
Word noun("bool");
void foo(){
	Word verb = noun;
}
     這個地方的verb對象明顯是根據noun對象進行初始化的,那麼編譯器是否需要合成一個拷貝構造函數呢?具體的需要看一下頭文件裏面到底是什麼東西。

//這個地方就展現出了bitwise copy 的意思。
//也就是暗示編譯器你簡單的淺拷貝就行了,
//至於這個地方存在指針會導致淺拷貝出現問題
//編譯器纔不會管。
class Word{
private:
	int cnt;
	char* str;
public:
	Word(const char*);
	~Word();
};
     可以觀察到私有的數據成員都是基本的數據類型,這個時候簡單的淺拷貝是可以解決問題的,暫時迴避指針的問題。

      下面看一下暗示編譯器沒有淺拷貝的意思的例子:

//這個例子就沒有展現出bitwise copy 的意思。
#include <string>
class Word{
private:
	string str;
	int cnt;
public:
	Word(const string&);
	~Word();
};
//而在string 的定義中存在一個拷貝構造函數。
class string{
public:
	string(const string&); //我顯式的定義了一個拷貝構造函數。
	string(const char*);
};
//一個被合成出來的拷貝構造函數可能是如下的(僞碼):
inline Word::Word(const Word& wd){
	str.string::string(wd.str);//這個地方就調用了string 的拷貝狗仔函數。
	cnt = wd.cnt;
}
    不知道你們看到這個地方是否與上面的default memberwise initialization 那一節對比一下,是否產生了疑惑。你看,這個地方我的類中內含了一個成員對象str,你上面不是講了嘛,是按照default memberwise initialization 方式初始化的嘛?一開始我沒有把這句話理解成一個目標,而是理解成一種實現方式了,導致我後面看一點,就衝突一點。

四種特殊情況:

  1.內含對象成員:

   也就是上面例子中str,並且內含的成員對象是帶有拷貝構造函數的(不論你的拷貝構造函數是如何來的,譬如拷貝啊,編譯器合成)。

  2.繼承的情況:

   基類是有拷貝構造函數的(同上面類似,不關注你拷貝構造函數的來源渠道)

  3.存在虛函數。

  4.有虛基類

相關解釋:

  一和二的解釋:   

     對於1和2 兩種情況,很好理解的。可以對比下上一篇博文的情況進行理解。

    因爲我內含對象成員或者是我有父類,並且他們都有拷貝構造函數的,那麼我當我想用同類的對象進行初始化的時候,那麼我幹嘛不調用他們的拷貝構造函數,萬無一失嘛。

   從編譯器的角度來說,編譯器是幹嘛的,幫助程序員的,既然存在了拷貝構造函數了,你就要按照程序員的要求做事情。

   所以這兩種情況下,編譯器檢查到你有拷貝構造函數了,那麼編譯器就默認你是想用他們的,所以編譯器直接合成一個拷貝構造函數以方便調用他們的拷貝構造函數。  

  不然你不合成,拷貝構造函數就在那裏放着,你不調用一輩子也用不上。

  所以我的理解就是這裏的拷貝構造函數被合成兩個目的:

    一是滿足程序員的需求,因爲編譯器認爲你是想要讓它用拷貝構造函數的;

    二就是爲了合理的調用內含成員的拷貝構造函數,總不能憑空就出現了內含成員對象的拷貝構造函數吧。

  這些都是我自己現階段的理解,僅供參考。

  其次比較重要的一點就是不要被上面的東西繞進去,你要明白這個問題的產生是由於類對象之間的初始化或者賦值,不然談何構造函數呢?

 第三的解釋:

    根源我們都知道的,是類對象的之間的賦值造成的,那麼你是否只是簡單的認爲同類對象的直接的複雜,如果是父類對象和子類對象之間的初始化會怎麼樣呢?尤其是當存在虛函數的情況。

    回憶下,虛函數出現的時候,類的行爲變化:

     1.是產生虛表(虛函數的地址表,virtual function table ,也稱vtbl)

     2.是對象裏面都出現了一個指向虛表的指針(vptr)

   那麼當我們用子類的成員給父類成員進行初始化時會發生什麼有趣的事情呢?

   示例:

class ZooAnimal{
public:
	ZooAnimal();
	virtual ~ZooAnimal();
	virtual void animate();
	virtual void draw();
private:
	//所需要的數據
};
class Bear :public ZooAnimal{
public:
	Bear();
	void animate();//虛函數,virtual 可以省略。
	void draw();//同上
	virtual void dance();//自己又定義了一個虛函數。
private:
	//所需要的數據.
};
    可以看到,Bear 繼承了ZooAnimal 類,並且都沒用定義拷貝構造函數。那麼當用同類對象進行初始化的時候,會發生什麼?

如:Bear yogi;
    Bear winne=yogi; //當你用同類對象進行初始化的時候,會發生什麼呢?
    用圖片解釋一下:


    這個地方排除了數據成員中有指針的情況。,可以看到雖然vptr指針進行淺拷貝,但是行爲卻是符合我們的要求的,因爲vptr指針必須是指向一個地方的。也就是說同類對象成員直接的初始化操作是可以同過簡單bitwise copy 完成的,不需要合成一個拷貝構造函數的。

    但是當你用子類對象給父類對象進行初始化的時候會發生什麼?

如:ZooAnimal franny=yogi; //看過前面文章的人應該是知道會發生切割行爲的。這個時候的vptr會怎麼樣?
   先看一下編譯器不干涉的情況:

   我畫圖解釋下:

    在第一篇文章中說到內存模型,那當你用子類對象給父類對象初始化的時候,那麼會發生切割,繼承的可能就是subobject那一部分,但是那裏面的指針指向的虛表是誰的呢?如果編譯器不干預的話,那麼肯定是指向子類的虛表的,也就是圖中的Bear。

     但是讓他指向了Bear的虛表根本就不符合我們的意願啊,我這裏可不是多態啊。

     那麼編譯器又會默認爲們是不想這麼做的,所以它要發威了,我幫你重新設定一下虛表的指針的指向位置,那麼可能出現這樣的情況:

    可以看到虛線的情況是我們不想看見的,但是在編譯器不干預的情況下,卻又會發生這種事情,所以編譯器只能出馬了。幫父類強制矯正它的vptr指針的指向位置。

    也就是說,合成出來的拷貝構造函數會顯式的設定對象的vptr,讓它指向ZooAnimal的虛表,而不是簡單的bitwise copy.

   第四的解釋:

   還是重複一下拷貝構造函數產生的根源性目的,用類對象進行初始化。

   當存在虛基類的時候,拷貝構造函數被合成是因爲要正確處理virtual base class subobject 的位置(這個地方會涉及到虛基類的內存模型,我現在理解的不好,我會在後面的虛擬繼承一節中總結下虛基類的問題),所以這個地方我只是一句話帶過,感興趣的可以去看看虛基類的知識,涉及到偏移位置。

總結:

    上述的四種情況中,類已經不在保持bitwise copy 的含義,並且還沒有聲明一個拷貝構造函數,並且你還想用對象初始化對象,那麼編譯器就有必要出手了,合成一個拷貝構造函數幫助你

後話:

    這篇文章着實花了不少時間,寫的着實頭暈,如果我犯錯了,請指出。如果感覺博文字體太小不便於閱讀,可以放大到133%。體驗不錯。

參考文獻:

    <<深度探索C++對象模型>>

    CSDN 博文:前面的鋪墊知識是這位大大給我普及的。

    地址:點擊打開鏈接

End

    下面的可能會接着記筆記,應該是記錄程序轉化語意學。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章