C++ multiple definition 에러 | "중복 정의" 링커 에러 완벽 해결
이 글의 핵심
C++ multiple definition 에러에 대한 실전 가이드입니다.
들어가며: “컴파일은 되는데 링크에서 multiple definition…"
"헤더 파일에 함수를 정의했더니 에러가 나요”
C++ 프로젝트를 여러 파일로 나누다 보면 multiple definition of 에러를 만나게 됩니다. 컴파일은 성공하는데 링크 단계에서 실패합니다.
// utils.h
void foo() { // ❌ 헤더에 정의
std::cout << "foo\n";
}
// main.cpp
#include "utils.h"
// other.cpp
#include "utils.h"
// 링크 에러:
// multiple definition of 'foo()'
// first defined here: main.o
// also defined here: other.o
이 글에서 다루는 것:
- multiple definition 에러가 왜 나는지
- ODR (One Definition Rule) 이해하기
- 5가지 주요 원인과 해결법
- 헤더 파일 작성 규칙
- inline, static, extern 차이
목차
- multiple definition 에러란?
- ODR (One Definition Rule)
- 5가지 주요 원인과 해결법
- inline vs static vs extern
- 헤더 파일 작성 규칙
- 정리
1. multiple definition 에러란?
에러 메시지
/usr/bin/ld: other.o: in function `foo()':
other.cpp:(.text+0x0): multiple definition of `foo()';
main.o:main.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
의미: foo() 함수가 main.o와 other.o 두 곳에서 정의되어 링커가 어느 것을 사용할지 모릅니다.
발생 과정
flowchart TB
H[utils.h: foo 정의]
M[main.cpp: #include utils.h]
O[other.cpp: #include utils.h]
M --> M1[main.o: foo 정의]
O --> O1[other.o: foo 정의]
M1 --> L[링커]
O1 --> L
L --> E[multiple definition 에러]
핵심: 헤더 파일을 여러 .cpp가 포함하면, 각 .cpp마다 함수가 정의됩니다.
2. ODR (One Definition Rule)
규칙
C++ 표준의 ODR(One Definition Rule)은:
- 변수·함수는 전체 프로그램에서 최대 한 번만 정의되어야 함
- 클래스·템플릿·inline 함수는 여러 번 정의 가능 (단, 모두 동일해야 함)
ODR 위반 예제
// ❌ ODR 위반
// file1.cpp
int globalVar = 42;
// file2.cpp
int globalVar = 99; // ❌ 중복 정의
// 링크 에러: multiple definition of 'globalVar'
ODR 준수 예제
// ✅ ODR 준수
// header.h
extern int globalVar; // 선언만
// file1.cpp
int globalVar = 42; // 정의 (한 곳에만)
// file2.cpp
#include "header.h"
// globalVar 사용 가능 (정의 없음)
3. 5가지 주요 원인과 해결법
원인 1: 헤더에 함수 정의
가장 흔한 원인: 헤더 파일에 일반 함수를 정의함.
// ❌ utils.h
void foo() { // 헤더에 정의
std::cout << "foo\n";
}
// main.cpp
#include "utils.h"
// other.cpp
#include "utils.h"
// 링크 에러: multiple definition of 'foo()'
해결법 1: 선언과 정의 분리 (권장)
// ✅ utils.h
void foo(); // 선언만
// utils.cpp
void foo() { // 정의
std::cout << "foo\n";
}
해결법 2: inline 키워드
// ✅ utils.h
inline void foo() { // inline: 여러 번 정의 허용
std::cout << "foo\n";
}
해결법 3: static 키워드 (비권장)
// ✅ utils.h
static void foo() { // 각 .cpp마다 별도 복사본
std::cout << "foo\n";
}
주의: static은 각 .cpp 파일마다 별도 함수를 만들어 코드 크기가 증가합니다.
원인 2: 헤더에 전역 변수 정의
// ❌ config.h
int maxConnections = 100; // 헤더에 정의
// main.cpp
#include "config.h"
// server.cpp
#include "config.h"
// 링크 에러: multiple definition of 'maxConnections'
해결법 1: extern 사용 (권장)
// ✅ config.h
extern int maxConnections; // 선언만
// config.cpp
int maxConnections = 100; // 정의 (한 곳에만)
해결법 2: inline 변수 (C++17)
// ✅ config.h
inline int maxConnections = 100; // C++17: inline 변수
해결법 3: constexpr
// ✅ config.h
constexpr int maxConnections = 100; // constexpr는 암시적 inline
원인 3: 클래스 멤버 함수를 헤더에 정의
주의: 클래스 내부에 정의하면 암시적 inline이므로 OK.
// ✅ OK (암시적 inline)
// MyClass.h
class MyClass {
public:
void foo() { // 클래스 내부 정의 → 암시적 inline
std::cout << "foo\n";
}
};
에러 발생: 클래스 외부에 정의하면 inline 명시 필요.
// ❌ MyClass.h
class MyClass {
public:
void foo(); // 선언
};
void MyClass::foo() { // ❌ 클래스 외부 정의 (inline 없음)
std::cout << "foo\n";
}
// 링크 에러: multiple definition of 'MyClass::foo()'
해결:
// ✅ 해결 1: inline 명시
inline void MyClass::foo() {
std::cout << "foo\n";
}
// ✅ 해결 2: .cpp로 이동 (권장)
// MyClass.cpp
void MyClass::foo() {
std::cout << "foo\n";
}
원인 4: 템플릿 특수화를 헤더에 정의
// ❌ utils.h
template <typename T>
void print(T value) {
std::cout << value << '\n';
}
// 명시적 특수화 (inline 없음)
template <>
void print<int>(int value) { // ❌ 중복 정의
std::cout << "int: " << value << '\n';
}
// 링크 에러: multiple definition of 'void print<int>(int)'
해결:
// ✅ 해결 1: inline 명시
template <>
inline void print<int>(int value) {
std::cout << "int: " << value << '\n';
}
// ✅ 해결 2: .cpp로 이동
// utils.cpp
template <>
void print<int>(int value) {
std::cout << "int: " << value << '\n';
}
원인 5: 헤더 가드 없음 (같은 파일 내 중복)
// ❌ utils.h (헤더 가드 없음)
void foo() {
std::cout << "foo\n";
}
// main.cpp
#include "utils.h"
#include "utils.h" // 두 번 포함 → 같은 파일 내 중복 정의
// 컴파일 에러: redefinition of 'foo'
해결:
// ✅ utils.h
#ifndef UTILS_H
#define UTILS_H
void foo(); // 선언만
#endif
// 또는 #pragma once (대부분의 컴파일러 지원)
#pragma once
void foo();
4. inline vs static vs extern
inline: 여러 번 정의 허용 (권장)
// utils.h
inline void foo() { // 여러 .cpp에서 포함해도 OK
std::cout << "foo\n";
}
특징:
- 링커가 하나만 선택
- 코드 크기 증가 없음
- 인라인 최적화 가능
static: 각 파일마다 별도 복사본
// utils.h
static void foo() { // 각 .cpp마다 별도 함수
std::cout << "foo\n";
}
특징:
- 각 .cpp 파일마다 별도 함수
- 코드 크기 증가
- 내부 링크 (다른 파일에서 접근 불가)
단점: 전역 변수를 static으로 만들면 각 파일마다 다른 인스턴스가 됩니다.
// ❌ 의도와 다름
// config.h
static int counter = 0;
// main.cpp
#include "config.h"
++counter; // main.cpp의 counter
// other.cpp
#include "config.h"
++counter; // other.cpp의 counter (다른 변수!)
extern: 선언만 (정의는 한 곳에)
// config.h
extern int maxConnections; // 선언만
// config.cpp
int maxConnections = 100; // 정의 (한 곳에만)
// main.cpp, other.cpp
#include "config.h"
// maxConnections 사용 가능 (같은 변수)
비교표
| 키워드 | 헤더에 정의 | 복사본 | 코드 크기 | 사용 시기 |
|---|---|---|---|---|
| inline | 가능 | 하나 | 작음 | 작은 함수 (권장) |
| static | 가능 | 여러 개 | 큼 | 파일 내부 전용 |
| extern | 선언만 | 하나 | 작음 | 전역 변수 |
| (없음) | 불가 | 하나 | 작음 | .cpp에 정의 |
5. 헤더 파일 작성 규칙
헤더에 넣어도 되는 것
// ✅ OK
class MyClass {
void foo() { // 클래스 내부 정의 → 암시적 inline
// ...
}
};
// ✅ OK
inline void bar() { // inline 명시
// ...
}
// ✅ OK
template <typename T>
void baz(T value) { // 템플릿 → 암시적 inline
// ...
}
// ✅ OK
constexpr int MAX_SIZE = 100; // constexpr → 암시적 inline
// ✅ OK (C++17)
inline int globalVar = 42; // inline 변수
헤더에 넣으면 안 되는 것
// ❌ 일반 함수 정의
void foo() {
// ...
}
// ❌ 전역 변수 정의
int globalVar = 42;
// ❌ static 멤버 변수 정의
class MyClass {
static int count;
};
int MyClass::count = 0; // .cpp로 이동해야 함
// ❌ 템플릿 명시적 특수화 (inline 없이)
template <>
void print<int>(int value) {
// ...
}
올바른 헤더/소스 분리
// ✅ MyClass.h
class MyClass {
public:
void foo(); // 선언만
void bar() { // 클래스 내부 정의 (OK)
// ...
}
static int count; // 선언만
};
// ✅ MyClass.cpp
void MyClass::foo() { // 정의
// ...
}
int MyClass::count = 0; // static 멤버 정의
실전 사례 분석
사례 1: 유틸리티 함수 중복 정의
문제 상황: 여러 소스 파일에서 사용하는 유틸리티 함수를 헤더에 정의했더니 링크 에러 발생.
에러 코드:
// utils.h
#ifndef UTILS_H
#define UTILS_H
#include <string>
#include <algorithm>
// 문자열 앞뒤 공백 제거
std::string trim(const std::string& s) { // ❌ inline 없음
auto start = s.find_first_not_of(" \t\n\r");
auto end = s.find_last_not_of(" \t\n\r");
if (start == std::string::npos) {
return "";
}
return s.substr(start, end - start + 1);
}
#endif
// main.cpp
#include "utils.h"
#include <iostream>
int main() {
std::string text = " hello ";
std::cout << trim(text) << '\n'; // "hello"
return 0;
}
// parser.cpp
#include "utils.h"
#include <iostream>
void parse(const std::string& input) {
std::string cleaned = trim(input);
std::cout << "Parsed: " << cleaned << '\n';
}
// 컴파일:
// g++ main.cpp parser.cpp -o app
//
// 링크 에러:
// /usr/bin/ld: parser.o: in function `trim(std::string const&)':
// parser.cpp:(.text+0x0): multiple definition of `trim(std::string const&)';
// main.o:main.cpp:(.text+0x0): first defined here
에러 원인 분석:
1. main.cpp가 utils.h를 include
→ main.o에 trim() 함수 정의 포함
2. parser.cpp가 utils.h를 include
→ parser.o에 trim() 함수 정의 포함
3. 링커가 main.o와 parser.o를 합칠 때
→ trim() 함수가 두 곳에 정의되어 충돌!
해결법 1: inline 키워드 추가 (권장)
// ✅ utils.h
#ifndef UTILS_H
#define UTILS_H
#include <string>
#include <algorithm>
inline std::string trim(const std::string& s) { // inline 추가
auto start = s.find_first_not_of(" \t\n\r");
auto end = s.find_last_not_of(" \t\n\r");
if (start == std::string::npos) {
return "";
}
return s.substr(start, end - start + 1);
}
#endif
// 이제 링크 에러 없음!
// inline 함수는 여러 번 정의되어도 링커가 하나만 선택
해결법 2: 선언과 정의 분리 (큰 함수에 적합)
// ✅ utils.h (선언만)
#ifndef UTILS_H
#define UTILS_H
#include <string>
std::string trim(const std::string& s); // 선언만
#endif
// ✅ utils.cpp (정의)
#include "utils.h"
#include <algorithm>
std::string trim(const std::string& s) { // 정의 (한 곳에만)
auto start = s.find_first_not_of(" \t\n\r");
auto end = s.find_last_not_of(" \t\n\r");
if (start == std::string::npos) {
return "";
}
return s.substr(start, end - start + 1);
}
// 컴파일:
// g++ main.cpp parser.cpp utils.cpp -o app
// 이제 utils.cpp만 trim() 정의를 가지므로 에러 없음
inline vs 분리 선택 기준:
| 함수 특징 | 권장 방법 | 이유 |
|---|---|---|
| 작은 함수 (3-5줄) | inline | 인라인 최적화, 헤더에 정의 편리 |
| 큰 함수 (10줄+) | 분리 | 컴파일 시간 단축, 코드 크기 감소 |
| 템플릿 함수 | 헤더에 정의 | 템플릿은 헤더에 있어야 함 |
| 자주 변경되는 함수 | 분리 | 헤더 변경 시 전체 재컴파일 방지 |
사례 2: 전역 설정 변수
문제 상황: 프로젝트 전체에서 사용하는 설정 값을 헤더에 정의했더니 링크 에러 발생.
에러 코드:
// config.h
#ifndef CONFIG_H
#define CONFIG_H
// 전역 설정 변수
int maxThreads = 8; // ❌ 헤더에 정의
int maxConnections = 100;
int port = 8080;
#endif
// main.cpp
#include "config.h"
#include <iostream>
int main() {
std::cout << "Port: " << port << '\n';
std::cout << "Max threads: " << maxThreads << '\n';
return 0;
}
// server.cpp
#include "config.h"
#include <iostream>
void start_server() {
std::cout << "Starting server on port " << port << '\n';
std::cout << "Max connections: " << maxConnections << '\n';
}
// 컴파일:
// g++ main.cpp server.cpp -o app
//
// 링크 에러:
// /usr/bin/ld: server.o:(.data+0x0): multiple definition of `maxThreads';
// main.o:(.data+0x0): first defined here
// /usr/bin/ld: server.o:(.data+0x4): multiple definition of `maxConnections';
// main.o:(.data+0x4): first defined here
해결법 1: extern 사용 (권장)
// ✅ config.h (선언만)
#ifndef CONFIG_H
#define CONFIG_H
// 선언: 이 변수가 어딘가에 정의되어 있다고 알림
extern int maxThreads;
extern int maxConnections;
extern int port;
#endif
// ✅ config.cpp (정의 - 한 곳에만)
#include "config.h"
// 정의: 실제 메모리 할당
int maxThreads = 8;
int maxConnections = 100;
int port = 8080;
// 컴파일:
// g++ main.cpp server.cpp config.cpp -o app
// 이제 config.cpp만 변수를 정의하므로 에러 없음
extern의 동작 원리:
config.h (선언):
"maxThreads라는 int 변수가 어딘가에 있어요"
config.cpp (정의):
"maxThreads를 여기서 실제로 만듭니다" (메모리 할당)
main.cpp, server.cpp:
"config.h를 include하여 maxThreads 존재를 알고 사용"
링커:
"모든 .o 파일을 합칠 때 maxThreads는 config.o에만 있네요. OK!"
해결법 2: inline 변수 (C++17)
C++17부터는 inline 변수를 사용할 수 있습니다.
// ✅ config.h (C++17)
#ifndef CONFIG_H
#define CONFIG_H
inline int maxThreads = 8; // C++17: inline 변수
inline int maxConnections = 100;
inline int port = 8080;
#endif
// config.cpp 불필요!
// 컴파일:
// g++ -std=c++17 main.cpp server.cpp -o app
해결법 3: constexpr (컴파일 타임 상수)
값이 변하지 않으면 constexpr를 사용하세요.
// ✅ config.h
#ifndef CONFIG_H
#define CONFIG_H
constexpr int maxThreads = 8; // constexpr는 암시적 inline
constexpr int maxConnections = 100;
constexpr int port = 8080;
#endif
// config.cpp 불필요!
// constexpr는 컴파일 타임 상수이므로 중복 정의 문제 없음
비교표:
| 방법 | C++ 버전 | 런타임 수정 | 사용 시기 |
|---|---|---|---|
| extern + .cpp | C++98+ | ✅ 가능 | 런타임에 값 변경 필요 |
| inline 변수 | C++17+ | ✅ 가능 | 간단한 설정 값 |
| constexpr | C++11+ | ❌ 불가능 | 컴파일 타임 상수 |
| #define | C++98+ | ❌ 불가능 | 매크로 (타입 없음) |
실전 패턴 - 설정 클래스:
// ✅ config.h
#ifndef CONFIG_H
#define CONFIG_H
class Config {
public:
static int maxThreads;
static int maxConnections;
static int port;
static void load_from_file(const std::string& path);
};
#endif
// ✅ config.cpp
#include "config.h"
// static 멤버 변수 정의 (한 곳에만)
int Config::maxThreads = 8;
int Config::maxConnections = 100;
int Config::port = 8080;
void Config::load_from_file(const std::string& path) {
// 파일에서 설정 로드
}
// 사용
// Config::port = 9000; // 런타임에 변경 가능
사례 3: 템플릿 특수화
에러 코드:
// printer.h
template <typename T>
void print(T value) {
std::cout << value << '\n';
}
template <>
void print<std::string>(std::string value) { // ❌ inline 없음
std::cout << "String: " << value << '\n';
}
// 링크 에러: multiple definition of 'void print<std::string>'
해결:
// ✅ printer.h
template <typename T>
void print(T value) {
std::cout << value << '\n';
}
template <>
inline void print<std::string>(std::string value) { // inline 추가
std::cout << "String: " << value << '\n';
}
헤더 가드 vs 중복 정의
헤더 가드의 역할
헤더 가드는 같은 파일 내에서 중복 포함을 방지합니다.
// utils.h
#ifndef UTILS_H
#define UTILS_H
void foo();
#endif
// main.cpp
#include "utils.h"
#include "utils.h" // 두 번째는 무시됨 (헤더 가드)
헤더 가드로 막을 수 없는 것:
// utils.h
#ifndef UTILS_H
#define UTILS_H
void foo() { // ❌ 정의
// ...
}
#endif
// main.cpp
#include "utils.h" // foo 정의 포함
// other.cpp
#include "utils.h" // foo 정의 포함
// 링크 에러: 헤더 가드로는 막을 수 없음!
이유: main.cpp와 other.cpp는 별도로 컴파일되므로, 각각 foo를 정의합니다.
정리
에러 해결 체크리스트
| 에러 메시지 | 원인 | 해결법 |
|---|---|---|
multiple definition of 'foo()' | 헤더에 함수 정의 | inline 추가 또는 .cpp로 이동 |
multiple definition of 'globalVar' | 헤더에 전역 변수 정의 | extern 선언 + .cpp에 정의 |
multiple definition of 'MyClass::count' | static 멤버 정의 중복 | .cpp에 한 번만 정의 |
redefinition of 'foo' | 같은 파일 내 중복 | 헤더 가드 추가 |
헤더 파일 작성 규칙 요약
헤더에 넣어도 되는 것:
- 함수 선언
- 클래스 정의 (멤버 함수 포함)
- inline 함수 정의
- template 함수 정의
- constexpr 변수/함수
- inline 변수 (C++17)
헤더에 넣으면 안 되는 것:
- 일반 함수 정의 (inline 없이)
- 전역 변수 정의 (extern 없이)
- static 멤버 변수 정의
- 템플릿 명시적 특수화 (inline 없이)
핵심 규칙
- 헤더에는 선언만, .cpp에는 정의
- 작은 함수는 inline으로 헤더에 정의 (성능 향상)
- 전역 변수는 extern + .cpp 정의
- 헤더 가드는 필수 (
#pragma once또는#ifndef) - 클래스 내부 정의는 암시적 inline
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ LNK2019 | “unresolved external symbol” 링커 에러 해결
- C++ ODR | “단일 정의 규칙” 가이드
- C++ 헤더 파일 | 선언과 정의 분리 완벽 가이드
- C++ 링킹 | “Linking” 가이드
마치며
multiple definition 에러는 헤더와 소스 파일의 역할을 이해하면 쉽게 해결됩니다.
핵심 원칙:
- 헤더에는 선언, .cpp에는 정의
- inline을 활용하세요 (작은 함수, C++17 변수)
- 헤더 가드는 필수
- extern으로 전역 변수 선언
이 규칙을 지키면 링커 에러를 99% 방지할 수 있습니다. 프로젝트가 커질수록 헤더/소스 분리가 중요해지므로, 처음부터 올바른 습관을 들이세요.
다음 단계: 링커 에러를 해결했다면, C++ 컴파일 과정 완벽 가이드에서 전처리·컴파일·링크 단계를 깊이 이해해 보세요.
관련 글
- C++ 헤더 온리 라이브러리 |
- C++ LNK2019 |
- C++ vtable 에러 |
- C++ Segmentation fault 원인 5가지와 디버깅 방법 | GDB로 추적하기
- Visual Studio C++ 빌드 느림 |