1. 程式人生 > >為什麼C++呼叫空指標物件的成員函式可以執行通過

為什麼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指標的成員,那樣執行沒有問題啊。 

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