C++錯誤賦值對象引起崩潰
本文記錄一次錯誤的賦值引起的崩潰問題
1.C++函數中對象的聲明和使用的常見方式
C++函數中使用對象有兩種常見的方法,可以使用對象的指針來new一個堆上的對象,後續由自己delete回收。或者是確定對象的生命週期只在當前函數,就使用一個棧上的對象,直接聲明對象,並調用構造函數進行賦值。以下兩種處理顯然都是最基本的使用方式。
class foo{
public:
foo(){};
~foo(){};
void execute() { ... };
};
int main(){
foo * pTarget = new foo();
foo Target = foo();
pTarget->execute();
Target.execute();
delete pTarget;
return 0;
}
2.一種容易出錯的場景,先聲明,後賦值
但是如果先行聲明一個對象,並在函數的後續處理中對對象再進行賦值,就會有一種場景容易出現錯誤。比如像下面這樣。
#include <iostream>
class foo{
public:
foo(){ std::cout<< "construct foo" << std::endl; };
~foo(){};
void execute(){return;};
};
int main(){
foo * pTarget = new foo();
foo Target ;
pTarget->execute();
Target.execute();
Target = foo();
Target.execute();
delete pTarget;
return 0;
}
這段程序的輸出是像下面這樣的
construct foo
construct foo
construct foo
也就是foo的構造函數被執行了3次,此處在第一次聲明Target對象時,其實也會調用構造函數創建對象,這裏顯然會調用默認構造函數,而在後面一次繼續給Target對象賦值時,又會再調用一次foo的構造函數。然而Target變量是棧上的,即使這裏是一般的函數調用,其實也沒有影響,棧上的對象在函數運行結束之後,內存就被回收了。
3.先聲明後賦值過程中出現析構的流程
但是這還並不是流程的全部,讓我們再增加一點打印信息。
#include <iostream>
class foo{
public:
foo(){ std::cout<< "construct foo" << std::endl; };
~foo(){ std::cout<< "destroy foo" << std::endl; };
void execute(){ std::cout<< "execute foo" << std::endl; };
};
int main(){
//foo * pTarget = new foo();
foo Target ;
//pTarget->execute();
Target.execute();
Target = foo();
Target.execute();
//delete pTarget;
return 0;
}
此處我們在構造函數和execute函數中都增加了打印,並把pTarget給註釋掉了,因爲那並不是我們關心的。可以發現如下的打印結果。
construct foo
execute foo
construct foo
destroy foo
execute foo
destroy foo
在執行完第二次構造函數之後,在執行execute之前,foo的析構函數也被執行了。但是這裏執行的究竟是哪個析構函數呢?。是在使用默認構造函數創建對象賦值Target對象,還是在後續手動調用構造函數創建foo對象後調用的析構函數呢?。我們需要賦予對象一些屬性,並把這些屬性打印出來才能看到結果。修改測試程序如下。
#include <iostream>
class foo{
private:
int value ;
public:
foo(){ std::cout<< "construct foo" << std::endl; value = 1; };
foo(int input) {
value = input ;
std::cout<< "construct foo , value " << value << std::endl;
};
~foo(){
std::cout<< "destroy foo, value " << value << std::endl;
};
void execute(){
std::cout<< "execute foo , value " << value << std::endl;
};
};
int main(){
foo Target ;
Target.execute();
Target = foo(10);
Target.execute();
return 0;
}
去除了pTarget的部分,然後添加了value屬性,以及一個新的帶參數的構造函數。我們在帶參的構造,析構和執行時都打印出這個對象對應的value值,可以看到結果如下
construct foo
execute foo , value 1
construct foo , value 10
destroy foo, value 10
execute foo , value 10
destroy foo, value 10
可以發現,析構的是後續手動調用構造函數創建foo對象後調用的析構函數。第一個默認構造函數創建出來的對象相當於是被覆蓋了。使用上述賦值的方法,就是調用foo的構造函數,創建出一個對象,將這個對象賦值給已經聲明的Target變量,然後再調用析構函數終結這個對象自己的生命週期,保證這個棧上沒有多餘的對象。
4.出現崩潰的場景,成員中帶有指針變量
光從這裏來看,這樣的操作也沒有太大的問題,但是要注意的是這裏的拷貝是不充分。通過下面一個例子就可以看出來,如果類的屬性中帶有指針,那麼問題就會出現。
#include <iostream>
#include <string>
using std::string;
class foo{
private:
int value ;
string * name ;
public:
foo(){
std::cout<< "construct foo" << std::endl;
value = 1;
name = new string("tom"); };
foo(int input, string * input_name){
value = input ;
name = new string(input_name->c_str()) ;
std::cout<< "construct foo , value " << value << " name " << name->c_str() << std::endl;
};
~foo(){
std::cout<< "destroy foo, value " << value << std::endl;
};
void execute(){
std::cout<< "execute foo , value " << value << " name " << name->c_str() << std::endl;
};
};
int main(){
string newname("andy");
foo Target ;
Target.execute();
Target = foo(10, &newname);
Target.execute();
return 0;
}
可以看到,此處的差異僅在於我們新增加了一個指針對象,指向的是一個string對象,並在帶入參的構造函數中,new了一塊空間,並寫入外部傳入的值。可以得到如下結果。
construct foo
execute foo , value 1 name tom
construct foo , value 10 name andy
destroy foo, value 10
execute foo , value 10 name andy
destroy foo, value 10
從結果來看也沒啥問題,但是細心的人肯定會發現,這裏的析構函數處理,會導致name變量指向的內存泄露,故而需要在析構中增加內存釋放的處理。gu5將析構函數修改爲如下形式。
~foo(){
std::cout<< "destroy foo, value " << value << std::endl;
delete name;
};
這步操作成了壓垮駱駝的最後一個稻草。再執行一遍程序,發現結果如下
construct foo
execute foo , value 1 name tom
construct foo , value 10 name andy
destroy foo, value 10
execute foo , value 10 name Segmentation fault
崩潰了。原因就在於我們上面說的,執行了析構函數。在創建對象進行賦值的情況下,將值拷貝之後,自身的析構函數被執行。
拷貝中指針也會被拷貝,但是指針指向的對象卻不會被拷貝,如果析構函數中釋放了指針指向的內容,就會導致後續對象再進行訪問時出現非法訪問地址的情況。從而導致崩潰。
5.總結
所以大家要注意使用這樣的方式處理時,需要格外地小心。