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 <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.總結

所以大家要注意使用這樣的方式處理時,需要格外地小心。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章