컴파일러는 위에서 아래로 읽습니다
C 컴파일러는 소스 파일을 첫 줄부터 마지막 줄까지 한 번의 패스로 처리합니다. 함수 호출에 다다랐을 때 이미 세 가지를 알고 있어야 합니다. 함수가 무엇을 반환하는지, 인자를 몇 개 받는지, 그 타입이 무엇인지입니다. 그것 없이는 올바른 코드를 생성할 수도 호출을 검사할 수도 없습니다.
그래서 이것이 문제입니다.
#include <stdio.h>
int main(void) {
printf("%d\n", add(2, 3)); /* add를 아직 본 적이 없다 */
return 0;
}
int add(int a, int b) {
return a + b;
}
해결책은 프로토타입입니다. 호출보다 앞에 쓴 함수의 시그니처이며, 본문이 올 자리에 세미콜론이 있습니다.
이제 main이 먼저 올 수 있고, 대부분의 C 파일이 이렇게 구성됩니다. 위쪽의 프로토타입들이 이 파일이 무엇을 제공하는지 말하고, main이 개요처럼 읽히며, 세부 사항이 아래에 따라옵니다.
선언과 정의
흐려지기 쉽지만 구분해 둘 가치가 있는 두 단어입니다.
- 선언은 함수가 존재한다고 말하고 그 타입을 줍니다. 세미콜론으로 끝나고 본문이 없습니다. 원하는 만큼 여러 번 쓸 수 있습니다.
- 정의는 본문을 줍니다. 프로그램 전체에서 정확히 한 번 나타나야 합니다. 같은 함수의 정의가 둘이면 링커 오류("multiple definition of")입니다.
int add(int a, int b); /* 선언 (프로토타입) */
int add(int a, int b) { return a + b; } /* 정의 - 선언이기도 하다 */
프로토타입에서 매개변수 이름은 선택 사항입니다. 컴파일러에게는 타입만 중요합니다.
int add(int, int); /* 합법이며 동등하다 */
int add(int a, int b); /* 더 낫다: 이름이 순서를 문서화한다 */
이름을 쓰세요. void drawRect(int, int, int, int);는 읽는 사람에게 아무것도 알려 주지 않지만, void drawRect(int x, int y, int width, int height);는 모든 것을 알려 줍니다.
프로토타입이 사 주는 것
정의 순서를 바꿀 수 있게 해 줄 뿐만이 아닙니다. 컴파일러가 여러분의 호출을 검사하게 만드는 것이 프로토타입입니다.
프로토타입이 스코프에 있으면 3과 2는 호출 전에 double로 변환됩니다. 그것이 없으면 int로 밀어 넣어지고 scale은 그것을 double로 읽습니다 - 쓰레기 값이고, 오래된 C 방언에서는 진단조차 없습니다.
프로토타입은 잘못된 인자 개수와 호환되지 않는 타입도 컴파일 타임에 잡아냅니다.
scale(3.0); /* 오류: 인자가 너무 적음 */
scale(3.0, "two"); /* 오류: double 자리에 char *를 넘김 */
이런 것들이 바라는 오류입니다. 요란하게, 컴파일 타임에, 해당 줄을 가리키면서요.
Implicit declaration 오류
컴파일러가 들어 본 적 없는 함수를 호출하면 C에서 가장 흔한 메시지 중 하나를 만납니다.
warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]
C89에서 컴파일러는 추측했습니다. 함수가 int를 반환한다고 가정하고 넘긴 인자가 무엇이든 받아들였죠. 그 추측은 맞을 때보다 틀릴 때가 많고, 진짜 함수가 double이나 포인터를 반환하면 결과는 말이 되지 않습니다. C99는 암묵적 선언을 언어에서 제거했으므로, C99 이후에서는 이것이 오류입니다. 요즘 GCC와 clang은 최근 버전에서 기본적으로 거부합니다.
원인은 둘, 해결도 둘입니다.
아직 선언되지 않은 내 함수. 호출 위에 프로토타입을 추가하거나 정의를 앞으로 옮기세요.
헤더를 잊은 라이브러리 함수. 라이브러리의 프로토타입은 그 헤더에 있으므로 #include가 필요합니다.
printf, scanf, fopen -> #include <stdio.h>
malloc, free, exit, atoi -> #include <stdlib.h>
strlen, strcpy, strcmp -> #include <string.h>
sqrt, pow, sin, fabs -> #include <math.h>
isdigit, toupper -> #include <ctype.h>
bool, true, false -> #include <stdbool.h>
관련된 메시지인 conflicting types for 'add'는 프로토타입과 정의가 어긋난다는 뜻입니다. 매개변수 타입이 다르거나 반환 타입이 다른 것이죠. 틀린 쪽을 고치세요. 둘은 정확히 일치해야 합니다.
상호 재귀에는 프로토타입이 필요합니다
순서를 바꿔서는 해결되지 않을 때가 있습니다. 서로를 호출하는 두 함수가 둘 다 먼저 올 수는 없습니다.
프로토타입 하나가 원을 끊습니다. 평범한 재귀 함수가 의지하는 것과 같은 장치입니다. 함수 자신의 이름은 그 본문 안에서 스코프에 있으므로 직접 재귀에는 프로토타입이 필요 없지만, 상호 재귀에는 필요합니다.
헤더 파일 속의 프로토타입
프로그램이 여러 .c 파일에 걸치면, 모든 파일이 한곳에서 같은 선언을 보도록 프로토타입이 헤더로 옮겨 갑니다.
mathutils.h - 인터페이스:
#ifndef MATHUTILS_H
#define MATHUTILS_H
int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);
#endif
mathutils.c - 구현:
#include "mathutils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
double average(const int values[], int count) {
if (count == 0) return 0.0;
int total = 0;
for (int i = 0; i < count; i++) total += values[i];
return (double) total / count;
}
main.c - 사용자:
#include <stdio.h>
#include "mathutils.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
그리고:
gcc main.c mathutils.c -o program
여기서 익힐 습관이 둘 있습니다. #ifndef / #define / #endif 감싸기는 인클루드 가드입니다. 헤더가 두 경로로 포함되더라도 선언이 두 번 처리되지 않게 막습니다. 그리고 mathutils.c는 자기 헤더를 포함하는데, 불필요해 보이지만 그렇지 않습니다. 다른 파일들이 쓸 선언과 모든 정의를 컴파일러가 대조하게 만들기 때문입니다. 둘이 어긋나면 링크 시점이 아니라 즉시 알게 됩니다. 나머지는 헤더 파일에서 다룹니다.
꺾쇠괄호(<stdio.h>)는 시스템 인클루드 경로를 찾고, 따옴표("mathutils.h")는 프로젝트 디렉터리를 먼저 찾습니다. 직접 만든 헤더에는 따옴표를 쓰세요.
f(void)와 f()
이건 정말로 놀라운 부분이고, 이 문서의 모든 예제가 int main(void)라고 쓰는 이유입니다.
void ping(void); /* 인자를 받지 않는다 - 인자가 있는 호출은 거부된다 */
void pong(); /* 매개변수에 대해 아무 말도 하지 않는다 */
void ping(void);는 프로토타입입니다. 함수가 매개변수를 받지 않는다고 선언하므로 ping(1, 2, 3)은 컴파일 오류입니다.
void pong();는 표준화 이전 C에서 물려받은 구식 선언입니다. 반환 타입을 선언하고 매개변수에 대해서는 아무것도 말하지 않으므로 컴파일러가 호출을 검사할 수 없습니다. pong(1, 2, 3)은 조용히 컴파일되고 미정의한 일을 합니다.
int main(void) { /* 옳다: main은 인자를 받지 않는다 */
int main() { /* 합법이지만 인자 검사가 꺼진다 */
매개변수 없는 함수에는 항상 (void)라고 쓰세요. C23은 ()를 (void)와 같은 뜻으로 바꿔 마침내 이 함정을 없애지만, 많은 코드와 많은 컴파일러가 아직 거기까지 가지 못했고 (void)는 모든 C 표준에서 옳습니다.
같은 구분이 정의에도 적용됩니다. void ping(void) { }는 프로토타입 스타일의 정의이고, void ping() { }는 그렇지 않으며 그 앞에 나오는 호출의 검사를 활성화하지 않습니다.
흔한 실수
- 정의에 세미콜론 붙이기.
int add(int a, int b); { return a + b; }는add를 선언한 다음 떠돌이 블록을 남깁니다. 오류 메시지는 혼란스럽지만 원인은 글자 하나입니다. - 프로토타입의 세미콜론 빠뜨리기. 컴파일러가 뒤따르는 무엇이든 계속 읽어 들여 몇 줄 아래에서 영문 모를 소리를 보고합니다.
- 프로토타입과 정의의 불일치.
conflicting types for .... 둘을 동일하게 만들거나, 더 나은 방법으로 구현 파일에서 헤더를 포함해 검사가 자동으로 되게 하세요. - 다른 함수 안에서 함수 선언하기. 합법이지만(
main안의int add(int, int);) 그 선언은 그 함수 안으로만 스코프가 한정됩니다. 프로토타입은 파일 스코프에 두세요. - 헤더에 함수 정의하기. 두
.c파일에서 포함하면 링커가 중복 정의를 보고합니다. 헤더는 선언을 담고, 정의는.c파일에 갑니다. - 검사를
()에 의존하기. 검사하지 않습니다.(void)라고 쓰세요.
자주 묻는 질문
C에서 함수 프로토타입이란 무엇인가요?
함수 시그니처 - 반환 타입, 이름, 매개변수 타입 - 를 본문 대신 세미콜론으로 끝내 선언한 것입니다. int add(int a, int b);처럼요. 함수가 무슨 일을 하는지는 말하지 않은 채, 어떻게 호출되는지를 컴파일러에게 알려 호출을 검사할 수 있게 합니다.
C에서 선언과 정의의 차이는 무엇인가요?
선언은 이름과 타입을 소개하며(int add(int, int);) 여러 번 나타날 수 있습니다. 정의는 본문을 제공하며(int add(int a, int b) { return a + b; }) 프로그램 전체에서 정확히 한 번만 나타나야 합니다. 모든 정의는 선언이기도 합니다.
C에서 "implicit declaration of function"은 어떻게 고치나요?
호출 앞에 프로토타입을 추가하세요. 직접 만든 함수라면 파일 위쪽이나 헤더에 반환타입 이름(매개변수타입);을 두고, 라이브러리 함수라면 알맞은 헤더를 포함하세요. printf에는 #include <stdio.h>, malloc에는 <stdlib.h>, strlen에는 <string.h>, sqrt에는 <math.h>입니다.
C에서 f(void)와 f()의 차이는 무엇인가요?
void f(void);는 인자를 전혀 받지 않는 함수를 선언하며, 컴파일러는 인자를 넘기는 호출을 거부합니다. void f();는 매개변수에 대해 아무 말도 하지 않는 구식 선언이라 인자 검사가 꺼집니다. 항상 (void)라고 쓰세요. C23에서 마침내 둘이 같은 뜻이 되지만, 오래된 코드와 컴파일러는 여전히 다릅니다.