簡體   English   中英

訪問 NULL 指針上的 class 成員

[英]Accessing class members on a NULL pointer

我正在試驗 C++ 並發現下面的代碼很奇怪。

class Foo{
public:
    virtual void say_virtual_hi(){
        std::cout << "Virtual Hi";
    }

    void say_hi()
    {
        std::cout << "Hi";
    }
};

int main(int argc, char** argv)
{
    Foo* foo = 0;
    foo->say_hi(); // works well
    foo->say_virtual_hi(); // will crash the app
    return 0;
}

我知道虛擬方法調用會崩潰,因為它需要 vtable 查找並且只能使用有效對象。

我有以下問題

  1. 非虛方法say_hi如何在 NULL 指針上工作?
  2. object foo在哪里分配?

有什么想法嗎?

object fooFoo*類型的局部變量。 就像任何其他局部變量一樣,該變量可能會在main function 的堆棧上分配。 但是foo中存儲的是一個 null 指針。 它沒有指向任何地方。 在任何地方都沒有Foo類型的實例。

要調用虛擬 function,調用者需要知道 function 是在哪個 object 上被調用的。 那是因為 object 本身就是真正應該調用哪個 function 的原因。 (這通常通過給 object 一個指向 vtable 的指針、一個函數指針列表來實現,而調用者只知道它應該調用列表中的第一個 function,而事先不知道該指針指向的位置。)

但是要調用非虛擬 function,調用者不需要知道所有這些。 編譯器確切地知道哪個 function 將被調用,因此它可以生成一個CALL機器代碼指令到 go 直接到所需的 function。 它只是將一個指向 object 的指針傳遞給 function 作為隱藏參數調用 function。 換句話說,編譯器將您的 function 調用轉換為:

void Foo_say_hi(Foo* this);

Foo_say_hi(foo);

現在,由於 function 的實現從未引用其this參數指向的 object 的任何成員,因此您有效地避開了取消引用 null 指針的項目符號。

形式上,在 null 指針上調用任何function(甚至是非虛擬的)都是未定義的行為。 未定義行為的允許結果之一是您的代碼似乎完全按照您的預期運行。 應該依賴它,盡管有時您會從編譯器供應商那里找到依賴它的庫。 但是編譯器供應商的優勢在於能夠為原本未定義的行為添加進一步的定義。 不要自己做。

say_hi()成員 function 通常由編譯器實現為

void say_hi(Foo *this);

由於您不訪問任何成員,因此您的調用成功(即使您根據標准輸入未定義的行為)。

Foo根本沒有被分配。

取消引用 NULL 指針會導致“未定義的行為”,這意味着任何事情都可能發生 - 您的代碼甚至可能看起來正常工作。 但是,您不能依賴於此 - 如果您在不同的平台上(甚至可能在同一平台上)運行相同的代碼,它可能會崩潰。

在您的代碼中沒有 Foo object,只有一個用值 NULL 初始化的指針。

這是未定義的行為。 但是如果您不訪問成員變量和虛擬表,大多數編譯器都會做出正確處理這種情況的指令。

讓我們看看 Visual Studio 中的反匯編,了解會發生什么

   Foo* foo = 0;
004114BE  mov         dword ptr [foo],0 
    foo->say_hi(); // works well
004114C5  mov         ecx,dword ptr [foo] 
004114C8  call        Foo::say_hi (411091h) 
    foo->say_virtual_hi(); // will crash the app
004114CD  mov         eax,dword ptr [foo] 
004114D0  mov         edx,dword ptr [eax] 
004114D2  mov         esi,esp 
004114D4  mov         ecx,dword ptr [foo] 
004114D7  mov         eax,dword ptr [edx] 
004114D9  call        eax  

如您所見, Foo:say_hi 像往常一樣調用 function在 ecx 寄存器中。 為簡化起見,您可以假設this作為隱式參數傳遞,我們在您的示例中從未使用過。
但在第二種情況下,我們計算 function 由於虛擬表的地址 - 由於 foo 地址並獲取核心。

重要的是要認識到這兩個調用都會產生未定義的行為,並且這種行為可能會以意想不到的方式表現出來。 即使電話似乎奏效,它也可能正在埋下雷區。

考慮對您的示例進行這個小改動:

Foo* foo = 0;
foo->say_hi(); // appears to work
if (foo != 0)
    foo->say_virtual_hi(); // why does it still crash?

如果foo是 null,那么第一次調用foo會啟用未定義的行為,因此編譯器現在可以自由地假設foo不是null。 這使得if (foo != 0)變得多余,編譯器可以對其進行優化,您可能認為這是一個非常無意義的優化,但編譯器編寫者已經變得非常激進。 並且在實際代碼中發生了類似的事情。

a) 它之所以有效,是因為它不會通過隱含的“this”指針取消引用任何內容。 一旦你這樣做,繁榮。 我不是 100% 確定,但我認為 null 指針取消引用是通過 RW 保護 memory 空間的前 1K 完成的,所以如果你只取消引用它超過 1K 行(即一些實例變量這將被分配得很遠,例如:

 class A {
     char foo[2048];
     int i;
 }

那么當 A 為 null 時,a->i 可能未被捕獲。

b)無處,您只聲明了一個指針,該指針分配在 main():s 堆棧上。

對 say_hi 的調用是靜態綁定的。 所以計算機實際上只是簡單地對 function 進行標准調用。 function沒有使用任何字段,所以沒有問題。

對 virtual_say_hi 的調用是動態綁定的,因此處理器會轉到虛擬表,由於那里沒有虛擬表,它會隨機跳轉到某個地方並使程序崩潰。

在C++的原始日子里,C++代碼被轉換為C。 Object 方法被轉換為像這樣的非對象方法(在你的情況下):

foo_say_hi(Foo* thisPtr, /* other args */) 
{
}

當然,名稱 foo_say_hi 是簡化的。 有關更多詳細信息,請查看 C++ 名稱修改。

如您所見,如果 thisPtr 從未被取消引用,則代碼很好並且成功。 在您的情況下,沒有使用實例變量或任何依賴於 thisPtr 的東西。

但是,虛函數是不同的。 有很多 object 查找以確保正確的 object 指針作為參數傳遞給 function。 這將取消引用 thisPtr 並導致異常。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM