Khi nào việc gọi một hàm thành viên trên một cá thể null dẫn đến hành vi không xác định?


120

Hãy xem xét đoạn mã sau:

#include <iostream>

struct foo
{
    // (a):
    void bar() { std::cout << "gman was here" << std::endl; }

    // (b):
    void baz() { x = 5; }

    int x;
};

int main()
{
    foo* f = 0;

    f->bar(); // (a)
    f->baz(); // (b)
}

Chúng tôi mong đợi (b)sự cố, vì không có thành viên tương ứng xcho con trỏ null. Trong thực tế, (a)không sụp đổ vì thiscon trỏ không bao giờ được sử dụng.

Bởi vì (b)tham chiếu đến thiscon trỏ ( (*this).x = 5;) và thislà null, chương trình sẽ nhập hành vi không xác định, vì null tham chiếu luôn được cho là hành vi không xác định.

(a)dẫn đến hành vi không xác định không? Còn nếu cả hai hàm (và x) đều tĩnh thì sao?


Nếu cả hai hàm đều tĩnh , làm thế nào x có thể được tham chiếu bên trong baz ? (x là một biến thành viên không tĩnh)
legends2k

4
@ Legends2k: Giả vờ xcũng được làm tĩnh. :)
GManNickG

Chắc chắn, nhưng đối với trường hợp (a), nó hoạt động giống nhau trong mọi trường hợp, tức là, hàm được gọi. Tuy nhiên, việc thay thế giá trị của con trỏ từ 0 thành 1 (giả sử thông qua reinterpret_cast), nó gần như luôn luôn bị treo. Việc phân bổ giá trị 0 và do đó NULL, như trong trường hợp a, có biểu thị điều gì đó đặc biệt cho trình biên dịch không? Tại sao nó luôn gặp sự cố với bất kỳ giá trị nào khác được phân bổ cho nó?
Siddharth Shankaran

5
Thật thú vị: Đến phiên bản tiếp theo của C ++, sẽ không còn sự tham chiếu của các con trỏ nữa. Bây giờ chúng ta sẽ thực hiện chuyển hướng thông qua các con trỏ. Để tìm hiểu thêm, xin vui lòng thực hiện gián tiếp thông qua liên kết này: N3362
James McNellis

3
Gọi một hàm thành viên trên con trỏ null luôn là hành vi không xác định. Chỉ cần nhìn vào mã của bạn, tôi đã có thể cảm thấy hành vi không xác định đang từ từ bò lên cổ mình!
fredoverflow 27/07/12

Câu trả lời:


113

Cả hai (a)(b)dẫn đến hành vi không xác định. Hành vi luôn luôn không xác định khi gọi một hàm thành viên thông qua một con trỏ null. Nếu hàm là tĩnh, về mặt kỹ thuật, nó cũng không được xác định, nhưng có một số tranh chấp.


Điều đầu tiên cần hiểu là tại sao hành vi không xác định lại bỏ qua một con trỏ null. Trong C ++ 03, thực sự có một chút mơ hồ ở đây.

Mặc dù "tham chiếu đến một con trỏ null dẫn đến hành vi không xác định" được đề cập trong các ghi chú trong cả §1.9 / 4 và §8.3.2 / 4, nó không bao giờ được nêu rõ ràng. (Ghi chú không mang tính quy chuẩn.)

Tuy nhiên, người ta có thể thử suy luận nó từ §3.10 / 2:

Một giá trị liên quan đến một đối tượng hoặc chức năng.

Khi tham chiếu, kết quả là một giá trị. Một con trỏ null không tham chiếu đến một đối tượng, do đó, khi chúng ta sử dụng lvalue, chúng ta có hành vi không xác định. Vấn đề là câu trước không bao giờ được nêu, vậy "sử dụng" lvalue nghĩa là gì? Thậm chí chỉ cần tạo ra nó, hay sử dụng nó theo nghĩa chính thức hơn để thực hiện chuyển đổi giá trị thành giá trị?

Bất kể, nó chắc chắn không thể được chuyển đổi thành rvalue (§4.1 / 1):

Nếu đối tượng mà lvalue tham chiếu đến không phải là đối tượng thuộc kiểu T và không phải là đối tượng thuộc kiểu dẫn xuất từ ​​T hoặc nếu đối tượng chưa được khởi tạo, chương trình yêu cầu chuyển đổi này có hành vi không xác định.

Đây chắc chắn là hành vi không xác định.

Sự không rõ ràng xuất phát từ việc có hay không hành vi không xác định để xác định nhưng không sử dụng giá trị từ một con trỏ không hợp lệ (nghĩa là nhận một giá trị nhưng không chuyển đổi nó thành một giá trị). Nếu không, sau đó int *i = 0; *i; &(*i);được xác định rõ. Đây là một vấn đề đang hoạt động .

Vì vậy, chúng tôi có một chế độ xem "bỏ tham chiếu một con trỏ null, nhận hành vi không xác định" nghiêm ngặt và một chế độ xem yếu "sử dụng một con trỏ null được tham chiếu, nhận hành vi không xác định".

Bây giờ chúng ta xem xét câu hỏi.


Có, (a)dẫn đến hành vi không xác định. Trên thực tế, nếu thislà null thì bất kể nội dung của hàm là gì, kết quả là không xác định.

Điều này tuân theo §5.2.5 / 3:

Nếu E1có kiểu "con trỏ tới lớp X", thì biểu thức E1->E2được chuyển đổi thành dạng tương đương(*(E1)).E2;

*(E1)sẽ dẫn đến hành vi không xác định với một diễn giải chặt chẽ và .E2chuyển đổi nó thành một giá trị, làm cho hành vi không xác định đối với diễn giải yếu.

Nó cũng theo sau đó là hành vi không xác định trực tiếp từ (§9.3.1 / 1):

Nếu một hàm thành viên không tĩnh của lớp X được gọi cho một đối tượng không thuộc kiểu X hoặc thuộc kiểu dẫn xuất từ ​​X, thì hành vi đó là không xác định.


Với các hàm tĩnh, việc giải thích chặt chẽ so với yếu tạo nên sự khác biệt. Nói một cách chính xác, nó không được xác định:

Một thành viên tĩnh có thể được đề cập đến bằng cách sử dụng cú pháp truy cập thành viên lớp, trong trường hợp này biểu thức đối tượng được đánh giá.

Có nghĩa là, nó được đánh giá giống như thể nó không tĩnh và một lần nữa chúng tôi bỏ qua một con trỏ null với (*(E1)).E2.

Tuy nhiên, vì E1không được sử dụng trong một lệnh gọi hàm thành viên tĩnh, nếu chúng ta sử dụng diễn giải yếu thì cuộc gọi được xác định rõ ràng. *(E1)dẫn đến một giá trị, hàm tĩnh được giải quyết, *(E1)bị loại bỏ và hàm được gọi. Không có chuyển đổi giá trị thành giá trị nào, vì vậy không có hành vi không xác định.

Trong C ++ 0x, kể từ n3126, sự mơ hồ vẫn còn. Bây giờ, hãy an toàn: sử dụng cách diễn giải chặt chẽ.


5
+1. Tiếp tục sơ đồ, theo "định nghĩa yếu", hàm thành viên không tĩnh không được gọi "cho một đối tượng không thuộc loại X". Nó đã được gọi cho một giá trị không phải là một đối tượng nào cả. Vì vậy, giải pháp được đề xuất thêm văn bản "hoặc nếu giá trị là giá trị trống" vào mệnh đề bạn trích dẫn.
Steve Jessop

Bạn có thể làm rõ một chút? Đặc biệt, với liên kết "vấn đề đóng" và "vấn đề đang hoạt động" của bạn, số phát hành là gì? Ngoài ra, nếu đây là một vấn đề đã đóng, thì câu trả lời có / không chính xác cho các hàm tĩnh là gì? Tôi cảm thấy như mình đang bỏ lỡ bước cuối cùng trong việc cố gắng hiểu câu trả lời của bạn.
Brooks Moses

4
Tôi không nghĩ rằng lỗi 315 của CWG là "đóng" như sự hiện diện của nó trên trang "các vấn đề đã đóng" ngụ ý. Cơ sở lý luận nói rằng nó nên được cho phép bởi vì " *pkhông phải là một lỗi khi plà null trừ khi lvalue được chuyển đổi thành rvalue." Tuy nhiên, điều đó dựa trên khái niệm "giá trị trống", là một phần của giải pháp đề xuất cho khiếm khuyết 232 của CWG , nhưng chưa được thông qua. Vì vậy, với ngôn ngữ trong cả C ++ 03 và C ++ 0x, tham chiếu đến con trỏ null vẫn không được xác định, ngay cả khi không có chuyển đổi giá trị-giá trị nào.
James McNellis

1
@JamesMcNellis: Theo hiểu biết của tôi, nếu plà một địa chỉ phần cứng sẽ kích hoạt một số hành động khi đọc, nhưng không được khai báo volatile, thì câu lệnh *p;sẽ không bắt buộc, nhưng sẽ được phép , thực sự đọc địa chỉ đó; tuyên bố &(*p);, tuy nhiên, sẽ bị cấm làm như vậy. Nếu *pvolatile, đọc sẽ được yêu cầu. Trong cả hai trường hợp, nếu con trỏ không hợp lệ, tôi không thể biết câu lệnh đầu tiên sẽ không là Hành vi không xác định như thế nào, nhưng tôi cũng không thể hiểu tại sao câu lệnh thứ hai lại như vậy.
supercat

1
".E2 chuyển đổi nó thành một rvalue" - Uh, không, không
MM

30

Rõ ràng là không xác định có nghĩa là nó không được xác định , nhưng đôi khi nó có thể dự đoán được. Thông tin tôi sắp cung cấp không bao giờ được dựa vào để làm việc mã vì nó chắc chắn không được đảm bảo, nhưng nó có thể hữu ích khi gỡ lỗi.

Bạn có thể nghĩ rằng việc gọi một hàm trên con trỏ đối tượng sẽ tham chiếu đến con trỏ và gây ra UB. Trên thực tế nếu các chức năng không phải là ảo, trình biên dịch sẽ chuyển đổi nó vào một cuộc gọi chức năng đơn giản đi qua các con trỏ như tham số đầu tiên này , qua mặt dereference và tạo ra một quả bom thời gian cho các chức năng thành viên gọi. Nếu hàm thành viên không tham chiếu đến bất kỳ biến thành viên hoặc hàm ảo nào, nó thực sự có thể thành công mà không có lỗi. Hãy nhớ rằng thành công nằm trong vũ trụ của "không xác định"!

Chức năng MFC của Microsoft GetSafeHwnd thực sự dựa trên hành vi này. Tôi không biết họ đang hút gì.

Nếu bạn đang gọi một hàm ảo, con trỏ phải được tham chiếu để truy cập vtable và chắc chắn rằng bạn sẽ nhận được UB (có thể là sự cố nhưng hãy nhớ rằng không có gì đảm bảo).


1
GetSafeHwnd đầu tiên thực hiện kiểm tra này và nếu đúng, trả về NULL. Sau đó, nó bắt đầu một khung SEH và tham chiếu đến con trỏ. nếu có Vi phạm Truy cập Bộ nhớ (0xc0000005), điều này sẽ bị bắt và NULL được trả về cho người gọi :) Khác thì HWND được trả về.
Петър Петров

@ ПетърПетров đã được một vài năm kể từ khi tôi xem mã GetSafeHwnd, có thể là họ đã nâng cao nó kể từ đó. Và đừng quên rằng họ có kiến ​​thức nội bộ về hoạt động của trình biên dịch!
Mark Ransom

Tôi nêu một thực hiện mẫu có thể có tác dụng tương tự, những gì nó thực sự làm là phải đảo ngược thiết kế sử dụng một trình gỡ lỗi :)
Петър Петров

1
"họ có kiến ​​thức nội bộ về hoạt động của trình biên dịch!" - nguyên nhân gây ra rắc rối vĩnh viễn cho các dự án như MinGW cố gắng cho phép g ++ biên dịch mã gọi API Windows
MM

@MM Tôi nghĩ tất cả chúng ta đều đồng ý rằng điều này là không công bằng. Và vì thế, tôi cũng nghĩ rằng có một luật về tính tương thích khiến việc giữ nó như vậy là hơi bất hợp pháp.
v.oddou
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.