C++ multiple definition 에러 | "중복 정의" 링커 에러 완벽 해결

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 차이

목차

  1. multiple definition 에러란?
  2. ODR (One Definition Rule)
  3. 5가지 주요 원인과 해결법
  4. inline vs static vs extern
  5. 헤더 파일 작성 규칙
  6. 정리

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.oother.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)은:

  1. 변수·함수는 전체 프로그램에서 최대 한 번만 정의되어야 함
  2. 클래스·템플릿·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 + .cppC++98+✅ 가능런타임에 값 변경 필요
inline 변수C++17+✅ 가능간단한 설정 값
constexprC++11+❌ 불가능컴파일 타임 상수
#defineC++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.cppother.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 없이)

핵심 규칙

  1. 헤더에는 선언만, .cpp에는 정의
  2. 작은 함수는 inline으로 헤더에 정의 (성능 향상)
  3. 전역 변수는 extern + .cpp 정의
  4. 헤더 가드는 필수 (#pragma once 또는 #ifndef)
  5. 클래스 내부 정의는 암시적 inline

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ LNK2019 | “unresolved external symbol” 링커 에러 해결
  • C++ ODR | “단일 정의 규칙” 가이드
  • C++ 헤더 파일 | 선언과 정의 분리 완벽 가이드
  • C++ 링킹 | “Linking” 가이드

마치며

multiple definition 에러는 헤더와 소스 파일의 역할을 이해하면 쉽게 해결됩니다.

핵심 원칙:

  1. 헤더에는 선언, .cpp에는 정의
  2. inline을 활용하세요 (작은 함수, C++17 변수)
  3. 헤더 가드는 필수
  4. extern으로 전역 변수 선언

이 규칙을 지키면 링커 에러를 99% 방지할 수 있습니다. 프로젝트가 커질수록 헤더/소스 분리가 중요해지므로, 처음부터 올바른 습관을 들이세요.

다음 단계: 링커 에러를 해결했다면, C++ 컴파일 과정 완벽 가이드에서 전처리·컴파일·링크 단계를 깊이 이해해 보세요.


관련 글

  • C++ 헤더 온리 라이브러리 |
  • C++ LNK2019 |
  • C++ vtable 에러 |
  • C++ Segmentation fault 원인 5가지와 디버깅 방법 | GDB로 추적하기
  • Visual Studio C++ 빌드 느림 |