常見筆/面試題-之構造函數和析構函數

常見筆/面試題-之構造函數和析構函數

構造函數是用來初始化一個對象的,而析構函數的作用則是釋放對象佔用的空間。如果將虛函數、構造函數和析構函數結合起來會有怎麼樣的效果呢?

  1. 構造函數可以是虛函數嗎?

    答:構造函數不可以是虛函數!基於以下幾點原因:

    (1)構造一個對象的時候,必須知道對象的實際類型,而虛函數行爲是在運行期間動態確定實際類型的。在構建一個對象時,構造函數執行期間,對象未完全構建完成,編譯器無法知道對象的實際類型,如果構造函數爲虛函數,虛函數的執行是基於對象類型確定的,然而構建的對象本身自己都無法確定自己的類型,虛函數更加無法正確執行!

    (2)虛函數的執行依賴於虛函數表。而虛函數表的初始化工作是在構造函數中完成的,即在構造函數中初始化vptr,讓他指向正確的虛函數表。而在構造對象期間,虛函數表還未被初始化,將導致虛函數無法工作。

    (3)虛函數的意思就是開啓動態綁定,程序會根據對象的動態類型來選擇要調用的方法。然而在構造函數運行的時候,這個對象的動態類型還不完整,沒有辦法確定它到底是什麼類型,故構造函數不能動態綁定。(動態綁定是根據對象的動態類型而不是函數名,在調用構造函數之前,這個對象根本就不存在,它怎麼動態綁定?)

  2. 析構函數可以是純虛函數嗎?能舉個例子嗎?

    答:析構函數可以是純虛函數!如果一個類要作爲基類存在,最好將該類的析構函數申明虛函數(除非不使用基類指針的方式構造子類)。
    (1)擁有純虛函數的類稱爲抽象類,專門提供函數接口,不能實例化。如果某個類需要作爲一個抽象類,但是其中並沒有其他方法,這時可以將析構函數聲明爲純虛函數。(因爲構造函數不能是虛函數,更不必提純虛函數了)
    (2)例子:

    
    #include <iostream>
    
    using namespace std;
    
    class test
    {
    public:
        test(){ cout << "test constructor" << endl; }
        virtual ~test() = 0;
    };
    
    test::~test()
    {
    cout << "test destructor! it's specail!" << endl;
    }
    
    class derived:public test
    {
    public:
        derived(){ cout << "derived constructor" << endl; };
        virtual ~derived(){cout << "derived destructor" << endl;};
    };
    
    int main()
    {
        derived d;
        //system("pause");
        return 0;
    }

    運行結果:

    注意test基類中的析構函數是純虛函數,但是該析構函數還是有實現,而且必須要有實現!!!test::~test(){}這個定義是必需的,因爲虛析構函數工作的方式是:最底層的派生類的析構函數最先被調用,然後各個基類的析構函數被調用。這就是說,即使是抽象類,編譯器也要產生對~test的調用,所以要保證爲它提供函數體。如果不這麼做,鏈接器就會檢測出來(報錯error link 1120),最後還是得回去把它添上。雖然抽象類的析構函數可以是純虛函數,但要實例化其派生類對象,仍必須提供抽象基類中析構函數的函數體。

    **通常情況下,抽象類的純虛函數實現必須且只能由派生類實現,但是對於基類的純虛析構函數實現可以由自身給出,也可以由派生類給出。
    擁有純虛析構函數的類也是抽象類!!!**

  3. 下面代碼執行後會發生內存泄漏嗎?如果存在該如何修改?

    
    #include <iostream> 
    
        using namespace std;
        //基類
        class Base{
        public:
            Base(){}
            ~Base(){cout << "Base destructor" << endl;}
            void dosomething(){cout << "do something in Base" << endl;}
        };
        //派生類 
        class Derived : public Base{
        public:
            Derived(){}
            ~Derived(){cout << "Derived destructor" << endl;}
            void dosomething(){cout << "do something in Derived" << endl;}
        };
    
        int main(){
            Base *base = new Derived;
            base->dosomething();
            delete base;
            system("pause");
            return 0;
        }

    答:此段代碼執行後將會發生內存泄漏,由於基類指針指向的是子類對象,但是由於基類的析構函數不是虛函數,基類指針無法找到他所指向的實際類型,從而在delete的時候只能釋放derived對象中從基類繼承的部分,其餘部分將無法得到釋放導致內存泄漏!

    修改:

    
    #include <iostream> 
    
    using namespace std;
    
    //情景1:普通成員函數和析構函數,都不是虛函數 
    class Base{
    public:
        Base(){}
        //父類的析構函數,是虛函數,只做了這裏的更改 
        virtual ~Base(){
            cout << "Base destructor" << endl;
        }
        //普通成員 
        void dosomething(){
            cout << "do something in Base" << endl;
        }
    };
    //派生類 
    class Derived : public Base{
    public:
        Derived(){}
        ~Derived(){
            cout << "Derived destructor" << endl;
        }
        void dosomething(){
            cout << "do something in Derived" << endl;
        }
    };
    
    int main(){
        //這個時候,雖然父類指針實際指向子類,可是沒有虛函數表,
        //所以不能調用實際的類型,因此輸出的只能是父類指針自身能看到的內容 
        Base *base = new Derived;
        base->dosomething();//輸出的是父類的函數 
        delete base;//調用的是父類的析構函數 
        system("pause");
        return 0;
    }

    運行結果2
    由此可以看出,這個時候,就真正實現了將實際類型對象進行釋放的。同時可以得到,如果父類析構是虛函數,子類調用析構函數的話,會先調用子類的析構函數,之後會調用父類的析構函數

    其實這裏父類的析構函數加上了virtual,並不是說pbase釋放的時候,同時調用了子類的析構函數和父類的析構函數,它實際上指向的是子類的虛函數表,那麼就是說父類指針最終只調用了子類的析構函數,由C++類本身特性,當子類析構函數調用的時候,會自動調用父類的析構函數,完成了釋放。

    相關小結

    對於Base *pbase = new Derived;

    如果父類函數不是析構函數,那麼pbase只能“看見”父類本身的函數,這是因爲沒有虛函數表讓它可以找到本身
    如果父類析構函數是虛函數,如果delete pbase,將會先調用子類函數的析構函數,然後子類析構函數自動調用父類的析構函數,真正實現了資源釋放,防止了內存泄露構造派生類的時候,會先構造基類部分,然後構造子類部分;撤銷派生類對象的時候,會先撤銷派生類部分,然後撤銷基類部分

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