1. 程式人生 > >如何避免被C++預設拷貝建構函式忽悠?

如何避免被C++預設拷貝建構函式忽悠?

一、背景介紹

          因為工作關係,需要用到C++程式設計。對於我來說,雖然一直從事的是linux平臺下的嵌入式軟體開發,但深入用到C++的特性的地方並不多。對於C++,用得最多的無非是指標、封裝、繼承、組合以及虛擬函式。對於複製建構函式、過載操作符、智慧指標等概念,雖然也時有接觸,但真正自己寫程式碼需要用到的時候,並不多。

          本文嘗試對複製建構函式的定義、作用及需要注意的地方做一個簡單的解剖。希望能拋磚引玉,對大家的學習起到一個幫助作用。

           雖然複製建構函式對於基本的C++程式設計來說,可能不太用得著。不過這並不說明覆制建構函式沒什麼用,其實複製建構函式能解決一些我們常常會忽略的問題。

            假設有一個CStudent類,有資料成員char* name;

            現在有一個物件CStudent stu1("tom");//資料成員name="tom";

            再生成一物件,stu2,把stu1拷貝給stu2,即CStudent stu2(stu1);

            如果我們沒有寫拷貝建構函式,那麼編譯器會呼叫預設的拷貝建構函式.即進行淺拷貝.

淺拷貝的意思就是記憶體中只有一份"tom",物件stu1的name指標指向了它.把stu1拷貝給stu2後,stu2的name指向也指向了"tom",指向的是記憶體中同一塊地址.這就會出現一個問題:當stu1的物件被銷燬了,那麼stu1的name指向的"tom"也會被銷燬,由於stu2中的name也是指向"tom"的,所以這時stu2中的name就變成空的了,甚至是亂碼.

           如果我們自己寫了拷貝建構函式,那麼在記憶體中就會有兩份"tom"了,這時stu1的name和stu2的name就沒有什麼關係了,只不過他們的值都是"tom"而已!

           類似的問題,我們看看如下程式碼,並編譯執行看看結果:

#include <iostream>
 
class Array {
public:
    int size;
    int* data;
 
    implicit Array(int size) 
        : size(size), data(new int[size])
    {
    }
 
    ~Array() 
    {
        delete[] this->data;
    }
};
 
int main() 
{
    Array first(20);
    first.data[0] = 25;
 
    {
        Array copy = first;
        std::cout << first.data[0] << " " << copy.data[0] << std::endl;
    }    // (1)
 
    first.data[0] = 10;    // (2)
}


執行結果:

25 25

Segmentation fault

為什麼會出現segmentation fault段錯誤這麼嚴重的問題呢?

根本原因跟前面那個例子一樣,我們沒有寫拷貝建構函式。因此,編譯器會為我們生成一個預設的拷貝建構函式。預設的建構函式形式如下:

Array(const Array& other)
  : size(other.size), data(other.data) {}

該預設建構函式有什麼問題呢?

它對data 指標執行的是淺拷貝。也就是說,它只是拷貝原始data成員的地址,這樣就帶來了兩個物件共享指向同一塊記憶體的指標的風險。程式碼執行到main函式(1)處時,即執行到:

    {
        Array copy = first;
        std::cout << first.data[0] << " " << copy.data[0] << std::endl;
    }    // (1)


(1)處時,copy的解構函式會被呼叫,因為分配在stack上的物件,是自動釋放的。當我們進入一個特定的作用域時,堆疊指標會向下移動一個單位,為那個作用域內建立的、以堆疊為基礎的所有物件分配儲存空間。而當我們離開作用域的時候(呼叫完畢所有區域性構建器後),堆疊指標會向上移動一個單位,也就是copy物件的解構函式在執行到(1)處後會自動得到呼叫。Array的解構函式,刪除了copy的data.由於first和copy物件都共享一個data指標,因此當執行到(2)處,即呼叫first.data[0]=10時,就會產生令人頭痛的段錯誤::segmentation fault.

如何解決這個問題呢?

寫一個自己的拷貝建構函式,並執行深拷貝

// for std::copy
#include <algorithm>
 
Array(const Array& other)
    : size(other.size), data(new int[other.size]) 
{
    std::copy(other.data, other.data + other.size, data); 
}

二、淺拷貝與深拷貝

淺拷貝:僅僅逐個成員拷貝,而不拷貝資源的方式;淺拷貝是指源物件與拷貝物件共用一份實體,僅僅是引用的變數不同(名稱不同)。對其中任何一個物件的改動都會影響另外一個物件。舉個例子,一個人一開始叫張三,後來改名叫李四了,可是還是同一個人,不管是張三缺胳膊少腿還是李四缺胳膊少腿,都是這個人倒黴。比較典型的就有Reference(引用)物件,如Class(類)。

深拷貝:既拷貝成員,又拷貝資源的方式。深拷貝是指源物件與拷貝物件互相獨立,其中任何一個物件的改動都不會對另外一個物件造成影響。舉個例子,一個人名叫張三,後來用他克隆(假設法律允許)了另外一個人,叫李四,不管是張三缺胳膊少腿還是李四缺胳膊少腿都不會影響另外一個人。比較典型的就是Value(值)物件,如預定義型別Int32,Double,以及結構(struct),列舉(Enum)等。

系統提供的預設拷貝建構函式為淺拷貝,深拷貝必須自己定義。

這也解釋了背景中的段錯誤原因及為什麼要自己寫拷貝建構函式.


三、什麼是拷貝建構函式

拷貝建構函式,是一種特殊的建構函式,它由編譯器呼叫來完成一些基於同一類的其他物件的構建及初始化。其唯一的引數(物件的引用)是不可變的(const型別)。也就是說,當用一個已經存在的物件為一個新的物件進行賦值時,首先要給新物件的資料成員分配空間資源以建立新物件,然後用源物件的成員值進行初始化。這個行為必須在物件構造的時候進行完成,而普通的建構函式無法完成這項工作,因此拷貝建構函式應運而生!

在C++中,下面三種物件需要拷貝的情況。因此,拷貝建構函式將會被呼叫。
  1). 一個物件以值傳遞的方式傳入函式體
  2). 一個物件以值傳遞的方式從函式返回
  3). 一個物件需要通過另外一個物件進行初始化搜尋
  以上的情況需要拷貝建構函式的呼叫。如果在前兩種情況不使用拷貝建構函式的時候,就會導致一個指標指向已經被刪除的記憶體空間。對於第三種情況來說,初始化和賦值的不同含義是建構函式呼叫的原因。事實上,拷貝建構函式是由普通建構函式和賦值操作賦共同實現的。

拷貝建構函式的標準寫法如下:


class  Base

    public:

       Base(){} 
       Base( const Base &b){..}  //拷貝建構函式的引數必須是該物件的引用

}

 進一步總結:

1) 拷貝建構函式的作用就是用來複制物件的,在使用這個物件的例項來初始化這個物件的一個新的例項。
2)如果不顯式宣告拷貝建構函式的時候,編譯器也會生成一個預設的拷貝建構函式,只執行淺拷貝在一般的情況下,淺拷貝執行的也很好。

但是在遇到類有指標資料成員時就出現問題了:因為預設的拷貝建構函式是按成員拷貝構造,這導致了兩個不同的指標(如ptr1=ptr2)指向了相同的 記憶體。當一個例項銷燬時,呼叫解構函式free(ptr1)釋放了這段記憶體,那麼剩下的一個例項的指標ptr2就無效了,在被銷燬的時候free(ptr2)就會出現錯誤了, 這相當於重複釋放
一塊記憶體兩次。這種情況必須顯式宣告並實現自己的拷貝建構函式,來為新的例項的指標分配新的記憶體。

相關推薦

如何避免C++預設拷貝建構函式忽悠

一、背景介紹           因為工作關係,需要用到C++程式設計。對於我來說,雖然一直從事的是linux平臺下的嵌入式軟體開發,但深入用到C++的特性的地方並不多。對於C++,用得最多的無非是指標、封裝、繼承、組合以及虛擬函式。對於複製建構函式、過載操作符、智慧指標等

C++11 delete關鍵字 禁止預設拷貝建構函式和複製操作

在講解delete關鍵字之前,我們先說說最早之前我們是如何禁止拷貝建構函式的! 把delete關鍵字引入的前因後果都深入的理解一下 ! class A { public: A(){} private: //拷貝建構函式 A(const

C++ 中拷貝建構函式呼叫情況

1、當用類的一個物件初始化該類的另一個物件時.例如: Int main(){       Point A;       Point B(A);// } 2 如果函式的形參是類的物件,呼叫函式時,進行形參和實參結合時. void fun(Point P){ } int

c++的預設拷貝建構函式,從深度拷貝和淺拷貝說起

1. c++類的預設拷貝建構函式的弊端 c++類的中有兩個特殊的建構函式,(1)無參建構函式,(2)拷貝建構函式。它們的特殊之處在於: (1)當類中沒有定義任何建構函式時,編譯器會預設提供一個無參建構函式且其函式體為空; (2)當類中沒有定義拷貝建構函式時

C++】拷貝建構函式

目錄 拷貝建構函式 拷貝建構函式的呼叫 預設的拷貝建構函式 深度拷貝  看完類的建構函式後,再來了解下拷貝建構函式。 拷貝建構函式 拷貝建構函式是一種特殊的建構函式。 (1)它是建構函式,所以函式名就是類名,沒有返回值; (2)它是特殊的建構函式,引

預設拷貝建構函式預設賦值函式

當一個類中有動態分配記憶體時,應當自己定義拷貝建構函式和賦值函式 class A { int *p; public: A() { p = new int[10]; } ~A() {

C++的拷貝建構函式、operator=運算子過載,深拷貝和淺拷貝、explicit關鍵字

1、在C++編碼過程中,類的建立十分頻繁。 簡單的功能,當然不用考慮太多,但是從進一步深刻理解C++的內涵,類的結構和用法,編寫更好的程式碼的角度去考慮,我們就需要用到標題所提到的這些內容。 最近,在看單例模式,覺得十分有趣,然而如果想要掌握單例模式,就必須掌握這些內容。下

C++中拷貝建構函式、淺拷貝與深拷貝的詳解

拷貝建構函式呼叫時機: 1、物件需要通過另外一個物件進行初始 化:     T t1(10, 20);T t2(0, 0);T t3 = t1; // 拷貝建構函式呼叫的時機一:T t4(t2);  // 拷貝建構函式呼叫的時機 二:2、實參傳遞給形參時呼叫賦值建構函式 拷

C++之拷貝建構函式和複製運算子過載

1、C++拷貝建構函式 拷貝建構函式是為了解決如神明物件時候就用一個已經存在的物件來初始化這個新的物件,如MyString A(B):這裡B是已經存在MyString物件。但是這裡需要注意拷貝建構函式裡面的內部實現細節。這裡面其實是在這個A物件類的定義中定義了拷貝建構函式的

預設拷貝建構函式,淺拷貝,深拷貝

#include using namespace std; class Person { public: Person(char *pN) { cout <<"Constructing "<<pn<<endl; pName=new char (st

C++下拷貝建構函式的引數型別必須是引用

摘自拷貝建構函式的引數型別必須是引用 在下面幾種情況下會呼叫拷貝建構函式: 1. 顯式或隱式地用同類型的一個物件來初始化另外一個物件。如上例中,用物件c初始化d; 2. 作為實參(argume

為什麼C++中拷貝建構函式的引數型別必須是引用?

  在C++中, 建構函式,拷貝建構函式,解構函式和賦值函式(賦值運算子過載)是最基本不過的需要掌握的知識。 但是如果我問你“拷貝建構函式的引數為什麼必須使用引用型別?”這個問題, 你會怎麼回答? 或許你會回答為了減少一次記憶體拷貝? 很慚愧的是,我的第一感覺也是這麼回答

深度分析C++預設建構函式拷貝建構函式

對於C++初學者來說,時常不難看到他們說: 1.任何class如果沒有定義預設建構函式,那麼就會由編譯器來合成一個出來。 2.編譯器合成來的建構函式會明確確定裡面所有成員的值。(比如int型別成員會初始化成0) 呃,這當然是一部分C++新手的一廂情願吧

批註:C++中複製建構函式與過載賦值操作符總結:預設拷貝,帶指標的需要深拷貝

前言 這篇文章將對C++中複製建構函式和過載賦值操作符進行總結,包括以下內容: 複製建構函式和過載賦值操作符的定義;複製建構函式和過載賦值操作符的呼叫時機;複製建構函式和過載賦值操作符的實現要點;複製建構函式的一些細節。 複製建構函式和過載賦值操作符的定義 我們都知道

關於c++預設建構函式、解構函式拷貝建構函式、move函式

在c++中,當我們定義一個類的時候,如果我們什麼都不定義的時候,c++編譯器會預設的為我們生成一些函式。 例如定義一個Example類。 class Example{ }; 當我們定義一個Example類的時候,不定義任何操作的時候,c++編譯系統將為Example類生成如

c++:類拷貝控制 - 拷貝建構函式 & 拷貝賦值運算子

一、拷貝控制 當定義一個類時,我們可以顯式或隱式地指定此型別的物件拷貝、移動、賦值和銷燬時做什麼。 一個類可以通過定義五種特殊的成員函式來控制這些操作,包括:++拷貝建構函式++、++拷貝賦值函式++、++移動建構函式++、++移動複製函式++和++解構函式++。我們稱這些操作為

1、【C++】類&物件/建構函式/拷貝建構函式/操作符過載/解構函式

一、C++類 & 物件     C++ 在 C 語言的基礎上增加了面向物件程式設計,C++ 支援面向物件程式設計。類是 C++ 的核心特性,通常被稱為使用者定義的型別。     類用於指定物件的形式,它包含了資料表示法和用於處理資料的方法。類中的資料和方法稱為類的成員。函式在

C++ 拷貝建構函式程式碼筆記

拷貝建構函式是一種特殊的建構函式,它在建立物件時,是使用同一類中之前建立的物件來初始化新建立的物件。拷貝建構函式通常用於: 通過使用另一個同類型的物件來初始化新建立的物件。 複製物件把它作為引數傳遞給函式。 複製物件,並從函式返回這個物件 #include

C++ 賦值號過載的拷貝建構函式程式碼筆記

#include <iostream> using namespace std; class A { public: A(int); //建構函式 A(const A &); //拷貝建構函

C++ 拷貝建構函式this指標練習

總時間限制:  1000ms   記憶體限制:  65536kB // 在此處補充你的程式碼 描述 程式填空,使其按要求輸出 #include <iostream> using namespace std; class A {