구조체와 열거형으로 데이터 묶는 감각이 생겼다면, 이제는 코드를 파일로 나눠 관리할 차례입니다. C에서는 함수나 구조체 정의를 여러 파일에 나누고, 선언은 헤더로 모아 공유하는 패턴이 기본입니다. 이렇게 나누면 한 파일에 모든 코드를 몰아넣지 않아도 되고, 어떤 파일이 어떤 역할을 맡는지 더 분명하게 볼 수 있습니다. 이번 글에서는 가장 작은 2파일 프로젝트를 직접 만들어 보며 .h와 .c가 어떤 역할을 맡는지 확인합니다.
이번 글에서 새로 나오는 용어
- 헤더 파일 (Header File): 함수·구조체·상수의 선언을 담아 다른 파일과 공유하는
.h파일 - 선언 (Declaration): 이름과 형식만 알려 주는 구문으로, 컴파일러가 "이 함수가 어딘가에 정의된다"고 믿게 하는 정보
- 정의 (Definition): 함수 본문이나 변수 실제 저장 공간을 제공하는 구문
- Include Guard: 같은 헤더가 여러 번 포함되어도 중복 선언을 막는 전처리기 패턴
핵심 개념
학습 메모
- 소요 시간: 70분 내외
- 준비물: 구조체, 함수 기초,
clang명령 사용 경험- 학습 목표: 헤더/소스 분리, 선언과 정의 차이, 다중 파일 컴파일 명령 이해
이번 글에서 집중할 사항은 세 가지입니다.
- 헤더에는 선언과 상수, typedef 같은 메타 정보를 넣고,
.c에는 실제 구현을 둔다. #ifndef/#define패턴으로 include guard를 구성해 중복 포함을 막는다.- 여러
.c파일을 한 번에 컴파일하거나 목적 파일로 나눠 빌드하는 명령을 익힌다.
코드로 따라하기
프로젝트 구조 잡기
project/
├── main.c
├── stats.c
└── stats.h
stats.h에는 함수 선언과 필요한 구조체를 적고, stats.c에는 구현을 둡니다. main.c는 헤더 파일만 포함한 뒤 함수를 사용합니다.
stats.h: 이름과 형식 소개stats.c: 실제 계산 구현main.c: 기능 사용과 결과 출력
헤더 파일 작성
// stats.h
#ifndef STATS_H
#define STATS_H
#include <stddef.h>
typedef struct {
double average;
int max;
int min;
} Stats;
Stats calculate_stats(const int *numbers, size_t length);
#endif
#ifndef로 시작해 #define을 선언하고, 파일 끝에서 #endif로 닫으면 같은 헤더를 여러 번 포함하더라도 한 번만 처리됩니다. 이런 패턴을 인클루드 가드라고 부르며, 헤더에는 구현이 아닌 선언만 두는 것이 기본 원칙입니다.
구현 파일 작성
// stats.c
#include "stats.h"
Stats calculate_stats(const int *numbers, size_t length) {
size_t i;
Stats stats = {0.0, 0, 0};
int total = 0;
if (length == 0) {
return stats;
}
stats.max = numbers[0];
stats.min = numbers[0];
for (i = 0; i < length; i++) {
total += numbers[i];
if (numbers[i] > stats.max) {
stats.max = numbers[i];
}
if (numbers[i] < stats.min) {
stats.min = numbers[i];
}
}
stats.average = (double)total / (double)length;
return stats;
}
구현 파일은 헤더를 포함해 선언과 정의가 일치하도록 만듭니다. 이렇게 하면 함수 시그니처가 바뀌었을 때 컴파일 오류로 바로 확인할 수 있습니다.
main.c에서 함수 사용하기
// main.c
#include <stdio.h>
#include "stats.h"
int main(void) {
int numbers[5] = {72, 85, 90, 68, 77};
Stats stats = calculate_stats(numbers, 5);
printf("avg = %.1f\n", stats.average);
printf("max = %d\n", stats.max);
printf("min = %d\n", stats.min);
return 0;
}
헤더는 쌍따옴표로 포함해 같은 프로젝트 안의 파일을 찾도록 합니다. 표준 라이브러리처럼 설치된 헤더는 꺾쇠 괄호(<...>)를 사용합니다.
다중 파일 컴파일하기
clang main.c stats.c -o stats-app
또는 목적 파일을 분리해 더 세밀하게 빌드할 수도 있습니다.
clang -c main.c -o main.o
clang -c stats.c -o stats.o
clang main.o stats.o -o stats-app
프로젝트가 커질수록 두 번째 방식이 더 유리합니다. 파일 하나를 고친 뒤에는 해당 .c만 다시 컴파일하면 되고, 나머지 목적 파일은 재사용할 수 있기 때문입니다. 첫 번째 명령에서 main.c와 stats.c를 함께 적는 이유는, main.c가 호출하는 calculate_stats의 실제 구현이 stats.c 안에 있기 때문입니다.
외부 변수 선언하기
// config.h
#ifndef CONFIG_H
#define CONFIG_H
extern int MAX_USERS;
#endif
// config.c
int MAX_USERS = 100;
전역 변수를 여러 파일에서 공유해야 할 때는 헤더에 extern 선언을 두고, .c 파일 중 하나에서만 실제 정의를 제공합니다. extern은 "메모리를 새로 만드는 것"이 아니라 "다른 파일에 이미 있는 변수를 여기서 쓰겠다"고 알리는 문장에 가깝습니다. 이렇게 하면 중복 정의 오류를 피할 수 있습니다.
왜 중요한가
프로그램이 커지면 한 파일에 모든 코드를 넣는 방식은 금방 한계에 부딪힙니다. 헤더와 소스를 구분하면 팀원이 서로 다른 부분을 동시에 작업할 수 있고, 컴파일러도 각 단위를 독립적으로 빌드해 시간을 절약할 수 있습니다. 또한 선언과 정의가 따로 있는 구조 덕분에 인터페이스(헤더)만 보고도 함수를 사용할 수 있어 테스트와 문서화가 쉬워집니다.
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Universal starter입니다. C는 터미널에서 직접 컴파일하고 다시 실행하는 흐름이 중요하니, 이번 글의 코드를 파일로 만들고 빌드-실행 사이클을 다시 따라가 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요