나만의 작은 도서관
[C++][Build] 컴파일 단계 - 번역 단위(translation unit)와 선언과 정의, 그리고 ODR 본문
C++/문법 및 메소드(STL)
[C++][Build] 컴파일 단계 - 번역 단위(translation unit)와 선언과 정의, 그리고 ODR
pledge24 2025. 4. 14. 17:23개요
컴파일 단계는 프로그래밍 언어로 된 코드를 기계어로 변환하는 과정이다. 이 과정을 수행하는 컴파일러는 ODR(One Definition Rule, 유일 정의 규칙)이라는 정해진 규칙을 준수하는데, 이러한 ODR을 이해하기 위해 필요한 개념이 바로 번역 단위와 선언과 정의이다.
이 글에서는 컴파일 과정에서 준수하는 ODR이라는 규칙을 이해하기 위해 번역 단위가 무엇인지, 선언과 정의가 어떻게 구분되는지, 그리고 마지막으로 ODR 정의에 대한 해석을 간단하게 다룬다.
번역 단위(translation unit)
- 번역 단위(이하 TU)란 하나의 소스 파일(.cpp)이 전처리 단계를 거친 후에 생긴 결과물을 의미하며, 컴파일러가 소스 파일(.cpp)을 처리하는 단위이다.
- 즉, TU는 #include, #define과 같은 전처리기 지시문이 처리되어 모든 헤더파일과 매크로가 펼쳐진 순수한 C/C++ 코드 덩어리이다.
- 각 TU는 컴파일러에게 전달되어 오브젝트 파일로 컴파일되고, 오브젝트 파일은 링커에게 전달되어 링킹과정을 통해 여러 오브젝트 파일(.o)을 하나의 실행 파일(.exe)로 결합한다.
TU 예제
- 아래 코드에서 main.cpp는 #include "foo.h"가 전처리되고, foo.cpp는 #include "foo.h"와 #include <iostream>가 전처리된다.
- 전처리는 각 소스파일에 대해 독립적으로 처리되며, 처리 후 각각 하나의 TU가 된다.
- 이후 각 TU들은 컴파일러에 의해 오브젝트 파일로 컴파일된다.
// main.cpp
#include "foo.h"
int main() {
foo();
}
// foo.h
void foo();
// foo.cpp
#include "foo.h"
#include <iostream>
void foo() {
std::cout << "Hello\\n";
}
선언(declaration)과 정의(definition)
- C++에서 선언과 정의는 엄격히 구분한다.
- 선언(declaration)이란 TU에 새로운 식별자를 추가하거나, 기존의 선언된 식별자를 재선언하는 것을 의미한다.
- 즉, C++에서 선언은 우리가 흔히 말하는 “XXX을 선언했다”의 선언과 의미가 같다.
- 정의(definition)는 선언을 포함하는 개념으로, 선언된 개체(entity)를 완전히 정의함을 뜻한다.
- 이렇게만 보면 선언과 정의의 차이점이 굉장히 헷갈리는데, 개인적으로 “구현부(중괄호 몸통)가 있으면 정의, 없으면 선언”이라고 이해했다.
- 선언과 정의의 차이를 정리하자면 아래표와 같다.
구분 | 설명 |
선언 (Declaration) | "이런 게 있을 거야"라고 알려주는 것. 크기나 위치를 알 필요는 없음. |
정의 (Definition) | 실제로 공간을 할당하거나 구현을 제공하는 것. 링커가 찾는 대상. |
선언과 정의가 동시에 일어나는 유형들
- 대표적으로 기본 타입의 일반 변수가 있다. 기본 타입의 일반 변수는 전역 변수이든, 멤버 변수이든, 초기화를 하든 안 하든 항상 모든 경우에서 선언과 정의가 동시에 일어난다.
- 여기서 기본 타입의 일반 변수는 아무 키워드(static이나 inline)도 붙지 않은 기본 타입 변수를 의미한다.
int a; // 대입이 없지만, 선언과 정의가 동시에 일어난다.
선언과 정의가 분리되는 유형들
- 식별자가 존재하지만 내부 구현이 되어있지 않은 경우를 “선언되었지만 정의되지 않았다”라고 한다. 이런 경우 컴파일 단계에서 오류가 발생하지 않지만, 링킹 단계에서 구현부를 찾지 못해 링크 오류가 발생하게 된다.
- “선언되었지만 정의되지 않은” 경우가 발생하려면 해당 식별자의 유형이 선언과 정의가 분리될 수 있어야 한다. C++에서는 선언과 정의가 분리되는 유형이 다양한데, 해당 유형들은 다음과 같다.
// 선언과 정의과 분리되는 유형 예제
// 1) 함수(템플릿 함수의 경우 분리가 되지만 같은 공간에 정의도 존재해야한다.)
int add(int a, int b); // 함수 선언.
int add(int a, int b) {return a + b;} // 함수 정의
// 2) 구조체 또는 클래스
struct myStruct; // 구조체 선언(전방 선언 취급)
struct myStruct {}; // 구조체 정의
class myClass; // 클래스 선언(전방 선언 취급)
class myClass {}; // 클래스 정의
// 3) 멤버 함수
class myClass2{
public:
void doSomething(); // 멤버 함수 선언
}
myClass2::doSomething(){}; // 멤버 함수 정의
// 4) 열거형
enum Color; // 열거형 선언
enum Color { RED, GREEN, BLUE }; // 열거형 정의
// 5) extern 전역 변수
extern int g_value; // extern 전역 변수는 선언만 한다.
- static 멤버 변수의 경우 클래스 내부에서 선언이, 클래스 외부에서 정의가 되기 때문에 선언과 정의가 분리되지만, inline static(C++17) 멤버 변수는 클래스 내부에서 정의되기 때문에 분리되지 않는다.
struct S{
int n; // S::n의 정의
static int i; // S::i를 선언, But 정의는 아님
inline static int x; // S::x를 정의
}; // S를 정의
int S::i; // S::i를 정의
유일 정의 규칙(One Definition Rule, ODR)
- 유일 정의 규칙이란 “각 TU에 존재하는 모든 변수, 함수, 클래스, enum, 템플릿 등등의 정의는 유일해야 하며, inline이 아닌 모든 함수나 변수들의 정의는 전체 프로그램에서 유일해야 한다”는 규칙이다.
문장 해석
- “각 TU에 존재하는 모든 변수, 함수, 클래스, enum, 템플릿 등등의 정의는 유일해야 하며,”
- 문장 그대로, 같은 TU안에 여러 번 정의하면 안 된다는 의미이다. 즉, 아래와 같은 코드는 ODR을 위반한다.
int f() { // 함수 f의 첫번째 정의 return 0; } int f() { // 함수 f의 두번째 정의 <-- ODR 위반 return 0; } int f();
- 하지만 "선언이 유일해야 한다"는 내용을 없었으므로, 같은 시그니처를 가진 함수를 여러 번 선언해도 ODR을 위반하지 않는다.
- 따라서, 아래와 같은 코드를 같은 소스 파일에 작성해도 문제가 되지 않는다.
int f(); int f(); int f();
- “inline이 아닌 모든 함수나 변수들의 정의는 프로그램 전체에서 유일해야 한다.”
- 위 문장에서 알 수 있듯 inline 함수나 변수는 ODR의 예외 대상이기 때문에, inline 키워드를 붙이면 여러 번 정의하는 것이 가능하다.
- 하지만 그렇다고 inline 함수가 서로 다른 정의를 가질 수 있는 것은 아니다. inline 함수는 그저 여러 번 정의할 수 있을 뿐이다.
- 만약 inline 함수를 여러 번 정의한 경우, 오류가 발생하진 않지만 링커가 이 중 하나를 선택해 사용하므로, 의도치 않은 inline 함수 정의가 각 TU에서 사용될 수 있다.(이는 굉장히 좋지 못한 결과이다.)
// file1.cpp
inline int func() { return 1; } // OK
// inline int func() { return 2; } // 재정의는 안됨.
// file2.cpp
inline int func() { return 1; } // OK
참고 자료
'C++ > 문법 및 메소드(STL)' 카테고리의 다른 글
[C++][Build] 컴파일 단계 - 심볼, 저장 방식 지정자, 그리고 이름 맹글링 (0) | 2025.04.14 |
---|---|
[C++][Build] 전처리 단계 - #pragma 지시문 (0) | 2025.04.14 |
[C++][Build] 전처리 단계 - 전처리기 지시문(preprocessor directive) (0) | 2025.04.14 |
[C++][Build] 빌드와 빌드의 각 단계 (0) | 2025.04.14 |
[C++] extern 키워드와 전역 변수 (0) | 2025.04.09 |