本文记录一次错误的赋值引起的崩溃问题
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;
}
但是如果先行声明一个对象,并在函数的后续处理中对对象再进行赋值,就会有一种场景容易出现错误。比如像下面这样。
#include
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变量是栈上的,即使这里是一般的函数调用,其实也没有影响,栈上的对象在函数运行结束之后,内存就被回收了。
但是这还并不是流程的全部,让我们再增加一点打印信息。
#include
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
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变量,然后再调用析构函数终结这个对象自己的生命周期,保证这个栈上没有多余的对象。
光从这里来看,这样的操作也没有太大的问题,但是要注意的是这里的拷贝是不充分。通过下面一个例子就可以看出来,如果类的属性中带有指针,那么问题就会出现。
#include
#include
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
崩溃了。原因就在于我们上面说的,执行了析构函数。在创建对象进行赋值的情况下,将值拷贝之后,自身的析构函数被执行。
拷贝中指针也会被拷贝,但是指针指向的对象却不会被拷贝,如果析构函数中释放了指针指向的内容,就会导致后续对象再进行访问时出现非法访问地址的情况。从而导致崩溃。
所以大家要注意使用这样的方式处理时,需要格外地小心。