Tại sao các tuyên bố không có hiệu lực được coi là hợp pháp trong C?


13

Xin lỗi nếu câu hỏi này là ngây thơ. Hãy xem xét chương trình sau:

#include <stdio.h>

int main() {
  int i = 1;
  i = i + 2;
  5;
  i;
  printf("i: %d\n", i);
}

Trong ví dụ trên, các câu lệnh 5;i;dường như hoàn toàn thừa, tuy nhiên mã sẽ biên dịch mà không có cảnh báo hoặc lỗi theo mặc định (tuy nhiên, gcc không đưa ra warning: statement with no effect [-Wunused-value]cảnh báo khi chạy với -Wall). Chúng không có tác dụng đối với phần còn lại của chương trình, vậy tại sao chúng được coi là tuyên bố hợp lệ ngay từ đầu? Có trình biên dịch đơn giản bỏ qua chúng? Có bất kỳ lợi ích để cho phép tuyên bố như vậy?


5
Những lợi ích của việc cấm tuyên bố như vậy là gì?
Vịt Mooing

2
Bất kỳ biểu thức có thể là một tuyên bố bằng cách đặt ;sau nó. Nó sẽ làm phức tạp ngôn ngữ để thêm nhiều quy tắc về thời điểm biểu thức không thể là câu lệnh
MM

3
Bạn có muốn mã của bạn không biên dịch được vì bạn bỏ qua giá trị trả về của printf()? Báo cáo kết quả 5;về cơ bản nói "làm bất cứ điều gì 5không (không có gì) và bỏ qua kết quả. Tuyên bố của bạn printf(...)là 'làm bất cứ điều gì printf(...)không và bỏ qua những kết quả (giá trị trả về từ printf())'. C đối xử với những người như vậy. Điều này cũng cho phép mã như (void) i;nơi ilà một tham số cho một chức năng mà bạn sử dụng voidđể đánh dấu nó là không được sử dụng một cách có chủ ý.
Andrew Henle

1
@AndrewHenle: Điều đó không hoàn toàn giống nhau, vì việc gọi điện printf()có ảnh hưởng, ngay cả khi bạn bỏ qua giá trị mà cuối cùng nó trả về. Ngược lại 5;không có tác dụng gì cả.
Nate Eldredge

1
Bởi vì Dennis Ritchie, và anh ấy không có mặt để nói với chúng tôi.
dùng207421

Câu trả lời:


10

Một lợi ích cho phép các tuyên bố như vậy là từ mã được tạo bởi macro hoặc các chương trình khác, thay vì được viết bởi con người.

Ví dụ, hãy tưởng tượng một hàm int do_stuff(void)được cho là trả về 0 khi thành công hoặc -1 khi thất bại. Nó có thể là hỗ trợ cho "công cụ" là tùy chọn và vì vậy bạn có thể có một tệp tiêu đề

#if STUFF_SUPPORTED
#define do_stuff() really_do_stuff()
#else
#define do_stuff() (-1)
#endif

Bây giờ hãy tưởng tượng một số mã muốn làm công cụ nếu có thể, nhưng có thể hoặc không thực sự quan tâm liệu nó thành công hay thất bại:

void func1(void) {
    if (do_stuff() == -1) {
        printf("stuff did not work\n");
    }
}

void func2(void) {
    do_stuff(); // don't care if it works or not
    more_stuff();
}

Khi bằng STUFF_SUPPORTED0, bộ tiền xử lý sẽ mở rộng cuộc gọi func2thành một câu lệnh chỉ đọc

    (-1);

và do đó, trình biên dịch sẽ chỉ thấy loại câu lệnh "thừa thãi" dường như làm phiền bạn. Tuy nhiên, những gì người khác có thể làm gì? Nếu bạn #define do_stuff() // nothing, sau đó mã trong func1sẽ phá vỡ. (Và bạn vẫn sẽ có một câu lệnh trống trong func2đó chỉ đọc ;, có lẽ còn thừa hơn nữa.) Mặt khác, nếu bạn phải thực sự xác định do_stuff()hàm trả về -1, bạn có thể phải trả chi phí cho một cuộc gọi hàm Vì không có lý do chính đáng.


Một phiên bản cổ điển hơn (hoặc ý tôi là phiên bản phổ biến) của no-op là ((void)0).
Jonathan Leffler

Một ví dụ tốt về điều này là assert.
Neil

3

Báo cáo đơn giản trong C được chấm dứt bằng dấu chấm phẩy.

Báo cáo đơn giản trong C là biểu thức. Một biểu thức là sự kết hợp của các biến, hằng và toán tử. Mỗi biểu thức dẫn đến một số giá trị của một loại nhất định có thể được gán cho một biến.

Đã nói rằng một số "trình biên dịch thông minh" có thể loại bỏ 5; và tôi; các câu lệnh.


Tôi không thể tưởng tượng bất kỳ trình biên dịch nào sẽ làm bất cứ điều gì với những câu lệnh đó ngoài việc loại bỏ chúng. Nó có thể làm gì khác với họ?
Jeremy Friesner

@JeremyFriesner: Một trình biên dịch rất đơn giản, không tối ưu hóa rất có thể tạo mã để tính giá trị và đưa kết quả vào một thanh ghi (từ đó nó sẽ bị bỏ qua).
Nate Eldredge

Tiêu chuẩn C không có thuật ngữ "tuyên bố đơn giản". Một câu lệnh biểu thức bao gồm một biểu thức (tùy chọn) theo sau là dấu chấm phẩy. Không phải mọi biểu thức đều dẫn đến một giá trị; một biểu thức của loại voidkhông có giá trị.
Keith Thompson

2

Tuyên bố không có hiệu lực được cho phép bởi vì sẽ khó cấm chúng hơn là cho phép chúng. Điều này phù hợp hơn khi C được thiết kế lần đầu tiên và trình biên dịch nhỏ hơn và đơn giản hơn.

Một tuyên bố biểu thức bao gồm một biểu thức theo sau dấu chấm phẩy. Hành vi của nó là đánh giá biểu thức và loại bỏ kết quả (nếu có). Thông thường mục đích là việc đánh giá biểu thức có tác dụng phụ, nhưng không phải lúc nào cũng dễ dàng hoặc thậm chí có thể xác định liệu một biểu thức đã cho có tác dụng phụ hay không.

Ví dụ, một lệnh gọi hàm là một biểu thức, vì vậy một lệnh gọi hàm theo sau dấu chấm phẩy là một câu lệnh. Liệu tuyên bố này có bất kỳ tác dụng phụ?

some_function();

Không thể nói mà không thấy việc thực hiện some_function.

Còn cái này thì sao?

obj;

Có lẽ là không - nhưng nếu objđược định nghĩa là volatile, thì nó làm.

Cho phép bất kỳ biểu thức nào được thực hiện thành một câu lệnh biểu thức bằng cách thêm dấu chấm phẩy làm cho định nghĩa ngôn ngữ đơn giản hơn. Yêu cầu biểu thức để có tác dụng phụ sẽ thêm độ phức tạp cho định nghĩa ngôn ngữ và trình biên dịch. C được xây dựng trên một bộ quy tắc nhất quán (các hàm gọi là biểu thức, phép gán là biểu thức, biểu thức theo sau dấu chấm phẩy là câu lệnh) và cho phép lập trình viên làm những gì họ muốn mà không ngăn họ làm những việc có thể hoặc không có ý nghĩa.


2

Các câu lệnh bạn liệt kê không có tác dụng là các ví dụ về câu lệnh biểu thức , có cú pháp được đưa ra trong phần 6.8.3p1 của tiêu chuẩn C như sau:

 biểu thức-tuyên bố :
    biểu thức opt  ;

Tất cả phần 6.5 dành riêng cho định nghĩa của biểu thức, nhưng nói một cách lỏng lẻo một biểu thức bao gồm các hằng số và định danh được liên kết với các toán tử. Đáng chú ý, một biểu thức có thể có hoặc không chứa toán tử gán và nó có thể có hoặc không chứa lệnh gọi hàm.

Vì vậy, bất kỳ biểu thức nào theo sau dấu chấm phẩy đều đủ điều kiện là một tuyên bố biểu thức. Trong thực tế, mỗi dòng trong mã của bạn là một ví dụ về câu lệnh biểu thức:

i = i + 2;
5;
i;
printf("i: %d\n", i);

Một số toán tử chứa các tác dụng phụ như tập hợp các toán tử gán và toán tử tăng / giảm trước / sau và toán tử gọi hàm () có thể có tác dụng phụ tùy thuộc vào chức năng được đề cập. Tuy nhiên, không có yêu cầu nào là một trong các toán tử phải có tác dụng phụ.

Đây là một ví dụ khác:

atoi("1");

Đây là gọi một hàm và loại bỏ kết quả, giống như cuộc gọi printftrong ví dụ của bạn nhưng không giống như printflệnh gọi chính nó không có tác dụng phụ.


1

Đôi khi một tuyên bố như vậy rất tiện dụng:

int foo(int x, int y, int z)
{
    (void)y;   //prevents warning
    (void)z;

    return x*x;
}

Hoặc khi hướng dẫn tham khảo bảo chúng ta chỉ cần đọc các thanh ghi để lưu trữ một cái gì đó - ví dụ để xóa hoặc đặt một số cờ (tình huống rất phổ biến trong thế giới uC)

#define SREG   ((volatile uint32_t *)0x4000000)
#define DREG   ((volatile uint32_t *)0x4004000)

void readSREG(void)
{
    *SREG;   //we read it here
    *DREG;   // and here
}

https://godbolt.org/z/6wjh_5


Khi *SREGdễ bay hơi, *SREG;không có hiệu lực trong mô hình được chỉ định bởi tiêu chuẩn C. Tiêu chuẩn C chỉ định nó có tác dụng phụ có thể quan sát được.
Eric Postpischil

@EricPostpischil - không, nó không có tác dụng quan sát được , nhưng nếu có tác dụng. Không có đối tượng nhìn thấy C đã thay đổi.
P__J__

C 2018 5.1.2.3 6 xác định hành vi có thể quan sát được của chương trình để bao gồm rằng Truy cập vào các đối tượng dễ bay hơi được đánh giá đúng theo các quy tắc của máy trừu tượng. Không có câu hỏi về giải thích hoặc khấu trừ; đây là định nghĩa của hành vi quan sát được.
Eric Postpischil
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.