Proven C Book←↑→

56 여러 파일로 나누어 짓기 — 번역 단위와 연결

먼저 알아야 할 것

17장 일반적인 컴파일 과정 · 번역 단위와 링크
25장 함수 선언과 정의 · 선언과 정의

돌아보기

25장에서 “원형을 파일 위쪽에 적어 두면 정의는 아래 어디에 있어도 되고, 더 중요하게는 다른 파일에 있어도 된다”고 했다. 그 말이 성립하는 근거를 17장의 릴레이로 설명하면?

답. 컴파일 단계는 선언만 보고 일하고, 몸통을 찾아 잇는 것은 링커이기 때문이다(17장). 그래서 파일 하나를 컴파일할 때 다른 파일의 내용은 전혀 필요하지 않다 — 필요한 것은 “이런 이름의 함수가 이런 서명으로 어딘가에 있다”는 약속뿐이고, 그 약속을 담아 나르는 그릇이 헤더 파일 이다. 이 장은 그 분업을 실물로 본다.

이 장의 필요성과 맥락

17장에서 네 단계 릴레이를 구경만 했는데, 그중 마지막 주자(링크)는 파일이 하나면 할 일이 없다. 파일이 여럿이 되는 지금이 링크가 실제로 무엇을 하는가를 배울 유일한 때다. 25장의 선언과 정의 구별이 여기서 비로소 제 뜻을 드러낸다.

이 장이 끝나면

지금까지의 프로그램은 파일 하나였다. 이제 여러 파일로 자란다 — 헤더와 소스의 분업, 번역 단위라는 개념, 이름의 연결(외부·내부), 그리고 링크 오류를 읽는 법까지. 17장의 릴레이와 25장의 선언/정의 구별이 여기서 하나로 합쳐진다. 링크가 실제로 어떻게 일하고, 그 결과물이 무엇을 약속하는가는 바로 다음 장(57장)의 몫이다.

이 장에서 답할 질문

  1. 링크 오류 메시지는 어떻게 읽는가?

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이라는 낱말이 45장(정적 수명)과 여기(내부 연결)에서 다른 뜻으로 쓰이는 것은 C의 유명한 낱말 재활용이다 — 함수 안의 static은 수명을, 파일 수준의 static은 연결을 뜻한다.

내부 연결은 실무의 기본 무기다. C에는 프로그래머가 새로 파는 이름 공간이 없어서(59장) 외부 이름은 모두 한 마당에서 부딪히는데, 파일 안에서만 쓰는 도우미 함수· 변수에 static을 붙이면 그 이름은 아예 바깥 마당에 나가지 않는다 — 충돌도 없고, 컴파일러가 더 공격적으로 최적화할 수 있다(그 이름이 다른 파일에서 불릴 리 없음을 아니까 — 14장의 편집자에게 주는 정직한 신호다). 이 무기를 포함해 이름 충돌(name collision)을 막는 방법 전체는 60장에서 다룬다.

흔한 오해. “헤더 파일에 함수 정의를 넣어도 편하니 괜찮다”

포함되는 파일마다 정의가 복사되므로, 두 파일이 그 헤더를 포함하면 같은 함수의 정의가 둘이 되어 링커가 중복 정의 오류를 낸다(“multiple definition of …”). 헤더의 역할은 계약을 나르는 것이지 구현을 나르는 것이 아니다 — 헤더에는 선언, 소스에는 정의가 기본형이다. (예외가 없지는 않다: static inline 함수나 매크로는 헤더에 두는 것이 관행 이고, C23의 constexpr 상수도 그렇다. 예외를 알고 쓰는 것과 규칙을 모르고 어기는 것은 다르다.)

문. 링크 오류 메시지는 어떻게 읽는가?

답. 두 문장만 알면 대부분 풀린다. “undefined reference to X” — 선언은 보았으나 정의를 못 찾았다는 뜻이다(17장). 소스 파일을 빌드에서 빠뜨렸거나, 라이브러리를 안 이어 줬거나, 철자가 다르다. “multiple definition of X” — 정의가 둘 이상이다. 헤더에 정의를 넣었거나, 전역 변수를 헤더에서 선언하며 초기화했을 때 흔하다. 두 메시지 모두 컴파일러 가 아니라 링커가 내는 것이라는 점이 진단의 출발점이다 — 문법은 멀쩡했다는 뜻이니까.

이 장은 소스를 어떻게 나누는가였다. 나눈 것을 다시 잇는 일 — 링커가 실제로 무엇을 하며, 이어 붙인 결과물이 바깥 세계에 무엇을 약속하는가 — 가 다음 장이다.