Mengapa konstruk ini menggunakan perilaku tidak terdefinisi sebelum dan sesudah kenaikan?

815
#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
PiX
sumber
12
@ Jarett, tidak, hanya perlu beberapa pointer ke "urutan poin". Saat bekerja saya menemukan sepotong kode dengan i = i ++, saya pikir "Ini tidak mengubah nilai i". Saya diuji dan saya bertanya-tanya mengapa. Sejak itu, saya telah menghapus statment ini dan menggantinya dengan i ++;
PiX
198
Saya pikir itu menarik bahwa semua orang SELALU mengasumsikan bahwa pertanyaan seperti ini ditanyakan karena penanya ingin MENGGUNAKAN konstruk yang dimaksud. Asumsi pertama saya adalah bahwa PiX tahu bahwa ini buruk, tetapi ingin tahu mengapa perilaku mereka seperti pada kompiler mana yang ia gunakan ... Dan ya, apa yang dikatakan angin ... itu tidak terdefinisi, itu bisa melakukan apa saja. .. termasuk JCF (Jump and Catch Fire)
Brian Postow
32
Saya ingin tahu: Mengapa kompiler tampaknya tidak memperingatkan pada konstruksi seperti "u = u ++ + ++ u;" jika hasilnya tidak terdefinisi?
Pelajari OpenGL ES
5
(i++)masih dievaluasi menjadi 1, terlepas dari tanda kurung
Drew McGowen
2
Apa pun i = (i++);yang dimaksudkan untuk dilakukan, pasti ada cara yang lebih jelas untuk menulisnya. Itu akan benar bahkan jika itu didefinisikan dengan baik. Bahkan di Jawa, yang mendefinisikan perilaku i = (i++);, itu masih kode yang buruk. Tulis sajai++;
Keith Thompson

Jawaban:

566

C memiliki konsep perilaku tidak terdefinisi, yaitu beberapa konstruksi bahasa sah secara sintaksis tetapi Anda tidak dapat memprediksi perilaku ketika kode dijalankan.

Sejauh yang saya tahu, standar itu tidak secara eksplisit mengatakan mengapa konsep perilaku tidak terdefinisi ada. Dalam pikiran saya, itu hanya karena perancang bahasa ingin ada beberapa kelonggaran dalam semantik, bukannya mengharuskan semua implementasi menangani bilangan bulat bilangan bulat dengan cara yang sama persis, yang sangat mungkin akan menimbulkan biaya kinerja yang serius, mereka hanya meninggalkan perilaku tidak terdefinisi sehingga jika Anda menulis kode yang menyebabkan integer overflow, apa pun bisa terjadi.

Jadi, dengan itu dalam pikiran, mengapa "masalah" ini? Bahasa tersebut dengan jelas mengatakan bahwa hal-hal tertentu mengarah pada perilaku yang tidak terdefinisi . Tidak ada masalah, tidak ada "harus" terlibat. Jika perilaku tidak terdefinisi berubah ketika salah satu variabel yang terlibat dideklarasikan volatile, itu tidak membuktikan atau mengubah apa pun. Itu tidak terdefinisi ; Anda tidak dapat memberi alasan tentang perilaku tersebut.

Contoh Anda yang paling menarik, contohnya

u = (u++);

adalah contoh buku teks dari perilaku yang tidak terdefinisi (lihat entri Wikipedia tentang poin urutan ).

beristirahat
sumber
8
@PiX: Berbagai hal tidak terdefinisi untuk sejumlah alasan yang memungkinkan. Ini termasuk: tidak ada "hasil yang benar" yang jelas, arsitektur mesin yang berbeda akan sangat mendukung hasil yang berbeda, praktik yang ada tidak konsisten, atau di luar ruang lingkup standar (misalnya nama file apa yang valid).
Richard
Hanya untuk membingungkan semua orang, beberapa contoh seperti itu sekarang didefinisikan dengan baik dalam C11, misalnya i = ++i + 1;.
MM
2
Membaca Standar dan pemikiran yang diterbitkan, Jelas mengapa konsep UB ada. Standar ini tidak pernah dimaksudkan untuk menggambarkan secara lengkap segala sesuatu yang harus dilakukan oleh implementasi C agar sesuai untuk tujuan tertentu (lihat diskusi tentang aturan "Satu Program"), tetapi sebaliknya bergantung pada penilaian dan keinginan pelaksana untuk menghasilkan implementasi kualitas yang bermanfaat. Implementasi kualitas yang cocok untuk pemrograman sistem tingkat rendah perlu mendefinisikan perilaku tindakan yang tidak diperlukan dalam aplikasi pengelompokan angka kelas atas. Alih-alih mencoba menyulitkan Standar ...
supercat
3
... dengan masuk ke perincian ekstrem tentang kasus sudut mana yang atau tidak didefinisikan, para penulis Standar mengakui bahwa pelaksana harus lebih mondar-mandir untuk menilai jenis perilaku apa yang akan dibutuhkan oleh jenis program yang diharapkan akan mereka dukung . Para penyusun hiper-modernis berpura-pura bahwa melakukan tindakan tertentu yang dimaksudkan UB menyiratkan bahwa tidak ada program yang berkualitas yang membutuhkannya, tetapi Standar dan dasar pemikirannya tidak konsisten dengan maksud yang seharusnya.
supercat
1
@jrh: Saya menulis jawaban itu sebelum saya menyadari betapa tidak adanya filosofi hyper-modernis. Yang mengganggu saya adalah kelanjutan dari "Kami tidak perlu secara resmi mengenali perilaku ini karena platform yang diperlukan dapat mendukungnya" menjadi "Kami dapat menghapus perilaku ini tanpa memberikan penggantian yang dapat digunakan karena tidak pernah dikenali dan dengan demikian kode apa pun membutuhkannya rusak ". Banyak perilaku seharusnya sudah lama ditinggalkan demi penggantian yang dalam segala hal lebih baik , tetapi itu akan membutuhkan pengakuan legitimasi mereka.
supercat
78

Hanya kompilasi dan bongkar baris kode Anda, jika Anda sangat ingin tahu bagaimana tepatnya Anda mendapatkan apa yang Anda dapatkan.

Inilah yang saya dapatkan di mesin saya, bersama dengan apa yang saya pikir sedang terjadi:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(Saya ... misalkan instruksi 0x00000014 adalah semacam optimisasi kompiler?)

badp
sumber
bagaimana cara mendapatkan kode mesin? Saya menggunakan Dev C ++, dan saya bermain-main dengan opsi '
Pembuatan
5
@ronnieaka gcc evil.c -c -o evil.bin dan gdb evil.bindisassemble evil, atau apa pun yang setara dengan Windows :)
badp
21
Jawaban ini tidak benar-benar menjawab pertanyaan Why are these constructs undefined behavior?.
Shafik Yaghmour
9
Selain itu, akan lebih mudah untuk dikompilasi ke assembly (with gcc -S evil.c), yang hanya itu yang dibutuhkan di sini. Merakit kemudian membongkar itu hanyalah cara bundaran untuk melakukannya.
Kat
50
Sebagai catatan, jika untuk alasan apa pun Anda bertanya-tanya apa yang diberikan oleh konstruksi - dan terutama jika ada kecurigaan bahwa itu mungkin perilaku yang tidak terdefinisi - saran kuno "coba saja dengan kompiler Anda dan lihat" adalah berpotensi cukup berbahaya. Anda akan belajar, paling baik, apa yang dilakukannya di bawah versi kompiler Anda ini, dalam keadaan ini, hari ini . Anda tidak akan belajar banyak jika ada sesuatu tentang apa yang dijamin untuk dilakukan. Secara umum, "coba saja dengan kompiler Anda" mengarah ke program yang tidak dapat diakses yang hanya berfungsi dengan kompiler Anda.
Steve Summit
64

Saya pikir bagian yang relevan dari standar C99 adalah 6,5 Ekspresi, §2

Antara titik urutan sebelumnya dan berikutnya objek harus memiliki nilai yang tersimpan dimodifikasi paling banyak sekali dengan evaluasi suatu ekspresi. Selanjutnya, nilai sebelumnya harus dibaca hanya untuk menentukan nilai yang akan disimpan.

dan 6.5.16 Operator penugasan, §4:

Urutan evaluasi operan tidak ditentukan. Jika suatu upaya dilakukan untuk memodifikasi hasil dari operator penugasan atau untuk mengaksesnya setelah titik urutan berikutnya, perilaku tidak terdefinisi.

Christoph
sumber
2
Apakah hal di atas menyiratkan bahwa 'i = i = 5; "akan menjadi Perilaku Tidak Terdefinisi?
supercat
1
@supercat sejauh yang saya tahu i=i=5adalah perilaku yang tidak terdefinisi
dhein
2
@Zaibis: Dasar pemikiran yang ingin saya gunakan untuk sebagian besar tempat aturan berlaku bahwa secara teori platform mutli-prosesor dapat mengimplementasikan sesuatu seperti A=B=5;"Write-lock A; Write-Lock B; Store 5 to A; store 5 to B; store 5 to B; Unlock B ; Buka Kunci A; ", dan pernyataan seperti C=A+B;" Baca-kunci A; Baca-kunci B; Hitung A + B; Buka kunci A dan B; Tulis-kunci C; Hasil penyimpanan; Buka Kunci C; ". Itu akan memastikan bahwa jika satu utas melakukan A=B=5;sedangkan utas lainnya lakukan C=A+B;maka utas yang kedua akan melihat keduanya menulis sebagai telah terjadi atau tidak. Berpotensi jaminan yang bermanfaat. I=I=5;Namun, jika satu utas melakukannya , ...
supercat
1
... dan kompiler tidak memperhatikan bahwa kedua penulisan berada di lokasi yang sama (jika salah satu atau kedua nilai melibatkan pointer, yang mungkin sulit untuk ditentukan), kode yang dihasilkan dapat menemui jalan buntu. Saya tidak berpikir implementasi dunia nyata menerapkan penguncian seperti itu sebagai bagian dari perilaku normal mereka, tetapi itu akan diizinkan di bawah standar, dan jika perangkat keras dapat menerapkan perilaku seperti itu dengan murah, itu mungkin berguna. Pada perangkat keras saat ini perilaku seperti itu akan terlalu mahal untuk diterapkan sebagai default, tetapi itu tidak berarti akan selalu demikian.
supercat
1
@supercat tetapi bukankah aturan akses titik urutan c99 saja sudah cukup untuk menyatakannya sebagai perilaku yang tidak terdefinisi? Jadi tidak masalah apa yang bisa diterapkan oleh perangkat keras secara teknis?
dhein
55

Sebagian besar jawaban di sini dikutip dari standar C yang menekankan bahwa perilaku konstruksi ini tidak ditentukan. Untuk memahami mengapa perilaku konstruksi ini tidak terdefinisi , mari kita pahami istilah-istilah ini terlebih dahulu mengingat standar C11:

Diurutkan: (5.1.2.3)

Diberikan dua evaluasi Adan B, jika Adiurutkan sebelumnya B, maka pelaksanaan Aharus mendahului pelaksanaan B.

Tidak dilanjutkan:

Jika Atidak diurutkan sebelum atau sesudah B, maka Adan Btidak dilanjutkan.

Evaluasi dapat menjadi salah satu dari dua hal:

  • perhitungan nilai , yang menghasilkan hasil dari ekspresi; dan
  • efek samping , yang merupakan modifikasi objek.

Titik Urutan:

Kehadiran titik urutan antara evaluasi ekspresi Adan Bmenyiratkan bahwa setiap perhitungan nilai dan efek samping yang terkait dengan Adiurutkan sebelum setiap perhitungan nilai dan efek samping yang terkait dengan B.

Sekarang sampai pada pertanyaan, untuk ungkapan suka

int i = 1;
i = i++;

standar mengatakan bahwa:

6.5 Ekspresi:

Jika efek samping pada objek skalar adalah unecessenced relatif terhadap baik efek samping yang berbeda pada objek skalar yang sama atau perhitungan nilai menggunakan nilai dari objek skalar yang sama, perilaku tidak terdefinisi . [...]

Oleh karena itu, ungkapan di atas memanggil UB karena dua efek samping pada objek yang sama itidak saling berhubungan satu sama lain. Itu berarti tidak diurutkan apakah efek samping dengan tugasi akan dilakukan sebelum atau setelah efek samping oleh ++.
Bergantung pada apakah penugasan terjadi sebelum atau setelah kenaikan, hasil yang berbeda akan dihasilkan dan itulah salah satu kasus perilaku tidak terdefinisi .

Mari kita ganti nama menjadi idi sebelah kiri penugasanil dan di sebelah kanan penugasan (dalam ekspresi i++) menjadi ir, maka ekspresi menjadi seperti

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Poin penting mengenai ++operator Postfix adalah:

hanya karena ++variabel setelah datang tidak berarti bahwa kenaikan terjadi terlambat . Peningkatan tersebut dapat terjadi sejak kompiler suka selama kompilator memastikan bahwa nilai asli digunakan .

Itu artinya ungkapan il = ir++ dapat dievaluasi sebagai

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

atau

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

menghasilkan dua hasil yang berbeda 1dan 2yang tergantung pada urutan efek samping berdasarkan penugasan ++dan karenanya memanggil UB.

haccks
sumber
52

Perilaku tidak dapat benar-benar dijelaskan karena memunculkan perilaku yang tidak ditentukan dan perilaku tidak terdefinisi , jadi kami tidak dapat membuat prediksi umum tentang kode ini, meskipun jika Anda membaca karya Olve Maudal seperti Deep C dan Unspecified dan Undefined terkadang Anda dapat membuat yang baik menebak dalam kasus yang sangat spesifik dengan kompiler dan lingkungan spesifik tetapi tolong jangan lakukan itu di dekat produksi.

Jadi beralih ke perilaku yang tidak ditentukan , dalam konsep c99 bagian standar6.5 ayat 3 mengatakan ( penekanan milikku ):

Pengelompokan operator dan operan ditunjukkan oleh sintaks.74) Kecuali sebagaimana ditentukan kemudian (untuk fungsi-panggilan (), &&, ||,?:, Dan operator koma), urutan evaluasi subekspresi dan urutan dalam efek samping mana yang terjadi keduanya tidak ditentukan.

Jadi ketika kita memiliki garis seperti ini:

i = i++ + ++i;

kita tidak tahu apakah i++atau ++iakan dievaluasi pertama. Ini terutama untuk memberikan kompiler opsi yang lebih baik untuk optimisasi .

Kami juga memiliki perilaku undefined sini juga karena program ini memodifikasi variabel ( i, u, dll ..) lebih dari sekali antara urutan poin . Dari rancangan bagian standar 6.5ayat 2 ( penekanan ):

Antara titik urutan sebelumnya dan berikutnya objek harus memiliki nilai tersimpan dimodifikasi paling banyak sekali dengan evaluasi suatu ekspresi. Selanjutnya, nilai sebelumnya harus dibaca hanya untuk menentukan nilai yang akan disimpan .

itu mengutip contoh kode berikut sebagai tidak terdefinisi:

i = ++i + 1;
a[i++] = i; 

Dalam semua contoh ini kode berusaha untuk memodifikasi objek lebih dari sekali di titik urutan yang sama, yang akan diakhiri dengan ; di masing-masing kasus ini:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

Perilaku yang tidak ditentukan ditentukan dalam rancangan standar c99 di bagian 3.4.4sebagai:

penggunaan nilai yang tidak ditentukan, atau perilaku lain di mana Standar Internasional ini menyediakan dua atau lebih kemungkinan dan tidak memaksakan persyaratan lebih lanjut yang dipilih dalam hal apa pun

dan perilaku tidak terdefinisi didefinisikan pada bagian3.4.3 sebagai:

perilaku, setelah menggunakan konstruksi program yang tidak dapat diakses atau salah atau data yang salah, yang untuknya Standar Internasional ini tidak mengharuskan persyaratan

dan perhatikan bahwa:

Kemungkinan perilaku yang tidak terdefinisi mulai dari mengabaikan situasi sepenuhnya dengan hasil yang tidak terduga, hingga berperilaku selama penerjemahan atau pelaksanaan program dengan cara yang terdokumentasi sebagai karakteristik lingkungan (dengan atau tanpa penerbitan pesan diagnostik), hingga penghentian terjemahan atau eksekusi (dengan penerbitan pesan diagnostik).

Shafik Yaghmour
sumber
33

Cara lain untuk menjawab ini, alih-alih terjebak dalam detail-detail misterius dari titik-titik urutan dan perilaku yang tidak terdefinisi, adalah dengan bertanya, apa artinya? Apa yang coba dilakukan oleh programmer?

Fragmen pertama yang ditanyakan tentang,, i = i++ + ++icukup jelas gila di buku saya. Tidak seorang pun akan menulisnya dalam program nyata, tidak jelas apa fungsinya, tidak ada algoritma yang dapat dipikirkan seseorang yang bisa mencoba kode yang akan menghasilkan urutan operasi yang dibuat khusus ini. Dan karena tidak jelas bagi Anda dan saya apa yang harus dilakukan, tidak apa-apa dalam buku saya jika kompiler tidak dapat mengetahui apa yang seharusnya dilakukan.

Fragmen kedua,, i = i++sedikit lebih mudah dimengerti. Seseorang jelas berusaha untuk meningkatkan i, dan menetapkan hasilnya kembali ke i. Tetapi ada beberapa cara melakukan ini dalam C. Cara paling dasar untuk menambahkan 1 ke i, dan menetapkan hasilnya kembali ke i, adalah sama di hampir semua bahasa pemrograman:

i = i + 1

C, tentu saja, memiliki pintasan praktis:

i++

Ini berarti, "tambahkan 1 ke i, dan tetapkan hasilnya kembali ke i". Jadi jika kita membangun gado-gado dari keduanya, dengan menulis

i = i++

apa yang sebenarnya kami katakan adalah "tambahkan 1 ke saya, dan tetapkan hasilnya kembali ke saya, dan tetapkan hasilnya kembali ke saya". Kami bingung, jadi tidak terlalu mengganggu saya jika kompiler juga bingung.

Secara realistis, satu-satunya saat ekspresi gila ini ditulis adalah ketika orang menggunakannya sebagai contoh buatan bagaimana ++ seharusnya bekerja. Dan tentu saja penting untuk memahami bagaimana ++ bekerja. Tetapi satu aturan praktis untuk menggunakan ++ adalah, "Jika tidak jelas apa arti ungkapan menggunakan ++, jangan tulis itu."

Kami dulu menghabiskan banyak waktu di comp.lang.c untuk membahas ekspresi seperti ini dan mengapa mereka tidak terdefinisi. Dua jawaban saya yang lebih panjang, yang mencoba menjelaskan mengapa, diarsipkan di web:

Lihat juga mempertanyakan 3.8 dan sisanya dari pertanyaan-pertanyaan di bagian 3 dari daftar C FAQ .

Steve Summit
sumber
1
Gotcha yang agak buruk sehubungan dengan Perilaku Tidak Terdefinisi adalah bahwa sementara itu digunakan pada 99,9% dari kompiler yang digunakan *p=(*q)++;untuk berarti if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;yang tidak lagi menjadi masalah. Hyper-modern C akan membutuhkan penulisan sesuatu seperti formulasi yang terakhir (walaupun tidak ada cara standar untuk menunjukkan kode tidak peduli apa yang ada di dalamnya *p) untuk mencapai tingkat efisiensi yang digunakan kompiler dengan yang sebelumnya ( elseklausa diperlukan untuk membiarkan kompiler mengoptimalkan apa ifyang dibutuhkan beberapa kompiler baru).
supercat
@supercat Saya sekarang percaya bahwa setiap kompiler yang "pintar" cukup untuk melakukan optimasi semacam itu juga harus cukup pintar untuk mengintip assertpernyataan, sehingga programmer dapat mendahului baris yang dimaksud dengan sederhana assert(p != q). (Tentu saja, mengambil kursus itu juga akan membutuhkan penulisan ulang <assert.h>untuk tidak langsung menghapus pernyataan dalam versi non-debug, melainkan mengubahnya menjadi sesuatu seperti __builtin_assert_disabled()yang dapat dilihat oleh kompiler, dan kemudian tidak memancarkan kode.)
Steve Summit
25

Seringkali pertanyaan ini ditautkan sebagai duplikat dari pertanyaan yang terkait dengan kode suka

printf("%d %d\n", i, i++);

atau

printf("%d %d\n", ++i, i++);

atau varian serupa.

Sementara ini juga perilaku yang tidak terdefinisi sebagaimana telah dinyatakan, ada perbedaan halus ketika printf()terlibat ketika membandingkan dengan pernyataan seperti:

x = i++ + i++;

Dalam pernyataan berikut:

printf("%d %d\n", ++i, i++);

yang urutan evaluasi dari argumen dalam printf()adalah tidak ditentukan . Itu berarti, ekspresi i++dan ++idapat dievaluasi dalam urutan apa pun. Standar C11 memiliki beberapa deskripsi yang relevan tentang ini:

Lampiran J, perilaku yang tidak ditentukan

Urutan di mana penunjuk fungsi, argumen, dan sub-ekspresi dalam argumen dievaluasi dalam panggilan fungsi (6.5.2.2).

3.4.4, perilaku yang tidak ditentukan

Penggunaan nilai yang tidak ditentukan, atau perilaku lain di mana Standar Internasional ini menyediakan dua atau lebih kemungkinan dan tidak memaksakan persyaratan lebih lanjut yang dipilih dalam contoh apa pun.

CONTOH Contoh perilaku yang tidak ditentukan adalah urutan di mana argumen untuk suatu fungsi dievaluasi.

The perilaku yang tidak ditentukan sendiri tidak masalah. Pertimbangkan contoh ini:

printf("%d %d\n", ++x, y++);

Ini juga memiliki perilaku yang tidak ditentukan karena urutan evaluasi ++xdan y++tidak ditentukan. Tapi itu pernyataan yang sah dan sah. Tidak ada perilaku tidak terdefinisi dalam pernyataan ini. Karena modifikasi ( ++xdan y++) dilakukan untuk objek yang berbeda .

Apa yang membuat pernyataan berikut

printf("%d %d\n", ++i, i++);

sebagai perilaku yang tidak terdefinisi adalah kenyataan bahwa dua ekspresi ini memodifikasi objek yang samai tanpa titik urutan intervensi .


Detail lainnya adalah bahwa koma yang terlibat dalam panggilan printf () adalah pemisah , bukan operator koma .

Ini adalah perbedaan penting karena operator koma memperkenalkan titik sekuens antara evaluasi operan mereka, yang membuat hukum berikut ini:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

Operator koma mengevaluasi operannya dari kiri ke kanan dan hanya menghasilkan nilai operan terakhir. Jadi j = (++i, i++);, ++ikenaikan iuntuk 6dan i++hasil nilai lama i( 6) yang ditugaskan untuk j. Kemudian imenjadi 7karena kenaikan.

Jadi jika koma dalam panggilan fungsi itu menjadi operator koma maka

printf("%d %d\n", ++i, i++);

tidak akan menjadi masalah. Tetapi ini memunculkan perilaku yang tidak terdefinisi karena koma di sini adalah pemisah .


Bagi mereka yang baru mengenal perilaku yang tidak terdefinisi akan mendapat manfaat dari membaca Apa yang Harus Diketahui Setiap Programer C Tentang Perilaku yang Tidak Terdefinisi untuk memahami konsep dan banyak varian lain dari perilaku tidak terdefinisi dalam C.

Posting ini: Perilaku tidak terdefinisi, tidak spesifik, dan terdefinisi implementasi juga relevan.

PP
sumber
Urutan ini int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));tampaknya memberikan perilaku yang stabil (evaluasi argumen kanan-ke-kiri di gcc v7.3.0; hasil "a = 110 b = 40 c = 60"). Apakah karena penugasan dianggap sebagai 'pernyataan lengkap' dan dengan demikian memperkenalkan titik urutan? Tidakkah hal itu menghasilkan evaluasi argumen / pernyataan dari kiri ke kanan? Atau, apakah itu hanya manifestasi dari perilaku yang tidak terdefinisi?
kavadias
@ kavadias Pernyataan printf itu melibatkan perilaku tidak terdefinisi, untuk alasan yang sama dijelaskan di atas. Anda masing-masing menulis bdan cdalam argumen 3 & 4 dan membaca argumen 2. Tetapi tidak ada urutan di antara ekspresi ini (argumen 2, 3, & 4). gcc / dentang memiliki opsi -Wsequence-pointyang dapat membantu menemukan ini juga.
PP
23

Meskipun tidak mungkin bahwa kompiler dan prosesor benar-benar melakukannya, itu akan legal, di bawah standar C, bagi kompiler untuk mengimplementasikan "i ++" dengan urutan:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

Walaupun saya tidak berpikir ada prosesor yang mendukung perangkat keras untuk memungkinkan hal seperti itu dilakukan secara efisien, orang dapat dengan mudah membayangkan situasi di mana perilaku seperti itu akan membuat kode multi-threaded lebih mudah (misalnya itu akan menjamin bahwa jika dua thread mencoba untuk melakukan hal di atas) urutan secara bersamaan, iakan bertambah dua) dan itu tidak sepenuhnya tak terbayangkan bahwa beberapa prosesor masa depan mungkin menyediakan fitur sesuatu seperti itu.

Jika kompiler harus menulis i++seperti yang ditunjukkan di atas (legal di bawah standar) dan harus menyelingi instruksi di atas selama evaluasi keseluruhan ekspresi (juga legal), dan jika tidak kebetulan melihat bahwa salah satu instruksi lain terjadi untuk mengakses i, dimungkinkan (dan legal) bagi kompiler untuk membuat urutan instruksi yang akan menemui jalan buntu. Yang pasti, kompiler hampir pasti akan mendeteksi masalah dalam kasus di mana variabel yang sama idigunakan di kedua tempat, tetapi jika rutin menerima referensi ke dua pointer pdan q, dan menggunakan (*p)dan dua kali) kompiler tidak akan diminta untuk mengenali atau menghindari kebuntuan yang akan terjadi jika alamat objek yang sama diteruskan untuk keduanya dan(*q) dalam ekspresi di atas (daripada menggunakanipq.

supercat
sumber
16

Sementara sintaks ekspresi suka a = a++atau a++ + a++legal, perilaku konstruk ini tidak ditentukan karena harus dalam standar C tidak dipatuhi. C99 6.5p2 :

  1. Antara titik urutan sebelumnya dan berikutnya objek harus memiliki nilai yang tersimpan dimodifikasi paling banyak sekali dengan evaluasi suatu ekspresi. [72] Selanjutnya, nilai sebelumnya hanya akan dibaca untuk menentukan nilai yang akan disimpan [73]

Dengan catatan kaki 73 semakin memperjelas hal itu

  1. Paragraf ini membuat ekspresi pernyataan yang tidak ditentukan seperti

    i = ++i + 1;
    a[i++] = i;

    sambil memungkinkan

    i = i + 1;
    a[i] = i;

Berbagai titik urutan tercantum dalam Lampiran C C11 (dan C99 ):

  1. Berikut ini adalah titik-titik urutan yang dijelaskan dalam 5.1.2.3:

    • Antara evaluasi penunjuk fungsi dan argumen aktual dalam panggilan fungsi dan panggilan aktual. (6.5.2.2).
    • Antara evaluasi operan pertama dan kedua dari operator berikut: logis AND && (6.5.13); logis ATAU || (6.5.14); koma, (6.5.17).
    • Antara evaluasi operan pertama dari kondisi? : operator dan operan mana pun dari operan kedua dan ketiga dievaluasi (6.5.15).
    • Akhir deklarator lengkap: deklarator (6.7.6);
    • Antara evaluasi ekspresi penuh dan ekspresi penuh berikutnya untuk dievaluasi. Berikut ini adalah ekspresi penuh: penginisialisasi yang bukan bagian dari senyawa literal (6.7.9); ekspresi dalam pernyataan ekspresi (6.8.3); ekspresi pengendali dari pernyataan pemilihan (jika atau beralih) (6.8.4); ekspresi pengendali pernyataan sementara atau do (6.8.5); masing-masing (opsional) ekspresi pernyataan for (6.8.5.3); ekspresi (opsional) dalam pernyataan pengembalian (6.8.6.4).
    • Segera sebelum fungsi perpustakaan kembali (7.1.4).
    • Setelah tindakan yang terkait dengan setiap specifier konversi fungsi input / output yang diformat (7.21.6, 7.29.2).
    • Segera sebelum dan segera setelah setiap panggilan ke fungsi perbandingan, dan juga antara setiap panggilan ke fungsi perbandingan dan setiap pergerakan objek yang dilewatkan sebagai argumen untuk panggilan itu (7.22.5).

Kata-kata dari paragraf yang sama dalam C11 adalah:

  1. Jika efek samping pada objek skalar tidak diikutsertakan relatif terhadap efek samping yang berbeda pada objek skalar yang sama atau perhitungan nilai menggunakan nilai objek skalar yang sama, perilaku tidak terdefinisi. Jika ada beberapa urutan subekspresi dari ekspresi yang diijinkan, perilaku tidak terdefinisi jika efek samping seperti itu terjadi pada salah satu urutan.84)

Anda dapat mendeteksi kesalahan seperti itu dalam suatu program dengan misalnya menggunakan versi terbaru GCC dengan -Walldan -Werror, dan kemudian GCC akan langsung menolak untuk mengkompilasi program Anda. Berikut ini adalah output dari gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function main’:
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

Bagian yang penting adalah mengetahui apa itu titik sekuensial - dan apa itu titik sekuensial dan apa yang tidak . Misalnya operator koma adalah titik urutan, jadi

j = (i ++, ++ i);

didefinisikan dengan baik, dan akan bertambah isatu, menghasilkan nilai lama, membuang nilai itu; kemudian pada operator koma, menyelesaikan efek samping; dan kemudian bertambah isatu, dan nilai yang dihasilkan menjadi nilai ekspresi - yaitu ini hanya cara yang dibuat untuk menulis j = (i += 2)yang sekali lagi merupakan cara "pintar" untuk menulis

i += 2;
j = i;

Namun, ,daftar argumen fungsi di bukan operator koma, dan tidak ada titik urutan antara evaluasi argumen yang berbeda; alih-alih evaluasi mereka tidak dilanjutkan berkenaan dengan satu sama lain; begitu panggilan fungsi

int i = 0;
printf("%d %d\n", i++, ++i, i);

memiliki perilaku yang tidak terdefinisi karena tidak ada titik urutan antara evaluasi i++dan ++idalam argumen fungsi , dan nilai ikarenanya dimodifikasi dua kali, oleh keduanya i++dan ++i, antara titik urutan sebelumnya dan berikutnya.

Antti Haapala
sumber
14

Standar C mengatakan bahwa variabel hanya boleh ditugaskan paling banyak sekali antara dua titik urutan. Semi-kolon misalnya adalah titik urutan.
Jadi setiap pernyataan dalam bentuk:

i = i++;
i = i++ + ++i;

dan seterusnya melanggar aturan itu. Standar ini juga mengatakan bahwa perilaku tidak terdefinisi dan tidak ditentukan. Beberapa kompiler mendeteksi ini dan menghasilkan beberapa hasil tetapi ini tidak sesuai standar.

Namun, dua variabel yang berbeda dapat ditingkatkan antara dua titik urutan.

while(*src++ = *dst++);

Di atas adalah praktik pengkodean yang umum saat menyalin / menganalisis string.

Nikhil Vidhani
sumber
Tentu saja itu tidak berlaku untuk variabel yang berbeda dalam satu ekspresi. Ini akan menjadi kegagalan desain total jika itu terjadi! Yang Anda butuhkan dalam contoh ke-2 adalah agar keduanya bertambah antara akhir pernyataan dan yang berikutnya dimulai, dan itu dijamin, justru karena konsep titik urut di pusat semua ini.
underscore_d
11

Dalam /programming/29505280/incrementing-array-index-in-c seseorang bertanya tentang pernyataan seperti:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

yang mencetak 7 ... OP mengharapkannya untuk mencetak 6.

The ++ibertahap tidak dijamin untuk semua lengkap sebelum sisa perhitungan. Bahkan, kompiler yang berbeda akan mendapatkan hasil yang berbeda di sini. Dalam contoh yang Anda berikan, pertama 2 ++idieksekusi, maka nilai-nilai k[]yang dibaca, maka yang terakhir ++iitu k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

Kompiler modern akan mengoptimalkan ini dengan sangat baik. Bahkan, mungkin lebih baik daripada kode yang Anda tulis semula (dengan asumsi kode itu bekerja seperti yang Anda harapkan).

TomOnTime
sumber
5

Penjelasan yang baik tentang apa yang terjadi dalam jenis perhitungan ini disediakan dalam dokumen n1188 dari situs ISO W14 .

Saya menjelaskan ide-idenya.

Aturan utama dari standar ISO 9899 yang berlaku dalam situasi ini adalah 6.5p2.

Antara titik urutan sebelumnya dan berikutnya objek harus memiliki nilai yang tersimpan dimodifikasi paling banyak sekali dengan evaluasi suatu ekspresi. Selanjutnya, nilai sebelumnya harus dibaca hanya untuk menentukan nilai yang akan disimpan.

Urutan poin dalam ekspresi seperti i=i++adalah sebelum i=dan sesudah i++.

Dalam makalah yang saya kutip di atas dijelaskan bahwa Anda dapat mengetahui program sebagai dibentuk oleh kotak-kotak kecil, setiap kotak berisi instruksi antara 2 titik urutan berturut-turut. Titik-titik urutan didefinisikan dalam lampiran C dari standar, dalam hal i=i++ada 2 titik urutan yang membatasi ekspresi penuh. Ungkapan seperti itu secara sintaksis setara dengan entri expression-statementdalam bentuk Backus-Naur dari tata bahasa (tata bahasa disediakan dalam lampiran A dari Standar).

Jadi urutan instruksi di dalam kotak tidak memiliki urutan yang jelas.

i=i++

dapat diartikan sebagai

tmp = i
i=i+1
i = tmp

atau sebagai

tmp = i
i = tmp
i=i+1

karena kedua bentuk ini untuk menafsirkan kode i=i++itu valid dan karena keduanya menghasilkan jawaban yang berbeda, perilaku tidak terdefinisi.

Jadi titik urutan dapat dilihat oleh awal dan akhir setiap kotak yang menyusun program [kotak adalah unit atom dalam C] dan di dalam kotak urutan instruksi tidak didefinisikan dalam semua kasus. Mengubah urutan itu seseorang kadang-kadang dapat mengubah hasilnya.

EDIT:

Sumber bagus lainnya untuk menjelaskan ambiguitas tersebut adalah entri dari situs c-faq (juga diterbitkan sebagai buku ), yaitu di sini dan di sini dan di sini .

alinsoar
sumber
Bagaimana jawaban ini ditambahkan baru ke jawaban yang ada? Juga penjelasannya i=i++sangat mirip dengan jawaban ini .
pukul
@haccks Saya tidak membaca jawaban lainnya. Saya ingin menjelaskan dalam bahasa saya sendiri apa yang saya pelajari dari dokumen yang disebutkan dari situs resmi ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar
5

Pertanyaan Anda mungkin bukan, "Mengapa konstruk ini perilaku yang tidak terdefinisi dalam C?". Pertanyaan Anda mungkin, "Mengapa kode ini (menggunakan ++) tidak memberi saya nilai yang saya harapkan?", Dan seseorang menandai pertanyaan Anda sebagai duplikat, dan mengirim Anda ke sini.

IniJawaban mencoba menjawab pertanyaan itu: mengapa kode Anda tidak memberikan jawaban yang Anda harapkan, dan bagaimana Anda bisa belajar mengenali (dan menghindari) ekspresi yang tidak akan berfungsi seperti yang diharapkan.

Saya berasumsi Anda telah mendengar definisi dasar C ++dan --operator sekarang, dan bagaimana bentuk awalan ++xberbeda dari bentuk postfix x++. Tetapi operator ini sulit untuk dipikirkan, jadi untuk memastikan Anda mengerti, mungkin Anda menulis sebuah program uji kecil yang melibatkan sesuatu seperti

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

Tetapi, yang mengejutkan Anda, program ini tidak membantu Anda memahami - program ini mencetak beberapa hasil yang aneh, tak terduga, dan tidak dapat dijelaskan, menunjukkan bahwa mungkin ++melakukan sesuatu yang sama sekali berbeda, sama sekali tidak seperti yang Anda pikirkan.

Atau, mungkin Anda sedang melihat ekspresi yang sulit dipahami seperti

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

Mungkin seseorang memberi Anda kode itu sebagai teka-teki. Kode ini juga tidak masuk akal, terutama jika Anda menjalankannya - dan jika Anda mengkompilasi dan menjalankannya di bawah dua kompiler yang berbeda, Anda kemungkinan akan mendapatkan dua jawaban yang berbeda! Ada apa dengan itu? Jawaban mana yang benar? (Dan jawabannya adalah keduanya, atau tidak keduanya.)

Seperti yang sudah Anda dengar sekarang, semua ungkapan ini tidak terdefinisi , yang berarti bahwa bahasa C tidak menjamin apa yang akan mereka lakukan. Ini adalah hasil yang aneh dan mengejutkan, karena Anda mungkin berpikir bahwa program apa pun yang dapat Anda tulis, asalkan dikompilasi dan dijalankan, akan menghasilkan output yang unik dan terdefinisi dengan baik. Tetapi dalam kasus perilaku tidak terdefinisi, itu tidak benar.

Apa yang membuat ekspresi tidak terdefinisi? Apakah ekspresi melibatkan ++dan-- selalu tidak terdefinisi? Tentu saja tidak: ini adalah operator yang berguna, dan jika Anda menggunakannya dengan benar, mereka didefinisikan dengan sangat baik.

Untuk ekspresi yang kita bicarakan adalah apa yang membuat mereka tidak terdefinisi adalah ketika ada terlalu banyak hal yang terjadi sekaligus, ketika kita tidak yakin urutan apa yang akan terjadi, tetapi ketika urutan penting bagi hasil yang kita dapatkan.

Mari kita kembali ke dua contoh yang saya gunakan dalam jawaban ini. Ketika saya menulis

printf("%d %d %d\n", x, ++x, x++);

pertanyaannya adalah, sebelum memanggil printf, apakah kompiler menghitung nilai xpertama, atau x++, atau mungkin ++x? Tapi ternyata kita tidak tahu . Tidak ada aturan dalam C yang mengatakan bahwa argumen untuk suatu fungsi dievaluasi dari kiri ke kanan, atau kanan ke kiri, atau dalam urutan lain. Jadi kita tidak bisa mengatakan apakah compiler akan melakukan xpertama, kemudian ++x, kemudian x++, atau x++kemudian ++xkemudian x, atau beberapa urutan lainnya. Tetapi urutan jelas penting, karena bergantung pada urutan yang digunakan kompiler, kami akan jelas mendapatkan hasil yang berbeda dicetak oleh printf.

Bagaimana dengan ekspresi gila ini?

x = x++ + ++x;

Masalah dengan ungkapan ini adalah bahwa ia mengandung tiga upaya berbeda untuk mengubah nilai x: (1) x++bagian mencoba menambahkan 1 ke x, menyimpan nilai baru di x, dan mengembalikan nilai lama dari x; (2) ++xbagian mencoba menambahkan 1 ke x, menyimpan nilai baru x, dan mengembalikan nilai baru x; dan (3) x =bagian mencoba untuk menetapkan jumlah dari dua lainnya kembali ke x. Manakah dari tiga tugas percobaan yang akan "menang"? Manakah dari tiga nilai yang benar-benar akan ditugaskan x? Sekali lagi, dan mungkin secara mengejutkan, tidak ada aturan dalam C untuk memberi tahu kami.

Anda mungkin membayangkan bahwa prioritas atau asosiatif atau evaluasi kiri-ke-kanan memberi tahu Anda urutan hal-hal yang terjadi, tetapi tidak. Anda mungkin tidak mempercayai saya, tapi tolong ambil kata saya untuk itu, dan saya akan mengatakannya lagi: diutamakan dan asosiatifitas tidak menentukan setiap aspek dari urutan evaluasi ekspresi dalam C. Secara khusus, jika dalam satu ekspresi ada banyak tempat-tempat berbeda di mana kami mencoba menetapkan nilai baru untuk sesuatu seperti x, diutamakan dan asosiatif tidak memberi tahu kami upaya mana yang terjadi pertama, atau terakhir, atau apa pun.


Jadi dengan semua latar belakang dan pengantar itu, jika Anda ingin memastikan bahwa semua program Anda terdefinisi dengan baik, ekspresi mana yang dapat Anda tulis, dan mana yang tidak bisa Anda tulis?

Semua ungkapan ini baik-baik saja:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

Semua ungkapan ini tidak terdefinisi:

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

Dan pertanyaan terakhir adalah, bagaimana Anda bisa tahu ekspresi mana yang didefinisikan dengan baik, dan ekspresi mana yang tidak ditentukan?

Seperti yang saya katakan sebelumnya, ekspresi yang tidak terdefinisi adalah ekspresi di mana ada terlalu banyak hal yang terjadi sekaligus, di mana Anda tidak dapat memastikan di mana urutan hal-hal terjadi, dan di mana urutannya:

  1. Jika ada satu variabel yang dimodifikasi (ditugaskan) di dua atau lebih tempat yang berbeda, bagaimana Anda tahu modifikasi mana yang terjadi pertama kali?
  2. Jika ada variabel yang dimodifikasi di satu tempat, dan nilainya digunakan di tempat lain, bagaimana Anda tahu apakah itu menggunakan nilai lama atau nilai baru?

Sebagai contoh # 1, dalam ekspresi

x = x++ + ++x;

ada tiga upaya untuk memodifikasi `x.

Sebagai contoh # 2, dalam ekspresi

y = x + x++;

kami berdua menggunakan nilai x, dan memodifikasinya.

Jadi itulah jawabannya: pastikan bahwa dalam ekspresi apa pun yang Anda tulis, setiap variabel diubah paling banyak satu kali, dan jika variabel diubah, Anda juga tidak berusaha menggunakan nilai variabel itu di tempat lain.

Steve Summit
sumber
3

Alasannya adalah bahwa program ini menjalankan perilaku yang tidak terdefinisi. Masalahnya terletak pada urutan evaluasi, karena tidak ada titik urutan yang diperlukan sesuai dengan standar C ++ 98 (tidak ada operasi yang diurutkan sebelum atau sesudah yang lain sesuai dengan terminologi C ++ 11).

Namun jika Anda tetap menggunakan satu kompiler, Anda akan menemukan perilaku tersebut tetap ada, selama Anda tidak menambahkan pemanggilan fungsi atau petunjuk, yang akan membuat perilaku tersebut lebih berantakan.

  • Jadi pertama-tama GCC: Menggunakan Nuwen MinGW 15 GCC 7.1 Anda akan mendapatkan:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2

    }

Bagaimana cara kerja GCC? ia mengevaluasi sub ekspresi pada urutan kiri ke kanan untuk sisi kanan (RHS), kemudian memberikan nilai ke sisi kiri (LHS). Inilah tepatnya bagaimana Java dan C # berperilaku dan mendefinisikan standar mereka. (Ya, perangkat lunak yang setara di Java dan C # telah mendefinisikan perilaku). Ini mengevaluasi setiap sub ekspresi satu per satu dalam Pernyataan RHS dalam urutan kiri ke kanan; untuk setiap sub ekspresi: ++ c (pra-kenaikan) dievaluasi terlebih dahulu kemudian nilai c digunakan untuk operasi, lalu kenaikan pos c ++).

menurut GCC C ++: Operator

Dalam GCC C ++, diutamakan operator mengontrol urutan di mana masing-masing operator dievaluasi

kode setara dalam perilaku yang didefinisikan C ++ sebagaimana dipahami GCC:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

Lalu kita pergi ke Visual Studio . Visual Studio 2015, Anda mendapatkan:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

Bagaimana cara kerja studio visual, dibutuhkan pendekatan lain, ia mengevaluasi semua ekspresi pra-kenaikan di lintasan pertama, kemudian menggunakan nilai variabel dalam operasi di lintasan kedua, menetapkan dari RHS ke LHS di lintasan ketiga, lalu pada lintasan terakhir ia mengevaluasi semua ekspresi pasca kenaikan dalam satu pass.

Jadi, ekuivalen dalam perilaku yang didefinisikan C ++ sebagai Visual C ++ mengerti:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

sebagaimana dinyatakan oleh dokumentasi Visual Studio di Precedence and Order of Evaluation :

Ketika beberapa operator muncul bersama, mereka memiliki prioritas yang sama dan dievaluasi sesuai dengan asosiatifitas mereka. Operator dalam tabel dijelaskan pada bagian yang dimulai dengan Postfix Operator.

Mohamed El-Nakib
sumber
1
Saya telah mengedit pertanyaan untuk menambahkan UB dalam evaluasi argumen fungsi, karena pertanyaan ini sering digunakan sebagai duplikat untuk itu. (Contoh terakhir)
Antti Haapala
1
Juga pertanyaannya adalah tentang c sekarang, bukan C ++
Antti Haapala