56 여러 파일로 나누어 짓기 — 번역 단위와 연결
먼저 알아야 할 것
돌아보기
25장에서 “원형을 파일 위쪽에 적어 두면 정의는 아래 어디에 있어도 되고, 더 중요하게는 다른 파일에 있어도 된다”고 했다. 그 말이 성립하는 근거를 17장의 릴레이로 설명하면?
답. 컴파일 단계는 선언만 보고 일하고, 몸통을 찾아 잇는 것은 링커이기 때문이다(17장). 그래서 파일 하나를 컴파일할 때 다른 파일의 내용은 전혀 필요하지 않다 — 필요한 것은 “이런 이름의 함수가 이런 서명으로 어딘가에 있다”는 약속뿐이고, 그 약속을 담아 나르는 그릇이 헤더 파일 이다. 이 장은 그 분업을 실물로 본다.
이 장의 필요성과 맥락
이 장이 끝나면
이 장에서 답할 질문
- 링크 오류 메시지는 어떻게 읽는가?
56.1 번역 단위와 헤더#
전처리가 끝난 소스 파일 하나 — #include가 전부 펼쳐진 결과 — 를 번역 단위(translation unit)라 부른다. 컴파일러는 언제나 번역 단위 하나만 본다. 여러 파일 프로그램이란 번역 단위 여럿을 각각 목적 파일로 만든 뒤, 링커가 하나로 잇는 것이다.
세 파일짜리 예제로 분업을 본다.
examples/ch54/main.c
/* 여러 파일 예제 — 이 파일은 greet의 선언만 보고 컴파일된다. */
#include "greet.h"
int main(void)
{
greet("world");
printf_count();
return 0;
}
실행 결과
Hello, world!
greet was called 1 times
greet.h(헤더)에는 선언만 있다 — 계약 서명. greet.c에는 정의가 있고, main.c는 헤더만 포함해 계약을 알고 호출한다. 컴파일은 각각 따로 되고, 링커가 greet 호출과 정의를 이어 준다.
헤더의 #ifndef GREET_H ... #define ... #endif는 포함 보호(include guard)다. 헤더가 여러 경로로 두 번 붙어도 내용이 한 번만 펼쳐지게 하는 관용구로, 두 번 선언되면 오류가 나는 것들(구조체 정의 등)을 막는다. 실무에서는 #pragma once 한 줄로 대신하는 경우도 많다(표준은 아니지만 주요 컴파일러가 전부 지원한다).
56.2 연결 — 이름이 파일 경계를 넘는가#
이름에는 스코프(25장) 말고 또 하나의 성질이 있다 — 연결(linkage), 즉 다른 번역 단위에서 그 이름을 볼 수 있는가다.
- 외부 연결: 파일 밖에서도 보인다. 함수와 전역 변수의 기본값이다.
- 내부 연결: 이 번역 단위 안에서만 보인다. 파일 수준 선언에
static을 붙이면 된다 — 예제의static int calls가 그 예다.
static이라는 낱말이 45장(정적 수명)과 여기(내부 연결)에서 다른 뜻으로 쓰이는 것은 C의 유명한 낱말 재활용이다 — 함수 안의 static은 수명을, 파일 수준의 static은 연결을 뜻한다.
내부 연결은 실무의 기본 무기다. C에는 프로그래머가 새로 파는 이름 공간이 없어서(59장) 외부 이름은 모두 한 마당에서 부딪히는데, 파일 안에서만 쓰는 도우미 함수· 변수에 static을 붙이면 그 이름은 아예 바깥 마당에 나가지 않는다 — 충돌도 없고, 컴파일러가 더 공격적으로 최적화할 수 있다(그 이름이 다른 파일에서 불릴 리 없음을 아니까 — 14장의 편집자에게 주는 정직한 신호다). 이 무기를 포함해 이름 충돌(name collision)을 막는 방법 전체는 60장에서 다룬다.
흔한 오해. “헤더 파일에 함수 정의를 넣어도 편하니 괜찮다”
static inline 함수나 매크로는 헤더에 두는 것이 관행 이고, C23의 constexpr 상수도 그렇다. 예외를 알고 쓰는 것과 규칙을 모르고 어기는 것은 다르다.)문. 링크 오류 메시지는 어떻게 읽는가?
답. 두 문장만 알면 대부분 풀린다. “undefined reference to X” — 선언은 보았으나 정의를 못 찾았다는 뜻이다(17장). 소스 파일을 빌드에서 빠뜨렸거나, 라이브러리를 안 이어 줬거나, 철자가 다르다. “multiple definition of X” — 정의가 둘 이상이다. 헤더에 정의를 넣었거나, 전역 변수를 헤더에서 선언하며 초기화했을 때 흔하다. 두 메시지 모두 컴파일러 가 아니라 링커가 내는 것이라는 점이 진단의 출발점이다 — 문법은 멀쩡했다는 뜻이니까.
이 장은 소스를 어떻게 나누는가였다. 나눈 것을 다시 잇는 일 — 링커가 실제로 무엇을 하며, 이어 붙인 결과물이 바깥 세계에 무엇을 약속하는가 — 가 다음 장이다.