標籤:

為什麼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 為空指針,還能運行通過呢?


  1. foo(), pp(), FunctionB()不是virtual。
  2. 這些函數內沒有對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 die

this 指針是空指針 不去騷擾他 他就不搞死你

你敢動他試試


你可以認為非virtual成員函數有個static修飾符,每個class的成員函數只有一份在內存裡面,調用的時候直接取地址調用。


推薦閱讀:

在函數內new一個對象,如果作為引用返回,是不是就可以不用delete了?
為什麼一個空的class的大小是1個位元組?
C++非同步回調如何更優雅?
[C++] 能否設計一個一般的計時函數?
unity項目越大編譯速度越慢 ue4用藍圖秒編譯 背後分別的原因是什麼?

TAG:C |