C++核心——多態(tài)里的構造和析構函數
一、構造函數和析構函數
對象的初始化和清理也是兩個非常重要的安全問題
? 一個對象或者變量沒有初始狀態(tài),對其使用后果是未知
? 同樣的使用完一個對象或變量,沒有及時清理,也會造成一定的安全問題
c++利用了構造函數和析構函數解決上述問題,這兩個函數將會被編譯器自動調用,完成對象初始化和清理工作。
對象的初始化和清理工作是編譯器強制要我們做的事情,因此如果我們不提供構造和析構,編譯器會提供
編譯器提供的構造函數和析構函數是空實現。
- 構造函數:主要作用在于創(chuàng)建對象時為對象的成員屬性賦值,構造函數由編譯器自動調用,無須手動調用。
- 析構函數:主要作用在于對象銷毀前系統(tǒng)自動調用,執(zhí)行一些清理工作。
#include <iostream>
using namespace std;
class Test
{
public:
Test();
~Test();
};
Test::Test()
{
cout << "constructor" << endl;
}
Test::~Test()
{
cout << "destructor" << endl;
}
int main()
{
Test t;
return 0;
}
******************運行結果************************
PS F:\C_CPP\CPP_study> g++ .\test.cpp
PS F:\C_CPP\CPP_study> .\a.exe
constructor
destrtor
PS F:\C_CPP\CPP_study>
二、繼承時構造函數和析構函數的執(zhí)行過程
構造函數:先執(zhí)行父類構造函數后執(zhí)行子類構造函數。
析構函數:先執(zhí)行子類析構函數再執(zhí)行父類析構函數。
<u>有點像棧區(qū)的順序,先進后出。創(chuàng)建類時先把父類push進去,所以先執(zhí)行父類的構造函數,再把子類PUSH進去,所以再執(zhí)行子類的構造函數。釋放的時候子類在上面,所以先被POP出來,所以先執(zhí)行子類的析構函數,再POP父類時執(zhí)行父類的析構函數。</u>
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
string name;
int age;
virtual void getInfo() = 0;
Base()
{
cout << "Base's constructor" << endl;
}
~Base()
{
cout << "Base's destructor" << endl;
}
};
class Son : public Base
{
public:
Son(string name, int age);
virtual void getInfo();
~Son();
};
Son::Son(string name, int age)
{
cout << "son's constructor" << endl;
this->name = name;
this->age = age;
}
void Son::getInfo()
{
cout << "name: " << this->name <<
" \tage: " << this->age << endl;
}
Son::~Son()
{
cout << "son's destructor" << endl;
}
void test1()
{
Son *pson = new Son("tom", 20);
pson->getInfo();
delete pson;
}
int main()
{
test1();
return 0;
}
**********************執(zhí)行結果*********************************
PS F:\C_CPP\CPP_study> g++ .\01_虛析構函數.cpp
PS F:\C_CPP\CPP_study> .\a.exe
Base's constructor # 先執(zhí)行父類構造
son's constructor # 再執(zhí)行子類構造
name: tom age: 20
son's destructor # 先執(zhí)行子類析構
Base's destructor # 再執(zhí)行父類析構
PS F:\C_CPP\CPP_study>
三、多態(tài)時的執(zhí)行順序
這個重點,先上代碼
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
string name;
int age;
virtual void getInfo() = 0;
Base()
{
cout << "Base's constructor" << endl;
}
~Base()
{
cout << "Base's destructor" << endl;
}
};
class Son : public Base
{
public:
Son(string name, int age);
virtual void getInfo();
~Son();
};
Son::Son(string name, int age)
{
cout << "son's constructor" << endl;
this->name = name;
this->age = age;
}
void Son::getInfo()
{
cout << "name: " << this->name <<
" \tage: " << this->age << endl;
}
Son::~Son()
{
cout << "son's destructor" << endl;
}
void test2()
{
Base *p = new Son("jack", 22);
p->getInfo();
delete p;
}
int main()
{
test2();
return 0;
}
*****************************執(zhí)行結果***********************************
PS F:\C_CPP\CPP_study> g++ .\01_虛析構函數.cpp
PS F:\C_CPP\CPP_study> .\a.exe
Base's constructor # 先執(zhí)行父類構造
son's constructor # 再執(zhí)行子類構造
name: tom age: 20
Base's destructor # 再執(zhí)行父類析構
PS F:\C_CPP\CPP_study>
看出設么區(qū)別了么,和上已個代碼相比,這里執(zhí)行了父類的析構函數,可是子類的構造函數并沒有執(zhí)行。為什么會這樣呢,接著從代碼的區(qū)別著手分析
void test1()
{
Son *pson = new Son("tom", 20);
pson->getInfo();
delete pson;
}
************************************************
void test2()
{
Base *p = new Son("jack", 22);
p->getInfo();
delete p;
}
其實兩套代碼的區(qū)別就是這個函數的不同,具體到函數就成了函數體的第一條語句的不同。在test1中創(chuàng)建的是Son類型的指針,所以在delete的時候刪除的是Son類型指針,那么肯定會執(zhí)行Son的析構函數,可是test2中創(chuàng)建的是Base類型指針,所以在delete是刪除的是Base類型的指針,而Son雖然繼承了Base,但是卻沒有重寫B(tài)ase的析構函數,所以執(zhí)行不到Son里面去,也就無法執(zhí)行Son的析構函數,因為壓根兒delete的就不是Son指針。如果是這樣的話,那么就會帶來一個很嚴重的問題,那就是如果Son類中含有堆取開辟的空間需要在析構函數中釋放,就會導無法釋放,造成內存泄漏。
一種致命警告??
class Base
{
public:
string name;
int age;
virtual void getInfo() = 0;
Base()
{
cout << "Base's constructor" << endl;
}
};
如以上代碼,將Base類定義為這個樣子父類未定義析構函數時會造成警告,但是這個警告有的時候會導致程序無法正常運行下去,這個問題在我寫一個小項目時曾發(fā)生過,因為父類沒有析構函數體,變異時只有警告,但是運行時,就是執(zhí)行完這個語句后無法繼續(xù)執(zhí)行后面的語句,所以這里提醒各位遇到這種提示警告最好馬上解決,畢竟提示信息也很明顯
warning: delete called on 'Base' that is abstract but has non-virtual destructor [-Wdelete-non-virtual-dtor]
delete p;
^
1 warning generated.
為了解決上述問題,需要使用多天的動態(tài)綁定特效
靜態(tài)綁定:編譯時綁定,通過對象調用
動態(tài)綁定:運行時綁定,通過地址實現
為了實現動態(tài)綁定,所以需要將父類的析構函數定義成虛函數,從而使調用父類西溝函數時動態(tài)綁定到子類的析構函數上。當然這里編譯器還幫我們干了一件事,就是編譯器把父類的析構函數數和子類的析構函數進行了一個綁定,因為正常情況下,只有子類重寫了父類函數才會被動態(tài)綁定,但是這里雖然兩個析構函數名都不一樣,但是仍然能動態(tài)綁定,這就是編譯器進行的一種綁定。
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
string name;
int age;
virtual void getInfo() = 0;
Base()
{
cout << "Base's constructor" << endl;
}
virtual ~Base()
{}
};
class Son : public Base
{
public:
Son(string name, int age);
virtual void getInfo();
~Son();
};
Son::Son(string name, int age)
{
cout << "son's constructor" << endl;
this->name = name;
this->age = age;
}
void Son::getInfo()
{
cout << "name: " << this->name <<
" \tage: " << this->age << endl;
}
Son::~Son()
{
cout << "son's destructor" << endl;
}
void test1()
{
Son *pson = new Son("tom", 20);
pson->getInfo();
delete pson;
}
void test2()
{
Base *p = new Son("jack", 22);
p->getInfo();
delete p;
}
int main()
{
test2();
return 0;
}
**************************執(zhí)行結果*****************************
PS F:\C_CPP\CPP_study> g++ .\01_虛析構函數.cpp
PS F:\C_CPP\CPP_study> .\a.exe
Base's constructor
son's constructor
name: jack age: 22
son's destructor
PS F:\C_CPP\CPP_study>
從代碼的運行結果來看,成功調用的子類的的虛構函數,如果子類中有堆區(qū)的數據,就可以正常在析構函數中釋放。有人看到這里可能會有疑問,那父類的析構沒有執(zhí)行怎么辦?這個問題完全不用擔心,因為一般在實現多臺的時候,父類被定義為抽象類,無法實例化對象,內部函數都別子類重寫,調用的時候實質是在調用子類的函數。
四、總結
其實這篇博文是在我做一個小項目時遇到上面的警告,沒有在意,導致程序無法運行,然后查資料并總結出來的。這里總結一句,就是在實現多態(tài)的時候,定義抽象父類的時候記得定義虛析構函數,養(yǎng)成這個習慣,每次定義多態(tài)的父類時順手就把析構定義成虛函數就可以,可以省去很多不必要的麻煩。