Perhatikan kode berikut:
#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)
}
Kami berharap (b)
untuk crash, karena tidak ada anggota yang sesuai x
untuk penunjuk null. Dalam praktiknya, (a)
tidak macet karena this
penunjuk tidak pernah digunakan.
Karena (b)
dereferensi this
pointer ( (*this).x = 5;
), dan this
null, program memasuki perilaku tidak terdefinisi, karena dereferensi null selalu dikatakan sebagai perilaku tidak terdefinisi.
Apakah (a)
menghasilkan perilaku yang tidak terdefinisi? Bagaimana jika kedua fungsi (dan x
) statis?
x
dibuat statis juga. :)Jawaban:
Keduanya
(a)
dan(b)
menghasilkan perilaku yang tidak terdefinisi. Itu selalu merupakan perilaku yang tidak terdefinisi untuk memanggil fungsi anggota melalui penunjuk null. Jika fungsinya statis, secara teknis juga tidak ditentukan, tetapi ada beberapa perselisihan.Hal pertama yang harus dipahami adalah mengapa perilaku tidak terdefinisi dengan merujuk pointer nol. Di C ++ 03, sebenarnya ada sedikit ambiguitas di sini.
Meskipun "dereferensi penunjuk nol menghasilkan perilaku yang tidak ditentukan" disebutkan dalam catatan di §1.9 / 4 dan §8.3.2 / 4, hal itu tidak pernah dinyatakan secara eksplisit. (Catatan tidak normatif.)
Namun, seseorang dapat mencoba menyimpulkannya dari §3.10 / 2:
Saat melakukan dereferensi, hasilnya adalah nilai l. Pointer nol tidak merujuk ke objek, oleh karena itu ketika kita menggunakan lvalue kita memiliki perilaku yang tidak terdefinisi. Masalahnya adalah kalimat sebelumnya tidak pernah disebutkan, jadi apa artinya "menggunakan" lvalue? Bahkan hanya menghasilkannya sama sekali, atau menggunakannya dalam arti yang lebih formal yaitu melakukan konversi lvalue-to-rvalue?
Terlepas dari itu, pasti tidak dapat dikonversi ke nilai r (§4.1 / 1):
Ini jelas perilaku yang tidak terdefinisi.
Ambiguitas berasal dari apakah perilaku tidak terdefinisi menjadi deference atau tidak, tetapi tidak menggunakan nilai dari penunjuk yang tidak valid (yaitu, dapatkan nilai l tetapi tidak mengubahnya menjadi nilai r). Jika tidak, maka
int *i = 0; *i; &(*i);
sudah didefinisikan dengan baik. Ini adalah masalah aktif .Jadi kita memiliki tampilan "dereferensi null pointer, dapatkan perilaku tidak terdefinisi" dan tampilan "lemah" menggunakan pointer null yang terdefinisi, dapatkan perilaku tidak terdefinisi ".
Sekarang kita pertimbangkan pertanyaannya.
Ya,
(a)
menghasilkan perilaku tidak terdefinisi. Faktanya, jikathis
null maka terlepas dari konten fungsinya , hasilnya tidak ditentukan.Ini mengikuti dari §5.2.5 / 3:
*(E1)
akan menghasilkan perilaku tidak terdefinisi dengan interpretasi yang ketat, dan.E2
mengubahnya menjadi nilai r, menjadikannya perilaku tak terdefinisi untuk interpretasi yang lemah.Ini juga mengikuti bahwa itu perilaku yang tidak ditentukan langsung dari (§9.3.1 / 1):
Dengan fungsi statis, interpretasi yang ketat versus lemah membuat perbedaan. Sebenarnya, ini tidak ditentukan:
Artinya, itu dievaluasi seolah-olah itu non-statis dan kami sekali lagi dereferensi pointer nol dengan
(*(E1)).E2
.Namun, karena
E1
tidak digunakan dalam panggilan fungsi-anggota statis, jika kita menggunakan interpretasi yang lemah, panggilan tersebut didefinisikan dengan baik.*(E1)
menghasilkan nilai l, fungsi statis diselesaikan,*(E1)
dibuang, dan fungsi dipanggil. Tidak ada konversi lvalue-to-rvalue, jadi tidak ada perilaku yang tidak ditentukan.Di C ++ 0x, pada n3126, ambiguitas tetap ada. Untuk saat ini, berhati-hatilah: gunakan interpretasi yang ketat.
sumber
*p
bukan kesalahan ketikap
nol kecuali nilai l diubah menjadi nilai r." Namun, hal itu bergantung pada konsep "nilai l kosong", yang merupakan bagian dari resolusi yang diusulkan untuk CWG cacat 232 , tetapi belum diadopsi. Jadi, dengan bahasa di C ++ 03 dan C ++ 0x, dereferensi penunjuk nol masih belum ditentukan, bahkan jika tidak ada konversi lvalue-to-rvalue.p
ada alamat perangkat keras yang akan memicu beberapa tindakan saat dibaca, tetapi tidak dideklarasikanvolatile
, pernyataan*p;
itu tidak akan diperlukan, tetapi akan diizinkan , untuk benar-benar membaca alamat itu; pernyataan tersebut&(*p);
, bagaimanapun, akan dilarang untuk dilakukan. Jika*p
beradavolatile
, membaca akan diperlukan. Dalam kedua kasus, jika penunjuk tidak valid, saya tidak dapat melihat bagaimana pernyataan pertama tidak akan menjadi Perilaku Tidak Terdefinisi, tetapi saya juga tidak dapat melihat mengapa pernyataan kedua akan menjadi.Jelas tidak terdefinisi artinya tidak terdefinisi , namun terkadang dapat diprediksi. Informasi yang akan saya berikan tidak boleh diandalkan untuk kode yang berfungsi karena tentu saja tidak dijamin, tetapi mungkin berguna saat melakukan debug.
Anda mungkin berpikir bahwa memanggil fungsi pada penunjuk objek akan mendereferensi penunjuk dan menyebabkan UB. Dalam praktiknya jika fungsinya bukan virtual, kompilator akan mengubahnya menjadi panggilan fungsi biasa dengan meneruskan pointer sebagai parameter pertama ini , melewati dereferensi dan membuat bom waktu untuk fungsi anggota yang dipanggil. Jika fungsi anggota tidak mereferensikan variabel anggota atau fungsi virtual apa pun, itu mungkin benar-benar berhasil tanpa kesalahan. Ingatlah bahwa kesuksesan termasuk dalam alam semesta yang "tidak ditentukan"!
Fungsi MFC Microsoft GetSafeHwnd sebenarnya bergantung pada perilaku ini. Saya tidak tahu apa yang mereka merokok.
Jika Anda memanggil fungsi virtual, penunjuk harus didereferensi untuk sampai ke vtable, dan pasti Anda akan mendapatkan UB (mungkin macet tapi ingat bahwa tidak ada jaminan).
sumber
GetSafeHwnd
, mungkin saja mereka telah meningkatkannya sejak saat itu. Dan jangan lupa bahwa mereka memiliki pengetahuan orang dalam tentang cara kerja compiler!