爲什麼C++調用空指針對象的成員函數可以運行通過

先看一段代碼:

#include <iostream>
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++對象。

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 << "Bfoo " << endl; }
void pp(B *this) { cout << "Bpp" << endl; }
void FunctionB(B *this) { cout <<"funB" << endl; }

例如類 A 有一個子類 B,B 有一個虛函數 foo;假設有下面的代碼:

某個函數(){
B b;
b.foo();
}

或者

{
  A *p = new B();
  p->foo();
}

      由於構造過程是該局部可見的(所以對象類型在該局部就完全明確了),所以在編譯這段代碼時,編譯器能夠確定這個 foo 函數就是 B::foo() (假設B有定義foo的話)。所以這個時候,也可能有靜態綁定。

 即:虛函數不一定 都是運行時確定其地址的。

      和c++內存佈局有關,爲了節約內存和提高調用效率,一般類成員的存儲分成兩塊,一塊是單個instance所有,比如非靜態成員變量,另一塊是所有instances共享的,比如函數代碼。這樣的佈局是對於性能有好處的,代碼只要load一次,減少了cache佔用和miss。如果你的函數不引用任何instance獨有的內存部分,nullptr並無問題,因爲不會使用this,只會使用類instance共享的部分,這部分始終存在,即使你沒有任何類實例。反之就會出問題,因成員函數

class B{ void foo(){} };

在編譯的時候會被預先翻譯爲類似

void foo(struct B b){}

這樣的C語言形式

而你的代碼中沒有任何一行引用到空指針b(也就是this),因此不會崩潰。

       從某種意義上,this 指針可以看做成員函數的第一個參數。實際上,C語言模擬成員函數的做法就是定義一個 struct,然後定義一些自由函數,把 struct 的指針作爲第一個參數傳遞進去。

       看看Python class的成員函數的寫法,然後把cpp的寫法轉化成python的寫法你就會理解,參數傳入一個NULL但是沒有訪問是沒有太大問題的,這裏的訪問包括了函數內部邏輯訪問也包括了語言級別的訪問。 

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

somenull->foo()會被翻譯成foo(somenull),如果foo沒事用this指針的成員,那樣執行沒有問題啊。 

     簡單地說就是,你給函數傳遞了錯誤的參數,但在該函數內部並沒有使用該參數,所以其不影響函數的運行。


發佈了45 篇原創文章 · 獲贊 128 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章