C++——拷貝構造函數說明


一. 什麼是拷貝構造函數
首先對於普通類型的對象來講,它們之間的複製是很簡單的,例如:
[c-sharp] view plaincopy
1 int a = 100;
2 int b = a; ios

而類對象與普通對象不一樣,類對象內部結構通常較爲複雜,存在各類成員變量。
下面看一個類對象拷貝的簡單例子。
[c-sharp] view plaincopy
3 #include <iostream>
4 using namespace std;
5
6 class CExample {
7 private:
8  int a;
9 public:
10 //構造函數
11  CExample(int b)
12  { a = b;}
13
14 //通常函數
15  void Show ()
16  {
17 cout<<a<<endl;
18 }
19 };
20
21 int main()
22 {
23  CExample A(100);
24  CExample B = A; //注意這裏的對象初始化要調用拷貝構造函數,而非賦值
25   B.Show ();
26  return 0;
27 } 函數

運行程序,屏幕輸出100。從以上代碼的運行結果能夠看出,系統爲對象 B 分配了內存並完成了與對象 A 的複製過程。就類對象而言,相同類型的類對象是經過拷貝構造函數來完成整個複製過程的。
下面舉例說明拷貝構造函數的工做過程。
[c-sharp] view plaincopy
28 #include <iostream>
29 using namespace std;
30
31 class CExample {
32 private:
33 int a;
34 public:
35 //構造函數
36 CExample(int b)
37 { a = b;}
38
39 //拷貝構造函數
40 CExample(const CExample& C)
41 {
42 a = C.a;
43 }
44
45 //通常函數
46 void Show ()
47 {
48 cout<<a<<endl;
49 }
50 };
51
52 int main()
53 {
54 CExample A(100);
55 CExample B = A; // CExample B(A); 也是同樣的
56 B.Show ();
57 return 0;
58 } spa

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

二. 拷貝構造函數的調用時機
在C++中,下面三種對象須要調用拷貝構造函數!
1. 對象以值傳遞的方式傳入函數參數
59 class CExample
60 {
61 private:
62 int a;
63
64 public:
65 //構造函數
66 CExample(int b)
67 {
68 a = b;
69 cout<<"creat: "<<a<<endl;
70 }
71
72 //拷貝構造
73 CExample(const CExample& C)
74 {
75 a = C.a;
76 cout<<"copy"<<endl;
77 }
78
79 //析構函數
80 ~CExample()
81 {
82 cout<< "delete: "<<a<<endl;
83 }
84
85 void Show ()
86 {
87 cout<<a<<endl;
88 }
89 };
90
91 //全局函數,傳入的是對象
92 void g_Fun(CExample C)
93 {
94 cout<<"test"<<endl;
95 }
96
97 int main()
98 {
99 CExample test(1);
100 //傳入對象
101 g_Fun(test);
102
103 return 0;
104 } 對象

調用g_Fun()時,會產生如下幾個重要步驟:
(1).test對象傳入形參時,會先會產生一個臨時變量,就叫 C 吧。
(2).而後調用拷貝構造函數把test的值給C。 整個這兩個步驟有點像:CExample C(test);(3).等g_Fun()執行完後, 析構掉 C 對象。
2. 對象以值傳遞的方式從函數返回
105 class CExample
106 {
107 private:
108 int a;
109
110 public:
111 //構造函數
112 CExample(int b)
113 {
114 a = b;
115 }
116
117 //拷貝構造
118 CExample(const CExample& C)
119 {
120 a = C.a;
121 cout<<"copy"<<endl;
122 }
123
124 void Show ()
125 {
126 cout<<a<<endl;
127 }
128 };
129
130 //全局函數
131 CExample g_Fun()
132 {
133 CExample temp(0);
134 return temp;
135 }
136
137 int main()
138 {
139 g_Fun();
140 return 0;
141 } 內存

當g_Fun()函數執行到return時,會產生如下幾個重要步驟:
(1). 先會產生一個臨時變量,就叫XXXX吧。
(2). 而後調用拷貝構造函數把temp的值給XXXX。整個這兩個步驟有點像:CExample XXXX(temp);(3). 在函數執行到最後先析構temp局部變量。
(4). 等g_Fun()執行完後再析構掉XXXX對象。
3. 對象須要經過另一個對象進行初始化;
142 CExample A(100);
143 CExample B = A;
144 // CExample B(A); get

後兩句都會調用拷貝構造函數。編譯器

三. 淺拷貝和深拷貝
1. 默認拷貝構造函數
不少時候在咱們都不知道拷貝構造函數的狀況下,傳遞對象給函數參數或者函數返回對象都能很好的進行,這是由於編譯器會給咱們自動產生一個拷貝構造函數,這就是「默認拷貝構造函數」,這個構造函數很簡單,僅僅使用「老對象」的數據成員的值對「新對象」的數據成員一一進行賦值,它通常具備如下形式:
[c-sharp] view plaincopy
145 Rect::Rect(const Rect& r)
146 {
147 width = r.width;
148 height = r.height;
149 }

固然,以上代碼不用咱們編寫,編譯器會爲咱們自動生成。可是若是認爲這樣就能夠解決對象的複製問題,那就錯了,讓咱們來考慮如下一段代碼:[c-sharp] view plaincopy
150 class Rect
151 {
152 public:
153 Rect() // 構造函數,計數器加1
154 {
155 count++;
156 }
157 ~Rect() // 析構函數,計數器減1
158 {
159 count--;
160 }
161 static int getCount() // 返回計數器的值
162 {
163 return count;
164 }
165 private:
166 int width;
167 int height;
168 static int count; // 一靜態成員作爲計數器
169 };
170
171 int Rect::count = 0; // 初始化計數器
172
173 int main()
174 {
175 Rect rect1;
176 cout<<"The count of Rect: "<<Rect::getCount()<<endl;
177
178 Rect rect2(rect1); // 使用rect1複製rect2,此時應該有兩個對象
179 cout<<"The count of Rect: "<<Rect::getCount()<<endl;
180
181 return 0;
182 } io

  這段代碼對前面的類,加入了一個靜態成員,目的是進行計數。在主函數中,首先建立對象rect1,輸出此時的對象個數,而後使用rect1複製出對象rect2,再輸出此時的對象個數,按照理解,此時應該有兩個對象存在,但實際程序運行時,輸出的都是1,反應出只有1個對象。此外,在銷燬對象時,因爲會調用銷燬兩個對象,類的析構函數會調用兩次,此時的計數器將變爲負數。
說白了,就是拷貝構造函數沒有處理靜態數據成員。
出現這些問題最根本就在於在複製對象時,計數器沒有遞增,咱們從新編寫拷貝構造函數,以下:
[c-sharp] view plaincopy
183 class Rect
184 {
185 public:
186 Rect() // 構造函數,計數器加1
187 {
188 count++;
189 }
190 Rect(const Rect& r) // 拷貝構造函數
191 {
192 width = r.width;
193 height = r.height;
194 count++; // 計數器加1
195 }
196 ~Rect() // 析構函數,計數器減1
197 {
198 count--;
199 }
200 static int getCount() // 返回計數器的值
201 {
202 return count;
203 }
204 private:
205 int width;
206 int height;
207 static int count; // 一靜態成員作爲計數器
208 }; 編譯

2. 淺拷貝
所謂淺拷貝,指的是在對象複製時,只對對象中的數據成員進行簡單的賦值,默認拷貝構造函數執行的也是淺拷貝。大多狀況下「淺拷貝」已經能很好地工做了,可是一旦對象存在了動態成員,那麼淺拷貝就會出問題了,讓咱們考慮以下一段代碼:
[c-sharp] view plaincopy
209 class Rect
210 {
211 public:
212 Rect() // 構造函數,p指向堆中分配的一空間
213 {
214 p = new int(100);
215 }
216 ~Rect() // 析構函數,釋放動態分配的空間
217 {
218 if(p != NULL)
219 {
220 delete p;
221 }
222 }
223 private:
224 int width;
225 int height;
226 int *p; // 一指針成員
227 };
228
229 int main()
230 {
231 Rect rect1;
232 Rect rect2(rect1); // 複製對象
233 return 0;
234 }

在這段代碼運行結束以前,會出現一個運行錯誤。緣由就在於在進行對象複製時,對於動態分配的內容沒有進行正確的操做。咱們來分析一下:
在運行定義rect1對象後,因爲在構造函數中有一個動態分配的語句,所以執行後的內存狀況大體以下:

在使用rect1複製rect2時,因爲執行的是淺拷貝,只是將成員的值進行賦值,這時 rect1.p = rect2.p,也即這兩個指針指向了堆裏的同一個空間,以下圖所示:


固然,這不是咱們所指望的結果,在銷燬對象時,兩個對象的析構函數將對同一個內存空間釋放兩次,這就是錯誤出現的緣由。咱們須要的不是兩個p有相同的值,而是兩個p指向的空間有相同的值,解決辦法就是使用「深拷貝」。
3. 深拷貝
在「深拷貝」的狀況下,對於對象中動態成員,就不能僅僅簡單地賦值了,而應該從新動態分配空間,如上面的例子就應該按照以下的方式進行處理:
[c-sharp] view plaincopy

235 class Rect
236 {
237 public:
238 Rect() // 構造函數,p指向堆中分配的一空間
239 {
240 p = new int(100);
241 }
242 Rect(const Rect& r)
243 {
244 width = r.width;
245 height = r.height;
246 p = new int; // 爲新對象從新動態分配空間
247 *p = *(r.p);
248 }
249 ~Rect() // 析構函數,釋放動態分配的空間
250 {
251 if(p != NULL)
252 {
253 delete p;
254 }
255 }
256 private:
257 int width;
258 int height;
259 int *p; // 一指針成員
260 };

此時,在完成對象的複製後,內存的一個大體狀況以下:

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

3. 防止默認拷貝發生
經過對對象複製的分析,咱們發現對象的複製大多在進行「值傳遞」時發生,這裏有一個小技巧能夠防止按值傳遞——聲明一個私有拷貝構造函數。甚至沒必要去定義這個拷貝構造函數,這樣由於拷貝構造函數是私有的,若是用戶試圖按值傳遞或函數返回該類對象,將獲得一個編譯錯誤,從而能夠避免按值傳遞或返回對象。
[c-sharp] view plaincopy

261 // 防止按值傳遞
262 class CExample
263 {
264 private:
265 int a;
266
267 public:
268 //構造函數
269 CExample(int b)
270 {
271 a = b;
272 cout<<"creat: "<<a<<endl;
273 }
274
275 private:
276 //拷貝構造,只是聲明
277 CExample(const CExample& C);
278
279 public:
280 ~CExample()
281 {
282 cout<< "delete: "<<a<<endl;
283 }
284
285 void Show ()
286 {
287 cout<<a<<endl;
288 }
289 };
290
291 //全局函數
292 void g_Fun(CExample C)
293 {
294 cout<<"test"<<endl;
295 }
296
297 int main()
298 {
299 CExample test(1);
300 //g_Fun(test); 按值傳遞將出錯
301
302 return 0;
303 }


四. 拷貝構造函數的幾個細節
1. 拷貝構造函數裏能調用private成員變量嗎?
解答:這個問題是在網上見的,當時一會兒有點暈。其時從名子咱們就知道拷貝構造函數其時就是一個特殊的構造函數,操做的仍是本身類的成員變量,因此不受private的限制。

2. 如下函數哪一個是拷貝構造函數,爲何?
[c-sharp] view plaincopy

304 X::X(const X&);
305 X::X(X);
306 X::X(X&, int a=1);
307 X::X(X&, int a=1, int b=2);
解答:對於一個類X, 若是一個構造函數的第一個參數是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且沒有其餘參數或其餘參數都有默認值,那麼這個函數是拷貝構造函數.
[c-sharp] view plaincopy

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


3. 一個類中能夠存在多於一個的拷貝構造函數嗎?
解答:類中能夠存在超過一個拷貝構造函數。
[c-sharp] view plaincopy

311 class X {
312 public:
313 X(const X&); // const 的拷貝構造
314 X(X&); // 非const的拷貝構造
315 };

注意,若是一個類中只存在一個參數爲 X& 的拷貝構造函數,那麼就不能使用const X或volatile X的對象實行拷貝初始化.
[c-sharp] view plaincopy

316 class X {
317 public:
318 X();
319 X(X&);
320 };
321
322 const X cx;
323 X x = cx; // error

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

相關文章
相關標籤/搜索