Menu

C 흔한 오류: 초보자가 겪는 실수와 해결법

모든 C 프로그래머가 최소 한 번은 겪는 실수들 - 세미콜론 누락, == 대신 =, 암시적 선언, &를 빠뜨린 scanf, ==로 문자열 비교, 정수 나눗셈, 초기화하지 않은 변수, off-by-one - 각각을 증상, 원인, 해결로 정리했습니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

모든 C 프로그래머는 똑같이 짧은 실수 목록을 만납니다. 보통은 첫 주에, 가끔은 10년 뒤에도 말이죠. 이 실수들을 따로 정리해 둘 만한 이유는, 대부분이 자신을 설명해 주지 않는 오류 메시지를 내거나 - 더 나쁘게는 - 아예 아무 메시지도 내지 않기 때문입니다.

아래 각 항목은 증상, 그 뒤의 원인, 그리고 해결로 이루어져 있습니다.

세미콜론 누락과 연쇄 오류

증상: 오류가 무더기로 쏟아지는데, 모두 멀쩡해 보이는 줄에 찍힙니다.

program.c:6:5: error: expected ';' before 'printf'
program.c:7:5: error: expected declaration specifiers before 'return'

원인: C는 문장을 ;로 끝냅니다. 하나를 빠뜨리면 컴파일러가 다음 줄을 현재 줄에 붙여 읽고, 합쳐진 문장이 말이 되지 않는 지점 - 보통은 다음 줄 - 에서 혼란을 보고합니다.

int main(void) {
    int x = 5      /* 세미콜론 누락 */
    printf("%d\n", x);
    return 0;
}

해결: 오류가 한꺼번에 뜨면 첫 번째 것만 고치고 다시 컴파일하세요. 그 뒤의 오류는 전부 여파일 수 있습니다. 그리고 컴파일러가 지목한 줄의 바로 앞 줄을 항상 확인하세요.

닫지 않은 중괄호나 끝내지 않은 /* 주석에서도 같은 연쇄가 일어나며, 이때는 오류가 수십 줄 떨어진 곳에 나타나기도 합니다.

한 가지 덧붙이면, if, for, while의 헤더 뒤나 함수 정의의 닫는 중괄호 뒤에는 세미콜론을 붙이지 않습니다. if (x > 0);은 문제없이 컴파일되고 아무 일도 하지 않습니다. 그 ;이 본문 전체이기 때문입니다.

비교 대신 대입

증상: 항상 참인 조건, 또는 알 수 없이 값이 바뀌는 변수.

int x = 5;
if (x = 10) {              /* 10을 대입한 뒤 10을 검사 -> 참 */
    printf("x is ten\n");  /* 항상 출력됨. 이제 x는 10 */
}

원인: =는 대입하고 ==는 비교합니다. 대입식의 값은 대입된 값이므로, if (x = 10)10을 검사하고, 이는 0이 아니므로 참입니다. 가끔은 정말로 그걸 의도하는 사람이 있기 때문에 컴파일러는 이를 받아들입니다.

해결: 모든 조건에 ==를 쓰고, -Wall을 켜서 컴파일러가 경고하게 하세요.

warning: suggest parentheses around assignment used as truth value

조건 안에서 정말로 대입을 의도했다면 - while ((c = getchar()) != EOF)가 흔한 예입니다 - 괄호를 한 겹 더 씌워 그 의도를 밝히면 경고도 사라집니다.

함수의 암시적 선언

증상:

warning: implicit declaration of function 'printf'
warning: implicit declaration of function 'malloc'

...그리고 때로는 이상한 실행 결과나 링크 오류가 뒤따릅니다.

원인: 컴파일러가 선언이 없는 함수의 호출을 만났습니다. C99 이전 방언에서는 그 함수가 int를 반환한다고 가정하고 그냥 진행했습니다. 64비트 시스템에서 이 가정은 반환된 포인터를 32비트로 잘라내며, <stdlib.h>를 빠뜨린 것이 malloc을 크래시로 바꾸는 경로가 바로 이것입니다.

해결: 알맞은 헤더를 포함하세요.

함수헤더
printf, scanf, fopen<stdio.h>
malloc, free, atoi, rand, exit<stdlib.h>
strlen, strcpy, strcmp<string.h>
sqrt, pow, fabs<math.h>
isdigit, toupper<ctype.h>
time<time.h>

직접 만든 함수라면, 같은 경고는 정의하기 전에 호출했다는 뜻입니다. main 위에 프로토타입을 두세요.

&를 빠뜨린 scanf

증상: 입력을 받는 순간 프로그램이 죽거나, 아무것도 읽지 못하고 변수가 그대로 남습니다.

int age;
scanf("%d", age);     /* & 누락: 주소가 아니라 값을 넘김 */

원인: scanf는 변수 안에 써넣기 때문에 변수의 주소가 필요합니다. age를 그냥 넘기면 그 안에 있던 쓰레기 값이 전달되고, scanf는 그걸 쓸 위치로 여깁니다. 보통은 세그멘테이션 폴트로 이어집니다.

해결: 변수 앞에 &를 붙이세요. 이미 주소인 배열을 제외한 모든 타입에 필요합니다.

같은 계열의 이웃한 함정 두 가지. scanf에서 double에는 %lf를 쓰세요(거기서 %ffloat를 뜻하며, float 크기만큼의 바이트를 double에 써넣으면 값이 망가집니다). 그리고 %s에는 항상 폭을 지정하세요. 50바이트 버퍼라면 %49s입니다. 그러지 않으면 긴 단어가 버퍼를 넘칩니다.

더 나은 방법은 fgets로 한 줄을 통째로 읽고 파싱하는 것입니다. 오버플로가 일어날 수 없고 버퍼에 입력 찌꺼기를 남기지도 않습니다. 둘 다 scanf 문서에서 더 다룹니다.

==로 문자열 비교하기

증상: 누가 봐도 같은 두 문자열이 다르다고 나옵니다.

char a[] = "hello";
char b[] = "hello";

if (a == b) {              /* 두 주소를 비교: 거짓 */
    printf("same\n");
}

원인: C의 문자열은 값이 아니라 첫 문자를 가리키는 포인터입니다. ==는 그 포인터들을 비교합니다. 같은 텍스트를 담은 두 배열은 서로 다른 주소에 있으므로 비교 결과는 거짓입니다. (헷갈리게도, 동일한 리터럴 두 개를 비교하면 참이 나오기도 합니다. 컴파일러가 사본 하나만 저장할 수 있기 때문인데, 그래서 이 버그는 간헐적으로 나타납니다.)

해결: strcmp를 쓰되, 같을 때 0을 반환한다는 점을 기억하세요.

거꾸로 읽는 실수도 많습니다. if (strcmp(a, b))는 두 문자열이 다를 때 참입니다. 0이 아닌 결과가 "같지 않음"을 뜻하기 때문입니다. 항상 == 0을 명시적으로 쓰세요.

정수 나눗셈

증상: 평균이 0으로 나오고, 백분율이 늘 0 아니면 100이며, 비율에서 소수 부분이 사라집니다.

int correct = 7, total = 10;
double score = correct / total;      /* 0.7이 아니라 0.0 */

원인: 두 피연산자가 모두 int이므로 C는 정수 나눗셈을 하고, 결과가 double에 대입되기 전에 이미 잘라냅니다. 7 / 100이고, 0double로 바꾸면 0.0입니다.

해결: 나눗셈을 하기 전에 한쪽 피연산자를 부동소수점으로 만드세요.

한쪽 피연산자를 캐스팅하면 다른 쪽은 자동으로 승격됩니다. 결과를 캐스팅하는 건 너무 늦습니다. 잘림은 이미 일어난 뒤이니까요. 타입 캐스팅을 참고하세요.

같은 함정이 중간값을 구하는 (a + b) / 2나, x가 무엇이든 항상 0인 1 / 2 * x 같은 식에도 숨어 있습니다.

return 누락 또는 잘못된 return

증상: 함수가 그럴듯해 보이지만 틀린 값을 반환하고, 그 값이 실행할 때마다 또는 빌드할 때마다 달라집니다.

int add(int a, int b) {
    int sum = a + b;
    /* return 문이 없음 */
}

원인: void가 아닌 함수에서 반환하지 않고 끝까지 도달하면 명세되지 않은 값이 나옵니다. 실제로는 반환 레지스터에 마침 들어 있던 값입니다. 호출자가 그 값을 사용하면 미정의 동작입니다.

더 교묘한 형태는 일부 경로에서만 반환하는 경우입니다.

int classify(int n) {
    if (n > 0) return 1;
    if (n < 0) return -1;
    /* n == 0 이면 끝으로 흘러 나감 */
}

해결: 모든 경로에서 반환하고, -Wall로 컴파일하세요. GCC의 "control reaches end of non-void function"이 두 형태를 모두 잡아 줍니다.

초기화하지 않은 변수

증상: 쓰레기 값이 출력되거나, 실행할 때마다 그리고 최적화 수준에 따라 결과가 달라집니다.

int total;                       /* 스택에 있던 값이 그대로 들어 있음 */
for (int i = 1; i <= 5; i++) {
    total += i;                  /* 쓰레기 값에 더하는 중 */
}
printf("%d\n", total);           /* 어마어마하게 큰 숫자 */

원인: 지역 변수는 0으로 채워지지 않습니다. 전역 변수나 static 변수는 자동으로 0이 되지만, 지역 변수는 그 스택 주소에 이미 있던 바이트로 시작합니다.

해결: 선언하는 자리에서 초기화하세요. 비용은 없고 이 부류의 버그를 통째로 없애 줍니다.

-Wall -Wextra는 이 중 상당수를 경고하고("may be used uninitialized"), 나머지는 -fsanitize=memory나 valgrind가 잡아냅니다. 초기화하지 않은 포인터는 곧장 세그멘테이션 폴트로 이어지기 때문에 특히 고약합니다.

Off-by-One

증상: 마지막 항목이 빠지거나, 원소 하나를 더 건드려서 프로그램이 나중에 이상하게 동작합니다.

int arr[5];
for (int i = 0; i <= 5; i++) {   /* 존재하지 않는 arr[5]를 건드림 */
    arr[i] = i;
}

원인: 원소가 n개인 배열의 인덱스는 0부터 n - 1까지입니다. <=는 한 번을 더 돕니다.

해결: i < n 패턴을 쓰고, 개수를 두 번 적는 대신 배열에서 n을 계산하세요.

문자열 버전 - '\0' 자리를 잊는 것 - 은 모자만 바꿔 쓴 같은 오류이며, "hello"를 담은 char word[5]는 버퍼 오버런입니다.

알아 둘 만한 작은 함정 두 가지

반복문 헤더 뒤의 세미콜론. for (int i = 0; i < 10; i++); 뒤에 중괄호 블록이 오면, 반복문은 아무것도 하지 않으면서 열 번 돌고 그다음 블록이 한 번 실행됩니다. 경고 없이 깔끔하게 컴파일됩니다.

포인터에 쓴 sizeof. 함수 안에서 배열 매개변수는 포인터이므로, sizeof(arr)는 배열이 아니라 포인터의 크기(8바이트)입니다. 길이는 별도의 인자로 넘기세요.

이 대부분을 막아 주는 습관

첫 프로그램부터 경고를 켜고 컴파일하세요.

gcc -Wall -Wextra -g program.c -o program

-Wall -Wextra는 조건 안의 대입, 빠진 return, 초기화하지 않은 값 읽기, 쓰지 않는 변수, 인자와 맞지 않는 printf 지정자를 잡아냅니다. 개발 중에 -fsanitize=address,undefined까지 더하면 나머지도 거의 전부, 문제가 발생하는 바로 그 순간에 잡힙니다.

모든 경고를 아직 터지지 않은 오류로 여기세요. 경고 없이 컴파일된 C 프로그램이 반드시 올바르다는 보장은 없지만, 크래시가 난 C 프로그램은 거의 언제나 먼저 무언가를 경고하고 있었습니다.

자주 묻는 질문

C에서 'implicit declaration of function'은 무슨 뜻인가요?

컴파일러가 선언을 본 적 없는 함수의 호출을 만났다는 뜻입니다. 거의 항상 #include를 빠뜨린 경우입니다. printf에는 <stdio.h>, malloc에는 <stdlib.h>, strlen에는 <string.h>가 필요합니다. 직접 만든 함수를 정의하기 전에 호출한 경우일 수도 있는데, main 위에 프로토타입을 두면 해결됩니다.

멀쩡해 보이는 줄에서 C 컴파일 오류가 나는 이유는 무엇인가요?

보통 진짜 실수는 그 앞 줄에 있기 때문입니다. 세미콜론 누락, 닫지 않은 중괄호, 끝내지 않은 주석은 컴파일러가 두 줄을 한 줄로 읽게 만들고, 결국 문법이 깨지는 지점에서 오류를 보고합니다. 보고된 줄보다 항상 그 위 줄을 먼저 확인하세요.

C에서 ==로 문자열을 비교할 수 없는 이유는 무엇인가요?

C의 문자열은 char *, 즉 포인터이기 때문에 ==는 가리키는 문자가 아니라 두 주소를 비교합니다. 내용이 같은 두 문자열이 서로 다른 곳에 저장되어 있으면 결과는 거짓입니다. <string.h>strcmp(a, b) == 0을 쓰세요. 내용이 일치하면 0을 반환합니다.

C에서 5 / 2가 왜 2인가요?

양쪽 피연산자가 모두 정수라서 C가 정수 나눗셈을 수행하고 소수 부분을 버리기 때문입니다. 2.5를 얻으려면 한쪽을 부동소수점 값으로 만드세요. 5.0 / 2, 또는 변수를 다룰 때는 (double)a / b처럼 씁니다. 결과를 캐스팅하는 건 이미 늦습니다. (double)(5 / 2)는 2.0입니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기