一、前言
假設我們有一個Car類,用了表示一個車,它有id,名字,牌照等許多東西,還有一個表示車的部件CarPart。
但出於某方面的考慮,我們不打算在產生car這個對象的時候,就生產出這個車,你可以認為這個時候,只有一個紙糊的車擺在你的面前,它有id,有名字,有牌照,但是它不能動,只有我們打算啟動這個車的時候,才去給這個車配置發動機,輪胎等各個部件。
二、錯誤代碼1
//CarPart類 用了標識車內的各個部件
//Car類 用了標識車
我們定義了一個car類,它里面有一個_id標識這個car,也有一個_car來標識這個車的各個部件,在最開始的時候,_car指針是null,當我們調用getCar的時候,我們判斷這個車是否創建好了部件,有的話就返回部件,沒有的話,為這個車創建部件,至於具體的創建步驟,也許是在工廠制造,也許是從其他地方搶來的也有可能,然后返回車的部件
main函數
我們在一個循環里來創建car對象,創建這個車的部件,並把這個對象放進一個vector里,在這個循環里,我們只會循環一次,至於原因你在下面會看到
然后我們運行程序,剛開始看起來很正常,但是糟糕…程序出現了問題
g++ -g main.cpp -o main.out //(使用-g選項來生成調試文件)
./main.out
Start
Make 4 tires of car 0
Make engine of car 0
-------------------
End
*** glibc detected *** double free or corruption (fasttop): 0x0000000000503010 ***
我們看到程序出現了一些問題,產生了一個core文件
我們用gdb查看一下這個core文件
gdb main.out
(gdb) core-file core.45393
(gdb) bt
#0 0x0000003f0b02e2ed in raise () from /lib64/tls/libc.so.6
#1 0x0000003f0b02fa3e in abort () from /lib64/tls/libc.so.6
#2 0x0000003f0b062d41 in __libc_message () from /lib64/tls/libc.so.6
#3 0x0000003f0b06881e in _int_free () from /lib64/tls/libc.so.6
#4 0x0000003f0b068b66 in free () from /lib64/tls/libc.so.6
#5 0x000000342cfae19e in operator delete () from /usr/lib64/libstdc++.so.6
#6 0x0000000000400dc0 in ~Car (this=0x503030) at Car.h:29
#7 0x00000000004016fd in std::_Destroy<Car> (__pointer=0x503030) at /usr/lib/gcc/x86_64-redhat-linux/3.4.5/../../../../include/c++/3.4.5/bits/stl_construct.h:107
#8 0x000000000040155b in std::__destroy_aux<Car*> (__first=0x503030, __last=0x503040) at /usr/lib/gcc/x86_64-redhat-linux/3.4.5/../../../../include/c++/3.4.5/bits/stl_construct.h:120
#9 0x0000000000401103 in std::_Destroy<Car*> (__first=0x503030, __last=0x503040) at /usr/lib/gcc/x86_64-redhat-linux/3.4.5/../../../../include/c++/3.4.5/bits/stl_construct.h:152
#10 0x0000000000400f89 in ~vector (this=0x7fff0f7371a0) at /usr/lib/gcc/x86_64-redhat-linux/3.4.5/../../../../include/c++/3.4.5/bits/stl_vector.h:256
#11 0x0000000000400d0a in main () at main.cpp:17
我們看到程序是從程序的第17行結束,調用析構函數時出現的問題
析構函數出錯的原因一般是多次釋放同一塊內存
那么這里的問題出現在那里呢?
我們想一想第12行我們創建了一個temp對象,然后第13行為這個temp對象創建了汽車組件
這個時候的內存看起來是這個樣子
接着我們把temp放進了vector中,這個時候會調用car的拷貝構造函數,由於car沒有定義自己的拷貝構造函數,因此將會執行默認的拷貝構造函數進行淺拷貝操作
這個時候的內存是這個樣子
當第一次循環結束的時候,temp被析構,汽車組件被delete掉
然后當程序結束的時候,對vcar[0]進行析構,由於Temp中的_car和Vcar[0]中的_car對象指向了同一塊內容,vcar[0]所指的汽車組件已經被釋放掉,再次delete的時候,造成錯誤
三、錯誤代碼2
我們剛剛看了一個版本的錯誤代碼,現在我們來看看另一個版本的錯誤代碼
CarPart和Car類和上一個版本的一樣
main函數有所不同
這里我們沒有直接操作temp對象,而是通過vcar.back()獲取剛剛push_back進去的對象,並在它上面進行getCar操作,這樣就避免了temp和vcar[0]中的指針指向同一塊內存
我們運行程序,看起來一切正常
Start
Make 4 tires of car 0
Make engine of car 0
-------------------
End
然后,我們把第10行 稍作一下修改,讓它循環2次,再次運行,該死,程序又出錯了
Start
Make 4 tires of car 0
Make engine of car 0
-------------------
Make 4 tires of car 1
Make engine of car 1
-------------------
End
*** glibc detected *** double free or corruption (fasttop): 0x0000000000503030 ***
查看core文件,發現又是在析構函數處出現了問題
(gdb) bt
#0 0x0000003f0b02e2ed in raise () from /lib64/tls/libc.so.6
#1 0x0000003f0b02fa3e in abort () from /lib64/tls/libc.so.6
#2 0x0000003f0b062d41 in __libc_message () from /lib64/tls/libc.so.6
#3 0x0000003f0b06881e in _int_free () from /lib64/tls/libc.so.6
#4 0x0000003f0b068b66 in free () from /lib64/tls/libc.so.6
#5 0x000000342cfae19e in operator delete () from /usr/lib64/libstdc++.so.6
#6 0x0000000000400f80 in ~Car (this=0x504080) at Car.h:42
#7 0x0000000000401b65 in std::_Destroy<Car> (__pointer=0x504080) at /usr/lib/gcc/x86_64-redhat-linux/3.4.5/../../../../include/c++/3.4.5/bits/stl_construct.h:107
#8 0x00000000004019d5 in std::__destroy_aux<Car*> (__first=0x504080, __last=0x5040a0) at /usr/lib/gcc/x86_64-redhat-linux/3.4.5/../../../../include/c++/3.4.5/bits/stl_construct.h:120
#9 0x0000000000401411 in std::_Destroy<Car*> (__first=0x504060, __last=0x5040a0) at /usr/lib/gcc/x86_64-redhat-linux/3.4.5/../../../../include/c++/3.4.5/bits/stl_construct.h:152
#10 0x0000000000401259 in ~vector (this=0x7fff3ead6110) at /usr/lib/gcc/x86_64-redhat-linux/3.4.5/../../../../include/c++/3.4.5/bits/stl_vector.h:256
#11 0x0000000000400ea2 in main () at main.cpp:18
(gdb)
為什么把循環從一次改成兩次就會出錯了呢
我們進如果打印vcar里對象中_car的地址,會發現他們竟然是一樣的
那么這又是為什么呢
在C++中,堆內存是存在復用的可能的,如果上一個內存已經被釋放調,在new新對象的時候,新對象的內存便可能建立在剛剛釋放的內存上
我們知道vector內部是類似數組的連續的儲存空間
vector在發現空間不足時,會在其他地方重新申請一塊內存空間,調用原來對象的拷貝構造函數 在新的地方進行創建,並把原來地方的對象析構調
第一次循環的時候 vector的大小是1,容量也是1,在第二次調用,由於這個時候,放進了第二個元素,所以vector的大小需要進行調整,便在新的地方重新申請了一塊內存,調用了car的拷貝構造函數,並將原來的對象進行析構,所以導致了第二次創建的對象的_car地址和第一個對象一樣
這樣當程序結束調用析構函數的時候,由於vcar[0]和vcar[1]中_car指向同一塊內存,在delete時就會出現問題
問題的根源依舊是沒有深拷貝構造函數
四、結論
1、賦值函數,拷貝構造函數,析構函數通常應該被視為一個整體,即需要析構函數的類也需要賦值函數和拷貝構造函數,反之亦然
2、為了支持快速訪問,vector將元素連續儲存,當不得不獲取新的內存空間的時候,vector會其他地方申請新的空間,並將元素從舊的地方移動到新的地方,這期間會調用元素的析構函數和拷貝構造函數
3、C++中堆內存是可以復用的,當你釋放一塊內存之后,又立即申請一塊內存,新申請的內存空間很可能在剛剛釋放的內存上