하나의 .c 파일은 2,000줄이 되기 전까지는 괜찮습니다. 프로그램을 여러 파일로 나누면 각 부분을 따로 컴파일하고, 다른 프로그램에서 재사용하고, 독립적으로 읽을 수 있습니다. 그런데 C에는 import 시스템이 없습니다. 있는 것은 텍스트를 붙여넣는 전처리기와, 마지막에 조각들을 이어 붙이는 링커뿐입니다.
헤더 파일(.h)은 그 조각들 사이의 공유 계약입니다. 구현을 담지 않으면서, 다른 곳에 무엇이 존재하는지를 모든 소스 파일에 알려줍니다.
선언과 정의
이 설계 전체가 하나의 구분 위에 서 있습니다.
선언은 이것이 어딘가에 존재하며 그 모양은 이렇다고 말합니다. 코드를 만들어내지 않으며 몇 번이든 나타날 수 있습니다.
int add(int a, int b); /* 함수 선언 (프로토타입) */
extern int error_count; /* 변수 선언 */
struct Point { int x, y; }; /* 타입 정의 - 파일마다 반복해도 안전 */
정의는 그 대상을 실제로 만듭니다. 프로그램 전체에서 정확히 한 번만 나타나야 합니다.
int add(int a, int b) { return a + b; } /* 함수 정의 */
int error_count = 0; /* 변수 정의 */
헤더는 선언을 담고, 소스 파일은 정의를 담습니다. 이를 거꾸로 하면 링커가 "multiple definition of ..."라고 불평합니다. 이 오류 메시지는 정의가 헤더로 흘러 들어갔다는 뜻으로 거의 확실하게 해석할 수 있습니다.
두 파일짜리 프로그램
가장 작으면서도 쓸모 있는 분할입니다. 함수 두 개를 선언하는 헤더입니다.
/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int max_of(int a, int b);
#endif
이를 구현하는 소스 파일입니다. 자기 헤더를 포함한다는 점에 주목하세요.
/* math_utils.c */
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int max_of(int a, int b) {
return (a > b) ? a : b;
}
그리고 이를 사용하는 프로그램입니다.
/* main.c */
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("add(3, 4) = %d\n", add(3, 4));
printf("max_of(3, 4) = %d\n", max_of(3, 4));
return 0;
}
두 소스 파일을 함께 컴파일합니다.
gcc main.c math_utils.c -o app
./app
눈여겨볼 점이 두 가지 있습니다. 첫째, math_utils.c가 자기 헤더를 포함하는 것은 중복이 아닙니다. 그래야 컴파일러가 각 정의와 그 선언이 일치하는지 검사하고, 헤더의 프로토타입을 바꾸고 .c 파일을 잊었을 때 링크 시점의 불일치가 아니라 즉시 오류를 받게 됩니다.
둘째, math_utils.h는 gcc 명령줄에 없습니다. 헤더는 절대 컴파일되지 않고, #include로 .c 파일 안에 붙여 넣어집니다. .h를 컴파일러에 넘기면 쓸모없는 미리 컴파일된 헤더 파일만 생기고 링크 가능한 코드는 나오지 않습니다.
같은 프로그램을 한 파일로 압축한 것입니다. 여기서 바로 실행해 볼 수 있습니다.
main 앞의 선언들이 실제 버전에서 헤더가 제공하는 것입니다. 그래서 함수 프로토타입과 헤더는 규모만 다를 뿐 정확히 같은 아이디어입니다.
인클루드 가드
#include는 텍스트를 붙여넣고, 두 번 붙인 텍스트는 중복된 텍스트입니다. 함수 프로토타입이라면 무해하지만 struct라면 치명적입니다.
/* 가드가 없는 shapes.h */
struct Point { int x, y; };
main.c가 shapes.h와 canvas.h를 모두 포함하고 canvas.h도 shapes.h를 포함한다면, 컴파일러는 하나의 번역 단위 안에서 struct Point가 두 번 정의된 것을 보고 "redefinition of 'struct Point'"와 함께 멈춥니다. 실제 프로젝트에서는 이런 연쇄가 손으로 추적할 수 없을 만큼 깊어집니다.
해법은 인클루드 가드입니다. "이 헤더는 이미 붙여 넣어졌다"고 기록하는 매크로입니다.
/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H
struct Point { int x, y; };
struct Point origin_point(void);
#endif /* SHAPES_H */
첫 번째 포함에서는 SHAPES_H가 정의되어 있지 않으므로 본문이 유지되고, 지나가는 길에 SHAPES_H를 정의합니다. 같은 파일에서 이후의 모든 포함은 그것이 정의된 것을 보고 곧장 #endif로 건너뜁니다. 매크로 이름은 프로젝트 전체에서 유일해야 하며, 경로에서 따온 FILENAME_H 형태가 일반적인 관례입니다.
한 줄짜리 대안은 주요 컴파일러 모두가 지원합니다.
/* shapes.h */
#pragma once
struct Point { int x, y; };
#pragma once는 이름 충돌을 겪을 일이 없고 #endif의 오타로 망가질 일도 없습니다. 유일한 단점은 C 표준에 없다는 것이라서, 특이한 컴파일러에서도 빌드되어야 하는 프로젝트라면 #ifndef 형태를 택하는 편이 좋습니다. 어느 쪽이든 모든 헤더에 하나씩 넣으세요. 다른 어떤 것도 포함하지 않을 것 같은 헤더까지 포함해서, 예외는 없습니다.
헤더에 들어갈 것
.h에 넣을 것들입니다.
- 함수 프로토타입
struct,union,enum정의typedef선언- 공유하려는 매크로
- 공유 전역 변수의
extern선언 - 그 헤더 자체가 자립하는 데 필요한
#include
.h에서 빼야 할 것들입니다.
- 함수 본문 (의도적인
static inline이 아닌 이상) - 변수 정의 - 헤더의
int counter;는 그것을 포함하는 모든 파일에 별개의 변수를 정의하거나, 컴파일러에 따라 링크 오류를 냅니다 - 그 헤더 자신에게 필요하지 않은 헤더의
#include- 그 의존성을 하류의 모두에게 떠넘깁니다
"자립적(self-contained)"이라는 것은 규칙으로 삼을 만합니다. 헤더는 다른 무엇보다 먼저, 맨 처음 포함되어도 컴파일되어야 합니다. shapes.h가 size_t를 쓴다면, 포함하는 쪽이 알아서 해줬기를 바라는 대신 스스로 <stddef.h>를 포함합니다.
완전하고 잘 만들어진 헤더입니다.
/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H
#include <stddef.h> /* 아래에서 쓰는 size_t를 위해 */
#define MAX_NAME 64
typedef struct {
char name[MAX_NAME];
int quantity;
double price;
} Item;
/* 프로그램 전체에서 공유, inventory.c에서 한 번 정의 */
extern int item_count;
void inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);
#endif /* INVENTORY_H */
extern으로 전역 변수 공유하기
전역 변수는 정확히 하나의 .c 파일에서 정의되고 다른 모든 곳에서는 선언되어야 합니다. 그 선언을 만드는 것이 extern입니다.
/* inventory.h - 선언, 저장 공간 없음 */
extern int item_count;
/* inventory.c - 유일한 정의 */
#include "inventory.h"
int item_count = 0;
/* main.c - 헤더를 통해 사용 */
#include <stdio.h>
#include "inventory.h"
int main(void) {
printf("%d items\n", item_count);
return 0;
}
헤더에서 extern을 빼면 포함하는 모든 파일이 자기만의 item_count를 정의합니다. 잘해야 "multiple definition" 링크 오류이고, 최악의 경우 서로 독립적인 두 개의 카운터가 됩니다.
그 반대의 필요도 그만큼 흔합니다. 하나의 .c 파일 안에만 머물러야 하는 변수나 헬퍼 함수 말입니다. 파일 범위의 static이 그 일을 합니다. 이름에 **내부 링키지(internal linkage)**를 부여해서 링커에게, 따라서 다른 모든 파일에게 보이지 않게 만듭니다.
/* inventory.c */
static Item storage[256]; /* 이 파일 전용 */
static int find_slot(const char *name); /* 전용 헬퍼 */
두 파일이 각각 static int counter;를 가져도 충돌하지 않습니다. 이것이 C 버전의 private 멤버이며, 기본적으로 손이 가야 할 선택지입니다. 다른 파일이 정말로 필요로 하는 것만 헤더로 갑니다.
더 큰 프로그램 컴파일하기
모든 파일을 나열하는 방식은 동작하지만 느립니다. 매번 모든 파일이 다시 컴파일되기 때문입니다.
gcc main.c inventory.c report.c -o app
확장 가능한 형태는 각 소스를 오브젝트 파일로 컴파일한 다음 링크하는 것입니다.
gcc -c main.c # main.o 생성
gcc -c inventory.c # inventory.o 생성
gcc -c report.c # report.o 생성
gcc main.o inventory.o report.o -o app
이제 report.c를 바꾸면 gcc -c report.c와 재링크만 하면 됩니다. 바로 이 장부 정리를 make가 자동화합니다.
app: main.o inventory.o report.o
gcc main.o inventory.o report.o -o app
%.o: %.c
gcc -Wall -Wextra -c $< -o $@
헤더가 하위 디렉터리에 있다면 -Iinclude가 그 디렉터리를 꺾쇠괄호 탐색 경로에 추가합니다.
오류와 그 의미
두 가지 실패 방식은, 어느 단계에서 나온 것인지만 알면 쉽게 구분됩니다.
"undefined reference to 'add'" - 링커 오류입니다. 선언은 찾았지만 정의를 찾지 못했습니다. 명령줄에 .c 파일을 나열하는 것을 잊었거나, 그 함수가 static이거나, 두 자리 중 한 곳에서 이름의 철자가 틀린 것입니다.
"multiple definition of 'item_count'" - 역시 링커 오류이며 정반대 경우입니다. 정의가 헤더에 들어갔거나 두 소스 파일에 있습니다. 하나의 .c로 옮기고 헤더에는 extern 선언만 남기세요.
"redefinition of 'struct Item'" - 컴파일러 오류이며, 헤더가 한 파일에 두 번 붙여 넣어졌다는 뜻입니다. 인클루드 가드를 추가하세요.
"implicit declaration of function 'add'" - 컴파일러 경고이며(C99 이상 모드에서는 오류), 프로토타입을 본 적이 없다는 뜻입니다. #include를 잊었거나, 그 헤더가 해당 함수를 선언하지 않는 것입니다.
프로그램이 여러 파일에 걸치게 되면 보통 그다음으로 원하게 되는 것은 플랫폼이나 빌드 종류에 따라 컴파일되는 내용을 달리하는 일이고, 그것이 조건부 컴파일입니다.
자주 묻는 질문
.h 파일에는 무엇을 넣고 .c 파일에는 무엇을 넣나요?
헤더는 선언을 담습니다. 함수 프로토타입, struct와 typedef 정의, enum, 매크로, 그리고 공유 전역 변수의 extern 선언입니다. .c 파일은 정의를 담습니다. 함수 본문과 실제 변수입니다. 경험 법칙: 헤더는 무엇이 존재하는지를 말하고, 소스 파일은 그것이 무엇을 하는지를 말합니다.
인클루드 가드란 무엇이고 왜 필요한가요?
#include는 텍스트를 붙여넣기 때문에, 헤더를 두 번 포함하면 그 내용이 두 번 붙습니다. 그러면 안에 있는 모든 struct와 typedef가 재정의되어 컴파일에 실패합니다. 인클루드 가드는 헤더를 #ifndef MYHEADER_H / #define MYHEADER_H / #endif로 감싸서, 두 번째 포함 때는 매크로가 이미 정의된 것을 보고 본문을 건너뛰게 합니다.
여러 파일로 된 C 프로그램은 어떻게 컴파일하나요?
모든 .c 파일을 명령줄에 나열합니다: gcc main.c math_utils.c -o app. .h 파일은 절대 거기에 넣지 마세요. 헤더는 #include로 붙여 넣어지는 것이지 따로 컴파일되는 것이 아닙니다. 더 큰 프로젝트에서는 오브젝트 파일로 컴파일한 다음(gcc -c main.c) 링크하며, 이것이 바로 Makefile이 자동화하는 일입니다.
#pragma once와 #ifndef 인클루드 가드 중 무엇을 써야 하나요?
둘 다 동작합니다. #pragma once는 한 줄이고 이름 충돌이 있을 수 없으며 주요 컴파일러가 모두 지원하지만, C 표준에는 없습니다. #ifndef/#define/#endif 형태는 표준이고 어디서나 동작합니다. 하나를 골라 프로젝트 안에서 일관되게 쓰세요. 이식성을 최대로 하려면 #ifndef 형태를 고르면 됩니다.