通常在C的編程中,我們經常使用memset函數將一塊連續的內存區域清零或設置為其它指定的值,最近在移植一段java代碼到C++的時候,不當使用memset函數花費了我幾個小時的調試時間。對于虛函數的底層機制很多資料都有較詳細闡述,但對我個人而言,這次的調試讓我感觸頗深。
先來看一段代碼,在繼承的類Advance之中,有很多屬性字段,我希望將其清成0或NULL,于是在構造函數中我通過memset將當前類的所有屬性置0。
class Base{
public:
virtual void kickoff() = 0;
};
class Advance:public Base{
public:
Advance(){
memset(this, 0, sizeof(Advance));
}
void kickoff(){
count++;
//... do something else;
}
private:
int attr1, attr2;
char* label;
int count;
//... other attributes, they should be initiated to 0 or NULL at beginning.
};
int _tmain(int argc, _TCHAR* argv[])
{
Base* ptr = new Advance();
ptr->kickoff();
return 0;
}
這樣看似能正常運行,但運行程序時,你會發現類似于下面的錯誤:
TestVirtual.exe 中的 0x00415390 處未處理的異常: 0xC0000005: 讀取位置 0x00000000 時發生訪問沖突
同時斷點停留在ptr->kickoff()處,從錯誤提示我們可以得知無法調用kickoff方法,這個方法的指針沒有被正確初始化,但為什么呢?
指出問題之前,先看看這段文獻上的關于虛函數機制的說明:
函數賴以生存的底層機制:vptr + vtable。虛函數的運行時實現采用了VPTR/VTBL的形式,這項技術的基礎:
①編譯器在后臺為每個包含虛函數的類產生一個靜態函數指針數組(虛函數表),在這個類或者它的基類中定義的每一個虛函數都有一個相應的函數指針。
②每個包含虛函數的類的每一個實例包含一個不可見的數據成員vptr(虛函數指針),這個指針被構造函數自動初始化,指向類的vtbl(虛函數表)
③當客戶調用虛函數的時候,編譯器產生代碼反指向到vptr,索引到vtbl中,然后在指定的位置上找到函數指針,并發出調用。
這里的問題,就出在
memset(this, 0, sizeof(Advance));
上面,虛函數指針應該在進入構造函數賦值體之前自動初始化的,而memset卻又將已經初始化好的指針清0了,這就是為什么會產生上面的訪問零址的錯誤。將上面的memset語句去除程序就可以正常運行了。
所以,從上面的問題中,我們可以看出在構造函數體內調用memset將整個對象清0是很有風險的,當沒有虛函數的時候上面程序可以正常運行(可以試著將Base類的純虛函數聲明改成非虛函數再運行程序)。初始化類的屬性對象時,比較穩妥的辦法還是手動逐個進行初使化
先來看一段代碼,在繼承的類Advance之中,有很多屬性字段,我希望將其清成0或NULL,于是在構造函數中我通過memset將當前類的所有屬性置0。
class Base{
public:
virtual void kickoff() = 0;
};
class Advance:public Base{
public:
Advance(){
memset(this, 0, sizeof(Advance));
}
void kickoff(){
count++;
//... do something else;
}
private:
int attr1, attr2;
char* label;
int count;
//... other attributes, they should be initiated to 0 or NULL at beginning.
};
int _tmain(int argc, _TCHAR* argv[])
{
Base* ptr = new Advance();
ptr->kickoff();
return 0;
}
這樣看似能正常運行,但運行程序時,你會發現類似于下面的錯誤:
TestVirtual.exe 中的 0x00415390 處未處理的異常: 0xC0000005: 讀取位置 0x00000000 時發生訪問沖突
同時斷點停留在ptr->kickoff()處,從錯誤提示我們可以得知無法調用kickoff方法,這個方法的指針沒有被正確初始化,但為什么呢?
指出問題之前,先看看這段文獻上的關于虛函數機制的說明:
函數賴以生存的底層機制:vptr + vtable。虛函數的運行時實現采用了VPTR/VTBL的形式,這項技術的基礎:
①編譯器在后臺為每個包含虛函數的類產生一個靜態函數指針數組(虛函數表),在這個類或者它的基類中定義的每一個虛函數都有一個相應的函數指針。
②每個包含虛函數的類的每一個實例包含一個不可見的數據成員vptr(虛函數指針),這個指針被構造函數自動初始化,指向類的vtbl(虛函數表)
③當客戶調用虛函數的時候,編譯器產生代碼反指向到vptr,索引到vtbl中,然后在指定的位置上找到函數指針,并發出調用。
這里的問題,就出在
memset(this, 0, sizeof(Advance));
上面,虛函數指針應該在進入構造函數賦值體之前自動初始化的,而memset卻又將已經初始化好的指針清0了,這就是為什么會產生上面的訪問零址的錯誤。將上面的memset語句去除程序就可以正常運行了。
所以,從上面的問題中,我們可以看出在構造函數體內調用memset將整個對象清0是很有風險的,當沒有虛函數的時候上面程序可以正常運行(可以試著將Base類的純虛函數聲明改成非虛函數再運行程序)。初始化類的屬性對象時,比較穩妥的辦法還是手動逐個進行初使化