본문으로 건너뛰기 C++ multiple definition 에러 | '중복 정의' 링커 에러 완벽 해결

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

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

이 글의 핵심

C++ multiple definition 에러의 C++, multiple, definition, 들어가며: "컴파일은 되는데 링크에서 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 에러란?

에러 메시지

/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 위반 예제

C/C++ 예제 코드입니다.

// ❌ 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: 선언과 정의 분리 (권장)

foo 함수의 구현 예제입니다.

// ✅ 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()'

해결:

C/C++ 예제 코드입니다.

// ✅ 해결 1: inline 명시
inline void MyClass::foo() {
    std::cout << "foo\n";
}

// ✅ 해결 2: .cpp로 이동 (권장)
// MyClass.cpp
void MyClass::foo() {
    std::cout << "foo\n";
}

원인 4: 템플릿 특수화를 헤더에 정의

print 함수의 구현 예제입니다.

// ❌ 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)'

해결:

C/C++ 예제 코드입니다.

// ✅ 해결 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'

해결:

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: 선언만 (정의는 한 곳에)

C/C++ 예제 코드입니다.

// 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 변수를 사용할 수 있습니다.

C/C++ 예제 코드입니다.

// ✅ 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를 사용하세요.

C/C++ 예제 코드입니다.

// ✅ 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: 템플릿 특수화

에러 코드:

print 함수의 구현 예제입니다.

// 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>'

해결:

print 함수의 구현 예제입니다.

// ✅ 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

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

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


마치며

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

핵심 원칙:

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

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

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


관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Everything about C++ multiple definition 에러 : from basic concepts to practical applications.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, 링커에러, multiple definition, ODR, 헤더가드, inline, 중복정의 등으로 검색하시면 이 글이 도움이 됩니다.