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 ++
và --
toán tử và cách biểu mẫu tiền tố ++x
khá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 ++
và --
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ể ++x
khô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 x
trướ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 x
và trả về giá trị cũ của x
; (2) ++x
phần cố gắng thêm 1 vào x, lưu giá trị mới vào x
và 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:
- 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?
- 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 x
và 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.