日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C++构造函数总结

發布時間:2024/9/27 c/c++ 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++构造函数总结 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一.構造函數基本概念(本部分源自C++ 類構造函數 & 析構函數)

1.1 類的構造函數

1.1.1 概念

類的構造函數是類的一種特殊的成員函數,它會在每次創建類的新對象時執行。

構造函數的名稱與類的名稱是完全相同的,并且不會返回任何類型,也不會返回 void。構造函數可用于為某些成員變量設置初始值。

下面的實例有助于更好地理解構造函數的概念:

#include <iostream>using namespace std;class Line {public:void setLength( double len );double getLength( void );Line(); // 這是構造函數private:double length; };// 成員函數定義,包括構造函數 Line::Line(void) {cout << "Object is being created" << endl; }void Line::setLength( double len ) {length = len; }double Line::getLength( void ) {return length; } // 程序的主函數 int main( ) {Line line;// 設置長度line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl;return 0; }

當上面的代碼被編譯和執行時,它會產生下列結果:

Object is being created Length of line : 6

1.2 帶參數的構造函數

默認的構造函數沒有任何參數,但如果需要,構造函數也可以帶有參數。這樣在創建對象時就會給對象賦初始值,如下面的例子所示:

#include <iostream>using namespace std;class Line {public:void setLength( double len );double getLength( void );Line(double len); // 這是構造函數private:double length; };// 成員函數定義,包括構造函數 Line::Line( double len) {cout << "Object is being created, length = " << len << endl;length = len; }void Line::setLength( double len ) {length = len; }double Line::getLength( void ) {return length; } // 程序的主函數 int main( ) {Line line(10.0);// 獲取默認設置的長度cout << "Length of line : " << line.getLength() <<endl;// 再次設置長度line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl;return 0; }

當上面的代碼被編譯和執行時,它會產生下列結果:

Object is being created, length = 10 Length of line : 10 Length of line : 6

1.3 使用初始化列表來初始化字段

使用初始化列表來初始化字段:

Line::Line( double len): length(len) {cout << "Object is being created, length = " << len << endl; }

上面的語法等同于如下語法:

Line::Line( double len) {length = len;cout << "Object is being created, length = " << len << endl; }

?假設有一個類 C,具有多個字段 X、Y、Z 等需要進行初始化,同理地,您可以使用上面的語法,只需要在不同的字段使用逗號進行分隔,如下所示:

C::C( double a, double b, double c): X(a), Y(b), Z(c) {.... }

1.4 類的析構函數

類的析構函數是類的一種特殊的成員函數,它會在每次刪除所創建的對象時執行。

析構函數的名稱與類的名稱是完全相同的,只是在前面加了個波浪號(~)作為前綴,它不會返回任何值,也不能帶有任何參數。析構函數有助于在跳出程序(比如關閉文件、釋放內存等)前釋放資源。

下面的實例有助于更好地理解析構函數的概念:

#include <iostream>using namespace std;class Line {public:void setLength( double len );double getLength( void );Line(); // 這是構造函數聲明~Line(); // 這是析構函數聲明private:double length; };// 成員函數定義,包括構造函數 Line::Line(void) {cout << "Object is being created" << endl; } Line::~Line(void) {cout << "Object is being deleted" << endl; }void Line::setLength( double len ) {length = len; }double Line::getLength( void ) {return length; } // 程序的主函數 int main( ) {Line line;// 設置長度line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl;return 0; }

當上面的代碼被編譯和執行時,它會產生下列結果:

Object is being created Length of line : 6 Object is being deleted

二、拷貝構造函數(本部分源自C++拷貝構造函數詳解)

2.1. 什么是拷貝構造函數

首先對于普通類型的對象來說,它們之間的復制是很簡單的,例如:

int a = 100; int b = a;

而類對象與普通對象不同,類對象內部結構一般較為復雜,存在各種成員變量。
下面看一個類對象拷貝的簡單例子。

#include <iostream> using namespace std;class CExample { private:int a; public://構造函數CExample(int b){ a = b;}//一般函數void Show (){cout<<a<<endl;} };int main() {CExample A(100);CExample B = A; //注意這里的對象初始化要調用拷貝構造函數,而非賦值B.Show ();return 0; }

運行程序,屏幕輸出100。從以上代碼的運行結果可以看出,系統為對象 B 分配了內存并完成了與對象 A 的復制過程。就類對象而言,相同類型的類對象是通過拷貝構造函數來完成整個復制過程的。

下面舉例說明拷貝構造函數的工作過程。

#include <iostream> using namespace std;class CExample { private:int a; public://構造函數CExample(int b){ a = b;}//拷貝構造函數CExample(const CExample& C){a = C.a;}//一般函數void Show (){cout<<a<<endl;} };int main() {CExample A(100);CExample B = A; // CExample B(A); 也是一樣的B.Show ();return 0; }

CExample(const CExample& C) 就是我們自定義的拷貝構造函數。可見,拷貝構造函數是一種特殊的構造函數,函數的名稱必須和類名稱一致,它必須的一個參數是本類型的一個引用變量

2.2 . 拷貝構造函數的調用時機

在C++中,下面三種對象需要調用拷貝構造函數!


2.2.1. 對象以值傳遞的方式傳入函數參數

class CExample { private:int a;public://構造函數CExample(int b){ a = b;cout<<"creat: "<<a<<endl;}//拷貝構造CExample(const CExample& C){a = C.a;cout<<"copy"<<endl;}//析構函數~CExample(){cout<< "delete: "<<a<<endl;}void Show (){cout<<a<<endl;} };//全局函數,傳入的是對象 void g_Fun(CExample C) {cout<<"test"<<endl; }int main() {CExample test(1);//傳入對象g_Fun(test);return 0; }

調用g_Fun()時,會產生以下幾個重要步驟:
(1).test對象傳入形參時,會先會產生一個臨時變量,就叫 C 吧。
(2).然后調用拷貝構造函數把test的值給C。 整個這兩個步驟有點像:CExample C(test);
(3).等g_Fun()執行完后, 析構掉 C 對象。

2.2.2. 對象以值傳遞的方式從函數返回

class CExample { private:int a;public://構造函數CExample(int b){ a = b;}//拷貝構造CExample(const CExample& C){a = C.a;cout<<"copy"<<endl;}void Show (){cout<<a<<endl;} };//全局函數 CExample g_Fun() {CExample temp(0);return temp; }int main() {g_Fun();return 0; }

?當g_Fun()函數執行到return時,會產生以下幾個重要步驟:
(1). 先會產生一個臨時變量,就叫XXXX吧。
(2). 然后調用拷貝構造函數把temp的值給XXXX。整個這兩個步驟有點像:CExample XXXX(temp);
(3). 在函數執行到最后先析構temp局部變量。
(4). 等g_Fun()執行完后再析構掉XXXX對象。

2.2.3. 對象需要通過另一個對象進行初始化;

CExample A(100); CExample B = A; // CExample B(A);

后兩句都會調用拷貝構造函數。

2.3. 淺拷貝和深拷貝

2.3.1. 默認拷貝構造函數

??? 很多時候在我們都不知道拷貝構造函數的情況下,傳遞對象給函數參數或者函數返回對象都能很好的進行,這是因為編譯器會給我們自動產生一個拷貝構造函數,這就是“默認拷貝構造函數”,這個構造函數很簡單,僅僅使用“老對象”的數據成員的值對“新對象”的數據成員一一進行賦值,它一般具有以下形式:

Rect::Rect(const Rect& r) {width = r.width;height = r.height; }

?當然,以上代碼不用我們編寫,編譯器會為我們自動生成。但是如果認為這樣就可以解決對象的復制問題,那就錯了,讓我們來考慮以下一段代碼:

class Rect { public:Rect() // 構造函數,計數器加1{count++;}~Rect() // 析構函數,計數器減1{count--;}static int getCount() // 返回計數器的值{return count;} private:int width;int height;static int count; // 一靜態成員做為計數器 };int Rect::count = 0; // 初始化計數器int main() {Rect rect1;cout<<"The count of Rect: "<<Rect::getCount()<<endl;Rect rect2(rect1); // 使用rect1復制rect2,此時應該有兩個對象cout<<"The count of Rect: "<<Rect::getCount()<<endl;return 0; }

?這段代碼對前面的類,加入了一個靜態成員,目的是進行計數。在主函數中,首先創建對象rect1,輸出此時的對象個數,然后使用rect1復制出對象rect2,再輸出此時的對象個數,按照理解,此時應該有兩個對象存在,但實際程序運行時,輸出的都是1,反應出只有1個對象。此外,在銷毀對象時,由于會調用銷毀兩個對象,類的析構函數會調用兩次,此時的計數器將變為負數。

說白了,就是拷貝構造函數沒有處理靜態數據成員。

出現這些問題最根本就在于在復制對象時,計數器沒有遞增,我們重新編寫拷貝構造函數,如下:

class Rect { public:Rect() // 構造函數,計數器加1{count++;}Rect(const Rect& r) // 拷貝構造函數{width = r.width;height = r.height;count++; // 計數器加1}~Rect() // 析構函數,計數器減1{count--;}static int getCount() // 返回計數器的值{return count;} private:int width;int height;static int count; // 一靜態成員做為計數器 };

2.3.2. 淺拷貝

??? 所謂淺拷貝,指的是在對象復制時,只對對象中的數據成員進行簡單的賦值,默認拷貝構造函數執行的也是淺拷貝。大多情況下“淺拷貝”已經能很好地工作了,但是一旦對象存在了動態成員,那么淺拷貝就會出問題了,讓我們考慮如下一段代碼:

class Rect { public:Rect() // 構造函數,p指向堆中分配的一空間{p = new int(100);}~Rect() // 析構函數,釋放動態分配的空間{if(p != NULL){delete p;}} private:int width;int height;int *p; // 一指針成員 };int main() {Rect rect1;Rect rect2(rect1); // 復制對象return 0; }

在這段代碼運行結束之前,會出現一個運行錯誤。原因就在于在進行對象復制時,對于動態分配的內容沒有進行正確的操作。我們來分析一下:

??? 在運行定義rect1對象后,由于在構造函數中有一個動態分配的語句,因此執行后的內存情況大致如下:

?在使用rect1復制rect2時,由于執行的是淺拷貝,只是將成員的值進行賦值,這時?rect1.p?= rect2.p,也即這兩個指針指向了堆里的同一個空間,如下圖所示:

當然,這不是我們所期望的結果,在銷毀對象時,兩個對象的析構函數將對同一個內存空間釋放兩次,這就是錯誤出現的原因。我們需要的不是兩個p有相同的值,而是兩個p指向的空間有相同的值,解決辦法就是使用“深拷貝”。

2.3.3. 深拷貝

??? 在“深拷貝”的情況下,對于對象中動態成員,就不能僅僅簡單地賦值了,而應該重新動態分配空間,如上面的例子就應該按照如下的方式進行處理:

class Rect { public:Rect() // 構造函數,p指向堆中分配的一空間{p = new int(100);}Rect(const Rect& r){width = r.width;height = r.height;p = new int; // 為新對象重新動態分配空間*p = *(r.p);}~Rect() // 析構函數,釋放動態分配的空間{if(p != NULL){delete p;}} private:int width;int height;int *p; // 一指針成員 };

此時,在完成對象的復制后,內存的一個大致情況如下:

此時rect1的p和rect2的p各自指向一段內存空間,但它們指向的空間具有相同的內容,這就是所謂的“深拷貝”。

2.3.4. 防止默認拷貝發生

??? 通過對對象復制的分析,我們發現對象的復制大多在進行“值傳遞”時發生,這里有一個小技巧可以防止按值傳遞——聲明一個私有拷貝構造函數。甚至不必去定義這個拷貝構造函數,這樣因為拷貝構造函數是私有的,如果用戶試圖按值傳遞或函數返回該類對象,將得到一個編譯錯誤,從而可以避免按值傳遞或返回對象。

// 防止按值傳遞 class CExample { private:int a;public://構造函數CExample(int b){ a = b;cout<<"creat: "<<a<<endl;}private://拷貝構造,只是聲明CExample(const CExample& C);public:~CExample(){cout<< "delete: "<<a<<endl;}void Show (){cout<<a<<endl;} };//全局函數 void g_Fun(CExample C) {cout<<"test"<<endl; }int main() {CExample test(1);//g_Fun(test); 按值傳遞將出錯return 0; }

2.4. 拷貝構造函數的幾個細節

1. 拷貝構造函數里能調用private成員變量嗎?
解答:
這個問題是在網上見的,當時一下子有點暈。其時從名子我們就知道拷貝構造函數其時就是一個特殊的構造函數,操作的還是自己類的成員變量,所以不受private的限制。

2. 以下函數哪個是拷貝構造函數,為什么?

X::X(const X&); X::X(X); X::X(X&, int a=1); X::X(X&, int a=1, int b=2);

解答:對于一個類X, 如果一個構造函數的第一個參數是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且沒有其他參數或其他參數都有默認值,那么這個函數是拷貝構造函數.

X::X(const X&); //是拷貝構造函數 X::X(X&, int=1); //是拷貝構造函數 X::X(X&, int a=1, int b=2); //當然也是拷貝構造函數

?3. 一個類中可以存在多于一個的拷貝構造函數嗎?
解答:
類中可以存在超過一個拷貝構造函數。

class X { public: X(const X&); // const 的拷貝構造X(X&); // 非const的拷貝構造 };

注意,如果一個類中只存在一個參數為 X& 的拷貝構造函數,那么就不能使用const X或volatile X的對象實行拷貝初始化.

class X { public:X(); X(X&); }; const X cx; X x = cx; // error

如果一個類中沒有定義拷貝構造函數,那么編譯器會自動產生一個默認的拷貝構造函數。
這個默認的參數可能為?X::X(const X&)或?X::X(X&),由編譯器根據上下文決定選擇哪一個。

三、禁用拷貝構造函數(源自為什么很多人禁用拷貝(復制)構造函數)

關于C++的拷貝構造函數,很多的建議是直接禁用。為什么大家會這么建議呢?沒有拷貝構 造函數會有什么限制呢?如何禁用拷貝構造呢?這篇文章對這些問題做一個簡單的總結。

這里討論的問題以拷貝構造函數為例子,但是通常賦值操作符是通過拷貝構造函數來實現 的( copy-and-swap 技術,詳見《Exceptional C++》一書),所以這里討論也適用于賦 值操作符,通常來說禁用拷貝構造函數的同時也會禁用賦值操作符。


3.1 為什么禁用拷貝構造函數

關于拷貝構造函數的禁用原因,我目前了解的主要是兩個原因。第一是淺拷貝問題,第二 個則是基類拷貝問題。

3.1.1 淺拷貝問題

編譯器默認生成的構造函數,是memberwise拷貝^1,也就是逐個拷貝成員變量,對于 下面這個類的定義

class Widget { public: Widget(const std::string &name) : name_(name), buf_(new char[10]) {} ~Widget() { delete buf_; } private: std::string name_; char *buf_; };

默認生成的拷貝構造函數,會直接拷貝buf_的值,導致兩個Widget對象指向同一個緩 沖區,這會導致析構的時候兩次刪除同一片區域的問題(這個問題又叫雙殺問題)。

解決這個問題的方式有很多:

  • 自己編寫拷貝構造函數,然后在拷貝構造函數中創建新的buf_,不過拷貝構造函數的 編寫需要考慮異常安全的問題,所以編寫起來有一定的難度。

  • 使用?shared_ptr?這樣的智能指針,讓所有的?Widget?對象共享一片?buf_,并 讓?shared_ptr?的引用計數機制幫你智能的處理刪除問題。

  • 禁用拷貝構造函數和賦值操作符。如果你根本沒有打算讓Widget支持拷貝,你完全可 以直接禁用這兩操作,這樣一來,前面提到的這些問題就都不是問題了。

  • 3.1.2 基類拷貝構造問題

    如果我們不去自己編寫拷貝構造函數,編譯器默認生成的版本會自動調用基類的拷貝構造 函數完成基類的拷貝:

    class Base { public: Base() { cout << "Base Default Constructor" << endl; } Base(const Base &) { cout << "Base Copy Constructor" << endl; } }; class Drived : public Base { public: Drived() { cout << "Drived Default Constructor" << endl; } }; int main(void) { Drived d1; Drived d2(d1); }

    上面這段代碼的輸出如下:

    Base Default Constructor Drived Default Constructor Base Copy Constructor // 自動調用了基類的拷貝構造函數

    但是如果我們出于某種原因編寫了,自己編寫了拷貝構造函數(比如因為上文中提到的淺 拷貝問題),編譯器不會幫我們安插基類的拷貝構造函數,它只會在必要的時候幫我們安 插基類的默認構造函數:

    class Base { public: Base() { cout << "Base Default Constructor" << endl; } Base(const Base &) { cout << "Base Copy Constructor" << endl; } }; class Drived : public Base { public: Drived() { cout << "Drived Default Constructor" << endl; } Drived(const Drived& d) { cout << "Drived Copy Constructor" << endl; } }; int main(void) { Drived d1; Drived d2(d1); }

    上面這段代碼的輸出如下:

    Base Default Constructor Drived Default Constructor Base Default Constructor // 調用了基類的默認構造函數 Drived Copy Constructor

    這當然不是我們想要看到的結果,為了能夠得到正確的結果,我們需要自己手動調用基類 的對應版本拷貝基類對象。

    Drived(const Drived& d) : Base(d) { cout << "Drived Copy Constructor" << endl; }

    這本來不是什么問題,只不過有些人編寫拷貝構造函數的時候會忘記這一點,所以導致基 類子對象沒有正常復制,造成很難察覺的BUG。所以為了一勞永逸的解決這些蛋疼的問題, 干脆就直接禁用拷貝構造和賦值操作符。


    3.2 沒有拷貝構造的限制

    在C++11之前對象必須有正常的拷貝語義才能放入容器中,禁用拷貝構造的對象無法直接放 入容器中,當然你可以使用指針來規避這一點,但是你又落入了自己管理指針的困境之中 (或許使用智能指針可以緩解這一問題)。

    C++11中存在移動語義,你可以通過移動而不是拷貝把數據放入容器中。

    拷貝構造函數的另一個應用在于設計模式中的原型模式,在C++中沒有拷貝構造函數,這 個模式實現可能比較困難。


    3.3 如何禁用拷貝構造

  • 如果你的編譯器支持 C++11,直接使用?delete

  • 否則你可以把拷貝構造函數和賦值操作符聲明成private同時不提供實現。

  • 你可以通過一個基類來封裝第二步,因為默認生成的拷貝構造函數會自動調用基類的拷 貝構造函數,如果基類的拷貝構造函數是?private,那么它無法訪問,也就無法正常 生成拷貝構造函數。

  • class NonCopyable { protected: ~NonCopyable() {} // 關于為什么聲明成為 protected,參考 // 《Exceptional C++ Style》 private: NonCopyable(const NonCopyable&); } class Widget : private NonCopyable { // 關于為什么使用 private 繼承 // 參考《Effective C++》第三版 } Widget widget(Widget()); // 錯誤

    上不會生成memberwise的拷貝構造函數,詳細內容可以參考《深度探索C++對象模型》一 書

    3.4??禁用拷貝


    禁用原因主要是兩個:
    1. 淺拷貝問題,也就是上面提到的二次析構。
    2. 自定義了基類和派生類的拷貝構造函數,但派生類對象拷貝時,調用了派生類的拷貝,沒有調用自定義的基類拷貝而是調用默認的基類拷貝。這樣可能造成不安全,比如出現二次析構問題時,因為不會調用我們自定義的基類深拷貝,還是默認的淺拷貝。

    Effective C++條款6規定,如果不想用編譯器自動生成的函數,就應該明確拒絕。方法一般有三種:
    1. C++11對函數聲明加delete關鍵字:Base(const Base& obj) = delete;,不必有函數體,這時再調用拷貝構造會報錯嘗試引用已刪除的函數。
    2. 最簡單的方法是將拷貝構造函數聲明為private
    3. 條款6給出了更好的處理方法:創建一個基類,聲明拷貝構造函數,但訪問權限是private,使用的類都繼承自這個基類。默認拷貝構造函數會自動調用基類的拷貝構造函數,而基類的拷貝構造函數是private,那么它無法訪問,也就無法正常生成拷貝構造函數。

    Qt就是這樣做的,QObject定義中有這樣一段,三條都利用了:

    第一種方法:最簡單的方法是將拷貝構造函數聲明為private

    private:Q_DISABLE_COPY(QMainWindow)#define Q_DISABLE_COPY(Class) \Class(const Class &) Q_DECL_EQ_DELETE;\Class &operator=(const Class &) Q_DECL_EQ_DELETE;


    類的不可拷貝特性是可以繼承的,例如凡是繼承自QObject的類都不能使用拷貝構造函數和賦值運算符。
    ?

    (2)第二種方法 繼承一個uncopyable類
    C++的編譯在鏈接之前,如果我們能在編譯期解決這個問題,會節省不少的時間,要想在編譯期解決問題,就需要人為制造一些bug。我們聲明一個專門阻止拷貝的基類uncopyable。

    class uncopyable{ protected:uncopyable(){}~uncopyable(){} private:uncopyable(const uncopyable&);uncopyable& operator=(const uncopyable&); }


    接下來,我們的類只要繼承uncopyable,如果要發生拷貝,編譯器都會嘗試調用基類的拷貝構造函數或者賦值運算符,但是因為這兩者是私有的,會出現編譯錯誤。

    四、C++類對象的賦值與復制(源自C++類對象的賦值與復制)

    本文主要介紹C++中類對象的賦值操作、復制操作,以及兩者之間的區別,另外還會講到“深拷貝”與“淺拷貝”的相關內容。

    本系列內容會分為三篇文章進行講解。

    4.1 對象的賦值


    4.1.1 what

    如同基本類型的賦值語句一樣,同一個類的對象之間也是可以進行賦值操作的,即將一個對象的值賦給另一個對象。

    對于類對象的賦值,只會對類中的數據成員進行賦值,而不對成員函數賦值。

    例如:obj1 和 obj2 是同一類 ClassA 的兩個對象,那么對象賦值語句“obj2 = obj1;” 就會把對象 obj1 的數據成員的值逐位賦給對象 obj2。


    4.1.2 代碼示例

    下面展示一個對象賦值的代碼示例(object_assign_and_copy_test1.cpp),如下:

    #include <iostream>using namespace std;class ClassA { public:// 設置成員變量的值void SetValue(int i, int j){m_nValue1 = i;m_nValue2 = j;}// 打印成員變量的值void ShowValue(){cout << "m_nValue1 is: " << m_nValue1 << ", m_nValue2 is: " << m_nValue2 << endl;} private:int m_nValue1;int m_nValue2; };int main() {// 聲明對象obj1和obj2ClassA obj1;ClassA obj2;obj1.SetValue(1, 2);// 對象賦值場景 —— 將obj1的值賦給obj2obj2 = obj1;cout << "obj1 info as followed: " << endl;obj1.ShowValue();cout << "obj2 info as followed: " << endl;obj2.ShowValue();return 0; }

    編譯并運行上述代碼,結果如下:

    上面的執行結果表明,通過對象賦值語句,我們將obj1的值成功地賦給了obj2。

    4.1.3 幾點說明

    對于對象賦值,進行以下幾點說明:

    • 進行對象賦值時,兩個對象的必須屬于同一個類,如對象所述的類不同,在編譯時將會報錯;
    • 兩個對象之間的賦值,只會讓這兩個對象中數據成員相同,而兩個對象仍然是獨立的。例如在上面的示例代碼中,進行對象賦值后,再調用 obj1.set() 設置 obj1 的值,并不會影響到 obj2 的值;
    • 對象賦值是通過賦值運算函數實現的。每一個類都有默認的賦值運算符,我們也可以根據需要,對賦值運算符進行重載。一般來說,需要手動編寫析構函數的類,都需要重載賦值運算符(具體原因下文會介紹);
    • 在對象聲明之后,進行的對象賦值運算,才屬于“真正的”對象賦值運算,即使用了賦值運算符“=”;而在對象初始化時,針對對象進行的賦值操作,其實是屬于對象的復制。示例如下:

    // 聲明obj1和obj2ClassA obj1;ClassA obj2;obj2 = obj1; // 此語句為對象的賦值// 聲明obj1ClassA obj1;// 聲明并初始化obj2ClassA obj2 = obj1; // 此語句屬于對象的復制

    4.1.4 進一步研究

    下面從內存分配的角度分析一下對象的賦值操作。


    4.1.4.1 C++中對象的內存分配方式

    在C++中,只要聲明了對象,對象實例在編譯的時候,系統就需要為其分配內存了。一段代碼示例如下:

    class ClassA { public:ClassA(int id, char* name){m_nId = id;m_pszName = new char[strlen(name) + 1];strcpy(m_pszName, name);} private:char* m_pszName;int m_nId; };int main() {ClassA obj1(1, "liitdar");ClassA obj2;return 0; }

    在上述代碼編譯之后,系統為 obj1 和 obj2 都分配相應大小的內存空間(只不過對象 obj1 的內存域被初始化了,而 obj2 的內存域的值為隨機值)。兩者的內存分配效果如下:


    4.1.4.2?默認的賦值運算符

    延續上面的示例代碼,我們執行“obj2 = obj1;”,即利用默認的賦值運算符將對象 obj1 的值賦給 obj2。使用類中默認的賦值運算符,會將對象中的所有位于 stack 中的域進行相應的復制操作;同時,如果對象有位于 heap 上的域,則不會為目標對象分配 heap 上的空間,而只是讓目標對象指向源對象 heap 上的同一個地址。

    執行了“obj2 = obj1;”默認的賦值運算后,兩個對象的內存分配效果如下:

    因此,對于類中默認的賦值運算,如果源對象域內沒有 heap 上的空間,其不會產生任何問題。但是,如果源對象域內需要申請 heap 上的空間,那么由于源對象和目標對象都指向?heap 的同一段內容,所以在析構對象的時候,就會連續兩次釋放 heap 上的那一塊內存區域,從而導致程序異常。

    ~ClassA(){delete m_pszName;}

    1.4.3?解決方案

    為了解決上面的問題,如果對象會在 heap 上存在內存域,則我們必須重載賦值運算符,從而在進行對象的賦值操作時,使不同對象的成員域指向不同的 heap 地址。

    重載賦值運算符的代碼如下:

    // 賦值運算符重載需要返回對象的引用,否則返回后其值立即消失ClassA& operator=(ClassA& obj){// 釋放heap內存if (m_pszName != NULL){delete m_pszName;}// 賦值stack內存的值this->m_nId = obj.m_nId;// 賦值heap內存的值int nLength = strlen(obj.m_pszName);m_pszName = new char[nLength + 1];strcpy(m_pszName, obj.m_pszName);return *this;}

    使用上面重載后的賦值運算符對對象進行賦值時,兩個對象的內存分配效果如下:

    這樣,在對象 obj1、obj2 退出其的作用域,調用相應的析構函數時,就會釋放不同 heap 空間的內存,也就不會出現程序異常了。

    4.2 對象的復制(源自C++類對象的賦值與復制(二))

    4.2.1 what

    相對于“對已聲明的對象使用賦值運算符進行的對象賦值”操作,使用拷貝構造函數操作對象的方式,稱為“對象的復制”。

    類的拷貝構造函數是一種特殊的構造函數,其形參是本類對象的引用。拷貝構造函數的作用為:在創建一個新對象時,使用一個已經存在的對象去初始化這個新對象。例如語句“ClassA?obj2(obj1);”就使用了拷貝構造函數,該語句在創建新對象 obj2 時,利用已經存在的對象 obj1 去初始化對象 obj2。

    對象的賦值與對象的復制,貌似都是只對類的成員變量進行拷貝,而不會對類的成員函數進行操作。—— 待進一步確認。

    4.2.2 拷貝構造函數的特點

    拷貝構造函數有以下特點:

    • 拷貝構造函數也是一種構造函數,所以其函數名與類名相同,并且該函數也沒有返回值類型;
    • 拷貝構造函數只有一個參數,并且該參數是其所屬類對象的引用;
    • 每一個類都必須有一個拷貝構造函數,我們可以根據需要重載默認的拷貝構造函數(自定義拷貝構造函數),如果沒有重載默認的拷貝構造函數,系統就會生成產生一個默認的拷貝構造函數,默認的拷貝構造函數將會復制出一個數據成員完全相同的新對象;

    4.2.3 自定義拷貝構造函數

    這里展示一個自定義拷貝構造函數的代碼示例(object_assign_and_copy_test2.cpp),如下:

    #include <iostream>using namespace std;class ClassA { public:// 普通構造函數ClassA(int i, int j){m_nValue1 = i;m_nValue2 = j;}// 自定義的拷貝構造函數ClassA(const ClassA& obj){m_nValue1 = obj.m_nValue1 * 2;m_nValue2 = obj.m_nValue2 * 2;}// 打印成員變量的值void ShowValue(){cout << "m_nValue1 is: " << m_nValue1 << ", m_nValue2 is: " << m_nValue2 << endl;} private:int m_nValue1;int m_nValue2; };int main() {// 創建并初始化對象obj1,此處調用了普通構造函數ClassA obj1(1, 2);// 創建并初始化對象obj2,此處調用了自定義的拷貝構造函數ClassA obj2(obj1);obj1.ShowValue();obj2.ShowValue();return 0; }

    編譯并執行上述代碼,結果如下:

    上述執行結果表明,通過調用自定義的拷貝構造函數,我們在創建對象 obj2 時,結合對象 obj1 的成員變量的值,完成了我們自定義的初始化過程。

    4.2.4 調用形式上的區別

    我們可以從調用形式上,對“對象的賦值”和“對象的復制”進行區分。在此,我們列出一些對應關系:

    • 對象的賦值:指的是調用了類的賦值運算符,進行的對象的拷貝操作;
    • 對象的復制:指的是調用了類的拷貝構造函數,進行的對象的拷貝操作。

    上面的對應關系是不嚴謹的,因為有些情況下,即使使用了賦值運算符“=”,但其實最終使用的仍然是類的拷貝構造函數,這就引出了拷貝構造函數的兩種調用形式。

    拷貝構造函數的調用語法分為兩種:

    • 類名 對象2(對象1)。例如:“ClassA obj2(obj1);”,這種調用拷貝構造函數的方法稱為“代入法”;
    • 類名 對象2 = 對象1。例如:“ClassA obj2 = obj1;”,這種調用拷貝構造函數的方法稱為“賦值法”。

    拷貝構造函數的“賦值法”就很容易與“對象的賦值”場景混淆,其二者之間的區別是:對象的賦值場景必須是建立在源對象與目標對象均已聲明的基礎上;而拷貝構造函數函數的賦值法,必須是針對新創建對象的場景。代碼如下:

    【對象的賦值】:

    // 聲明對象obj1和obj2ClassA obj1;ClassA obj2;obj1.SetValue(1, 2);// 對象賦值場景 —— 將obj1的值賦給obj2obj2 = obj1;

    【拷貝構造函數的“賦值法”】:

    // 創建并初始化對象obj1,此處調用了普通構造函數ClassA obj1(1, 2);// 創建并初始化對象obj2,此處調用了自定義的拷貝構造函數ClassA obj2 = obj1;

    當然,為了代碼的清晰化,建議使用拷貝構造函數的“代入法”,更可以讓人一眼就看出調用的是拷貝構造函數。

    4.2.5 調用拷貝構造函數的三個場景

    4.2.5.1 類對象初始化

    當使用類的一個對象去初始化另一個對象時,會調用拷貝構造函數(包括“代入法”和“賦值法”)。示例代碼如下:

    // 創建并初始化對象obj1,此處調用了普通構造函數ClassA obj1(1, 2);// 創建并初始化對象obj2,此處調用了自定義的拷貝構造函數ClassA obj2 = obj1; // 代入法ClassA obj3 = obj1; // 賦值法

    4.2.5.2?類對象作為函數參數

    當類對象作為函數形參時,在調用函數進行形參和實參轉換時,會調用拷貝構造函數。示例代碼如下:

    // 形參是類ClassA的對象obj void funA(ClassA obj) {obj.ShowValue(); }int main() {ClassA obj1(1, 2);// 調用函數funA時,實參obj1是類ClassA的對象// 這里會調用拷貝構造函數,使用實參obj1初始化形參對象objfunA(obj1);return 0; }

    說明:在上面的main函數內,語句“funA(obj1);”就會調用拷貝構造函數。


    4.2.5.3?類對象作為函數返回值

    當函數的返回值是類的對象、在函數調用完畢將返回值(對象)帶回函數調用處,此時會調用拷貝構造函數,將函數返回的對象賦值給一個臨時對象,并傳到函數的調用處。示例代碼如下:

    // 函數funB()的返回值類型是ClassA類類型 ClassA funB() {ClassA obj1(1, 2);// 函數的返回值是ClassA類的對象return obj1; }int main() {// 定義類ClassA的對象obj2ClassA obj2;// funB()函數執行完成、返回調用處時,會調用拷貝構造函數// 使用obj1初始化obj2obj2 = funB();return 0; }

    說明:在上面的main函數內,語句“obj2 = funB();”就會調用拷貝構造函數。由于對象obj1是函數funB中定義的,在函數funB結束時,obj1的生命周期就結束了,因此在函數funB結束之前,執行語句"return obj1"時,會調用拷貝構造函數將obj1的值拷貝到一個
    臨時對象中,這個臨時對象是系統在主程序中臨時創建的。funB函數結束時,對象obj1消失,但是臨時對象將會通過語句“obj2 = funB()”賦值給對象obj2,執行完這條語句后,臨時對象也自動消失了。?

    4.3 淺拷貝(源自C++類對象的賦值與復制(三))

    4.3.1 what

    淺拷貝:就是只拷貝類中位于 stack 域中的內容,而不會拷貝 heap 域中的內容。

    例如,使用類的默認的賦值運算符“=”,或默認的拷貝構造函數時,進行的對象拷貝都屬于淺拷貝。這也說明,“淺拷貝”與使用哪種方式(賦值運算符或是拷貝構造函數)進行對象拷貝無關。

    4.3.2 問題

    淺拷貝會有一個問題,當類中存在指針成員變量時,進行淺拷貝后,目標對象與源對象的該指針成員變量將會指向同一塊 heap 內存(而非每個對象單獨一塊內存),這就會導致由于共用該段內存而產生的內存覆蓋、重復釋放內存等等問題。詳情可參考本系列第一章內容。

    所以,對于帶有指針的類對象的拷貝操作,正確的做法應當使兩個對象的指針指向各自不同的內存,即在拷貝時不是簡單地拷貝指針,而是將指針指向的內存中的每一個元素都進行拷貝。由此也就引出了“深拷貝”的概念。

    4.4?深拷貝

    深拷貝:當進行對象拷貝時,將對象位于?stack 域和 heap 域中的數據都進行拷貝。

    前面也提到了,類默認提供的賦值運算符或拷貝構造函數,進行的都是淺拷貝,所以,為了實現對象的深拷貝,我們需要對賦值運算符或拷貝構造函數進行重載,以達到深拷貝的目的。

    4.4.1 賦值運算符的重載

    這里展示一段重載賦值運算符的示例代碼,如下:

    // 重載賦值運算符ClassA& operaton= (ClassA& obj){// 拷貝 stack 域的值m_nId = obj.m_nId;// 適應自賦值(obj = obj)操作if (this == &a){return *this;}// 釋放掉已有的 heap 空間if (m_pszName != NULL){delete m_pszName;}// 新建 heap 空間m_pszName = new char[strlen(obj.m_pszName) + 1];// 拷貝 heap 空間的內容if (m_pszName != NULL){strcpy(m_pszName, obj.m_pszName);}return *this;}private:int m_nId;char* m_pszName;

    4.4.2?拷貝構造函數的重載

    這里展示一段重載拷貝構造函數的示例代碼,如下:

    // 重載拷貝構造函數,重載后的拷貝構造函數支持深拷貝ClassA(ClassA &obj){// 拷貝 stack 域的值m_nId = obj.m_nId;// 新建 heap 空間m_pszName = new char[strlen(obj.m_pszName) + 1];// 拷貝 heap 空間的內容if (m_pszName != NULL){strcpy(m_pszName, obj.m_pszName);}}private:int m_nId;char* m_pszName;

    4.4.3?總結

    從上述兩個示例代碼可以看出,支持深拷貝的重載賦值運算符和重載拷貝構造函數相似,但兩者也存在以下區別:

    • 重載賦值運算符最好有返回值,以方便進行鏈式賦值(obj3=obj2=obj1),返回值類型也最好是對象的引用;而重載拷貝構造函數因為屬于構造函數的一種,所以不需要返回值;
    • 重載賦值運算符首先要釋放掉對象自身的 heap 空間(如果存在的話),然后再進行 heap 內容的拷貝操作;而重載拷貝構造函數無需如此,因為拷貝構造函數函數是在創建(并初始化)對象時調用的,對象此時還沒有分配 heap 空間呢。
    • 如果在重載賦值運算符和重載拷貝構造函數都可以解決問題時,建議選擇重載拷貝構造函數,因為貌似坑少一些^-^。

    五、C++中構造函數,拷貝構造函數和賦值函數的區別和實現(源自C++中構造函數,拷貝構造函數和賦值函數的區別和實現)

    C++中一般創建對象,拷貝或賦值的方式有構造函數,拷貝構造函數,賦值函數這三種方法。下面就詳細比較下三者之間的區別以及它們的具體實現

    5.1.構造函數

    構造函數是一種特殊的類成員函數,是當創建一個類的對象時,它被調用來對類的數據成員進行初始化和分配內存。(構造函數的命名必須和類名完全相同)

    首先說一下一個C++的空類,編譯器會加入哪些默認的成員函數

    ·默認構造函數和拷貝構造函數

    ·析構函數

    ·賦值函數(賦值運算符)

    ·取值函數

    **即使程序沒定義任何成員,編譯器也會插入以上的函數!

    注意:構造函數可以被重載,可以多個,可以帶參數;

    析構函數只有一個,不能被重載,不帶參數

    而默認構造函數沒有參數,它什么也不做。當沒有重載無參構造函數時,

    ? A a就是通過默認構造函數來創建一個對象

    下面代碼為構造函數重載的實現

    <span style="font-size:14px;">class A { int m_i; Public:A() {Cout<<”無參構造函數”<<endl; } A(int i):m_i(i) {} //初始化列表 }</span>

    5.2.拷貝構造函數

    拷貝構造函數是C++獨有的,它是一種特殊的構造函數,用基于同一類的一個對象構造和初始化另一個對象。

    當沒有重載拷貝構造函數時,通過默認拷貝構造函數來創建一個對象

    A a;

    A b(a);

    A b=a; ?都是拷貝構造函數來創建對象b

    強調:這里b對象是不存在的,是用a 對象來構造和初始化b的!!

    先說下什么時候拷貝構造函數會被調用:

    在C++中,3種對象需要復制,此時拷貝構造函數會被調用

    1)一個對象以值傳遞的方式傳入函數體

    2)一個對象以值傳遞的方式從函數返回

    3)一個對象需要通過另一個對象進行初始化

    什么時候編譯器會生成默認的拷貝構造函數:

    1)如果用戶沒有自定義拷貝構造函數,并且在代碼中使用到了拷貝構造函數,編譯器就會生成默認的拷貝構造函數。但如果用戶定義了拷貝構造函數,編譯器就不在生成。

    2)如果用戶定義了一個構造函數,但不是拷貝構造函數,而此時代碼中又用到了拷貝構造函數,那編譯器也會生成默認的拷貝構造函數。

    因為系統提供的默認拷貝構造函數工作方式是內存拷貝,也就是淺拷貝。如果對象中用到了需要手動釋放的對象,則會出現問題,這時就要手動重載拷貝構造函數,實現深拷貝。

    下面說說深拷貝與淺拷貝:

    淺拷貝:如果復制的對象中引用了一個外部內容(例如分配在堆上的數據),那么在復制這個對象的時候,讓新舊兩個對象指向同一個外部內容,就是淺拷貝。(指針雖然復制了,但所指向的空間內容并沒有復制,而是由兩個對象共用,兩個對象不獨立,刪除空間存在)

    深拷貝:如果在復制這個對象的時候為新對象制作了外部對象的獨立復制,就是深拷貝。

    拷貝構造函數重載聲明如下:

    A (const A&other)

    下面為拷貝構造函數的實現:

    <span style="font-size:14px;">class A {int m_iA(const A& other):m_i(other.m_i) {Cout<<”拷貝構造函數”<<endl; } }</span>

    5.3.賦值函數

    當一個類的對象向該類的另一個對象賦值時,就會用到該類的賦值函數。

    當沒有重載賦值函數(賦值運算符)時,通過默認賦值函數來進行賦值操作

    A a;

    A b;

    b=a;?

    強調:這里a,b對象是已經存在的,是用a 對象來賦值給b的!!

    賦值運算的重載聲明如下:

    ?A& operator = (const A& other)

    通常大家會對拷貝構造函數和賦值函數混淆,這兒仔細比較兩者的區別:

    1)拷貝構造函數是一個對象初始化一塊內存區域,這塊內存就是新對象的內存區,而賦值函數是對于一個已經被初始化的對象來進行賦值操作。

    class A; A a; A b=a; //調用拷貝構造函數(b不存在) A c(a) ; //調用拷貝構造函數/****/class A; A a; A b; b = a ; //調用賦值函數(b存在)</span>

    2)一般來說在數據成員包含指針對象的時候,需要考慮兩種不同的處理需求:一種是復制指針對象,另一種是引用指針對象。拷貝構造函數大多數情況下是復制,而賦值函數是引用對象

    3)實現不一樣。拷貝構造函數首先是一個構造函數,它調用時候是通過參數的對象初始化產生一個對象。賦值函數則是把一個新的對象賦值給一個原有的對象,所以如果原來的對象中有內存分配要先把內存釋放掉,而且還要檢察一下兩個對象是不是同一個對象,如果是,不做任何操作,直接返回。(這些要點會在下面的String實現代碼中體現)

    ?

    !!!如果不想寫拷貝構造函數和賦值函數,又不允許別人使用編譯器生成的缺省函數,最簡單的辦法是將拷貝構造函數和賦值函數聲明為私有函數,不用編寫代碼。如:

    <span style="font-size:14px;">class A {private:A(const A& a); //私有拷貝構造函數A& operate=(const A& a); //私有賦值函數 }</span>

    如果程序這樣寫就會出錯:

    <span style="font-size:14px;">A a; A b(a); //調用了私有拷貝構造函數,編譯出錯A b; b=a; //調用了私有賦值函數,編譯出錯</span>

    所以如果類定義中有指針或引用變量或對象,為了避免潛在錯誤,最好重載拷貝構造函數和賦值函數。

    下面以string類的實現為例,完整的寫了普通構造函數,拷貝構造函數,賦值函數的實現。String類的基本實現見我另一篇博文。

    String::String(const char* str) //普通構造函數{cout<<construct<<endl;if(str==NULL) //如果str 為NULL,就存一個空字符串“”{m_string=new char[1];*m_string ='\0'; }else{m_string= new char[strlen(str)+1] ; //分配空間strcpy(m_string,str);}}String::String(const String&other) //拷貝構造函數{cout<<"copy construct"<<endl;m_string=new char[strlen(other.m_string)+1]; //分配空間并拷貝strcpy(m_string,other.m_string); }String & String::operator=(const String& other) //賦值運算符 {cout<<"operator =funtion"<<endl ;if(this==&other) //如果對象和other是用一個對象,直接返回本身{return *this;}delete []m_string; //先釋放原來的內存m_string= new char[strlen(other.m_string)+1];strcpy(m_string,other.m_string);return * this; }</span>

    一句話記住三者:對象不存在,且沒用別的對象來初始化,就是調用了構造函數;

    ??????????????? 對象不存在,且用別的對象來初始化,就是拷貝構造函數(上面說了三種用它的情況!)

    ???????????????? 對象存在,用別的對象來給它賦值,就是賦值函數。

    以上為本人結合很多資料和圖書整理出來的,將核心的點都系統的理出來,全自己按條理寫的,現在大家對普通構造函數,拷貝構造函數,賦值函數的區別和實現應該都清楚了。

    六、C++類禁止copy構造函數和copy assign操作符(C++類禁止copy構造函數和copy assign操作符 - 十|里 - 博客園)

    在C++類中,編譯器可以暗自為class創建default構造函數、copy構造函數、copy assignment操作符,以及析構函數。注意,這些編譯器產生出來的函數都是public的,為了阻止這些函數被創建出來,我們可以把它們聲明為private,這樣就阻止了編譯器暗自創建其對應版本函數。

    class Node { public: Node(int _data = 0) : data(_data) {} int get() const { return data; } void set(int _data) { data = _data; } private: Node(const Node &); Node &operator=(const Node &); int data; };

    在上面的class定義中,當程序企圖拷貝Node對象時,編譯器就會阻止該操作。這樣的話,只要將copy構造函數和copy assign操作符聲明為private就可以了,還有另外一種方式,我們可以專門定義一個阻止copying動作的base class。這個base class如下所示:

    class Uncopyable { protected: Uncopyable() {} // 允許derived對象構造和析構 ~Uncopyable() {} private: Uncopyable(const Uncopyable &); // 阻止copying Uncopyable &operator=(const Uncopyable &); }; class Node : private Uncopyable { public: Node(int _data = 0) : data(_data) {} int get() const { return data; } void set(int _data) { data = _data; } private: int data; };

    這樣的話,在程序中,甚至在member函數或friend函數中,嘗試拷貝Node對象,編譯器就會試著生成一個copy構造函數或copy assign操作符,這些函數的“默認版本”會嘗試調用其base class的對應函數,但是這些調用會被阻止,因為它們是private的,即阻止了該類對象的copy操作。

    ?

    ?

    ?

    與50位技術專家面對面20年技術見證,附贈技術全景圖

    總結

    以上是生活随笔為你收集整理的C++构造函数总结的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。