C++ 컴파일 오류 해결 가이드 | 초보자가 자주 겪는 10가지 에러
이 글의 핵심
에러 메시지가 무슨 뜻인지 모르면 같은 실수를 반복하게 됩니다. 각 오류마다 실제 메시지 예시와 흔히 생기는 상황을 먼저 보여 주고 고친 코드를 제시하므로, 템플릿 함수 정의 위치나 헤더 중복 포함, 삭제된 함수 호출처럼 원인이 메시지에 바로 드러나지 않는 경우도 스스로 추적할 수 있게 됩니다.
들어가며: “에러 메시지가 무슨 뜻인지 모르겠어요”
C++을 처음 배울 때 가장 답답한 순간은 컴파일 오류 메시지를 마주할 때입니다. 에러 메시지는 길고 복잡하며, 무엇이 문제인지 파악하기 어렵습니다.
이 글에서는 C++ 초보자가 자주 겪는 10가지 컴파일 오류와 즉시 적용 가능한 해결 방법을 정리했습니다. 각 에러마다 원인, 에러 메시지, 해결 방법, 실전 예제를 포함했습니다.
undefined reference to ‘function’
원인
함수 선언은 있지만 정의(구현)가 없거나, 링커가 구현을 찾지 못할 때 발생합니다.
에러 메시지
undefined reference to `myFunction()'
흔한 상황
- 헤더에 선언만 하고 .cpp에 구현 안 함
- 링크 시 해당 .o 파일을 포함하지 않음
- 템플릿 함수를 .cpp에 정의함 (템플릿은 헤더에 정의해야 함)
해결 방법
// ❌ 잘못된 예: 선언만 있고 정의 없음
// header.h
void myFunction();
// main.cpp
#include "header.h"
int main() {
myFunction(); // undefined reference!
}
// ✅ 올바른 예: 정의 추가
// header.h
void myFunction();
// impl.cpp
#include "header.h"
void myFunction() {
// 구현
}
// 컴파일 시 모든 .cpp 파일 포함
// g++ main.cpp impl.cpp -o program
undefined reference는 컴파일 에러가 아니라 링크 에러라는 점부터 구분해야 합니다. 에러 메시지 앞에 main.cpp:10:5: 같은 줄 번호가 아니라 /usr/bin/ld:나 collect2: error: ld returned 1 exit status가 붙어 있다면, 각 .cpp 파일은 문제없이 컴파일되었고 마지막에 조각들을 합치는 단계에서 정의를 찾지 못한 것입니다. 그래서 코드를 아무리 봐도 문법 문제가 없어 보이는 것이 정상입니다. 위의 두 경우 외에 자주 보는 원인은 다음과 같습니다.
- static 멤버 변수 정의 누락: 클래스 안에
static int count;만 선언하고.cpp에int MyClass::count = 0;을 두지 않으면undefined reference to 'MyClass::count'가 납니다. C++17부터는 클래스 안에서static inline int count = 0;으로 해결할 수 있습니다. - 가상 함수 정의 누락: 가상 함수 중 하나라도 정의하지 않으면
undefined reference to 'vtable for MyClass'라는 알쏭달쏭한 메시지가 나옵니다. 보통 가상 소멸자나 첫 번째 비인라인 가상 함수의 정의를 빠뜨린 경우입니다. - 라이브러리 링크 누락·순서:
-lpthread,-lm같은 라이브러리를 빠뜨렸거나, GCC에서g++ -lfoo main.cpp처럼 라이브러리를 사용하는 파일보다 앞에 적으면 정의를 찾지 못합니다. 라이브러리는 명령줄 뒤쪽에 둡니다. - C 라이브러리 함수: C로 컴파일된 함수를 C++에서 부를 때 헤더에
extern "C"가 없으면, C++ 컴파일러가 이름을 다르게 꾸며(name mangling) 링커가 찾지 못합니다.
템플릿 함수의 경우
// ❌ 잘못된 예: 템플릿을 .cpp에 정의
// header.h
template<typename T>
void process(T value);
// impl.cpp
template<typename T>
void process(T value) {
// 구현
}
// ✅ 올바른 예: 템플릿을 헤더에 정의
// header.h
template<typename T>
void process(T value) {
// 구현 (헤더에 작성)
}
템플릿은 함수 자체가 아니라 “함수를 만드는 틀”이라서, process<int>가 필요하다는 사실을 아는 곳(main.cpp)에서 정의를 볼 수 있어야 실제 함수가 만들어집니다. impl.cpp에 정의만 두면 그 파일은 어떤 타입으로 쓰일지 모르니 아무 함수도 만들지 않고, main.cpp는 정의를 모르니 호출만 남겨 링크 단계에서 실패합니다. 쓰이는 타입이 정해져 있다면 impl.cpp 끝에 template void process<int>(int);처럼 명시적 인스턴스화를 적어 헤더에 구현을 노출하지 않는 방법도 있습니다.
segmentation fault (core dumped)
원인
잘못된 메모리 접근으로 프로그램이 크래시됩니다. 주로 nullptr 역참조, 배열 범위 초과, 메모리 해제 후 사용 등이 원인입니다.
에러 메시지
Segmentation fault (core dumped)
흔한 상황
- nullptr 역참조
- 배열 범위 초과
- delete 후 사용 (dangling pointer)
- 스택 오버플로우 (무한 재귀)
해결 방법
// ❌ nullptr 역참조
int* ptr = nullptr;
*ptr = 10; // segmentation fault!
// ✅ nullptr 체크
int* ptr = nullptr;
if (ptr != nullptr) {
*ptr = 10;
}
// ❌ 배열 범위 초과
int arr[5];
arr[10] = 100; // segmentation fault!
// ✅ 범위 체크 또는 vector 사용
std::vector<int> vec(5);
vec.at(10) = 100; // 예외 발생 (안전)
// ❌ delete 후 사용
int* ptr = new int(10);
delete ptr;
*ptr = 20; // segmentation fault!
// ✅ delete 후 nullptr 할당
int* ptr = new int(10);
delete ptr;
ptr = nullptr;
if (ptr != nullptr) {
*ptr = 20;
}
디버깅 팁
# gdb로 디버깅
g++ -g program.cpp -o program
gdb ./program
(gdb) run
# 크래시 지점 확인
(gdb) backtrace
주석에 segmentation fault!라고 적었지만, 배열 범위 초과와 해제 후 사용은 미정의 동작이라 크래시가 난다는 보장이 없다는 점이 가장 무서운 부분입니다. arr[10] = 100은 운영체제가 막는 메모리 영역을 건드릴 때만 segfault가 나고, 대부분은 옆에 있는 다른 지역 변수나 함수의 반환 주소를 조용히 덮어써서 전혀 다른 곳에서 이상한 값이나 크래시로 나타납니다. delete 후의 *ptr = 20도 해제된 메모리가 아직 프로세스에 남아 있으면 “잘 동작하는 것처럼” 보입니다. 그래서 처음 C++을 배울 때는 g++ -g -fsanitize=address,undefined main.cpp로 빌드하는 습관을 들이는 것을 권합니다. AddressSanitizer는 범위 초과나 해제 후 사용이 일어나는 바로 그 줄에서 heap-buffer-overflow, heap-use-after-free 같은 이름과 함께 멈춰 주므로, 원인을 찾는 시간이 크게 줄어듭니다. delete 후 ptr = nullptr을 넣는 습관은 같은 포인터 변수의 재사용은 막지만, 같은 메모리를 가리키던 다른 포인터 변수는 여전히 댕글링이라는 한계가 있습니다.
error: redefinition of ‘class/function’
원인
같은 클래스나 함수를 여러 번 정의했을 때 발생합니다. 주로 헤더 파일을 여러 번 포함할 때 나타납니다.
에러 메시지
error: redefinition of 'class MyClass'
error: redefinition of 'void myFunction()'
흔한 상황
- 헤더 파일에 include guard 없음
- 헤더에 함수 정의를 넣음 (inline 아닌 경우)
해결 방법
// ❌ include guard 없음
// header.h
class MyClass {
// ...
};
// ✅ include guard 사용
// header.h
#ifndef HEADER_H
#define HEADER_H
class MyClass {
// ...
};
#endif
// 또는 #pragma once 사용 (더 간단)
// header.h
#pragma once
class MyClass {
// ...
};
include guard가 막아 주는 것은 한 .cpp 파일 안에서 같은 헤더가 두 번 포함되는 경우뿐입니다. a.h와 b.h가 모두 common.h를 포함하고 main.cpp가 둘 다 포함하면 common.h의 클래스 정의가 두 번 들어가 redefinition 에러가 나는데, 가드가 두 번째 포함을 건너뛰게 해 줍니다. 반면 헤더에 함수 정의를 넣으면 가드가 있어도 문제가 남습니다. main.cpp와 util.cpp가 각각 그 헤더를 포함하면 두 파일에 같은 함수가 하나씩 생기고, 이번에는 컴파일이 아니라 링크 단계에서 multiple definition of 'myFunction()' 에러가 납니다. 아래처럼 정의를 .cpp로 옮기거나 inline을 붙이는 이유가 이것입니다.
함수 정의의 경우
// ❌ 헤더에 함수 정의 (여러 .cpp에서 include 시 중복)
// header.h
void myFunction() {
// 구현
}
// ✅ 헤더에는 선언만, .cpp에 정의
// header.h
void myFunction();
// impl.cpp
void myFunction() {
// 구현
}
// 또는 inline 사용
// header.h
inline void myFunction() {
// 구현
}
error: no matching function for call
원인
함수 호출 시 인자 타입이나 개수가 맞지 않을 때 발생합니다.
에러 메시지
error: no matching function for call to 'myFunction(int)'
note: candidate: void myFunction(double)
흔한 상황
- 인자 타입 불일치
- 인자 개수 불일치
- const 불일치
해결 방법
// ❌ 인자 타입 불일치
void process(double value) {
// ...
}
int main() {
std::string str = "hello";
process(str); // error: no matching function!
}
// ✅ 타입 맞춤
void process(const std::string& value) {
// ...
}
int main() {
std::string str = "hello";
process(str); // OK
}
// ❌ const 불일치
void print(std::string& str) {
// ...
}
int main() {
const std::string str = "hello";
print(str); // error: no matching function!
}
// ✅ const 추가
void print(const std::string& str) {
// ...
}
이 에러를 읽을 때는 첫 줄보다 note: candidate: 줄들이 중요합니다. 컴파일러가 호출할 수 있는 후보 함수를 모두 나열하고, 각 후보 아래에 no known conversion for argument 1 from 'std::string' to 'double'처럼 왜 탈락했는지를 적어 주기 때문입니다. const 불일치의 경우 cannot bind non-const lvalue reference of type 'std::string&' to an rvalue of type 'std::string' 같은 메시지로 나타나기도 하는데, print("hello")처럼 문자열 리터럴(임시 std::string)을 std::string&에 넘길 때 특히 자주 봅니다. 값을 수정하지 않는 매개변수는 처음부터 const std::string&로 받는 습관이 이 에러의 대부분을 없애 줍니다.
error: ‘identifier’ was not declared in this scope
원인
변수나 함수를 선언하지 않고 사용했을 때 발생합니다.
에러 메시지
error: 'myVariable' was not declared in this scope
error: 'myFunction' was not declared in this scope
흔한 상황
- 헤더 파일 include 안 함
- 네임스페이스 지정 안 함
- 변수 선언 전에 사용
해결 방법
// ❌ 헤더 include 안 함
int main() {
std::cout << "Hello\n"; // error: 'cout' was not declared!
}
// ✅ 헤더 include
#include <iostream>
int main() {
std::cout << "Hello\n"; // OK
}
// ❌ 네임스페이스 지정 안 함
#include <vector>
int main() {
vector<int> vec; // error: 'vector' was not declared!
}
// ✅ std:: 추가 또는 using namespace
#include <vector>
int main() {
std::vector<int> vec; // OK
}
// ❌ 변수 선언 전에 사용
int main() {
x = 10; // error: 'x' was not declared!
int x;
}
// ✅ 선언 후 사용
int main() {
int x;
x = 10; // OK
}
warning: implicit conversion loses integer precision
원인
타입 변환 시 데이터 손실 가능성이 있을 때 경고가 발생합니다.
에러 메시지
warning: implicit conversion loses integer precision: 'long' to 'int'
흔한 상황
- 큰 타입 → 작은 타입 변환
- size_t → int 변환
이 메시지는 Clang(특히 macOS의 Xcode 기본 컴파일러)이 내는 형식입니다. GCC는 기본 설정에서는 이 변환에 경고를 내지 않고,
-Wconversion을 켜야conversion from 'long int' to 'int' may change value처럼 표시합니다. 같은 코드가 한 환경에서는 조용하고 다른 환경에서는 경고투성이인 이유가 이것입니다. 경고를 없애려고static_cast를 붙이는 것은 “값이 잘릴 수 있다는 걸 알고 있다”는 표시일 뿐 잘림을 막아 주지는 않으므로, 실제로 값이int범위를 넘을 수 있다면 타입을 넓히는 것이 올바른 해결입니다.
해결 방법
// ❌ 암시적 변환 (경고 발생)
long bigNumber = 1000000000L;
int smallNumber = bigNumber; // warning!
// ✅ 명시적 캐스팅
long bigNumber = 1000000000L;
int smallNumber = static_cast<int>(bigNumber);
// ❌ size_t → int 변환
std::vector<int> vec = {1, 2, 3};
int size = vec.size(); // warning!
// ✅ size_t 사용 또는 명시적 캐스팅
std::vector<int> vec = {1, 2, 3};
size_t size = vec.size(); // OK
// 또는
int size = static_cast<int>(vec.size());
error: use of deleted function
원인
복사 생성자나 대입 연산자가 삭제된 객체를 복사하려 할 때 발생합니다.
에러 메시지
error: use of deleted function 'MyClass::MyClass(const MyClass&)'
흔한 상황
- unique_ptr 복사
- 복사 생성자가 = delete인 클래스
해결 방법
// ❌ unique_ptr 복사
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2 = ptr1; // error: use of deleted function!
// ✅ move 사용
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2 = std::move(ptr1); // OK
// ❌ 복사 생성자 삭제된 클래스
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
};
NonCopyable obj1;
NonCopyable obj2 = obj1; // error: use of deleted function!
// ✅ 참조 사용 또는 포인터 사용
NonCopyable obj1;
NonCopyable& obj2 = obj1; // OK (참조)
이 에러는 코드에 delete를 쓴 적이 없어도 나옵니다. 클래스에 std::unique_ptr나 std::mutex 같은 복사할 수 없는 멤버가 하나라도 있으면 컴파일러가 만든 복사 생성자가 자동으로 삭제되고, 그 클래스를 std::vector에 push_back하거나 값으로 넘기는 순간 use of deleted function 'MyClass::MyClass(const MyClass&)'와 함께 note: 'MyClass::MyClass(const MyClass&)' is implicitly deleted because the default definition would be ill-formed라는 설명이 붙습니다. 이 note 아래를 따라가면 어느 멤버 때문에 삭제되었는지 알려 주므로, 에러의 첫 줄만 보지 말고 note를 끝까지 읽는 것이 해결의 지름길입니다.
error: expected ’;’ before ’}‘
원인
세미콜론 누락 등 문법 오류가 있을 때 발생합니다.
에러 메시지
error: expected ';' before '}'
흔한 상황
- 클래스 정의 끝에 세미콜론 누락
- 변수 선언 끝에 세미콜론 누락
해결 방법
// ❌ 클래스 정의 끝에 세미콜론 누락
class MyClass {
int value;
} // error: expected ';'!
// ✅ 세미콜론 추가
class MyClass {
int value;
}; // OK
// ❌ 변수 선언 끝에 세미콜론 누락
int main() {
int x = 10
int y = 20; // error: expected ';'!
}
// ✅ 세미콜론 추가
int main() {
int x = 10;
int y = 20; // OK
}
세미콜론 누락은 컴파일러가 다음 줄을 읽고 나서야 문법이 깨졌다는 것을 알기 때문에, 에러가 가리키는 줄이 실제로 고칠 줄의 바로 아래인 경우가 많습니다. 특히 클래스 정의 끝의 ;를 빠뜨리면 그 헤더를 포함한 .cpp 파일의 첫 줄에서 expected ';' after class definition이나 multiple types in one declaration, 심하면 표준 헤더 안에서 수십 개의 에러가 쏟아져 헤더가 고장 난 것처럼 보입니다. 에러가 엉뚱한 파일을 가리킨다면 그 직전에 포함된 내 헤더의 마지막 줄부터 확인하세요.
error: invalid use of incomplete type
원인
전방 선언만 있고 정의가 없는 클래스를 사용하려 할 때 발생합니다.
에러 메시지
error: invalid use of incomplete type 'class MyClass'
흔한 상황
- 전방 선언만 하고 헤더 include 안 함
- 순환 의존성 (circular dependency)
해결 방법
// ❌ 전방 선언만 하고 사용
class MyClass; // 전방 선언
int main() {
MyClass obj; // error: invalid use of incomplete type!
obj.doSomething();
}
// ✅ 헤더 include
#include "MyClass.h"
int main() {
MyClass obj; // OK
obj.doSomething();
}
// 순환 의존성 해결
// A.h
#pragma once
class B; // 전방 선언
class A {
B* b; // 포인터는 OK
};
// B.h
#pragma once
class A; // 전방 선언
class B {
A* a; // 포인터는 OK
};
전방 선언만으로 할 수 있는 일과 없는 일을 구분하면 이 에러를 예측할 수 있습니다. 컴파일러가 크기나 멤버를 알아야 하는 작업(객체 생성, 멤버 함수 호출, 값으로 멤버에 두기, sizeof, 상속)에는 완전한 정의가 필요하고, 포인터나 참조를 선언하는 것은 크기가 정해져 있으므로 전방 선언으로 충분합니다. 그래서 헤더에서는 전방 선언과 포인터를 쓰고, 실제로 멤버를 호출하는 .cpp에서만 헤더를 include하는 방식이 순환 의존성을 끊는 기본 패턴입니다. PIMPL 관용구에서 std::unique_ptr<Impl>을 쓸 때도 같은 규칙이 적용되어, 소멸자를 헤더에서 암시적으로 만들게 두면 invalid application of 'sizeof' to incomplete type 'Impl' 에러가 납니다. 이때는 헤더에 ~Widget();을 선언하고 Impl이 정의된 .cpp에서 Widget::~Widget() = default;로 정의해야 합니다.
error: no member named ‘X’ in ‘Y’
원인
존재하지 않는 멤버 변수나 멤버 함수에 접근하려 할 때 발생합니다.
에러 메시지
error: no member named 'getValue' in 'MyClass'
흔한 상황
- 오타
- private 멤버에 접근
- 상속받지 않은 멤버 접근
해결 방법
// ❌ 오타
class MyClass {
public:
int getValue() const { return value; }
private:
int value;
};
int main() {
MyClass obj;
obj.getvalue(); // error: no member named 'getvalue'! (오타)
}
// ✅ 올바른 이름
int main() {
MyClass obj;
obj.getValue(); // OK
}
// ❌ private 멤버 접근
class MyClass {
private:
int value;
};
int main() {
MyClass obj;
obj.value = 10; // error: 'int MyClass::value' is private within this context
}
// ✅ public getter/setter 사용
class MyClass {
public:
void setValue(int v) { value = v; }
int getValue() const { return value; }
private:
int value;
};
int main() {
MyClass obj;
obj.setValue(10); // OK
}
제목의 no member named 'X' in 'Y'는 Clang의 메시지이고, GCC는 같은 상황에서 'class MyClass' has no member named 'getvalue'; did you mean 'getValue'?처럼 비슷한 이름을 제안해 줍니다. 오타라면 이 제안이 거의 정답입니다. private 멤버 접근은 “멤버가 없다”가 아니라 is private within this context라는 별도 메시지로 나오므로, 두 경우를 구분해 읽으면 원인을 바로 알 수 있습니다. 템플릿 안에서 기반 클래스의 멤버를 쓸 때 this->를 빠뜨려 there are no arguments to 'foo' that depend on a template parameter 같은 에러가 나는 것도 비슷한 증상의 흔한 변형입니다.
에러 메시지 읽는 법
C++ 컴파일러 에러 메시지는 길고 복잡하지만, 패턴을 알면 쉽게 읽을 수 있습니다.
에러 메시지 구조
파일명:줄번호:열번호: error: 에러 종류: 상세 설명
예시
main.cpp:10:5: error: 'x' was not declared in this scope
x = 10;
^
읽는 방법:
- 파일명과 줄번호 확인:
main.cpp:10→ main.cpp의 10번째 줄 - 에러 종류 확인:
'x' was not declared in this scope→ x가 선언되지 않음 - 코드 확인:
x = 10;→ x를 선언하지 않고 사용
여러 에러가 연쇄적으로 발생할 때
첫 번째 에러부터 해결하세요. 첫 에러를 고치면 나머지 에러가 자동으로 사라지는 경우가 많습니다.
디버깅 전략
컴파일러 경고 활성화
# 모든 경고 활성화
g++ -Wall -Wextra -Werror main.cpp -o program
# -Wall: 기본 경고
# -Wextra: 추가 경고
# -Werror: 경고를 에러로 처리
디버그 심볼 포함
# 디버그 정보 포함
g++ -g main.cpp -o program
# gdb로 디버깅
gdb ./program
단계별 디버깅
- 컴파일 에러 → 링크 에러 → 런타임 에러 순서로 해결
- 한 번에 하나씩 에러 해결
- 에러 메시지를 구글에 검색 (Stack Overflow 활용)
코드 간소화
에러가 발생하면:
- 최소 재현 코드를 만들어 테스트
- 복잡한 코드를 단순화해서 문제 원인 파악
- 한 줄씩 주석 처리하며 문제 지점 찾기
자주 하는 실수 방지 체크리스트
컴파일 전 체크
- 모든 헤더 파일에 include guard 또는
#pragma once추가 - 함수 선언과 정의가 모두 있는지 확인
- 템플릿 함수는 헤더에 정의
- 모든 변수 선언 후 사용
- nullptr 체크 후 역참조
코딩 습관
- 항상
nullptr사용 (NULL, 0 대신) - 범위 기반 for 사용 (인덱스 에러 방지)
- 스마트 포인터 사용 (메모리 누수 방지)
- const 적극 활용 (의도치 않은 수정 방지)
- 컴파일러 경고 무시하지 않기
마무리
C++ 컴파일 오류는 처음에는 어렵지만, 패턴을 익히면 빠르게 해결할 수 있습니다. 이 글에서 다룬 10가지 에러는 C++ 초보자가 가장 자주 겪는 문제들입니다.
핵심 요약:
- 에러 메시지를 정확히 읽기 (파일명, 줄번호, 에러 종류)
- 첫 번째 에러부터 해결 (연쇄 에러 방지)
- 컴파일러 경고 활성화 (-Wall -Wextra)
- nullptr 체크, 범위 체크 습관화
- include guard 사용 (헤더 중복 방지)
자주 묻는 질문 (FAQ)
Q. error: use of deleted function 에러는 왜 나고 어떻게 고치나요?
A. 복사 생성자나 복사 대입 연산자가 삭제된 타입의 객체를 복사하려 할 때 발생하며, 가장 흔한 경우는 std::unique_ptr을 복사하거나 unique_ptr 멤버를 가진 클래스를 복사하는 것입니다. 소유권을 넘기려면 std::move로 이동하고, 원본을 그대로 두고 함께 쓰려면 참조나 포인터로 전달해야 합니다. vector에 넣을 때나 함수 인자로 값 전달할 때도 암묵적인 복사가 일어나므로, 에러 메시지가 가리키는 호출 위치에서 어디서 복사가 일어나는지 먼저 찾아야 합니다.
같이 보면 좋은 글
- C++ nullptr vs NULL: NULL이 정수라서 생기는 오버로드·템플릿 버그
- C++ 클래스와 객체 입문
- C++ 스마트 포인터 | 3일 동안 찾지 못한 순환 참조 버그 해결법
- C++ std::vector 기초: 생성·삽입·삭제와 배열 대신 쓰는 이유
- C++ 포인터 기초