Hàm C này sẽ luôn trả về false, nhưng nó không


317

Tôi đã vấp phải một câu hỏi thú vị trong một diễn đàn từ lâu và tôi muốn biết câu trả lời.

Xét hàm C sau:

F1.c

#include <stdbool.h>

bool f1()
{
    int var1 = 1000;
    int var2 = 2000;
    int var3 = var1 + var2;
    return (var3 == 0) ? true : false;
}

Điều này sẽ luôn luôn trở lại falsekể từ đó var3 == 3000. Các mainchức năng trông như thế này:

C chính

#include <stdio.h>
#include <stdbool.h>

int main()
{
    printf( f1() == true ? "true\n" : "false\n");
    if( f1() )
    {
        printf("executed\n");
    }
    return 0;
}

f1()luôn luôn quay trở lại false, người ta sẽ mong đợi chương trình chỉ in một sai lên màn hình. Nhưng sau khi biên dịch và chạy nó, thực thi cũng được hiển thị:

$ gcc main.c f1.c -o test
$ ./test
false
executed

Tại sao vậy? Liệu mã này có một số loại hành vi không xác định?

Lưu ý: Tôi đã biên dịch nó với gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2.


9
Những người khác đã đề cập bạn cần một nguyên mẫu vì các chức năng của bạn nằm trong các tệp riêng biệt. Nhưng ngay cả khi bạn đã sao chép f1()vào cùng một tệp như vậy main(), bạn sẽ nhận được một số điều kỳ lạ: Mặc dù nó đúng trong C ++ để sử dụng ()cho danh sách tham số trống, nhưng trong C được sử dụng cho một hàm với danh sách tham số chưa được xác định ( về cơ bản, nó mong đợi một danh sách tham số kiểu K & R sau )). Để được chính xác C, bạn nên thay đổi mã của bạn thành bool f1(void).
uliwitness

1
main()thể được đơn giản hóa int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }- điều này sẽ cho thấy sự khác biệt tốt hơn.
Palec

@uliwitness Điều gì về K & R lần 1. (1978) khi không có void?
Ho1

@uliwitness Không có truefalsetrong K & R 1st ed., Vì vậy không có vấn đề nào như vậy cả. Đó chỉ là 0 và khác không đối với đúng và sai. Phải không? Tôi không biết liệu nguyên mẫu có sẵn tại thời điểm đó.
Ho1

1
K & R 1st Edn đi trước các nguyên mẫu (và tiêu chuẩn C) trong hơn một thập kỷ (1978 cho cuốn sách so với 1989 cho tiêu chuẩn) - thực sự, C ++ (C with Classes) vẫn còn trong tương lai khi K & R1 được xuất bản. Ngoài ra, trước C99, không có _Boolloại và không có <stdbool.h>tiêu đề.
Jonathan Leffler

Câu trả lời:


396

Như đã lưu ý trong các câu trả lời khác, vấn đề là bạn sử dụng gccmà không có tùy chọn trình biên dịch nào được đặt. Nếu bạn làm điều này, nó mặc định là "gnu90", đây là một triển khai không chuẩn của tiêu chuẩn C90 cũ, đã rút từ năm 1990.

Trong tiêu chuẩn C90 cũ, có một lỗ hổng lớn trong ngôn ngữ C: nếu bạn không khai báo nguyên mẫu trước khi sử dụng hàm, nó sẽ mặc định là int func ()(trong đó ( )có nghĩa là "chấp nhận bất kỳ tham số nào"). Điều này thay đổi quy ước gọi của hàm func, nhưng nó không thay đổi định nghĩa hàm thực tế. Vì kích thước của boolintkhác nhau, mã của bạn gọi hành vi không xác định khi hàm được gọi.

Hành vi vô nghĩa nguy hiểm này đã được sửa chữa vào năm 1999, với việc phát hành tiêu chuẩn C99. Khai báo hàm ẩn đã bị cấm.

Thật không may, GCC lên đến phiên bản 5.xx vẫn sử dụng chuẩn C cũ theo mặc định. Có lẽ không có lý do nào khiến bạn muốn biên dịch mã của mình thành bất cứ thứ gì ngoài tiêu chuẩn C. Vì vậy, bạn phải nói rõ với GCC rằng nó nên biên dịch mã của bạn thành mã C hiện đại, thay vì một crap GNU không chuẩn hơn 25 tuổi .

Khắc phục sự cố bằng cách luôn biên dịch chương trình của bạn dưới dạng:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 nói với nó để thực hiện một nỗ lực nửa vời để biên dịch theo tiêu chuẩn C (hiện tại) (được gọi một cách không chính thức là C11).
  • -pedantic-errors nói với nó toàn tâm toàn ý làm những điều trên và đưa ra lỗi trình biên dịch khi bạn viết mã không đúng vi phạm tiêu chuẩn C.
  • -Wall có nghĩa là cho tôi một số cảnh báo bổ sung có thể tốt để có.
  • -Wextra có nghĩa là cho tôi một số cảnh báo bổ sung khác có thể tốt để có.

19
Câu trả lời này hoàn toàn chính xác, nhưng đối với các chương trình phức tạp -std=gnu11hơn có khả năng hoạt động như mong đợi hơn -std=c11, do bất kỳ hoặc tất cả: cần chức năng thư viện ngoài C11 (POSIX, X / Open, v.v.) có sẵn trong "gnu" chế độ mở rộng nhưng bị triệt tiêu trong chế độ tuân thủ nghiêm ngặt; lỗi trong các tiêu đề hệ thống được ẩn trong các chế độ mở rộng, ví dụ giả sử có sẵn các typedefs không đạt tiêu chuẩn; vô tình sử dụng các vật liệu ba chiều (lỗi không chuẩn này bị vô hiệu hóa trong chế độ "gnu").
zwol

5
Vì những lý do tương tự, trong khi tôi thường khuyến khích sử dụng các mức cảnh báo cao, tôi không thể hỗ trợ việc sử dụng các chế độ cảnh báo là lỗi. -pedantic-errorsít rắc rối hơn -Werrornhưng cả hai đều có thể và khiến các chương trình không thể biên dịch trên các hệ điều hành không có trong thử nghiệm của tác giả ban đầu, ngay cả khi không có vấn đề thực sự.
zwol

7
@Lundin Ngược lại, vấn đề thứ hai tôi đã đề cập (lỗi trong các tiêu đề hệ thống bị phơi bày bởi các chế độ tuân thủ nghiêm ngặt) là rất phổ biến ; Tôi đã thực hiện thử nghiệm rộng rãi, có hệ thống và không có hệ điều hành nào được sử dụng rộng rãi mà không có ít nhất một lỗi như vậy (kể từ hai năm trước, dù sao đi nữa). Các chương trình C chỉ yêu cầu chức năng của C11, không có thêm bổ sung, cũng là ngoại lệ thay vì quy tắc theo kinh nghiệm của tôi.
zwol

6
@joop Nếu bạn sử dụng C bool/ tiêu chuẩn, _Boolbạn có thể viết mã C theo cách "C ++ - esque", trong đó bạn giả sử rằng tất cả các phép so sánh và toán tử logic trả về boolgiống như trong C ++, mặc dù chúng trả về intbằng C, vì lý do lịch sử . Điều này có lợi thế lớn là bạn có thể sử dụng các công cụ phân tích tĩnh để kiểm tra mức độ an toàn của tất cả các biểu thức như vậy và phơi bày tất cả các loại lỗi tại thời điểm biên dịch. Đó cũng là một cách để thể hiện ý định dưới dạng mã tự ghi. Và ít quan trọng hơn, nó cũng tiết kiệm một vài byte RAM.
Lundin

7
Lưu ý rằng hầu hết các công cụ mới trong C99 đến từ crap GNU 25 tuổi.
Shahbaz

141

Bạn không có một nguyên mẫu được khai báo f1()trong main.c, do đó, nó được định nghĩa ngầm định int f1(), nghĩa là nó là một hàm lấy một số lượng đối số không xác định và trả về một int.

Nếu intboolcó kích thước khác nhau, điều này sẽ dẫn đến hành vi không xác định . Ví dụ, trên máy của tôi, intlà 4 byte và boollà một byte. Vì hàm được định nghĩa là trả về bool, nên nó đặt một byte trên ngăn xếp khi nó trả về. Tuy nhiên, vì nó được khai báo hoàn toàn trở về inttừ main.c, nên hàm gọi sẽ cố đọc 4 byte từ ngăn xếp.

Các tùy chọn trình biên dịch mặc định trong gcc sẽ không cho bạn biết rằng nó đang làm điều này. Nhưng nếu bạn biên dịch -Wall -Wextra, bạn sẽ nhận được điều này:

main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’

Để sửa lỗi này, hãy thêm một khai báo cho f1trong main.c, trước main:

bool f1(void);

Lưu ý rằng danh sách đối số được đặt rõ ràng void, cho biết trình biên dịch hàm không có đối số, trái ngược với danh sách tham số trống có nghĩa là số lượng đối số không xác định. Định nghĩa f1trong F1.c cũng nên được thay đổi để phản ánh điều này.


2
Một cái gì đó tôi từng làm trong các dự án của mình (khi tôi vẫn sử dụng GCC) đã được thêm -Werror-implicit-function-declarationvào các tùy chọn của GCC, theo cách này, cái này không còn trôi qua nữa. Một lựa chọn thậm chí tốt hơn là -Werrorbiến tất cả các cảnh báo thành lỗi. Buộc bạn sửa tất cả các cảnh báo khi chúng xuất hiện.
uliwitness

2
Bạn cũng không nên sử dụng dấu ngoặc đơn trống vì làm như vậy là một tính năng lỗi thời. Có nghĩa là họ có thể cấm mã như vậy trong phiên bản tiếp theo của tiêu chuẩn C.
Lundin

1
@uliwitness Ah. Thông tin tốt cho những người đến từ C ++, người chỉ biết học hỏi về C.
SeldomNeedy

Giá trị trả về thường không được đưa vào ngăn xếp, mà vào một thanh ghi. Xem câu trả lời của Owen. Ngoài ra, bạn thường không bao giờ đặt một byte vào ngăn xếp, mà là bội số của kích thước từ.
rsanchez

Các phiên bản mới hơn của GCC (5.xx) đưa ra cảnh báo mà không cần thêm cờ.
Overv

36

Tôi nghĩ thật thú vị khi thấy sự không phù hợp về kích thước được đề cập trong câu trả lời xuất sắc của Lundin thực sự xảy ra.

Nếu bạn biên dịch với --save-temps, bạn sẽ nhận được các tập tin lắp ráp mà bạn có thể xem. Đây là phần f1()thực hiện == 0so sánh và trả về giá trị của nó:

cmpl    $0, -4(%rbp)
sete    %al

Phần trở lại là sete %al. Trong các quy ước gọi x86 của C, các giá trị trả về 4 byte hoặc nhỏ hơn (bao gồm intbool) được trả về qua thanh ghi %eax. %allà byte thấp nhất của %eax. Vì vậy, 3 byte trên %eaxđược để lại trong trạng thái không kiểm soát.

Bây giờ trong main():

call    f1
testl   %eax, %eax
je  .L2

Kiểm tra này xem toàn bộ các %eaxbằng không, bởi vì nó nghĩ rằng nó đang thử nghiệm một int.

Thêm một khai báo hàm rõ ràng thay đổi main()thành:

call    f1
testb   %al, %al
je  .L2

đó là những gì chúng ta muốn.


27

Vui lòng biên dịch bằng một lệnh như lệnh này:

gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c

Đầu ra:

main.c: In function 'main':
main.c:14:5: error: implicit declaration of function 'f1' [-Werror=impl
icit-function-declaration]
     printf( f1() == true ? "true\n" : "false\n");
     ^
cc1.exe: all warnings being treated as errors

Với một thông điệp như vậy, bạn nên biết phải làm gì để sửa nó.

Chỉnh sửa: Sau khi đọc một nhận xét (hiện đã bị xóa), tôi đã cố gắng biên dịch mã của bạn mà không có cờ. Vâng, điều này dẫn tôi đến các lỗi liên kết không có cảnh báo trình biên dịch thay vì lỗi trình biên dịch. Và những lỗi liên kết đó khó hiểu hơn, vì vậy ngay cả khi -std-gnu99không cần thiết, vui lòng cố gắng sử dụng ít nhất -Wall -Werrornó sẽ giúp bạn tiết kiệm rất nhiều đau đớn ở mông.

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.