Câu trả lời tùy thuộc vào quan điểm của bạn:
Nếu bạn đánh giá theo tiêu chuẩn C ++, bạn không thể nhận được tham chiếu rỗng vì trước tiên bạn nhận được hành vi không xác định. Sau lần xuất hiện đầu tiên của hành vi không xác định, tiêu chuẩn cho phép mọi thứ xảy ra. Vì vậy, nếu bạn viết *(int*)0
, bạn đã có hành vi không xác định như hiện tại, theo quan điểm tiêu chuẩn của ngôn ngữ, tham chiếu đến một con trỏ null. Phần còn lại của chương trình là không liên quan, một khi biểu thức này được thực thi, bạn đã ra khỏi trò chơi.
Tuy nhiên, trong thực tế, tham chiếu null có thể dễ dàng được tạo từ con trỏ null và bạn sẽ không nhận thấy cho đến khi bạn thực sự cố gắng truy cập giá trị đằng sau tham chiếu null. Ví dụ của bạn có thể hơi quá đơn giản, vì bất kỳ trình biên dịch tối ưu hóa tốt nào sẽ thấy hành vi không xác định và chỉ cần tối ưu hóa bất kỳ thứ gì phụ thuộc vào nó (tham chiếu null thậm chí sẽ không được tạo, nó sẽ được tối ưu hóa đi).
Tuy nhiên, việc tối ưu hóa đi phụ thuộc vào trình biên dịch để chứng minh hành vi không xác định, điều này có thể không thực hiện được. Hãy xem xét chức năng đơn giản này bên trong tệp converter.cpp
:
int& toReference(int* pointer) {
return *pointer;
}
Khi trình biên dịch nhìn thấy hàm này, nó không biết liệu con trỏ có phải là con trỏ null hay không. Vì vậy, nó chỉ tạo mã biến bất kỳ con trỏ nào thành tham chiếu tương ứng. (Btw: Đây là một vấn đề vì con trỏ và tham chiếu là cùng một con thú trong trình hợp dịch.) Bây giờ, nếu bạn có một tệp khác user.cpp
có mã
#include "converter.h"
void foo() {
int& nullRef = toReference(nullptr);
cout << nullRef; //crash happens here
}
trình biên dịch không biết điều đó toReference()
sẽ bỏ qua con trỏ được truyền và giả sử rằng nó trả về một tham chiếu hợp lệ, điều này sẽ xảy ra là một tham chiếu rỗng trong thực tế. Cuộc gọi thành công, nhưng khi bạn cố gắng sử dụng tham chiếu, chương trình bị treo. Hy vọng. Tiêu chuẩn cho phép bất cứ điều gì xảy ra, bao gồm cả sự xuất hiện của những con voi màu hồng.
Bạn có thể hỏi tại sao điều này lại có liên quan, rốt cuộc, hành vi không xác định đã được kích hoạt bên trong toReference()
. Câu trả lời là gỡ lỗi: Tham chiếu rỗng có thể lan truyền và phát triển giống như con trỏ null. Nếu bạn không biết rằng các tham chiếu rỗng có thể tồn tại và học cách tránh tạo chúng, bạn có thể mất khá nhiều thời gian để tìm ra lý do tại sao hàm thành viên của bạn dường như bị lỗi khi nó chỉ cố đọc một int
thành viên cũ đơn thuần (answer: the instance trong lời gọi của thành viên là tham chiếu this
null, con trỏ null cũng vậy, và thành viên của bạn được tính là nằm ở địa chỉ 8).
Vậy làm thế nào về việc kiểm tra các tham chiếu rỗng? Bạn đã cho dòng
if( & nullReference == 0 ) // null reference
trong câu hỏi của bạn. Chà, điều đó sẽ không hoạt động: Theo tiêu chuẩn, bạn có hành vi không xác định nếu bạn tham chiếu đến con trỏ null và bạn không thể tạo tham chiếu null mà không tham chiếu tới con trỏ null, vì vậy tham chiếu null chỉ tồn tại trong lĩnh vực hành vi không xác định. Vì trình biên dịch của bạn có thể giả định rằng bạn không kích hoạt hành vi không xác định, nên nó có thể giả định rằng không có cái gọi là tham chiếu null (mặc dù nó sẽ dễ dàng phát ra mã tạo ra tham chiếu null!). Như vậy, nó nhìn thấy if()
điều kiện, kết luận rằng nó không thể đúng, và chỉ cần loại bỏ toàn bộ if()
câu lệnh. Với sự ra đời của tối ưu hóa thời gian liên kết, việc kiểm tra các tham chiếu rỗng một cách rõ ràng đã trở nên rõ ràng.
TL; DR:
Tham chiếu rỗng có phần tồn tại kinh khủng:
Sự tồn tại của chúng dường như là không thể (= theo tiêu chuẩn),
nhưng chúng tồn tại (= bởi mã máy được tạo),
nhưng bạn không thể nhìn thấy chúng nếu chúng tồn tại (= nỗ lực của bạn sẽ được tối ưu hóa),
nhưng chúng có thể giết chết bạn mà không biết (= chương trình của bạn bị treo ở những điểm lạ hoặc tệ hơn).
Hy vọng duy nhất của bạn là chúng không tồn tại (= viết chương trình của bạn để không tạo ra chúng).
Tôi hy vọng điều đó sẽ không đến ám ảnh bạn!