이 C 함수는 항상 false를 반환해야하지만 그렇지 않습니다.


317

나는 오래 전에 포럼에서 흥미로운 질문을 우연히 발견했으며 그 답을 알고 싶습니다.

다음 C 함수를 고려하십시오.

f1.c

#include <stdbool.h>

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

false이후 항상 반환해야합니다 var3 == 3000. main기능은 다음과 같습니다 :

main.c

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

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

f1()항상 반환해야 하므로 false프로그램 이 화면에 하나의 거짓 만 인쇄 할 것으로 예상합니다 . 그러나 컴파일하고 실행 한 후에 실행 됨 도 표시됩니다.

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

왜 그런 겁니까? 이 코드에는 일종의 정의되지 않은 동작이 있습니까?

참고 :로 컴파일했습니다 gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2.


9
다른 사람들은 함수가 별도의 파일에 있기 때문에 프로토 타입이 필요하다고 언급했습니다. 그러나 f1()같은 파일에 복사하더라도 main()이상한 점이 있습니다 .C ++ ()에서는 빈 매개 변수 목록 에 사용하는 것이 맞지만 C에서는 아직 정의되지 않은 매개 변수 목록이있는 함수에 사용됩니다 ( 기본적으로) 다음에 K & R 스타일 매개 변수 목록이 필요합니다 ). 올바른 C가 되려면 코드를로 변경해야합니다 bool f1(void).
uliwitness

1
main()단순화 될 수있다 int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }이 더 나은 불일치를 보여주는 것입니다 -.
Palec

@uliwitness K & R 1st ed는 어떻습니까? 없을 때 (1978) void?
Ho1

@uliwitness가 없었다 truefalseK & R 1 에드에. 그래서 전혀 그런 문제가 없었다. 참과 거짓은 단지 0이고 0이 아닙니다. 그렇지 않습니까? 당시 프로토 타입을 사용할 수 있었는지 모르겠습니다.
Ho1

1
K & R 1st Edn은 프로토 타입 (및 C 표준)보다 10 년 이상 앞서 (책은 1978 년, 표준은 1989 년), 실제로 K & R1이 출판 될 때 C ++ (C 클래스)는 여전히 미래에있었습니다. 또한 C99 이전에는 _Bool유형과 <stdbool.h>헤더 가 없었습니다 .
Jonathan Leffler

답변:


396

다른 답변에서 언급했듯이 문제는 gcc컴파일러 옵션을 설정하지 않고 사용한다는 것 입니다. 이렇게하면 기본값은 1990 년부터 철회 된 C90 표준의 비표준 구현 인 "gnu90"으로 기본 설정됩니다.

이전 C90 표준에서 C 언어의 주요 결함이 있었다 : 당신이 기능을 사용하기 전에 프로토 타입을 선언하지 않은 경우, 그것은 기본값 것 int func ()(여기서 ( )의미는 "어떤 매개 변수를 받아"). 이것은 함수의 호출 규칙을 변경 func하지만 실제 함수 정의는 변경하지 않습니다. 의 크기 이후 boolint 가 다르기 코드가 함수를 호출 할 때 정의되지 않은 동작을 호출합니다.

이 위험한 말도 안되는 행동은 1999 년 C99 표준의 출시와 함께 수정되었습니다. 암시 적 함수 선언이 금지되었습니다.

불행히도, 버전 5.xx까지의 GCC는 여전히 기본적으로 이전 C 표준을 사용합니다. 표준 C 이외의 코드로 코드를 컴파일해야하는 이유는 없을 것입니다. 따라서 GCC에 25 년이 넘은 비표준 GNU 크랩 대신 최신 C 코드로 코드를 컴파일해야한다고 명시 적으로 명시해야합니다. .

항상 다음과 같이 프로그램을 컴파일하여 문제를 해결하십시오.

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 (현재의) C 표준 (비공식적으로 C11)에 따라 컴파일을 반쯤 시도하도록 지시합니다.
  • -pedantic-errors C 표준을 위반하는 잘못된 코드를 작성할 때 컴파일러가 위의 작업을 수행하고 컴파일러 오류를 발생시킵니다.
  • -Wall 나에게 좋은 경고를 알려줘
  • -Wextra 나에게 좋을지도 모르는 다른 추가 경고를 알려주십시오.

19
이 답변은 전체적으로 정확하지만 "gnu"에서 사용할 수있는 C11 (POSIX, X / Open 등) 이외의 라이브러리 기능이 필요하기 때문에 보다 복잡한 프로그램의 -std=gnu11경우 예상보다 훨씬 더 잘 작동합니다 -std=c11. 확장 모드이지만 엄격한 적합성 모드에서는 억제됩니다. 확장 모드에서 숨겨져있는 시스템 헤더의 버그 (예 : 비표준 typedef의 가용성 가정) 의도하지 않은 3 개의 그래프 사용 ( "gnu"모드에서는이 표준 기능이 비활성화됩니다).
zwol

5
비슷한 이유로, 나는 일반적으로 높은 경고 수준의 사용을 권장하지만 경고 오류 모드 사용을 지원할 수 없습니다. -pedantic-errors덜 번잡보다 -Werror하지만 둘 수 및 프로그램이 실제 문제가없는 경우에도, 원래 저자의 테스트에 포함되지 않은 운영 체제에 컴파일 실패 할 원인.
zwol

7
@Lundin 반대로, 내가 언급 한 두 번째 문제 (엄격한 준수 모드에 의해 노출되는 시스템 헤더의 버그)는 어디에나있다 . 나는 광범위하고 체계적인 테스트 를 수행했으며 적어도 2 년 전에는 그러한 버그 가 하나도없는 널리 사용되는 운영 체제가 없습니다. 더 이상 추가하지 않고 C11의 기능 만 필요로하는 C 프로그램도 제 경험의 규칙이 아닌 예외입니다.
zwol

6
당신은 표준 C 사용하는 경우 @joop bool/ _Bool"C ++ - 억양의"에서 당신이 쓸 수있는 C 코드 모든 비교 및 논리 연산자는를 반환한다고 가정 곳 방법을, boolC처럼 ++, 그들은는 반환에도 불구하고 int역사적인 이유로, C에 . 이것은 정적 분석 도구를 사용하여 이러한 모든 표현식의 유형 안전성을 확인하고 컴파일 타임에 모든 종류의 버그를 노출시킬 수 있다는 큰 이점이 있습니다. 또한 자체 문서화 코드 형식으로 의도를 표현하는 방법이기도합니다. 덜 중요한 것은 몇 바이트의 RAM도 절약한다는 것입니다.
Lundin

7
C99의 새로운 기능은 25 년이 지난 GNU 쓰레기에서 나온 것입니다.
Shahbaz

141

f1()main.c에 선언 된 프로토 타입이 없으므로 암시 적으로로 정의됩니다 int f1(). 즉, 알 수없는 수의 인수를 가져 와서int .

경우 intbool다른 크기의 있습니다,이 발생합니다 정의되지 않은 동작 . 예를 들어, 내 컴퓨터에서 int4 바이트이고 bool1 바이트입니다. 이 함수는 return 으로 정의bool 되었으므로 반환 될 때 스택에 1 바이트를 넣습니다. 그러나 암시 적 으로 반환하도록 선언 되었으므로int main.c에서 되었으므로 호출 함수는 스택에서 4 바이트를 읽으려고 시도합니다.

gcc의 기본 컴파일러 옵션은 이것이 수행 중임을 알려주지 않습니다. 그러나로 컴파일하면 다음을 -Wall -Wextra얻을 수 있습니다.

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

이 문제를 해결하려면 f1main.c에서 에 대한 선언을 추가하십시오 main.

bool f1(void);

인수 목록은 명시 적으로로 설정되어 void있으며, 알 수없는 수의 인수를 의미하는 빈 매개 변수 목록과 달리 함수가 인수를 사용하지 않도록 컴파일러에 지시합니다. 정의 f1f1.c에서도이를 반영하도록 변경해야합니다.


2
내가 GCC를 사용할 때 내 프로젝트에서 내가했던 일 -Werror-implicit-function-declaration이 GCC의 옵션 에 추가 되어 더 이상 지나치지 않습니다. 더 나은 선택은 -Werror모든 경고를 오류로 바꾸는 것입니다. 모든 경고가 표시되면이를 해결하도록합니다.
uliwitness

2
또한 빈 괄호는 사용하지 않는 기능이므로 사용하지 않아야합니다. C 코드의 다음 버전에서는 이러한 코드를 금지 할 수 있습니다.
Lundin

1
@uliwitness 아. C 만
다루는

반환 값은 일반적으로 스택에 저장되는 것이 아니라 레지스터에 저장됩니다. Owen의 답변을 참조하십시오. 또한 일반적으로 1 바이트를 스택에 넣지 않고 단어 크기의 배수를 두십시오.
rsanchez

최신 버전의 GCC (5.xx)는 추가 플래그없이 경고를 제공합니다.
Overv

36

Lundin의 훌륭한 답변에서 언급 된 크기 불일치가 실제로 발생하는 곳을 보는 것이 재미 있다고 생각합니다.

로 컴파일 --save-temps하면 볼 수있는 어셈블리 파일을 얻게됩니다. 다음 f1()== 0비교를 수행하고 해당 값을 반환 하는 부분입니다 .

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

반환 부분은 sete %al입니다. C의 x86 호출 규칙에서 4 바이트 이하 ( int및 포함 bool)를 반환 하는 값 은 register를 통해 반환됩니다 %eax. %al의 가장 낮은 바이트입니다 %eax. 따라서 상위 3 바이트%eax 는 제어되지 않은 상태로 남아 있습니다.

지금 main():

call    f1
testl   %eax, %eax
je  .L2

이것은 전체 의 여부를 확인%eax 테스트하고 있다고 생각하기 때문에 가 0 .

명시 적 함수 선언을 추가하면 다음과 같이 변경 main()됩니다.

call    f1
testb   %al, %al
je  .L2

우리가 원하는 것입니다.


27

다음과 같은 명령으로 컴파일하십시오 :

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

산출:

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

이러한 메시지가 있으면이를 정정하기 위해 수행 할 작업을 알아야합니다.

편집 : (현재 삭제 된) 주석을 읽은 후 플래그없이 코드를 컴파일하려고했습니다. 글쎄, 이로 인해 컴파일러 오류 대신 컴파일러 경고가없는 링커 오류가 발생했습니다. 그리고 이러한 링커 오류는 이해하기가 더 어려우므로 -std-gnu99필요하지 않더라도 적어도 항상 사용 -Wall -Werror하십시오. 엉덩이에 많은 고통을 덜어줍니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.