<sub id="gqw76"><listing id="gqw76"></listing></sub>
      <sub id="gqw76"><listing id="gqw76"></listing></sub>

    1. <form id="gqw76"><legend id="gqw76"></legend></form>
    2. 關于C++中構造函數的常見疑問

      基本概念

        我們已經知道在定義一個對象時,該對象會根據你傳入的參數來調用類中對應的構造函數。同時,在釋放這個對象時,會調用類中的析構函數。其中,構造函數有三種,分別是默認構造函數有參構造函數拷貝構造函數。在類中,如果我們沒有自行定義任何的構造函數,編譯器會為我們提供兩種構造函數(默認構造函數和拷貝構造函數)以及析構函數。其中默認構造函數和析構函數是空函數,而拷貝構造函數會為類中的每個成員變量進行淺拷貝。相關實現代碼如下:(注意,下面的代碼只是為了方便理解編譯器提供的函數是如何實現的。在實際中我們并不需要自行定義下面函數,因為我們前面有說,在聲明一個類后,編譯器會為我們自動提供這三個函數。)

       1 class Cls {
       2 private:
       3     int a_;
       4     int b_;
       5     
       6 public:
       7     Cls() {}                 // 編譯器提供的默認構造函數 
       8     Cls(const Cls &obj) {    // 編譯器提供的拷貝構造函數
       9         this -> a_ = obj.a_;
      10         this -> b_ = obj.b_;
      11     } 
      12     ~Cls() {}                // 編譯器提供的析構函數 
      13 };

         在類中,如果我們自己定義了相關的構造函數和析構函數,那么我們自己定義的函數會代替編譯器為我們默認提供的相關函數。通常我們定義構造函數,是為了在定義一個類后,調用我們自行定義的構造函數來為類中的成員變量進行初始化。例如,我們可以自己進行相關定義:

       1 class Cls {
       2 private:
       3     int a_;
       4     int b_;
       5     
       6 public:
       7     Cls() {                             // 默認構造函數 
       8         a_ = 1;
       9         b_ = 2;
      10     }
      11     Cls(int a, int b): a_(a), b_(b) {}  // 編譯器不會為我們提供有參構造函數。自行定義有參構造函數,并用初始化列表進行初始化 
      12 //  Cls(const Cls &obj) {               // 如果不需要進行深拷貝操作,一般不需要自行定義拷貝構造函數 
      13 //      this -> a_ = obj.a_;
      14 //      this -> b_ = obj.b_;
      15 //  } 
      16 //  ~Cls() {}                           // 析構函數,通常用來釋放我們在堆區申請的內存,一般情況下不需要自行定義 
      17 };

      利用上面的Cls類聲明,我們來定義一個對象:

      1 int main() {
      2     Cls obj1;       // 定義后,會調用默認構造函數,obj1對象中的a_初始化為1, b_初始化為2
      3     Cls obj2(0, 1); // 定義后,會調用有參構造函數,obj2對象中的a_初始化為0, b_初始化為1 
      4     Cls obj3(obj1); // 定義后,會調用拷貝構造函數,obj2中的成員變量的值會拷貝到obj3中來對obj3進行初始化,a_為1, b_為2
      5     
      6     return 0;
      7 }

       

      關于定義構造函數的注意事項  

      1. 當我們只在類中自定義了默認構造函數,如果需要對新建對象進行默認構造初始化,就會調用我們自己的默認構造函數,同時編譯器仍會提供拷貝構造函數
      2. 當我們只在類中自定義了有參構造函數編譯器就不會再提供默認構造函數,但仍會提供拷貝構造函數有參構造函數需要我們自己來定義,編譯器不會為我們提供。這里有個問題是,如果我們在新建一個對象時不傳入任何的參數,那么編譯器就會因為不能夠為對象調用默認構造函數而報錯。所以需要我們再去自定義一個默認構造函數。當然,這里還有一個方法,那就是為我們自定義的有參構造函數中的全部形參提供默認值,這樣子就不需要再定義默認構造函數了,做法如下:
        1 Cls(int a = 1, int b = 2): a_(a), b_(b) {}

         

      3. 當我們只在類中定義了拷貝構造函數,那么編譯器同樣不會提供默認構造函數,同時我們自定義的拷貝構造函數會取代編譯器原本為我們提供的拷貝構造函數。所以我們需要再自定義默認構造函數或有參構造函數。在一個類中,永遠存在拷貝構造函數。
      4. 無論我們是否自定義構造函數編譯器都會為我們提供析構函數(空實現的函數),只有我們自定義了析構函數,才能代替編譯器為我們提供的析構函數。也就是說,在一個類中,永遠存在析構函數。
      5. 下面會提到operator=函數,這也是編譯器為我們提供的一個函數,在為進行了對象初始化后調用,這個賦值函數會對屬于同一個類的對象進行淺拷貝,如obj1 = obj2。我們可以在類中進行'='號運算符重載,以調用我們定義的operator=函數。

       

      淺拷貝與深拷貝

        在前面我們有提到淺拷貝與深拷貝,這里我們進行詳細說明。

        淺拷貝就是簡單的賦值操作,編譯器為我們提供的拷貝構造函數就是進行淺拷貝。這里列出個淺拷貝的缺陷。比如我們聲明一個新的類,并且用這個類來創造兩個對象,并讓其中一個對象調用編譯器提供的拷貝構造函數,代碼如下:

       1 #include <iostream>
       2 using namespace std;
       3 
       4 class Cls {
       5 public:
       6     int *p_;
       7     
       8     Cls(int num = 0) {
       9         p_ = new int(num);
      10     }
      11     ~Cls() {
      12         if (p_ != NULL) {
      13             delete p_;
      14             p_ = NULL;
      15         }
      16     }
      17 };
      18 
      19 void test() {
      20     Cls obj1(10);                  // 調用有參構造函數 
      21     Cls obj2(obj1);                // 調用拷貝構造函數,進行淺拷貝 
      22     cout << *obj1.p_ << endl;
      23     cout << *obj2.p_ << endl;
      24 }
      25 
      26 int main() {
      27     test();
      28     
      29     return 0;
      30 }

      運行結果為下圖:  

        可以看到,雖然正確輸出對應的值,但main函數返回的結果不為0,也就是程序運行奔潰了。這是什么原因呢?先給出其中的原因:因為同一塊內存被釋放了兩次!下面我們來進行分析。

        由于obj2是調用編譯器提供的拷貝構造函數,通過淺拷貝進行初始化,在拷貝構造函數中進行的操作是obj2.p_ = obj1.p_;(注意,這里的表述并不嚴格!),也就是說,obj2.p_存放的地址與obj1.p_存放的地址相同,他們都指向同一塊內存。其實,在全局函數test調用結束前,一切都是正常運行的。而test函數調用結束后,由于函數內的變量要回收釋放,obj1和obj2都要調用析構函數。關鍵的地方來了!按照棧的規則,首先我們需要調用obj2的析構函數,在調用后,obj2.p_在堆區所指向的內存就釋放掉了,同時obj2.p_指向NULL。然后,再調用obj1的析構函數,因為obj1和obj2指向同一塊內存,但是obj2的析構函數剛剛已經把這塊內存給釋放了,而obj1又要釋放一次,所以就會造成同一塊內存空間被釋放兩次,從而程序奔潰!

        為了防止出現這些問題,我們必須對一個新的對象通過深拷貝來初始化。深拷貝就不是簡單地進行值的賦值操作,而是向堆區申請一塊新的內存空間,但這塊內存空間存放的值和你的傳入參數一樣,而這個對象中的成員變量存放的值(地址)與你傳入的參數中的成員變量存放的值(地址)不一樣,這樣就可以避免出現上述的問題。代碼改進如下:

       1 #include <iostream>
       2 using namespace std;
       3 
       4 class Cls {
       5 public:
       6     int *p_;
       7     
       8     Cls(int num = 0) {
       9         p_ = new int(num);
      10     }
      11     Cls(const Cls &obj) {         // 自定義拷貝構造函數,進行深拷貝操作 
      12         p_ = new int(*obj.p_);
      13     }
      14     ~Cls() {
      15         if (p_ != NULL) {
      16             delete p_;
      17             p_ = NULL;
      18         }
      19     }
      20 };
      21 
      22 void test() {
      23     Cls obj1(10);
      24     Cls obj2(obj1);               // 調用自定義拷貝構造函數,進行深拷貝 
      25     cout << *obj1.p_ << endl;
      26     cout << *obj2.p_ << endl;
      27 }
      28 
      29 int main() {
      30     test();
      31     
      32     return 0;
      33 }

       下面來通過一個圖來進行進一步的理解。

       

        在創建的對象調用了構造函數進行初始化后,如果需要讓對象通過'='號拷貝另外一個對象的值時,同樣應該進行的是深拷貝操作。需要補充的是,在創建一個類時編譯器除了會自動提供上述所說的默認構造函數,拷貝構造函數和析構函數外,還會提供一個賦值運算符operator=函數,對屬性進行值拷貝,這個拷貝是淺拷貝。拷貝構造函數其實和operator=函數幾乎是一樣的,都是對成員變量之間進行淺拷貝。不同的地方在于,拷貝構造函數只會調用一次,那就是在你剛創建一個新的對象,并且傳入屬于同一個類的對象作為參數,來調用拷貝構造函數進行初始化。比如:Cls obj1(obj2);。而operator=可以調用多次,在對象進行了初始化后(或者調用了構造函數后)所有屬于同一個類的對象之間進行的’=’號賦值運算都是調用operator=函數,來拷貝另一個對象的值。比如:obj1 = obj2。注意,Cls obj1 = obj2;(obj2已經初始化)是調用拷貝構造函數(隱式轉換法)!前面已經提到,編譯器提供的operator=進行的是淺拷貝操作,我們需要的是可以進行深拷貝操作的operator=函數。為了實現深拷貝,我們需要在類中對'='號運算符進行重載,代碼如下:

      1 Cls& operator=(Cls &obj) {    // 對'='運算符進行重載,實現深拷貝 
      2         if (this -> p_) {
      3             delete p_;
      4             p_ = NULL;
      5         }
      6         p_ = new int(*obj.p_);
      7         return *this;
      8     }

      當我們使對象進行'='賦值時,就會調用上面我們定義的operator=函數。例如:

      1 int main() {
      2     Cls obj1(10), obj2(obj1), obj3(20);    // *obj1.p_ == 10, *obj2.p_ == 10, *obj3.p_ == 20
      3     obj1 = obj2 = obj3;                    // *obj1.p_ == *obj2.p_ == *obj3.p_ == 20
      4     cout << "*obj1.p_ = " << *obj1.p_ << endl;
      5     cout << "*obj2.p_ = " << *obj2.p_ << endl;
      6     cout << "*obj3.p_ = " << *obj3.p_ << endl;
      7     return 0;
      8 }

      運行結果如下:

       

      對象初始化的時機

        前一段時間我很難理解這個問題,分不清什么時候調用構造函數和operator=函數,尤其是一個類中含有類成員的時候。

        下面我先用點類和圓類,定義一個含有類成員(Point)的一個類(Circle)。

       1 #include <iostream>
       2 using namespace std;
       3 
       4 class Point {
       5 private:
       6     double x_;
       7     double y_;
       8 
       9 public:
      10     Point() {
      11         cout << "Point::調用默認構造函數" << endl; 
      12     }
      13     Point(double x, double y): x_(x), y_(y) {
      14         cout << "Point::調用有參構造函數" << endl;
      15     }
      16     void setX(double x) {
      17         x_ = x;
      18     }
      19     double getX() {
      20         return x_;
      21     }
      22     void setY(double y) {
      23         y_ = y;
      24     }
      25     double getY() {
      26         return y_;
      27     }
      28 };
      29 
      30 class Circle {
      31 private:
      32     double r_;
      33     Point center_;
      34     
      35 public:
      36     Circle(int r, int x, int y): r_(r), center_(x, y) {
      37         cout << "Circle::調用有參構造函數" << endl; 
      38     }
      39     void setR(double r) {
      40         r_ = r;
      41     }
      42     double getR() {
      43         return r_;
      44     }
      45     void setCenter(Point center) {
      46         center_ = center;
      47     }
      48     Point getCenter() {
      49         return center_;
      50     }
      51 };
      52 
      53 int main()
      54 {
      55     Circle c(10, 10, 0);                // 半徑為10,圓心為(10,0) 
      56     cout << "r = " << c.getR() << endl; 
      57     cout << "center = (" << c.getCenter().getX() << ", " << c.getCenter().getY() << ")" << endl;
      58     return 0;
      59 }

       運行結果:

        可以看見是先調用Point中的有參構造函數,再調用Circle類中的有參構造函數。因為在Circle類的有參構造函數中,我們通過初始化列表進行賦值,所以在main函數中定義一個c對象并傳入參數后,并沒有先進入有參構造函數中,而是先在初始化列表中進行賦值,r_賦值為10,然后再構造Circle類中的Point類成員,調用Point類中的有參構造函數,最后才進入到Circle類中的有參構造函數中。我之前一直錯誤地認為在定義了一個對象后,先有Circle類,之后再有Point類成員center_,再對Point類的center成員進行賦值。很明顯,是先有類對象(center_)再有對象(c)

        讓我們來換一種賦值方式,在Circle類的有參構造函數中進行賦值,上述第36~38行代碼改為如下:

      1 Circle(int r, int x, int y) {
      2         cout << "Circle::調用有參構造函數" << endl;
      3         r_ = r;
      4         center_.setX(10);
      5         center_.setY(0); 
      6     }

        你會發現,居然是調用Point中的默認構造函數,而不是Point中的有參構造函數。就像前面說的,如果構造函數有初始化列表,會優先在初始化列表進行賦值進行,再進入到函數體中。那為什么cneter_會調用Point中的默認構造函數而不是其他的構造函數?

        首先我們應該知道,要先有這個類中的成員變量,才能構造出一個對象。(這里用一個比喻的例子:先有零件才能夠有車,而不是先有車再有零件)。在點園的例子中,一個類(Circle)中有類成員(Point center_),所以應該先有類成員(Point center_)(當然這個例子中還要有r_),才能有這個類的對象(Circle c)。在進入Circle這個類的構造函數前,應該先有了center_,才能對center_進行操作,從而初始化c,所以應該先調用Point類的構造函數來初始化構造center_。又因為,如果構造函數有初始化列表,先進入初始化列表,再進入函數體。在初始化列表中,會根據你傳入的參數對類成員(center_)調用匹配的構造函數。比如在上面總行數為59的代碼中,由于在第36行中給center_傳入的是兩個整形數據(對應Point類中的成員變量x_和y_),所以會調用它的有參構造函數;如果傳入的是一個Point類的變量,那么就會調用它的拷貝構造函數;如果不給center_傳入任何參數,或是像上面修改的代碼一樣沒有初始化列表,那么就會調用默認構造函數,或者是全部參數都有默認值的有參構造函數。

        所以總的來說,如果一個類中(Circle)含有類成員(Point center_),那么在進入這個類(Circle)的構造函數的函數體前,必須先聲明定義它的類成員(Point center_)。關于類成員(Point cent_)調用什么構造函數,取決于你有沒有在類(Circle)的構造函數使用初始化列表,或者你在初始化列表中為類成員(Point)傳入了什么參數。

         另外,在類(Circle)的構造函數中,如果'='賦值操作的的左值和右值屬于同一個類,那么應該調用的是operator=函數,而不是拷貝構造函數。對上面的代碼稍作修改(注釋的地方):

       1 #include <iostream>
       2 using namespace std;
       3 
       4 class Point {
       5 private:
       6     double x_;
       7     double y_;
       8 
       9 public:
      10     Point() {
      11         cout << "Point::調用默認構造函數" << endl;
      12     }
      13     Point(double x, double y): x_(x), y_(y) {
      14         cout << "Point::調用有參構造函數" << endl;
      15     }
      16     Point& operator=(const Point &p) {
      17         cout << "Point::調用operator=函數" << endl;
      18         this -> x_ = p.x_;
      19         this -> y_ = p.y_;
      20         return *this;
      21     }
      22     void setX(double x) {
      23         x_ = x;
      24     }
      25     double getX() {
      26         return x_;
      27     }
      28     void setY(double y) {
      29         y_ = y;
      30     }
      31     double getY() {
      32         return y_;
      33     }
      34 };
      35 
      36 class Circle {
      37 private:
      38     double r_;
      39     Point center_;
      40 
      41 public:
      42     Circle(int r, Point center) {
      43         cout << "Circle::調用有參構造函數" << endl;
      44         r_ = r;
      45         center_ = center;                  // 調用Point::operator=
      46     }
      47     void setR(double r) {
      48         r_ = r;
      49     }
      50     double getR() {
      51         return r_;
      52     }
      53     void setCenter(Point center) {
      54         center_ = center;
      55     }
      56     Point getCenter() {
      57         return center_;
      58     }
      59 };
      60 
      61 int main()
      62 {
      63     Circle c(10, Point(10, 0));            // 當然,你也可以先定義一個Point的對象,Point center(10, 0),再把center作為參數傳入,Circle c(10, cneter)
      64     cout << "r = " << c.getR() << endl;
      65     cout << "center = (" << c.getCenter().getX() << ", " << c.getCenter().getY() << ")" << endl;
      66     return 0;
      67 }

         解釋一下,運行結果的第1行發生在第63行中的Point(10, 0),創造出來的是一個匿名對象,創造出來后會作為參數傳入到c(Circle類)的有參構造函數的形參列表。然后運行結果的第2行發生在進入Circle類中的有參構造函數的函數體之前,來創造c這個對象中的成員變量center_。運行結果的第3行就是代碼中第43行的輸出語句。第四行調用center_(Point類)中的operator=函數。只要初始化了一個對象后通過'='號運算符且屬于同一個類的左值和右值的拷貝操作都是在調用operator=函數

        下面將舉例一些常見的錯誤操作。

      • 我們把上面代碼的第42~46行代碼修改為如下:
      1 Circle(int r, Point center) {
      2         cout << "Circle::調用有參構造函數" << endl;
      3         r_ = r;
      4         center_(center); 
      5     }

        編譯器會報錯,原因很簡單,因為第4行的操作只能夠發生在定義一個對象并對它初始化之時。在進入Circle(int r, Point center)這個函數體之前,類成員變量center_已經通過默認構造函數進行初始化了,在函數體中,再對center_進行初始化是錯誤的操作

      • 如果我們把Point中的默認構造函數注釋掉,如下:
       1 class Point {
       2 private:
       3     double x_;
       4     double y_;
       5 
       6 public:
       7 //  Point() {
       8 //      cout << "Point::調用默認構造函數" << endl; 
       9 //  }
      10     Point(double x, double y): x_(x), y_(y) {
      11         cout << "Point::調用有參構造函數" << endl;
      12     }13 };

       同時在main函數中有如下定義:

      1 int main()
      2 {
      3     Circle c(10, Point(10, 0));            
      4     return 0;
      5 }

        編譯器會報錯,因為在進入Circle(int r, Point center)這個函數體之前,并沒有為類成員變量center_提供任何的參數,所以應該調用默認構造函數。而我們在Point類中定義了有參構造函數,編譯器不會再提供默認構造函數,但我們已經把應該定義的默認構造給注釋掉了,所以center_無法調用默認構造函數,所以編譯不通過。

        類似的情況還有在有參構造函數的參數都提供了默認值,同時還定義了默認構造,這就會產生二義性,當你像這樣定義一個Point類對象時:Point p; 編譯器會報錯,因為我們并沒有傳入任何的參數,不知道應該調用默認構造函數還是有參構造函數。上述代碼修改修下:

       1 class Point {
       2 private:
       3     double x_;
       4     double y_;
       5 
       6 public:
       7     // 有二義性 
       8     Point() {
       9         cout << "Point::調用默認構造函數" << endl; 
      10     }
      11     Point(double x = 0, double y = 0): x_(x), y_(y) {
      12         cout << "Point::調用有參構造函數" << endl;
      13     }
      14 };

        建議的做法是,為有參構造函數的全部參數提供默認值,并且不定義默認構造函數

      • 另外還有的錯誤就是在類中定義了有參構造函數,并且沒有為全部參數提供默認值,同時也沒有定義默認構造函數,那么在用不傳入參數的方式來定義一個類的時候,比如:Cls obj; 編譯器就會報錯,原因與上述相同,這里不再重復。

       

      參考資料

        黑馬程序員匠心之作|C++教程從0到1入門編程,學習編程不再難:https://www.bilibili.com/video/BV1et411b73Z

      posted @ 2021-03-01 16:32  onlyblues  閱讀(140)  評論(0編輯  收藏
      最新chease0ldman老人|无码亚洲人妻下载|大香蕉在线看好吊妞视频这里有精品www|亚洲色情综合网

        <sub id="gqw76"><listing id="gqw76"></listing></sub>
        <sub id="gqw76"><listing id="gqw76"></listing></sub>

      1. <form id="gqw76"><legend id="gqw76"></legend></form>