2009-03-21 43 views
41

Tôi đã thử nghiệm với C++ và tìm thấy mã bên dưới là rất lạ.Truy cập các thành viên lớp trên con trỏ NULL

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; 
} 

Tôi biết rằng cuộc gọi phương thức ảo bị lỗi vì yêu cầu tra cứu vtable và chỉ có thể hoạt động với các đối tượng hợp lệ.

Tôi có câu hỏi sau

  1. như thế nào ảo phương pháp say_hi công việc phi về một con trỏ NULL?
  2. Đối tượng foo được phân bổ ở đâu?

Bạn nghĩ gì?

+3

Xem [this] (http://stackoverflow.com/questions/2474018/when-does-invoking-a-member-function-on-a-null-instance-result-in-undefined-behav) cho những gì ngôn ngữ nói về nó. Cả hai đều là hành vi không xác định. – GManNickG

Trả lời

80

Đối tượng foo là biến địa phương có loại Foo*. Biến đó có thể được phân bổ trên ngăn xếp cho hàm main, giống như bất kỳ biến cục bộ nào khác. Nhưng giá trị được lưu trữ trong foo là một con trỏ rỗng. Nó không trỏ đâu cả. Không có trường hợp nào của loại Foo được biểu thị ở bất kỳ đâu.

Để gọi hàm ảo, người gọi cần biết đối tượng nào đang được gọi. Đó là bởi vì đối tượng chính nó là những gì cho biết chức năng nào thực sự nên được gọi. (Điều đó thường được thực hiện bằng cách cho đối tượng con trỏ đến vtable, danh sách các con trỏ hàm và người gọi chỉ biết rằng nó được gọi là hàm đầu tiên trong danh sách, mà không biết trước điểm trỏ đó.)

Nhưng để gọi một chức năng không phải ảo, người gọi không cần biết tất cả điều đó. Trình biên dịch biết chính xác hàm nào sẽ được gọi, vì vậy nó có thể tạo ra một mã lệnh máy mã CALL để đi trực tiếp đến hàm mong muốn. Nó đơn giản chuyển một con trỏ tới đối tượng mà hàm được gọi là tham số ẩn cho hàm. Nói cách khác, trình biên dịch chuyển cuộc gọi chức năng của bạn vào đây:

void Foo_say_hi(Foo* this); 

Foo_say_hi(foo); 

Bây giờ, kể từ khi thực hiện chức năng mà không bao giờ làm cho tham chiếu đến bất kỳ thành viên của đối tượng được trỏ đến bởi this đối số của nó, bạn có hiệu quả né tránh đạn của dereferencing một con trỏ null bởi vì bạn không bao giờ dereference một.

Chính thức, gọi bất kỳ chức năng nào - ngay cả một chức năng không ảo - trên con trỏ rỗng là hành vi không xác định. Một trong những kết quả được cho phép của hành vi không xác định là mã của bạn dường như chạy chính xác như bạn dự định. Bạn không nên dựa vào điều đó, mặc dù đôi khi bạn sẽ tìm thấy thư viện từ nhà cung cấp trình biên dịch của mình rằng làm dựa vào điều đó. Nhưng nhà cung cấp trình biên dịch có lợi thế là có thể thêm định nghĩa thêm vào những gì nếu không sẽ là hành vi không xác định. Đừng tự làm.

+1

Câu trả lời hay. Cảm ơn –

+0

Có vẻ như có sự nhầm lẫn về thực tế là mã chức năng và dữ liệu đối tượng là hai thứ khác nhau. Hãy xem http://stackoverflow.com/questions/1966920/more-info-on-memory-layout-of-an-executable-program-process này. Dữ liệu đối tượng không có sẵn sau khi khởi tạo trong trường hợp này vì con trỏ null, nhưng mã có phần mềm thay thế có sẵn trong bộ nhớ ở nơi khác. – Loki

+0

FYI này có nguồn gốc từ '[C++ 11: 9.3.1/2]': "Nếu một hàm thành viên không tĩnh của một lớp' X' được gọi cho một đối tượng không thuộc kiểu 'X', hoặc của một kiểu bắt nguồn từ 'X', hành vi không xác định." Rõ ràng '* foo' không thuộc loại' Foo' (vì nó không tồn tại). –

7

Dereferencing con trỏ NULL gây ra "hành vi không xác định", Điều này có nghĩa là mọi thứ đều có thể xảy ra - mã của bạn thậm chí có thể hoạt động chính xác. Bạn không phải phụ thuộc vào điều này tuy nhiên - nếu bạn chạy cùng một mã trên một nền tảng khác nhau (hoặc thậm chí có thể trên cùng một nền tảng) nó có thể sẽ sụp đổ.

Trong mã của bạn không có đối tượng Foo, chỉ một con trỏ được initalised với giá trị NULL.

+0

Cảm ơn. Bạn nghĩ gì về câu hỏi thứ hai? * Foo * được phân bổ ở đâu? –

+0

foo không phải là một đối tượng, nó là một con trỏ. Con trỏ đó được cấp phát trên ngăn xếp (giống như bất kỳ biến nào không được đánh dấu 'tĩnh' hoặc được phân bổ bằng 'mới'. Và nó không bao giờ trỏ đến một đối tượng hợp lệ. – jalf

+0

Không có Foo :-) Foo là con trỏ. –

16

Các say_hi() chức năng thành viên thường được thực hiện bởi trình biên dịch như

void say_hi(Foo *this); 

Vì bạn không truy cập vào bất kỳ thành viên, cuộc gọi của bạn thành công (mặc dù bạn đang bước vào hành vi undefined theo tiêu chuẩn).

Foo không được phân bổ.

+0

Cảm ơn. Nếu * Foo * không được phân bổ, cuộc gọi sẽ diễn ra như thế nào? Tôi hơi bối rối .. –

+1

Bộ xử lý hoặc lắp ráp tương ứng, không có đầu mối về chi tiết HLL của mã. C++ các hàm không ảo chỉ đơn thuần là các hàm bình thường với một hợp đồng mà con trỏ 'this' được đặt ở vị trí đã cho (đăng ký hoặc stack, phụ thuộc vào trình biên dịch). Miễn là bạn không truy cập vào 'con trỏ' này mọi thứ đều ổn. – arul

2

a) Nó hoạt động vì nó không dereference bất cứ điều gì thông qua con trỏ "này" tiềm ẩn. Ngay sau khi bạn làm điều đó, sự bùng nổ. Tôi không chắc chắn 100%, nhưng tôi nghĩ rằng dereferences null pointer được thực hiện bởi RW bảo vệ 1K đầu tiên của không gian bộ nhớ, do đó, có một cơ hội nhỏ của nullreferencing không bị bắt nếu bạn chỉ dereference nó qua 1K dòng (ví dụ: một số biến ví dụ đó sẽ được phân bổ rất xa, như:

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

sau đó a-> i sẽ có thể được còn tự do khi A là null

b) Không có nơi nào, bạn chỉ tuyên bố một con trỏ, được phân bổ trên chính (.): s stack.

2

Cuộc gọi đến say_hi bị ràng buộc tĩnh. Vì vậy, máy tính thực sự chỉ đơn giản là thực hiện một cuộc gọi tiêu chuẩn đến một chức năng. Hàm không sử dụng bất kỳ trường nào, do đó không có vấn đề gì.

Cuộc gọi tới virtual_say_hi bị ràng buộc động, do đó bộ xử lý chuyển sang bảng ảo và vì không có bảng ảo ở đó, nó nhảy đến đâu đó ngẫu nhiên và làm hỏng chương trình.

+0

Điều đó có ý nghĩa hoàn hảo. Cảm ơn –

1

Trong những ngày ban đầu của C++, cáC++ mã C được chuyển đổi sang C. phương pháp đối tượng được chuyển đổi sang phương pháp phi vật thể như thế này (trong trường hợp của bạn):

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

Tất nhiên, tên foo_say_hi là giản thể. Để biết thêm chi tiết, hãy tra cứu tên C++ mangling.

Như bạn có thể thấy, nếu thisPtr không bao giờ được tham chiếu, thì mã này là tốt và thành công. Trong trường hợp của bạn, không có biến thể hiện hay bất kỳ thứ gì phụ thuộc vào thisPtr được sử dụng.

Tuy nhiên, các chức năng ảo khác nhau. Có rất nhiều tra cứu đối tượng để đảm bảo con trỏ đối tượng bên phải được truyền dưới dạng tham số cho hàm. Điều này sẽ dereference thisPtr và gây ra ngoại lệ.

5

Hành vi không xác định. Nhưng hầu hết các trình biên dịch thực hiện các hướng dẫn sẽ xử lý tình huống này một cách chính xác nếu bạn không truy cập vào các biến thành viên và bảng ảo.

let see tháo gỡ trong visual studio cho hiểu điều gì xảy ra

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 

như bạn có thể nhìn thấy Foo: say_hi gọi là chức năng bình thường nhưng với này trong ecx đăng ký. Để đơn giản hóa, bạn có thể giả định rằng này được truyền dưới dạng tham số ẩn mà chúng tôi không bao giờ sử dụng trong ví dụ của bạn.
Nhưng trong trường hợp thứ hai, chúng tôi tính toán địa chỉ của hàm do bảng ảo - do foo addres và lấy lõi.

+0

Cảm ơn. Bạn có thể cho tôi biết làm thế nào tôi có thể nhận được disassembly này trong Visual Studio? Tôi đang sử dụng VS2008 –

+2

Gỡ lỗi-> Windows-> Tháo gỡ theo trình gỡ lỗi – bayda

1

Điều quan trọng là nhận ra rằng cả hai cuộc gọi tạo ra hành vi không xác định và hành vi đó có thể biểu hiện theo những cách không mong muốn. Ngay cả khi cuộc gọi xuất hiện để hoạt động, nó có thể đang đặt xuống một bãi mìn.

Hãy xem xét sự thay đổi nhỏ này để ví dụ của bạn:

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

Kể từ khi cuộc gọi đầu tiên để foo cho phép hành vi undefined nếu foo là null, trình biên dịch tại là miễn phí để giả định rằng fookhông null. Điều đó làm cho if (foo != 0) dư thừa và trình biên dịch có thể tối ưu hóa nó! Bạn có thể nghĩ đây là một sự tối ưu rất vô nghĩa, nhưng các nhà văn biên dịch đã trở nên rất hung dữ, và một cái gì đó như thế này đã xảy ra trong mã thực tế.

Các vấn đề liên quan