為什麼C++調用空指針對象的成員函數可以運行通過?
#include &
using namespace std;
class B {
public:
void foo() { cout &<&< "B foo " &<&< endl; } void pp() { cout &<&< "B pp" &<&< endl; } void FunctionB() { cout &<&< "funB" &<&< endl; } }; int main() { B *somenull = NULL; somenull-&>foo();
somenull-&>pp();
somenull-&>FunctionB();return 0;
}
為什麼 somenull 為空指針,還能運行通過呢?
- foo(), pp(), FunctionB()不是virtual。
- 這些函數內沒有對this解引用
這個問題很好,可以闡明「靜態綁定」和「動態綁定」的區別。真正的原因是:因為對於非虛成員函數,C++這門語言是靜態綁定的。這也是C++語言和其它語言Java, Python的一個顯著區別。以此下面的語句為例:
somenull-&>foo();
這語句的意圖是:調用對象somenull的foo成員函數。如果這句話在Java或Python等動態綁定的語言之中,編譯器生成的代碼大概是:
找到somenull的foo成員函數,調用它。(注意,這裡的找到是程序運行的時候才找的,這也是所謂動態綁定的含義:運行時才綁定這個函數名與其對應的實際代碼。有些地方也稱這種機製為遲綁定,晚綁定。)
但是對於C++。為了保證程序的運行時效率,C++的設計者認為凡是編譯時能確定的事情,就不要拖到運行時再查找了。所以C++的編譯器看到這句話會這麼干:1:查找somenull的類型,發現它有一個非虛的成員函數叫foo。(編譯器乾的)2:找到了,在這裡生成一個函數調用,直接調B::foo(somenull)。所以到了運行時,由於foo()函數裡面並沒有任何需要解引用somenull指針的代碼,所以真實情況下也不會引發segment fault。這裡對成員函數的解析,和查找其對應的代碼的工作都是在編譯階段完成而非運行時完成的,這就是所謂的靜態綁定,也叫早綁定。正確理解C++的靜態綁定可以理解一些特殊情況下C++的行為。C++只關心你的指針類型,不關心指針指向的對象是否有效,C++要求程序員自己保證指針的有效性。況且在有些系統上,地址0也是有效的,理論上完全可以構造一個在地址0的C++對象。
非虛函數可以,因為編譯時期綁定函數地址。
虛函數不行,因為是運行期確定函數的地址,,,,你的對象地址為null,它沒辦法去找到虛函數表。。。---------
2015年8月13日補充:
原答案的結論不夠嚴謹準確。應該是「編譯期綁定」的函數(而不僅僅是非虛函數)可以。
因為即使是虛函數,編譯器也可能在能夠確定的情況下,靜態綁定。例如類 A 有一個子類 B,B 有一個虛函數 foo;假設有下面的代碼:
某個函數(){ B b; b.foo();}或者
{
A *p = new B();p-&>foo();}由於構造過程是該局部可見的(所以對象類型在該局部就完全明確了),所以在編譯這段代碼時,編譯器能夠確定這個 foo 函數就是 B::foo() (假設B有定義foo的話)。所以這個時候,也可能有靜態綁定。
即:虛函數不一定 都是運行時確定其地址的。somenull->foo()會被翻譯成foo(somenull),如果foo沒事用this指針的成員,那樣執行沒有問題啊。
class B {
public: void foo() { cout &<&< "B foo " &<&< endl; }void pp() { cout &<&< "B pp" &<&< endl; }
void FunctionB() { cout &<&< "funB" &<&< endl; }};
實際上,上面這段代碼編譯以後是下面這個樣子的,你自己覺得會不會異常呢?如果有興趣的話可以去查查編譯後生成的符號表驗證一下。
class B;
void foo(B *this) { cout &<&< "B foo " &<&< endl; }void pp(B *this) { cout &<&< "B pp" &<&< endl; }void FunctionB(B *this) { cout &<&< "funB" &<&< endl; }
就是靜態綁定和動態綁定的區別,前面已有高手回答過了。恰巧我也剛總結了這個區別,不介意附上外鏈吧:C++中的靜態綁定和動態綁定
成員函數
class B{
void foo(){}}在編譯的時候會被預先翻譯為類似void foo(struct B b){}這樣的C語言形式而你的代碼中沒有任何一行引用到空指針b(也就是this),因此不會崩潰看看Python class的成員函數的寫法,然後把cpp的寫法轉化成python的寫法你就會理解,參數傳人一個NULL但是沒有訪問是沒有太大問題的,這裡的訪問包括了函數內部邏輯訪問也包括了語言級別的訪問。
和c++內存布局有關,為了節約內存和提高調用效率,一般類成員的存儲分成兩塊,一塊是單個instance所有,比如非靜態成員變數,另一塊是所有instances共享的,比如函數代碼。這樣的布局是對於性能有好處的,代碼只要load一次,減少了cache佔用和miss。如果你的函數不引用任何instance獨有的內存部分,nullptr並無問題,因為不會使用this,只會使用類instance共享的部分,這部分始終存在,即使你沒有任何類實例。反之就會出問題,因為你試圖訪問不存在的數據
從某種意義上,this 指針可以看做成員函數的第一個參數。實際上,C語言模擬成員函數的做法就是定義一個 struct,然後定義一些自由函數,把 struct 的指針作為第一個參數傳遞進去。C++ 可以看作這種形式的語法糖。
所以,只要你不訪問 this 指針,函數就不會被玩壞
簡單理解,編譯期綁定
- 簡單地說就是,你給函數傳遞了錯誤的參數,但在該函數內部並沒有使用該參數,所以其不影響函數的運行。
- 百科:在類的非靜態成員函數中訪問類的非靜態成員的時候,編譯器會自動將對象本身的地址(即this指針)作為一個隱含參數傳遞給函數。你的函數並沒有涉及到對this指針的解引用。
void foo(B *this)
{
cout &<&< "B foo " &<&< endl; // 能正常運行
}
void foo(B *this)
{
cout &<&< this-&>n &<&< "B foo " &<&< endl; // 錯誤,n為對象內的一個變數
}
no zuo no diethis 指針是空指針 不去騷擾他 他就不搞死你 你敢動他試試
你可以認為非virtual成員函數有個static修飾符,每個class的成員函數只有一份在內存裡面,調用的時候直接取地址調用。
推薦閱讀:
※在函數內new一個對象,如果作為引用返回,是不是就可以不用delete了?
※為什麼一個空的class的大小是1個位元組?
※C++非同步回調如何更優雅?
※[C++] 能否設計一個一般的計時函數?
※unity項目越大編譯速度越慢 ue4用藍圖秒編譯 背後分別的原因是什麼?
TAG:C |