Apa saja perilaku umum yang tidak terdefinisi yang harus diketahui oleh seorang programmer C ++? [Tutup]

201

Apa saja perilaku umum yang tidak terdefinisi yang harus diketahui oleh seorang programmer C ++?

Katakan, seperti:

a[i] = i++;

yesraaj
sumber
3
Apakah kamu yakin Itu terlihat sangat jelas.
Martin York
17
6.2.2 Urutan Evaluasi [expr.evaluation] dalam bahasa pemrograman C ++ mengatakan demikian. Saya tidak punya referensi lain
yesraaj
4
Dia benar .. hanya melihat 6.2.2 di Bahasa Pemrograman C ++ dan dikatakan v [i] = i ++ tidak terdefinisi
dancavallaro
4
Saya akan membayangkan karena comiler melakukan eksekusi i ++ sebelum atau setelah menghitung lokasi memori v [i]. tentu, saya akan selalu ditugaskan di sana. tetapi bisa menulis ke v [i] atau v [i +1] tergantung pada urutan operasi ..
Evan Teran
2
Semua yang Bahasa Pemrograman C ++ katakan adalah "Urutan operasi subekspresi dalam ekspresi tidak ditentukan. Secara khusus, Anda tidak dapat mengasumsikan bahwa ekspresi dievaluasi dari kiri ke kanan."
dancavallaro

Jawaban:

233

Pointer

  • Mendereferensi NULLpointer
  • Mendereferensi pointer yang dikembalikan oleh alokasi "baru" dengan ukuran nol
  • Menggunakan pointer ke objek yang masa hidupnya telah berakhir (misalnya, menumpuk objek yang dialokasikan atau objek yang dihapus)
  • Mendereferensi pointer yang belum pasti diinisialisasi
  • Melakukan aritmetika pointer yang menghasilkan hasil di luar batas (baik di atas atau di bawah) dari sebuah array.
  • Mendereferensi pointer di lokasi di luar akhir array.
  • Mengonversi pointer ke objek dari tipe yang tidak kompatibel
  • Menggunakan memcpyuntuk menyalin buffer yang tumpang tindih .

Buffer meluap

  • Membaca atau menulis ke suatu objek atau array pada offset yang negatif, atau melebihi ukuran objek itu (stack / heap overflow)

Overflow Integer

  • Overflow integer yang ditandatangani
  • Mengevaluasi ekspresi yang tidak didefinisikan secara matematis
  • Nilai pergeseran kiri dengan jumlah negatif (pergeseran kanan dengan jumlah negatif ditentukan oleh penerapan)
  • Menggeser nilai dengan jumlah yang lebih besar dari atau sama dengan jumlah bit dalam angka (mis. Tidak int64_t i = 1; i <<= 72ditentukan)

Jenis, Pemain dan Konst

  • Memberi nilai numerik ke dalam nilai yang tidak dapat diwakili oleh tipe target (baik secara langsung atau melalui static_cast)
  • Menggunakan variabel otomatis sebelum ditetapkan (misalnya, int i; i++; cout << i;)
  • Menggunakan nilai dari objek jenis apa pun selain volatileatau sig_atomic_tpada saat menerima sinyal
  • Mencoba untuk memodifikasi string literal atau objek const lainnya selama masa pakainya
  • Menggabungkan sempit dengan string string yang luas selama preprocessing

Fungsi dan Templat

  • Tidak mengembalikan nilai dari fungsi pengembalian nilai (langsung atau dengan mengalir dari blok percobaan)
  • Beberapa definisi berbeda untuk entitas yang sama (kelas, templat, enumerasi, fungsi sebaris, fungsi anggota statis, dll.)
  • Rekursi tak terbatas dalam instantiasi templat
  • Memanggil suatu fungsi menggunakan berbagai parameter atau tautan ke parameter dan tautan yang didefinisikan sebagai menggunakan fungsi.

OOP

  • Penghancuran Cascading objek dengan durasi penyimpanan statis
  • Hasil penugasan ke objek yang tumpang tindih sebagian
  • Memasukkan kembali fungsi secara rekursif selama inisialisasi objek statisnya
  • Membuat panggilan fungsi virtual ke fungsi virtual murni suatu objek dari konstruktor atau penghancurnya
  • Mengacu pada anggota benda yang tidak statis yang belum dibangun atau telah dihancurkan

File sumber dan Preprocessing

  • File sumber tidak kosong yang tidak diakhiri dengan baris baru, atau diakhiri dengan garis miring terbalik (sebelum C ++ 11)
  • Garis miring terbalik diikuti oleh karakter yang bukan bagian dari kode pelarian yang ditentukan dalam karakter atau string konstan (ini adalah implementasi-didefinisikan dalam C ++ 11).
  • Melebihi batas implementasi (jumlah blok bersarang, jumlah fungsi dalam program, ruang stack yang tersedia ...)
  • Nilai numerik preprosesor yang tidak dapat diwakili oleh a long int
  • Arahan preprocessing di sisi kiri dari definisi makro seperti fungsi
  • Secara dinamis menghasilkan token yang ditentukan dalam #ifekspresi

Untuk diklasifikasikan

  • Panggilan keluar selama penghancuran program dengan durasi penyimpanan statis
Diomidis Spinellis
sumber
Hm ... NaN (x / 0) dan Infinity (0/0) dicakup oleh IEE 754, jika C ++ dirancang kemudian, mengapa ia mencatat x / 0 sebagai tidak terdefinisi?
new123456
Re: "Garis miring terbalik diikuti oleh karakter yang bukan bagian dari kode pelarian yang ditentukan dalam karakter atau konstanta string." Itu adalah UB di C89 (§3.1.3.4) dan C ++ 03 (yang menggabungkan C89), tetapi tidak di C99. C99 mengatakan bahwa "hasilnya bukan token dan diagnostik diperlukan" (§6.4.4.4). Agaknya C ++ 0x (yang menggabungkan C89) akan sama.
Adam Rosenfield
1
Standar C99 memiliki daftar perilaku yang tidak terdefinisi dalam lampiran J.2. Butuh beberapa pekerjaan untuk mengadaptasi daftar ini ke C ++. Anda harus mengubah referensi ke klausa C ++ yang benar daripada klausa C99, menghapus apa pun yang tidak relevan, dan juga memeriksa apakah semua hal itu benar-benar tidak terdefinisi dalam C ++ dan juga C. Tapi itu memberikan permulaan.
Steve Jessop
1
@ new123456 - tidak semua unit floating point kompatibel dengan IEE754. Jika C ++ membutuhkan kepatuhan IEE754, kompiler perlu menguji dan menangani kasus di mana RHS nol melalui pemeriksaan eksplisit. Dengan membuat perilaku tidak terdefinisi, kompiler dapat menghindari overhead dengan mengatakan "jika Anda menggunakan FPU non IEE754, Anda tidak akan mendapatkan perilaku FEE IEEE754".
SecurityMatt
1
"Mengevaluasi ekspresi yang hasilnya tidak dalam kisaran tipe yang sesuai" .... integer overflow didefinisikan dengan baik untuk tipe integral UNSIGNED, hanya saja tidak ditandatangani.
nacitar sevaht
31

Urutan yang parameter fungsi dievaluasi adalah perilaku yang tidak ditentukan . (Ini tidak akan membuat program Anda macet, meledak, atau memesan pizza ... tidak seperti perilaku yang tidak terdefinisi .)

Satu-satunya persyaratan adalah bahwa semua parameter harus dievaluasi sepenuhnya sebelum fungsi dipanggil.


Ini:

// The simple obvious one.
callFunc(getA(),getB());

Dapat setara dengan ini:

int a = getA();
int b = getB();
callFunc(a,b);

Atau ini:

int b = getB();
int a = getA();
callFunc(a,b);

Ini bisa berupa; terserah kompiler. Hasilnya bisa berarti, tergantung pada efek sampingnya.

Martin York
sumber
23
Pesanan tidak ditentukan, tidak ditentukan.
Rob Kennedy
1
Saya benci yang satu ini :) Saya kehilangan satu hari kerja setelah melacak salah satu dari kasus-kasus ini ... bagaimanapun juga mempelajari pelajaran saya dan untungnya tidak jatuh lagi
Robert Gould
2
@Rob: Saya akan berdebat dengan Anda tentang perubahan makna di sini, tapi saya tahu komite standar sangat pilih-pilih definisi yang tepat dari dua kata ini. Jadi saya hanya akan mengubahnya :-)
Martin York
2
Saya beruntung dalam hal ini. Saya digigit olehnya ketika saya masih di perguruan tinggi dan memiliki seorang profesor yang melihatnya dan mengatakan masalah saya dalam waktu sekitar 5 detik. Tidak tahu berapa banyak waktu yang saya habiskan untuk debugging.
Bill the Lizard
27

Kompiler bebas untuk memesan ulang bagian evaluasi ekspresi (dengan asumsi artinya tidak berubah).

Dari pertanyaan awal:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

Penguncian ganda diperiksa. Dan satu kesalahan mudah dibuat.

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.
Martin York
sumber
apa yang dimaksud dengan titik urutan?
yesraaj
1
Ooh ... itu jahat, terutama karena saya telah melihat bahwa struktur yang tepat direkomendasikan di Jawa
Tom
Perhatikan bahwa beberapa kompiler mendefinisikan perilaku dalam situasi ini. Dalam VC ++ 2005+, misalnya, jika a volatile, memory bariers yang diperlukan diatur untuk mencegah instruksi pemesanan ulang sehingga penguncian dua kali berfungsi.
Eclipse
Martin York: <i> // (c) dijamin akan terjadi setelah (a) dan (b) </i> Apakah itu? Diakui dalam contoh khusus itu satu-satunya skenario di mana itu bisa menjadi masalah adalah jika 'i' adalah variabel volatil yang dipetakan ke register perangkat keras, dan [i] (nilai lama dari 'i') telah ditambahkan, tetapi apakah ada menjamin bahwa kenaikan akan terjadi sebelum titik urutan?
supercat
5

Favorit saya adalah "rekursi tak terbatas dalam instantiasi templat" karena saya percaya itu satu-satunya di mana perilaku tidak terdefinisi terjadi pada waktu kompilasi.

Daniel Earwicker
sumber
Selesai sebelumnya, tapi saya tidak melihat bagaimana itu tidak terdefinisi. Cukup jelas Anda melakukan rekursi yang tak terbatas setelahnya.
Robert Gould
Masalahnya adalah bahwa kompiler tidak dapat memeriksa kode Anda dan memutuskan dengan tepat apakah itu akan mengalami rekursi tak terbatas atau tidak. Ini adalah contoh dari masalah penghentian. Lihat: stackoverflow.com/questions/235984/…
Daniel Earwicker
Ya itu benar-benar masalah terhenti
Robert Gould
itu membuat sistem saya crash karena bertukar disebabkan oleh terlalu sedikit memori.
Johannes Schaub - litb
2
Konstanta preprosesor yang tidak cocok dengan int juga merupakan waktu kompilasi.
Joshua
5

Menetapkan ke konstan setelah pengupasan constmenggunakan const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined
yesraaj
sumber
5

Selain perilaku tidak terdefinisi , ada juga perilaku implementasi-didefinisikan sama jahatnya .

Perilaku tidak terdefinisi terjadi ketika suatu program melakukan sesuatu yang hasilnya tidak ditentukan oleh standar.

Perilaku yang didefinisikan oleh implementasi adalah tindakan oleh suatu program yang hasilnya tidak ditentukan oleh standar, tetapi implementasi tersebut diperlukan untuk didokumentasikan. Contohnya adalah "Multibyte karakter literal", dari pertanyaan Stack Overflow Apakah ada kompiler C yang gagal mengkompilasi ini? .

Perilaku yang ditentukan oleh implementasi hanya menggigit Anda ketika Anda memulai porting (tetapi memutakhirkan ke versi baru dari compiler juga porting!)

Konstantin
sumber
4

Variabel hanya dapat diperbarui sekali dalam ekspresi (secara teknis satu kali antara titik urutan).

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.
Martin York
sumber
Infact setidaknya sekali antara dua titik urutan.
Prasoon Saurav
2
@Prasoon: Saya pikir maksud Anda: paling banyak sekali antara dua titik urutan. :-)
Nawaz
3

Pemahaman dasar tentang berbagai batasan lingkungan. Daftar lengkapnya ada di bagian 5.2.4.1 dari spesifikasi C. Berikut beberapa di antaranya;

  • 127 parameter dalam satu definisi fungsi
  • 127 argumen dalam satu panggilan fungsi
  • 127 parameter dalam satu definisi makro
  • 127 argumen dalam satu permintaan makro
  • 4095 karakter dalam baris sumber logis
  • 4095 karakter dalam string karakter literal atau string string luas (setelah penggabungan)
  • 65535 byte di sebuah objek (hanya di lingkungan yang dihosting)
  • 15 tingkat pencarian untuk file #include
  • 1023 label kasus untuk pernyataan switch (tidak termasuk label untuk pernyataan switch anynested)

Saya sebenarnya sedikit terkejut dengan batas 1023 label kasus untuk pernyataan switch, saya bisa melihat bahwa terlampaui untuk kode yang dihasilkan / lex / parser cukup mudah.

Jika batas ini terlampaui, Anda memiliki perilaku yang tidak terdefinisi (crash, cacat keamanan, dll ...).

Benar, saya tahu ini dari spesifikasi C, tetapi C ++ membagikan dukungan dasar ini.

RandomNickName42
sumber
9
Jika Anda mencapai batas ini, Anda memiliki lebih banyak masalah daripada perilaku yang tidak terdefinisi.
new123456
Anda dapat dengan mudah melampaui 65535 byte dalam suatu objek, seperti STD :: vector
Demi
2

Menggunakan memcpyuntuk menyalin antara wilayah memori yang tumpang tindih. Sebagai contoh:

char a[256] = {};
memcpy(a, a, sizeof(a));

Perilaku tidak terdefinisi menurut Standar C, yang digolongkan oleh Standar C ++ 03.

7.21.2.1 Fungsi memcpy

Ringkasan

1 / # sertakan void * memcpy (void * batasi s1, const void * batasi s2, size_t n);

Deskripsi

2 / Fungsi memcpy menyalin n karakter dari objek yang ditunjuk oleh s2 ke objek yang ditunjuk oleh s1. Jika penyalinan terjadi di antara objek yang tumpang tindih, perilaku tidak terdefinisi. Returns 3 Fungsi memcpy mengembalikan nilai s1.

7.21.2.2 Fungsi memmove

Ringkasan

1 # sertakan void * memmove (void * s1, const void * s2, size_t n);

Deskripsi

2 Fungsi memmove menyalin n karakter dari objek yang ditunjuk oleh s2 ke objek yang ditunjuk oleh s1. Menyalin terjadi seolah-olah n karakter dari objek yang ditunjuk oleh s2 pertama kali disalin ke array sementara karakter n yang tidak tumpang tindih objek yang ditunjukkan oleh s1 dan s2, dan kemudian karakter n dari array sementara disalin ke dalam objek yang ditunjukkan oleh s1. Kembali

3 Fungsi memmove mengembalikan nilai s1.

John Dibling
sumber
2

Satu-satunya tipe yang C ++ menjamin ukurannya char. Dan ukurannya adalah 1. Ukuran semua jenis lainnya tergantung platform.

JaredPar
sumber
Bukankan itu untuk apa <cstdint>? Ini mendefinisikan jenis seperti uint16_6 dan lain-lain.
Jasper Bekkers
Ya, tetapi ukuran sebagian besar jenis, katakanlah panjang, tidak didefinisikan dengan baik.
JaredPar
juga cstdint belum menjadi bagian dari standar c ++ saat ini. lihat boost / stdint.hpp untuk solusi portabel saat ini.
Evan Teran
Itu bukan perilaku yang tidak jelas. Standar mengatakan bahwa platform menyesuaikan mendefinisikan ukuran, daripada standar mendefinisikan mereka.
Daniel Earwicker
1
@JaredPar: Ini posting yang kompleks dengan banyak utas percakapan, jadi saya menyimpulkan semuanya di sini . Intinya adalah ini: "5. Untuk mewakili -2147483647 dan +2147483647 dalam biner, Anda membutuhkan 32 bit."
John Dibling
2

Objek level-Namespace di unit kompilasi yang berbeda tidak boleh bergantung satu sama lain untuk inisialisasi, karena urutan inisialisasi mereka tidak ditentukan.

yesraaj
sumber