C++错误赋值对象引起崩溃

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 
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 
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变量,然后再调用析构函数终结这个对象自己的生命周期,保证这个栈上没有多余的对象。

4.出现崩溃的场景,成员中带有指针变量

光从这里来看,这样的操作也没有太大的问题,但是要注意的是这里的拷贝是不充分。通过下面一个例子就可以看出来,如果类的属性中带有指针,那么问题就会出现。

#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

崩溃了。原因就在于我们上面说的,执行了析构函数。在创建对象进行赋值的情况下,将值拷贝之后,自身的析构函数被执行。
拷贝中指针也会被拷贝,但是指针指向的对象却不会被拷贝,如果析构函数中释放了指针指向的内容,就会导致后续对象再进行访问时出现非法访问地址的情况。从而导致崩溃。

5.总结

所以大家要注意使用这样的方式处理时,需要格外地小心。

你可能感兴趣的:(嵌入式开发)