Tại sao các cấu trúc này sử dụng hành vi không xác định trước và sau tăng không xác định?


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?
}

12
@Jarett, không, chỉ cần một số gợi ý để "điểm chuỗi". Trong khi làm việc tôi đã tìm thấy một đoạn mã với i = i ++, tôi thốt lên "Điều này không làm thay đổi giá trị của i". Tôi đã thử nghiệm và tôi tự hỏi tại sao. Kể từ đó, tôi đã loại bỏ thống kê này và thay thế nó bằng i ++;
PiX

198
Tôi nghĩ thật thú vị khi mọi người LUÔN LUÔN giả định rằng những câu hỏi như thế này được hỏi bởi vì người hỏi muốn sử dụng cấu trúc trong câu hỏi. Giả định đầu tiên của tôi là PiX biết rằng những điều này là xấu, nhưng tò mò tại sao hành vi mà họ làm theo trình biên dịch whataver mà anh ta đang sử dụng ... Và vâng, những gì unWind nói ... nó không được xác định, nó có thể làm bất cứ điều gì. .. bao gồm JCF (Nhảy và bắt lửa)
Brian Postow

32
Tôi tò mò: Tại sao các trình biên dịch dường như không cảnh báo về các cấu trúc như "u = u ++ + ++ u;" nếu kết quả không xác định?
Tìm hiểu OpenGL ES

5
(i++)vẫn đánh giá là 1, bất kể dấu ngoặc đơn
Drew McGowen

2
Bất cứ điều gì i = (i++);đã được dự định để làm, chắc chắn có một cách rõ ràng hơn để viết nó. Điều đó sẽ đúng ngay cả khi nó được xác định rõ. Ngay cả trong Java, định nghĩa hành vi của i = (i++);, nó vẫn là mã xấu. Chỉ cần viếti++;
Keith Thompson

Câu trả lời:


566

C có khái niệm về hành vi không xác định, tức là một số cấu trúc ngôn ngữ có giá trị cú pháp nhưng bạn không thể dự đoán hành vi khi mã được chạy.

Theo như tôi biết, tiêu chuẩn không nói rõ ràng lý do tại sao khái niệm hành vi không xác định tồn tại. Trong suy nghĩ của tôi, đơn giản là vì các nhà thiết kế ngôn ngữ muốn có một chút chậm trễ trong ngữ nghĩa, thay vì yêu cầu tất cả các triển khai xử lý tràn số nguyên theo cách chính xác, rất có thể sẽ gây ra chi phí hiệu suất nghiêm trọng, họ chỉ để lại hành vi không xác định để nếu bạn viết mã gây tràn số nguyên, bất cứ điều gì cũng có thể xảy ra.

Vì vậy, với ý nghĩ đó, tại sao những "vấn đề" này? Ngôn ngữ nói rõ ràng rằng những điều nhất định dẫn đến hành vi không xác định . Không có vấn đề gì, không có "nên" tham gia. Nếu hành vi không xác định thay đổi khi một trong các biến liên quan được khai báo volatile, điều đó không chứng minh hoặc thay đổi bất cứ điều gì. Nó không được xác định ; bạn không thể lý luận về hành vi.

Ví dụ thú vị nhất của bạn, ví dụ với

u = (u++);

là một ví dụ trong sách giáo khoa về hành vi không xác định (xem mục nhập của Wikipedia về các điểm theo trình tự ).


8
@PiX: Mọi thứ không được xác định vì một số lý do có thể. Chúng bao gồm: không có "kết quả đúng" rõ ràng, các kiến ​​trúc máy khác nhau sẽ ủng hộ mạnh mẽ các kết quả khác nhau, thực tiễn hiện tại không nhất quán hoặc vượt quá phạm vi của tiêu chuẩn (ví dụ tên tập tin nào là hợp lệ).
Richard

Chỉ để gây nhầm lẫn cho tất cả mọi người, một số ví dụ như vậy hiện được xác định rõ trong C11, ví dụ i = ++i + 1;.
MM

2
Đọc Tiêu chuẩn và cơ sở lý luận được công bố, rõ ràng lý do tại sao khái niệm về UB tồn tại. Tiêu chuẩn không bao giờ có ý định mô tả đầy đủ mọi thứ mà việc triển khai C phải làm để phù hợp với bất kỳ mục đích cụ thể nào (xem phần thảo luận về quy tắc "Một chương trình"), mà thay vào đó dựa vào phán đoán của người thực hiện và mong muốn tạo ra các triển khai chất lượng hữu ích. Việc triển khai chất lượng phù hợp với lập trình hệ thống cấp thấp sẽ cần xác định hành vi của các hành động không cần thiết trong crunching.applecting số cao cấp. Thay vì cố gắng làm phức tạp Tiêu chuẩn ...
supercat

3
... Bằng cách đi sâu vào chi tiết về trường hợp góc nào hoặc không được xác định, các tác giả của Tiêu chuẩn nhận ra rằng những người triển khai nên có nhịp độ tốt hơn để đánh giá loại hành vi nào sẽ cần cho các loại chương trình mà họ dự kiến ​​sẽ hỗ trợ . Các trình biên dịch siêu hiện đại giả vờ rằng thực hiện một số hành động nhất định mà UB dự định ngụ ý rằng không có chương trình chất lượng nào cần chúng, nhưng Tiêu chuẩn và lý do không phù hợp với mục đích giả định như vậy.
supercat

1
@jrh: Tôi đã viết câu trả lời đó trước khi tôi nhận ra triết lý siêu hiện đại đã vượt ra khỏi tầm tay như thế nào. Điều làm tôi khó chịu là sự tiến triển từ "Chúng tôi không cần phải chính thức nhận ra hành vi này bởi vì các nền tảng cần thiết có thể hỗ trợ nó bằng mọi cách" thành "Chúng tôi có thể xóa hành vi này mà không cần cung cấp thay thế có thể sử dụng được vì nó không bao giờ được nhận ra cần nó đã bị phá vỡ ". Nhiều hành vi lẽ ra đã bị từ chối từ lâu để ủng hộ việc thay thế bằng mọi cách tốt hơn , nhưng điều đó đòi hỏi phải thừa nhận tính hợp pháp của chúng.
supercat

78

Chỉ cần biên dịch và phân tách dòng mã của bạn, nếu bạn rất muốn biết chính xác bạn sẽ nhận được những gì bạn đang nhận được.

Đây là những gì tôi nhận được trên máy của mình, cùng với những gì tôi nghĩ đang diễn ra:

$ 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.

(Tôi ... giả sử rằng lệnh 0x00000014 là một loại tối ưu hóa trình biên dịch?)


Làm thế nào để tôi có được mã máy? Tôi sử dụng Dev C ++ và tôi đã chơi xung quanh với tùy chọn 'Tạo mã' trong cài đặt trình biên dịch, nhưng không có đầu ra tệp bổ sung hoặc bất kỳ đầu ra giao diện điều khiển nào
bad_keypoint

5
@ronnieaka gcc evil.c -c -o evil.bingdb evil.bindisassemble evil, hoặc bất cứ thứ gì tương đương với Windows trong số đó là :)
badp

21
Câu trả lời này không thực sự giải quyết câu hỏi Why are these constructs undefined behavior?.
Shafik Yaghmour

9
Bên cạnh đó, việc biên dịch để lắp ráp (với gcc -S evil.c) sẽ dễ dàng hơn , đó là tất cả những gì cần thiết ở đây. Lắp ráp sau đó tháo rời nó chỉ là một cách làm tròn.
Kat

50
Đối với bản ghi, nếu vì bất kỳ lý do gì bạn đang tự hỏi một cấu trúc đã cho là gì - và đặc biệt là nếu có bất kỳ nghi ngờ nào về việc đó có thể là hành vi không xác định - lời khuyên lâu đời về "chỉ cần thử với trình biên dịch của bạn và xem" là có khả năng khá nguy hiểm. Bạn sẽ học, tốt nhất, những gì nó làm trong phiên bản trình biên dịch này của bạn, trong những trường hợp này, ngày hôm nay . Bạn sẽ không học được nhiều nếu có bất cứ điều gì về những gì nó được đảm bảo để làm. Nói chung, "chỉ cần thử với trình biên dịch của bạn" dẫn đến các chương trình không thể truy cập chỉ hoạt động với trình biên dịch của bạn.
Hội nghị thượng đỉnh Steve

64

Tôi nghĩ rằng các phần có liên quan của tiêu chuẩn C99 là 6,5 Biểu thức, §2

Giữa điểm thứ tự trước và điểm tiếp theo, một đối tượng sẽ được sửa đổi giá trị lưu trữ của nó nhiều nhất một lần bằng cách đánh giá biểu thức. Hơn nữa, giá trị trước chỉ được đọc để xác định giá trị sẽ được lưu trữ.

và 6.5.16 Toán tử gán, §4:

Thứ tự đánh giá các toán hạng là không xác định. Nếu một nỗ lực được thực hiện để sửa đổi kết quả của toán tử gán hoặc để truy cập nó sau điểm chuỗi tiếp theo, hành vi không được xác định.


2
Điều trên có nghĩa là 'i = i = 5; "sẽ là Hành vi không xác định?
supercat

1
@supercat theo như tôi biết i=i=5cũng là hành vi không xác định
dhein

2
@Zaibis: Lý do tôi thích sử dụng cho hầu hết các quy tắc địa điểm áp dụng rằng trên lý thuyết, nền tảng bộ xử lý mutli có thể thực hiện một cái gì đó như A=B=5;"Khóa ghi A; Khóa B, Lưu trữ 5 đến A; lưu trữ 5 đến B; Mở khóa B ; Bỏ khóa A; "và một câu lệnh như C=A+B;" Khóa đọc A; Khóa đọc B; Tính A + B; Mở khóa A và B; Khóa ghi C; Lưu kết quả; Mở khóa C; ". Điều đó sẽ đảm bảo rằng nếu một luồng đã làm A=B=5;trong khi một luồng khác thì C=A+B;luồng sau sẽ thấy cả hai ghi là đã xảy ra hoặc không. Có khả năng là một đảm bảo hữu ích. I=I=5;Tuy nhiên, nếu một chủ đề đã làm , ...
supercat

1
... và trình biên dịch không nhận thấy rằng cả hai ghi đều ở cùng một vị trí (nếu một hoặc cả hai giá trị liên quan đến con trỏ, có thể khó xác định), mã được tạo có thể bị bế tắc. Tôi không nghĩ rằng bất kỳ triển khai trong thế giới thực nào cũng thực hiện việc khóa như một phần của hành vi thông thường của họ, nhưng nó sẽ được cho phép theo tiêu chuẩn và nếu phần cứng có thể thực hiện các hành vi đó với giá rẻ thì điều đó có thể hữu ích. Trên phần cứng ngày nay, hành vi như vậy sẽ quá tốn kém để thực hiện như một mặc định, nhưng điều đó không có nghĩa là nó sẽ luôn như vậy.
supercat

1
@supercat nhưng liệu quy tắc truy cập điểm chuỗi của c99 có đủ để tuyên bố đó là hành vi không xác định không? Vì vậy, không vấn đề gì về mặt kỹ thuật phần cứng có thể thực hiện?
dhein

55

Hầu hết các câu trả lời ở đây được trích dẫn từ tiêu chuẩn C nhấn mạnh rằng hành vi của các cấu trúc này là không xác định. Để hiểu lý do tại sao hành vi của các cấu trúc này không được xác định , trước tiên hãy hiểu các thuật ngữ này theo tiêu chuẩn C11:

Trình tự: (5.1.2.3)

Đưa ra bất kỳ hai đánh giá nào AB, nếu Ađược giải trình tự trước Bđó, thì việc thực hiện Asẽ đi trước việc thực hiện B.

Không có kết quả:

Nếu Akhông được giải trình tự trước hoặc sau B, thì ABkhông có kết quả.

Đánh giá có thể là một trong hai điều:

  • các tính toán giá trị , tính ra kết quả của một biểu thức; và
  • tác dụng phụ , đó là sửa đổi của các đối tượng.

Điểm trình tự:

Sự hiện diện của một điểm thứ tự giữa việc đánh giá các biểu thức ABngụ ý rằng mọi tính toán giá trịtác dụng phụ liên quan Ađược sắp xếp theo thứ tự trước mỗi tính toán giá trịtác dụng phụ liên quan đến B.

Bây giờ đến câu hỏi, cho các biểu thức như

int i = 1;
i = i++;

tiêu chuẩn nói rằng:

6.5 Biểu thức:

Nếu một tác dụng phụ trên một đối tượng vô hướng là unsequenced liên quan đến một trong hai một tác dụng phụ khác nhau trên các đối tượng vô hướng cùng một hoặc một tính toán giá trị sử dụng giá trị của đối tượng vô hướng tương tự, hành vi là undefined . [...]

Do đó, biểu thức trên gọi UB vì hai tác dụng phụ trên cùng một đối tượng không icó kết quả tương đối với nhau. Điều đó có nghĩa là nó không được giải trình tự cho dù tác dụng phụ bằng cách gán isẽ được thực hiện trước hay sau tác dụng phụ bởi ++.
Tùy thuộc vào việc chuyển nhượng xảy ra trước hay sau khi tăng, các kết quả khác nhau sẽ được tạo ra và đó là một trong những trường hợp hành vi không xác định .

Cho phép đổi tên iở bên trái của gán ilvà ở bên phải của gán (trong biểu thức i++) ir, sau đó biểu thức sẽ như thế nào

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

Một điểm quan trọng liên quan đến ++toán tử Postfix là:

chỉ vì ++xuất hiện sau biến không có nghĩa là sự gia tăng xảy ra muộn . Sự gia tăng có thể xảy ra ngay khi trình biên dịch thích miễn là trình biên dịch đảm bảo rằng giá trị ban đầu được sử dụng .

Nó có nghĩa là biểu thức il = ir++có thể được đánh giá là

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

hoặc là

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

dẫn đến hai kết quả khác nhau 12phụ thuộc vào chuỗi tác dụng phụ bằng cách gán ++và do đó gọi ra UB.


52

Hành vi thực sự không thể được giải thích vì nó gọi cả hành vi không xác địnhhành vi không xác định , vì vậy chúng tôi không thể đưa ra bất kỳ dự đoán chung nào về mã này, mặc dù nếu bạn đọc tác phẩm của Olve Maudal như Deep CKhông xác định đôi khi bạn có thể làm tốt đoán trong các trường hợp rất cụ thể với một trình biên dịch và môi trường cụ thể nhưng xin vui lòng không làm điều đó ở bất cứ đâu gần sản xuất.

Vì vậy, chuyển sang hành vi không xác định , trong dự thảo tiêu chuẩn c99 phần 6.5đoạn 3 nói ( tôi nhấn mạnh ):

Việc nhóm các toán tử và toán hạng được biểu thị bằng cú pháp.74) Trừ khi được chỉ định sau (đối với hàm gọi (), &&, ||,?: Và toán tử dấu phẩy), thứ tự đánh giá các biểu thức con và thứ tự trong tác dụng phụ nào xảy ra đều không xác định.

Vì vậy, khi chúng tôi có một dòng như thế này:

i = i++ + ++i;

chúng tôi không biết liệu i++hoặc ++isẽ được đánh giá đầu tiên. Điều này chủ yếu là để cung cấp cho trình biên dịch các tùy chọn tốt hơn để tối ưu hóa .

Chúng tôi cũng có hành vi undefined đây cũng kể từ khi chương trình được sửa đổi biến ( i, u, vv ..) nhiều hơn một lần giữa điểm chuỗi . Từ dự thảo phần tiêu chuẩn 6.5đoạn 2 ( nhấn mạnh của tôi ):

Giữa điểm thứ tự trước và điểm tiếp theo, một đối tượng sẽ được sửa đổi giá trị lưu trữ của nó nhiều nhất một lần bằng cách đánh giá biểu thức. Hơn nữa, giá trị trước chỉ được đọc để xác định giá trị sẽ được lưu trữ .

nó trích dẫn các ví dụ mã sau đây là không xác định:

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

Trong tất cả các ví dụ này, mã đang cố gắng sửa đổi một đối tượng nhiều lần trong cùng một điểm thứ tự, điều này sẽ kết thúc bằng ;một trong các trường hợp sau:

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

i = (i++);
^    ^

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

u = (u++);
^    ^

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

Hành vi không xác định được xác định trong dự thảo tiêu chuẩn c99 trong phần 3.4.4như:

sử dụng một giá trị không xác định hoặc hành vi khác trong đó Tiêu chuẩn quốc tế này cung cấp hai hoặc nhiều khả năng và không áp dụng thêm các yêu cầu nào được chọn trong bất kỳ trường hợp nào

hành vi không xác định được định nghĩa trong phần 3.4.3như:

hành vi, khi sử dụng một cấu trúc chương trình không thể truy cập hoặc có lỗi hoặc dữ liệu sai, mà Tiêu chuẩn quốc tế này áp đặt không có yêu cầu

và lưu ý rằng:

Hành vi không xác định có thể bao gồm từ bỏ qua hoàn toàn tình huống với kết quả không thể đoán trước, đến hành vi trong quá trình dịch hoặc thực hiện chương trình theo cách thức được ghi lại trong môi trường (có hoặc không có thông báo chẩn đoán), để chấm dứt dịch hoặc thực thi (với việc ban hành của một thông điệp chẩn đoán).


33

Một cách khác để trả lời điều này, thay vì bị sa lầy vào các chi tiết phức tạp của các điểm trình tự và hành vi không xác định, chỉ đơn giản là hỏi, chúng có nghĩa là gì? Lập trình viên đã cố gắng làm gì?

Đoạn đầu tiên được hỏi về i = i++ + ++i, là khá rõ ràng điên rồ trong cuốn sách của tôi. Không ai có thể viết nó trong một chương trình thực tế, không rõ nó làm gì, không có thuật toán có thể hiểu được mà ai đó có thể đã cố gắng viết mã sẽ dẫn đến chuỗi hoạt động cụ thể này. Và vì nó không rõ ràng đối với bạn và tôi những gì nó phải làm, nên trong cuốn sách của tôi cũng không sao nếu trình biên dịch không thể hiểu được nó phải làm gì.

Đoạn thứ hai, i = i++dễ hiểu hơn một chút. Ai đó rõ ràng đang cố gắng tăng i và gán kết quả lại cho i. Nhưng có một vài cách để thực hiện điều này trong C. Cách cơ bản nhất để thêm 1 vào i và gán kết quả cho i, giống như trong hầu hết mọi ngôn ngữ lập trình:

i = i + 1

C, tất nhiên, có một phím tắt tiện dụng:

i++

Điều này có nghĩa là, "thêm 1 vào i và gán kết quả lại cho i". Vì vậy, nếu chúng ta xây dựng một hodgepodge của hai, bằng cách viết

i = i++

điều chúng tôi thực sự nói là "thêm 1 vào i và gán kết quả lại cho i và gán kết quả lại cho i". Chúng tôi bối rối, vì vậy nó cũng không làm phiền tôi quá nhiều nếu trình biên dịch cũng bị lẫn lộn.

Trên thực tế, lần duy nhất những biểu hiện điên rồ này được viết là khi mọi người sử dụng chúng làm ví dụ nhân tạo về cách ++ được cho là hoạt động. Và tất nhiên điều quan trọng là phải hiểu làm thế nào ++ hoạt động. Nhưng một quy tắc thực tế để sử dụng ++ là "Nếu không rõ ràng biểu thức sử dụng ++ nghĩa là gì, đừng viết nó."

Chúng tôi thường dành vô số giờ cho comp.lang.c để thảo luận về các biểu thức như thế này và tại sao chúng không được xác định. Hai câu trả lời dài hơn của tôi, cố gắng giải thích tại sao, được lưu trữ trên web:

Xem thêm câu hỏi 3.8 và phần còn lại của các câu hỏi trong phần 3 của danh sách C FAQ .


1
Một vấn đề khá khó chịu liên quan đến Hành vi không xác định là trong khi nó được sử dụng an toàn trên 99,9% trình biên dịch để sử dụng *p=(*q)++;có nghĩa if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;là không còn như vậy nữa. Hyper-Modern C sẽ yêu cầu viết một cái gì đó giống như công thức sau (mặc dù không có cách nào để chỉ ra mã tiêu chuẩn không quan tâm đến cái gì *p) để đạt được mức trình biên dịch hiệu quả được sử dụng để cung cấp trước đây ( elsemệnh đề là cần thiết để cho phép trình biên dịch tối ưu hóa cái ifmà một số trình biên dịch mới hơn sẽ yêu cầu).
supercat

@supercat Bây giờ tôi tin rằng bất kỳ trình biên dịch nào đủ "thông minh" để thực hiện loại tối ưu hóa đó cũng phải đủ thông minh để xem trộm các assertcâu lệnh, để lập trình viên có thể đi trước câu hỏi đơn giản assert(p != q). (Tất nhiên, tham gia khóa học đó cũng sẽ yêu cầu viết lại <assert.h>để không xóa các xác nhận hoàn toàn trong các phiên bản không gỡ lỗi, nhưng thay vào đó, biến chúng thành một cái gì đó giống như __builtin_assert_disabled()trình biên dịch có thể nhìn thấy, và sau đó không phát ra mã cho.)
Steve Summit

25

Thông thường câu hỏi này được liên kết dưới dạng trùng lặp các câu hỏi liên quan đến mã như

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

hoặc là

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

hoặc các biến thể tương tự.

Mặc dù đây cũng là hành vi không xác định như đã nêu, có những khác biệt tinh tế khi printf()có liên quan khi so sánh với một tuyên bố như:

x = i++ + i++;

Trong tuyên bố sau:

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

các trình tự đánh giá của các đối số trong printf()không xác định . Điều đó có nghĩa là, biểu thức i++++icó thể được đánh giá theo bất kỳ thứ tự nào. Tiêu chuẩn C11 có một số mô tả có liên quan về điều này:

Phụ lục J, các hành vi không xác định

Thứ tự trong đó trình chỉ định hàm, đối số và biểu thức con trong các đối số được ước tính trong một lệnh gọi hàm (6.5.2.2).

3.4.4, hành vi không xác định

Việc sử dụng một giá trị không xác định hoặc hành vi khác trong đó Tiêu chuẩn quốc tế này cung cấp hai hoặc nhiều khả năng và không áp dụng thêm các yêu cầu nào được chọn trong bất kỳ trường hợp nào.

VÍ DỤ Một ví dụ về hành vi không xác định là thứ tự mà các đối số cho hàm được ước tính.

Các hành vi không xác định chính nó KHÔNG phải là một vấn đề. Xem xét ví dụ này:

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

Điều này cũng có hành vi không xác định bởi vì thứ tự đánh giá ++xy++không xác định. Nhưng đó là tuyên bố hợp pháp và hợp lệ. Không hành vi không xác định trong tuyên bố này. Bởi vì các sửa đổi ( ++xy++) được thực hiện cho các đối tượng riêng biệt .

Điều gì làm cho tuyên bố sau đây

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

như hành vi không xác định là thực tế là hai biểu thức này sửa đổi cùng một đối tượng imà không có điểm thứ tự can thiệp .


Một chi tiết khác là dấu phẩy liên quan đến lệnh gọi printf () là dấu phân cách , không phải toán tử dấu phẩy .

Đây là một sự khác biệt quan trọng bởi vì toán tử dấu phẩy giới thiệu một điểm thứ tự giữa việc đánh giá các toán hạng của chúng, điều này làm cho tính pháp lý sau:

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

Toán tử dấu phẩy đánh giá toán hạng của nó từ trái sang phải và chỉ mang lại giá trị của toán hạng cuối cùng. Vì vậy, trong j = (++i, i++);, ++ităng ilên 6i++mang lại giá trị cũ của i( 6) được gán cho j. Sau đó itrở thành 7do tăng sau.

Vì vậy, nếu dấu phẩy trong lệnh gọi hàm là toán tử dấu phẩy thì

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

sẽ không là vấn đề Nhưng nó gọi hành vi không xác địnhdấu phẩy ở đây là dấu phân cách .


Đối với những người chưa quen với hành vi không xác định sẽ có lợi khi đọc Điều mà mỗi lập trình viên C nên biết về hành vi không xác định để hiểu khái niệm và nhiều biến thể khác của hành vi không xác định trong C.

Bài đăng này: Hành vi không xác định, không xác định và thực hiện cũng có liên quan.


Trình tự này 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));xuất hiện để đưa ra hành vi ổn định (đánh giá đối số từ phải sang trái trong gcc v7.3.0; kết quả "a = 110 b = 40 c = 60"). Có phải vì các bài tập được coi là 'báo cáo đầy đủ' và do đó giới thiệu một điểm trình tự? Không nên dẫn đến việc đánh giá đối số / tuyên bố từ trái sang phải? Hoặc, nó chỉ là biểu hiện của hành vi không xác định?
kavadias

@kavadias Câu lệnh printf liên quan đến hành vi không xác định, với cùng lý do đã giải thích ở trên. Bạn đang viết bctrong các đối số thứ 3 & 4 tương ứng và đọc trong đối số thứ hai. Nhưng không có trình tự giữa các biểu thức này (các đối số thứ 2, 3 và 4). gcc / clang có một tùy chọn -Wsequence-pointcũng có thể giúp tìm thấy những thứ này.
PP

23

Mặc dù không có khả năng bất kỳ trình biên dịch và bộ xử lý nào thực sự làm như vậy, nhưng theo tiêu chuẩn C, trình biên dịch sẽ thực hiện "i ++" theo trình tự:

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

Mặc dù tôi không nghĩ rằng bất kỳ bộ xử lý nào hỗ trợ phần cứng để cho phép thực hiện một cách hiệu quả như vậy, nhưng người ta có thể dễ dàng tưởng tượng các tình huống trong đó hành vi đó sẽ làm cho mã đa luồng dễ dàng hơn (ví dụ: nó sẽ đảm bảo rằng nếu hai luồng cố gắng thực hiện ở trên trình tự đồng thời, isẽ tăng lên hai lần) và không hoàn toàn không thể tin được rằng một số bộ xử lý trong tương lai có thể cung cấp một tính năng tương tự.

Nếu trình biên dịch được viết i++như đã chỉ ra ở trên (hợp pháp theo tiêu chuẩn) và được xen kẽ các hướng dẫn ở trên trong suốt quá trình đánh giá biểu thức tổng thể (cũng hợp pháp), và nếu nó không xảy ra để ý rằng một trong những hướng dẫn khác đã xảy ra để truy cập i, trình biên dịch có thể tạo ra một chuỗi các lệnh sẽ bế tắc. Để chắc chắn, một trình biên dịch gần như chắc chắn sẽ phát hiện các vấn đề trong trường hợp cùng một biến iđược sử dụng trong cả hai nơi, nhưng nếu một thói quen chấp nhận tham chiếu đến hai con trỏ pq, và việc sử dụng (*p)(*q)trong biểu thức trên (thay vì sử dụngihai lần) trình biên dịch sẽ không được yêu cầu nhận ra hoặc tránh bế tắc sẽ xảy ra nếu địa chỉ của cùng một đối tượng được truyền cho cả hai pq.


16

Trong khi các cú pháp của biểu thức thích a = a++hoặc a++ + a++là hợp pháp, các hành vi của các cấu trúc được undefined vì một trách nhiệm trong tiêu chuẩn C không vâng lời. C99 6.5p2 :

  1. Giữa điểm thứ tự trước và điểm tiếp theo, một đối tượng sẽ được sửa đổi giá trị lưu trữ của nó nhiều nhất một lần bằng cách đánh giá biểu thức. [72] Hơn nữa, giá trị trước chỉ được đọc để xác định giá trị được lưu trữ [73]

Với chú thích 73 làm rõ thêm rằng

  1. Đoạn này biểu hiện các biểu thức câu không xác định như

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

    trong khi cho phép

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

Các điểm trình tự khác nhau được liệt kê trong Phụ lục C của C11 (và C99 ):

  1. Sau đây là các điểm trình tự được mô tả trong 5.1.2.3:

    • Giữa các đánh giá của người chỉ định chức năng và các đối số thực tế trong một cuộc gọi chức năng và cuộc gọi thực tế. (6.5.2.2).
    • Giữa các đánh giá của toán hạng thứ nhất và thứ hai của các toán tử sau: logic AND && (6.5.13); logic HOẶC | | (6,5,14); dấu phẩy, (6.5.17).
    • Giữa các đánh giá toán hạng đầu tiên của điều kiện? : toán tử và bất kỳ toán hạng thứ hai và thứ ba nào được ước tính (6.5.15).
    • Sự kết thúc của một người khai báo đầy đủ: người khai báo (6.7.6);
    • Giữa việc đánh giá một biểu thức đầy đủ và biểu thức đầy đủ tiếp theo được đánh giá. Sau đây là các biểu thức đầy đủ: một trình khởi tạo không phải là một phần của nghĩa đen (6.7.9); biểu thức trong một tuyên bố biểu thức (6.8.3); biểu thức kiểm soát của một tuyên bố lựa chọn (nếu hoặc chuyển đổi) (6.8.4); biểu thức kiểm soát của câu lệnh while hoặc do (6.8.5); mỗi biểu thức (tùy chọn) của câu lệnh for (6.8.5.3); biểu thức (tùy chọn) trong câu lệnh return (6.8.6.4).
    • Ngay trước khi một chức năng thư viện trả về (7.1.4).
    • Sau các hành động liên quan đến từng chỉ định chuyển đổi chức năng đầu vào / đầu ra được định dạng (7.21.6, 7.29.2).
    • Ngay lập tức trước và ngay sau mỗi cuộc gọi đến một chức năng so sánh, và giữa bất kỳ cuộc gọi nào đến chức năng so sánh và bất kỳ chuyển động nào của các đối tượng được truyền dưới dạng đối số cho cuộc gọi đó (7.22.5).

Từ ngữ của cùng một đoạn trong C11 là:

  1. Nếu một hiệu ứng phụ trên một đối tượng vô hướng không có kết quả tương ứng với một hiệu ứng phụ khác trên cùng một đối tượng vô hướng hoặc một tính toán giá trị sử dụng giá trị của cùng một đối tượng vô hướng, thì hành vi không được xác định. Nếu có nhiều thứ tự cho phép của các biểu thức con của biểu thức, hành vi không được xác định nếu tác dụng phụ không được xử lý như vậy xảy ra trong bất kỳ thứ tự nào.84)

Bạn có thể phát hiện lỗi như vậy trong một chương trình bằng cách ví dụ sử dụng một phiên bản mới của GCC với -Wall-Werror, và sau đó GCC sẽ hoàn toàn từ chối để biên dịch chương trình của bạn. Sau đây là đầu ra của gcc (Ubuntu 6.2.0-5ubfox12) 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

Phần quan trọng là để biết điểm chuỗi là - và điểm chuỗi gì và không phải là điểm nào . Ví dụ: toán tử dấu phẩy là một điểm tuần tự, vì vậy

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

được xác định rõ và sẽ tăng thêm imột, mang lại giá trị cũ, loại bỏ giá trị đó; sau đó tại toán tử dấu phẩy, giải quyết các tác dụng phụ; và sau đó tăng lên imột, và giá trị kết quả trở thành giá trị của biểu thức - tức là đây chỉ là một cách viết j = (i += 2)có thể được viết lại mà lại là một cách viết "thông minh"

i += 2;
j = i;

Tuy nhiên, ,danh sách đối số trong hàm không phải là toán tử dấu phẩy và không có điểm thứ tự giữa các đánh giá của các đối số riêng biệt; thay vào đó các đánh giá của họ không có kết quả liên quan đến nhau; vậy hàm gọi

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

hành vi không xác địnhkhông có điểm thứ tự giữa các đánh giá i++++itrong các đối số hàm và giá trị của ido đó được sửa đổi hai lần, bởi cả hai i++++i, giữa điểm thứ tự trước và điểm tiếp theo.


14

Tiêu chuẩn C nói rằng một biến chỉ nên được gán tối đa một lần giữa hai điểm chuỗi. Ví dụ, dấu chấm phẩy là một điểm thứ tự.
Vì vậy, mọi tuyên bố của mẫu:

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

và như vậy vi phạm quy tắc đó. Tiêu chuẩn cũng nói rằng hành vi là không xác định và không xác định. Một số trình biên dịch phát hiện những điều này và tạo ra một số kết quả nhưng điều này không theo tiêu chuẩn.

Tuy nhiên, hai biến khác nhau có thể được tăng lên giữa hai điểm chuỗi.

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

Trên đây là một thực hành mã hóa phổ biến trong khi sao chép / phân tích chuỗi.


Tất nhiên, nó không áp dụng cho các biến khác nhau trong một biểu thức. Nó sẽ là một thất bại hoàn toàn về thiết kế nếu nó đã làm! Tất cả những gì bạn cần trong ví dụ thứ 2 là cho cả hai được tăng lên giữa kết thúc câu lệnh và bắt đầu tiếp theo, và điều đó được đảm bảo, chính xác là do khái niệm điểm chuỗi ở trung tâm của tất cả điều này.
gạch dưới

11

Trong /programming/29505280/incrementing-array-index-in-c ai đó đã hỏi về một tuyên bố như:

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);

in 7 ... OP dự kiến ​​sẽ in 6.

Số ++igia không được đảm bảo hoàn thành trước khi tính toán phần còn lại. Trong thực tế, các trình biên dịch khác nhau sẽ nhận được kết quả khác nhau ở đây. Trong ví dụ mà bạn cung cấp, 2 đầu tiên ++ithực hiện, sau đó các giá trị của k[]đã được đọc, sau đó cuối cùng ++irồi k[].

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

Trình biên dịch hiện đại sẽ tối ưu hóa điều này rất tốt. Trong thực tế, có thể tốt hơn mã bạn đã viết ban đầu (giả sử nó đã hoạt động theo cách bạn đã hy vọng).


5

Một lời giải thích tốt về những gì xảy ra trong loại tính toán này được cung cấp trong tài liệu n1188 từ trang web ISO W14 .

Tôi giải thích các ý tưởng.

Quy tắc chính từ tiêu chuẩn ISO 9899 áp dụng trong tình huống này là 6,5p2.

Giữa điểm thứ tự trước và điểm tiếp theo, một đối tượng sẽ được sửa đổi giá trị lưu trữ của nó nhiều nhất một lần bằng cách đánh giá biểu thức. Hơn nữa, giá trị trước chỉ được đọc để xác định giá trị sẽ được lưu trữ.

Các điểm thứ tự trong một biểu thức như i=i++trước i=và sau i++.

Trong bài báo mà tôi đã trích dẫn ở trên, nó được giải thích rằng bạn có thể tìm ra chương trình được hình thành bởi các hộp nhỏ, mỗi hộp chứa các hướng dẫn giữa 2 điểm liên tiếp. Các điểm thứ tự được xác định trong phụ lục C của tiêu chuẩn, trong trường hợp i=i++có 2 điểm thứ tự phân định một biểu thức đầy đủ. Một biểu thức như vậy là tương đương về mặt cú pháp với một mục expression-statementtrong dạng Backus-Naur của ngữ pháp (một ngữ pháp được cung cấp trong phụ lục A của Tiêu chuẩn).

Vì vậy, thứ tự của các hướng dẫn bên trong một hộp không có thứ tự rõ ràng.

i=i++

có thể được hiểu là

tmp = i
i=i+1
i = tmp

hoặc như

tmp = i
i = tmp
i=i+1

bởi vì cả hai hình thức này để giải thích mã i=i++đều hợp lệ và vì cả hai đều tạo ra các câu trả lời khác nhau, nên hành vi không được xác định.

Vì vậy, một điểm thứ tự có thể được nhìn thấy ở đầu và cuối của mỗi hộp tạo ra chương trình [các hộp là các đơn vị nguyên tử trong C] và bên trong một hộp, thứ tự các hướng dẫn không được xác định trong mọi trường hợp. Thay đổi thứ tự đó đôi khi có thể thay đổi kết quả.

BIÊN TẬP:

Nguồn tốt khác để giải thích sự mơ hồ như vậy là các mục từ trang web c-faq (cũng được xuất bản dưới dạng một cuốn sách ), cụ thể là ở đâyở đâyở đây .


Làm thế nào câu trả lời này thêm mới vào câu trả lời hiện có? Ngoài ra các giải thích cho i=i++rất giống với câu trả lời này .
haccks

@haccks Tôi không đọc các câu trả lời khác. Tôi muốn giải thích bằng ngôn ngữ của mình những gì tôi học được từ tài liệu được đề cập từ trang web chính thức của ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar

5

Câu hỏi của bạn có lẽ là không, "Tại sao các cấu trúc này không xác định hành vi trong C?". Câu hỏi của bạn có lẽ là "Tại sao mã này (sử dụng ++) không mang lại cho tôi giá trị mà tôi mong đợi?", Và ai đó đã đánh dấu câu hỏi của bạn là trùng lặp và gửi cho bạn ở đây.

Câu trả lời này cố gắng trả lời câu hỏi đó: tại sao mã của bạn không cung cấp cho bạn câu trả lời bạn mong đợi và làm thế nào bạn có thể học cách nhận biết (và tránh) các biểu thức sẽ không hoạt động như mong đợi.

Tôi giả sử bây giờ bạn đã nghe định nghĩa cơ bản về C ++--toán tử và cách biểu mẫu tiền tố ++xkhác với biểu mẫu hậu tố x++. Nhưng các nhà khai thác này rất khó để suy nghĩ, vì vậy để chắc chắn rằng bạn hiểu, có lẽ bạn đã viết một chương trình thử nghiệm nhỏ bé liên quan đến một cái gì đó như

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

Nhưng, thật ngạc nhiên, chương trình này không giúp bạn hiểu - nó đã in một số đầu ra kỳ lạ, bất ngờ, không thể giải thích được, cho thấy rằng có thể ++làm một cái gì đó hoàn toàn khác, không phải là những gì bạn nghĩ.

Hoặc, có lẽ bạn đang nhìn vào một biểu hiện khó hiểu như

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

Có lẽ ai đó đã cho bạn mã đó như một câu đố. Mã này cũng không có ý nghĩa gì, đặc biệt nếu bạn chạy nó - và nếu bạn biên dịch và chạy nó dưới hai trình biên dịch khác nhau, bạn có thể nhận được hai câu trả lời khác nhau! Có chuyện gì thế? Câu trả lời nào đúng? (Và câu trả lời là cả hai đều như vậy, hoặc cả hai đều không.)

Như bạn đã nghe bây giờ, tất cả các biểu thức này đều không được xác định , điều đó có nghĩa là ngôn ngữ C không đảm bảo về những gì chúng sẽ làm. Đây là một kết quả kỳ lạ và đáng ngạc nhiên, bởi vì bạn có thể nghĩ rằng bất kỳ chương trình nào bạn có thể viết, miễn là nó được biên dịch và chạy, sẽ tạo ra một đầu ra duy nhất, được xác định rõ. Nhưng trong trường hợp hành vi không xác định, điều đó không phải vậy.

Điều gì làm cho một biểu thức không xác định? Là các biểu thức liên quan ++--luôn luôn không xác định? Tất nhiên là không: đây là những toán tử hữu ích và nếu bạn sử dụng chúng đúng cách, chúng hoàn toàn được xác định rõ.

Đối với các biểu thức chúng ta đang nói về điều khiến chúng không được xác định là khi có quá nhiều thứ xảy ra cùng một lúc, khi chúng ta không chắc chắn mọi thứ sẽ xảy ra theo thứ tự nào, nhưng khi thứ tự quan trọng đến kết quả chúng ta nhận được.

Hãy quay trở lại hai ví dụ tôi đã sử dụng trong câu trả lời này. Khi tôi viết

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

câu hỏi là, trước khi gọi printf, trình biên dịch có tính toán giá trị xđầu tiên, hoặc x++, hoặc có thể ++xkhông? Nhưng hóa ra chúng ta không biết . Không có quy tắc nào trong C nói rằng các đối số của hàm được đánh giá từ trái sang phải hoặc từ phải sang trái hoặc theo một số thứ tự khác. Vì vậy, chúng tôi không thể nói liệu trình biên dịch sẽ làm xtrước, sau ++xđó x++, hoặc x++sau ++xđó x, hoặc một số thứ tự khác. Nhưng thứ tự rõ ràng có vấn đề, bởi vì tùy thuộc vào trình tự sử dụng trình biên dịch, chúng tôi rõ ràng sẽ nhận được các kết quả khác nhau được in theo printf.

Còn biểu hiện điên rồ này thì sao?

x = x++ + ++x;

Vấn đề với biểu thức này là nó chứa ba lần thử khác nhau để sửa đổi giá trị của x: (1) x++phần cố gắng thêm 1 vào x, lưu giá trị mới vào xvà trả về giá trị cũ của x; (2) ++xphần cố gắng thêm 1 vào x, lưu giá trị mới vào xvà trả về giá trị mới của x; và (3) x =phần cố gắng gán tổng của hai phần còn lại cho x. Bài tập nào trong số ba bài tập đã cố gắng sẽ "thắng"? Giá trị nào trong ba giá trị sẽ thực sự được gán cho x? Một lần nữa, và có lẽ đáng ngạc nhiên, không có quy tắc nào trong C để nói với chúng tôi.

Bạn có thể tưởng tượng rằng sự ưu tiên hoặc sự kết hợp hoặc đánh giá từ trái sang phải cho bạn biết thứ gì xảy ra theo thứ tự, nhưng chúng thì không. Bạn có thể không tin tôi, nhưng xin hãy tin tôi và tôi sẽ nói lại: ưu tiên và kết hợp không xác định mọi khía cạnh của thứ tự đánh giá của một biểu thức trong C. Đặc biệt, nếu trong một biểu thức có nhiều biểu thức những điểm khác nhau trong đó chúng tôi cố gắng gán một giá trị mới cho một cái gì đó như x, ưu tiên và kết hợp không cho chúng tôi biết những nỗ lực nào xảy ra trước, hoặc cuối, hoặc bất cứ điều gì.


Vì vậy, với tất cả các nền tảng và giới thiệu ngoài lề, nếu bạn muốn đảm bảo rằng tất cả các chương trình của bạn được xác định rõ, bạn có thể viết những biểu thức nào, và những gì bạn không thể viết?

Những biểu thức này đều ổn:

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++;

Những biểu thức này đều không xác định:

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

Và câu hỏi cuối cùng là, làm thế nào bạn có thể biết biểu thức nào được xác định rõ và biểu thức nào không được xác định?

Như tôi đã nói trước đó, các biểu thức không xác định là những biểu thức có quá nhiều thứ xảy ra cùng một lúc, nơi bạn không thể chắc chắn thứ gì xảy ra theo thứ tự và nơi thứ tự quan trọng:

  1. Nếu có một biến được sửa đổi (gán cho) ở hai hoặc nhiều nơi khác nhau, làm thế nào để bạn biết sửa đổi nào xảy ra trước?
  2. Nếu có một biến được sửa đổi ở một nơi và sử dụng giá trị của nó ở một nơi khác, làm thế nào để bạn biết liệu nó sử dụng giá trị cũ hay giá trị mới?

Như một ví dụ về # 1, trong biểu thức

x = x++ + ++x;

có ba lần sửa đổi `x.

Như một ví dụ về # 2, trong biểu thức

y = x + x++;

cả hai chúng tôi sử dụng giá trị của xvà sửa đổi nó.

Vì vậy, đó là câu trả lời: đảm bảo rằng trong bất kỳ biểu thức nào bạn viết, mỗi biến được sửa đổi nhiều nhất một lần và nếu một biến được sửa đổi, bạn cũng không cố gắng sử dụng giá trị của biến đó ở nơi khác.


3

Lý do là chương trình đang chạy hành vi không xác định. Vấn đề nằm ở thứ tự đánh giá, vì không có điểm trình tự được yêu cầu theo tiêu chuẩn C ++ 98 (không có thao tác nào được sắp xếp trước hoặc sau một hoạt động khác theo thuật ngữ C ++ 11).

Tuy nhiên, nếu bạn dính vào một trình biên dịch, bạn sẽ thấy hành vi đó liên tục, miễn là bạn không thêm các lệnh gọi hàm hoặc con trỏ, điều này sẽ khiến hành vi trở nên lộn xộn hơn.

  • Vì vậy, trước tiên GCC: Sử dụng Nuwen MinGW 15 GCC 7.1, bạn sẽ nhận được:

    #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

    }

GCC hoạt động như thế nào? nó đánh giá các biểu thức con theo thứ tự từ trái sang phải cho phía bên phải (RHS), sau đó gán giá trị cho phía bên trái (LHS). Đây chính xác là cách Java và C # hành xử và xác định các tiêu chuẩn của chúng. (Có, phần mềm tương đương trong Java và C # có các hành vi được xác định). Nó đánh giá từng biểu thức phụ từng cái một trong Tuyên bố RHS theo thứ tự từ trái sang phải; đối với mỗi biểu thức con: ++ c (tăng trước) được ước tính trước sau đó giá trị c được sử dụng cho thao tác, sau đó tăng bài c ++).

theo GCC C ++: Toán tử

Trong GCC C ++, quyền ưu tiên của các toán tử kiểm soát thứ tự các toán tử riêng lẻ được đánh giá

mã tương đương trong hành vi được xác định C ++ như GCC hiểu:

#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
}

Sau đó, chúng tôi đi đến Visual Studio . Visual Studio 2015, bạn nhận được:

#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 
}

Làm thế nào để phòng thu trực quan hoạt động, nó có một cách tiếp cận khác, nó đánh giá tất cả các biểu thức tăng trước trong lần đầu tiên, sau đó sử dụng các giá trị biến trong các hoạt động ở lượt thứ hai, gán từ RHS cho LHS ở lượt thứ ba, rồi cuối cùng nó đánh giá tất cả biểu thức tăng sau trong một lần vượt qua.

Vì vậy, tương đương trong hành vi được xác định C ++ như Visual C ++ hiểu:

#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 
}

như tài liệu của Visual Studio nêu tại Ưu tiên và Thứ tự Đánh giá :

Khi một số toán tử xuất hiện cùng nhau, chúng có quyền ưu tiên như nhau và được đánh giá theo tính kết hợp của chúng. Các toán tử trong bảng được mô tả trong các phần bắt đầu bằng Toán tử Postfix.


1
Tôi đã chỉnh sửa câu hỏi để thêm UB để đánh giá các đối số hàm, vì câu hỏi này thường được sử dụng làm bản sao cho câu hỏi đó. (Ví dụ cuối cùng)
Antti Haapala

1
Ngoài ra câu hỏi là về c bây giờ, không phải C ++
Antti Haapala
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.