C++ 초보자가 자주 하는 실수 Top 15 | 컴파일 에러부터 런타임 크래시까지
이 글의 핵심
C++ 초보자가 자주 하는 실수 Top 15: 컴파일 에러부터 런타임 크래시까지. 컴파일 에러 Top 8·런타임 에러 Top 4.
들어가며: “C++ 시작했는데 에러만 100개…"
"Hello World도 컴파일이 안 돼요”
C++를 처음 배우는 사람들이 반드시 겪는 실수들이 있습니다. 세미콜론 하나, 헤더 파일 하나 빠뜨려도 수십 줄의 에러 메시지가 쏟아집니다. 이 글은 C++ 입문자가 가장 자주 하는 실수 15가지를 정리하고, 각각의 에러 메시지와 해결법을 알려드립니다. 이 글을 읽으면:
- 컴파일 에러 메시지를 읽고 5분 안에 해결할 수 있습니다
- 같은 실수를 반복하지 않게 됩니다
- C++ 문법의 핵심 규칙을 이해하게 됩니다
실전 경험에서 배운 교훈
이 기술을 실무 프로젝트에 처음 도입했을 때, 공식 문서만으로는 알 수 없었던 많은 함정들이 있었습니다. 특히 프로덕션 환경에서 발생하는 엣지 케이스들은 로컬 개발 환경에서는 재현조차 되지 않았죠.
가장 어려웠던 점은 성능 최적화였습니다. 처음엔 “동작만 하면 되겠지”라고 생각했지만, 실제 사용자 트래픽이 몰리면서 병목 지점들이 하나씩 드러났습니다. 특히 데이터베이스 쿼리 최적화, 캐싱 전략, 에러 핸들링 구조 등은 여러 번의 장애를 겪으면서 개선해 나갔습니다.
이 글에서는 그런 시행착오를 통해 얻은 실전 노하우와, “이렇게 하면 안 된다”는 교훈들을 함께 정리했습니다. 특히 트러블슈팅 섹션은 실제 장애 대응 경험을 바탕으로 작성했으니, 비슷한 문제를 마주했을 때 참고하시면 도움이 될 것입니다.
1. 컴파일 에러 Top 8
실수 1: 세미콜론 누락
가장 흔한 실수: 클래스/구조체 정의 끝에 세미콜론을 빼먹음.
// ❌ 에러 코드
class MyClass {
int x;
} // ← 세미콜론 없음!
int main() {
return 0;
}
// error: expected ';' after class definition
해결:
// ✅ 올바른 코드
class MyClass {
int x;
}; // ← 세미콜론 필수!
주의사항: 클래스 안의 중첩 클래스·friend 선언 위치를 바꿀 때 세미콜론 위치가 흔들리기 쉽습니다. 주의: 함수 정의 끝에는 세미콜론이 없어야 합니다.
void foo() {
// ...
} // ← 세미콜론 없음 (올바름)
실수 2: 헤더 파일 미포함
// ❌ 에러 코드
int main() {
cout << "Hello" << endl; // cout이 뭔지 모름
return 0;
}
// error: 'cout' was not declared in this scope
// error: 'endl' was not declared in this scope
해결:
// ✅ 올바른 코드
#include <iostream>
int main() {
std::cout << "Hello" << std::endl;
return 0;
}
주의사항: using namespace std;는 교육용 예제에는 편하지만, 실무 코드베이스에서는 이름 충돌을 피하기 위해 지양하는 편입니다.
자주 빠뜨리는 헤더:
| 기능 | 필요한 헤더 |
|---|---|
cout, cin | <iostream> |
string | <string> |
vector | <vector> |
sqrt, pow | <cmath> |
sort, find | <algorithm> |
실수 3: using namespace std 없이 std:: 생략
// ❌ 에러 코드
#include <iostream>
int main() {
cout << "Hello" << endl; // std:: 없음
return 0;
}
// error: 'cout' was not declared in this scope
해결법 1: std:: 접두사 사용 (권장)
// ✅ 올바른 코드
#include <iostream>
int main() {
std::cout << "Hello" << std::endl;
return 0;
}
해결법 2: using namespace std
// ✅ 동작하지만 권장하지 않음
#include <iostream>
using namespace std;
int main() {
cout << "Hello" << endl;
return 0;
}
주의: using namespace std;는 이름 충돌을 일으킬 수 있으므로 헤더 파일에는 절대 쓰지 마세요.
실수 4: main 함수 반환 타입 오류
// ❌ 에러 코드
void main() { // ❌ void는 표준이 아님
std::cout << "Hello\n";
}
// error: 'main' must return 'int'
해결:
// ✅ 올바른 코드
int main() {
std::cout << "Hello\n";
return 0;
}
실수 5: 변수 선언 위치 오류
// ❌ C++03 이전 스타일
int main() {
for (int i = 0; i < 10; ++i) {
// ...
}
std::cout << i << '\n'; // ❌ i는 for 블록 밖에서 접근 불가
}
// error: 'i' was not declared in this scope
해결:
// ✅ 올바른 코드
int main() {
int i; // 밖에서 선언
for (i = 0; i < 10; ++i) {
// ...
}
std::cout << i << '\n'; // 10
}
실수 6: const 불일치
// ❌ 에러 코드
void print(std::string& s) { // 비const 참조
std::cout << s << '\n';
}
int main() {
print("Hello"); // 임시 객체는 비const 참조에 바인딩 불가
}
// error: cannot bind non-const lvalue reference of type 'std::string&'
// to an rvalue of type 'std::string'
해결:
// ✅ 올바른 코드
void print(const std::string& s) { // const 참조
std::cout << s << '\n';
}
int main() {
print("Hello"); // OK
}
실수 7: 배열 초기화 오류
// ❌ 에러 코드
int arr[5];
arr = {1, 2, 3, 4, 5}; // ❌ 선언 후에는 이렇게 할당 불가
// error: invalid array assignment
해결:
// ✅ 올바른 코드
int arr[5] = {1, 2, 3, 4, 5}; // 선언과 동시에 초기화
// 또는
int arr[5];
for (int i = 0; i < 5; ++i) {
arr[i] = i + 1;
}
// 또는 C++11
int arr[] = {1, 2, 3, 4, 5}; // 크기 자동 추론
실수 8: 함수 선언과 정의 불일치
// ❌ 에러 코드
// header.h
void foo(int x);
// main.cpp
void foo(double x) { // ❌ 타입이 다름
// ...
}
int main() {
foo(42);
}
// error: undefined reference to 'foo(int)'
해결:
// ✅ 올바른 코드
// header.h
void foo(int x);
// main.cpp
void foo(int x) { // 타입 일치
// ...
}
2. 런타임 에러 Top 4
실수 9: 포인터 초기화 안 함
// ❌ 크래시 코드
int main() {
int* ptr; // 초기화 안 함 (쓰레기 값)
*ptr = 42; // ❌ 임의의 메모리에 쓰기 → 크래시
}
// Segmentation fault
해결:
// ✅ 올바른 코드
int main() {
int* ptr = nullptr; // 초기화
if (ptr == nullptr) {
ptr = new int(42);
}
delete ptr;
}
// ✅ 더 좋은 방법
int main() {
auto ptr = std::make_unique<int>(42);
// 자동 해제
}
실수 10: 배열 범위 초과
// ❌ 크래시 코드
int main() {
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i <= 5; ++i) { // ❌ i=5는 범위 밖
std::cout << arr[i] << '\n';
}
}
// 미정의 동작 (쓰레기 값 또는 크래시)
해결:
// ✅ 올바른 코드
int main() {
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; ++i) { // i < 5
std::cout << arr[i] << '\n';
}
}
// ✅ 더 안전한 방법: 범위 기반 for
int main() {
int arr[5] = {1, 2, 3, 4, 5};
for (int x : arr) {
std::cout << x << '\n';
}
}
실수 11: 문자열 비교를 == 로
// ❌ 잘못된 비교
int main() {
char str1[] = "hello";
char str2[] = "hello";
if (str1 == str2) { // ❌ 주소 비교 (항상 false)
std::cout << "Same\n";
} else {
std::cout << "Different\n"; // 이게 출력됨
}
}
해결:
// ✅ C 스타일 문자열
#include <cstring>
int main() {
char str1[] = "hello";
char str2[] = "hello";
if (strcmp(str1, str2) == 0) { // 내용 비교
std::cout << "Same\n";
}
}
// ✅ C++ std::string (권장)
#include <string>
int main() {
std::string str1 = "hello";
std::string str2 = "hello";
if (str1 == str2) { // OK
std::cout << "Same\n";
}
}
실수 12: 지역 변수 주소 반환
// ❌ 댕글링 포인터
int* createNumber() {
int x = 42;
return &x; // ❌ 지역 변수 주소 반환
} // x는 소멸됨
int main() {
int* ptr = createNumber();
std::cout << *ptr << '\n'; // ❌ 미정의 동작
}
// warning: address of local variable 'x' returned
해결:
// ✅ 해결 1: 힙 할당
int* createNumber() {
return new int(42); // 호출자가 delete 해야 함
}
// ✅ 해결 2: 스마트 포인터 (권장)
std::unique_ptr<int> createNumber() {
return std::make_unique<int>(42);
}
// ✅ 해결 3: 값 반환
int createNumber() {
return 42;
}
3. 논리 에러 Top 3
실수 13: = 와 == 혼동
// ❌ 버그 (컴파일은 됨)
int main() {
int x = 5;
if (x = 10) { // ❌ 대입 연산자 (항상 true)
std::cout << "x is 10\n"; // 항상 실행됨
}
std::cout << "x = " << x << '\n'; // x = 10
}
// warning: using the result of an assignment as a condition without parentheses
해결:
// ✅ 올바른 코드
int main() {
int x = 5;
if (x == 10) { // 비교 연산자
std::cout << "x is 10\n";
}
}
// 팁: 상수를 왼쪽에 두면 실수 방지
if (10 == x) { // 실수로 10 = x 쓰면 컴파일 에러
// ...
}
실수 14: 정수 나눗셈
// ❌ 버그
int main() {
int a = 5;
int b = 2;
double result = a / b; // ❌ 정수 나눗셈 → 2.0
std::cout << result << '\n'; // 2 (2.5가 아님!)
}
해결:
// ✅ 올바른 코드
int main() {
int a = 5;
int b = 2;
double result = static_cast<double>(a) / b; // 2.5
std::cout << result << '\n';
}
// 또는
double result = a / static_cast<double>(b);
// 또는
double result = static_cast<double>(a) / static_cast<double>(b);
실수 15: 부호 없는 정수 언더플로우
// ❌ 버그
int main() {
unsigned int x = 5;
unsigned int y = 10;
unsigned int diff = x - y; // ❌ 음수가 될 수 없음 → 큰 양수
std::cout << diff << '\n'; // 4294967291 (2^32 - 5)
}
해결:
// ✅ 올바른 코드
int main() {
int x = 5; // 부호 있는 정수
int y = 10;
int diff = x - y; // -5
std::cout << diff << '\n';
}
// 또는 조건 확인
unsigned int x = 5, y = 10;
if (x > y) {
unsigned int diff = x - y;
} else {
// x <= y인 경우 처리
}
4. 에러 메시지 읽는 법
컴파일 에러 메시지 구조
main.cpp:10:5: error: 'cout' was not declared in this scope
^^^^^^^ ^^ ^^ ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
파일 줄 열 타입 에러 내용
10 | cout << "Hello" << endl;
| ^~~~
| std::cout
읽는 순서:
- 파일:줄:
main.cpp:10- 어느 파일의 몇 번째 줄 - 에러 타입:
error(컴파일 실패) vswarning(경고) - 에러 내용:
'cout' was not declared- 무엇이 문제인지 - 제안:
std::cout- 어떻게 고칠지
자주 나오는 에러 메시지 패턴
| 에러 메시지 | 의미 | 해결법 |
|---|---|---|
expected ';' before | 세미콜론 누락 | 이전 줄 끝에 ; 추가 |
was not declared in this scope | 변수/함수를 찾을 수 없음 | 선언 추가 또는 헤더 포함 |
no matching function | 함수 오버로드를 찾지 못함 | 인자 타입·개수 확인 |
cannot convert | 타입 변환 불가 | 타입 일치시키기 또는 캐스팅 |
undefined reference | 링커 에러 (정의 없음) | 소스 파일 추가 또는 라이브러리 링크 |
invalid use of incomplete type | 전방 선언만 있음 | 헤더 포함 |
redefinition of | 중복 정의 | 헤더 가드 추가 |
더 많은 실수 패턴
실수 16: cin으로 문자열 입력 시 공백 처리
cin >> 연산자는 공백(스페이스, 탭, 엔터)을 구분자로 사용합니다. 따라서 공백이 포함된 문자열을 입력받을 수 없습니다.
// ❌ 공백 이후 잘림
#include <iostream>
#include <string>
int main() {
std::string name;
std::cout << "Enter name: ";
std::cin >> name; // "John Doe" 입력 시 "John"만 저장
std::cout << "Hello, " << name << '\n'; // Hello, John
// "Doe"는 입력 버퍼에 남아있음!
std::string remaining;
std::cin >> remaining;
std::cout << "Remaining: " << remaining << '\n'; // Remaining: Doe
}
해결법 1: getline 사용 (권장)
// ✅ getline으로 전체 줄 읽기
#include <iostream>
#include <string>
int main() {
std::string name;
std::cout << "Enter name: ";
std::getline(std::cin, name); // 엔터까지 전체 줄 읽기
std::cout << "Hello, " << name << '\n'; // Hello, John Doe
}
해결법 2: cin과 getline 혼용 시 주의
// ❌ 문제 상황
#include <iostream>
#include <string>
int main() {
int age;
std::string name;
std::cout << "Enter age: ";
std::cin >> age; // "25" 입력 후 엔터 → 엔터가 버퍼에 남음
std::cout << "Enter name: ";
std::getline(std::cin, name); // 버퍼의 엔터를 읽어서 빈 문자열!
std::cout << "Age: " << age << ", Name: " << name << '\n';
// Age: 25, Name: (빈 문자열)
}
// ✅ 해결: cin.ignore()로 버퍼 비우기
#include <iostream>
#include <string>
#include <limits>
int main() {
int age;
std::string name;
std::cout << "Enter age: ";
std::cin >> age;
// 버퍼에 남은 엔터 제거
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "Enter name: ";
std::getline(std::cin, name);
std::cout << "Age: " << age << ", Name: " << name << '\n';
// Age: 25, Name: John Doe
}
cin.ignore() 설명:
std::cin.ignore(n, delim): 최대 n개 문자를 무시하고, delim을 만나면 중단std::numeric_limits<std::streamsize>::max(): 버퍼 전체 크기'\n': 엔터까지 무시
실수 17: 벡터 크기 미확인
vector는 동적 배열이지만, operator[]는 범위 검사를 하지 않습니다. 빈 벡터에 접근하면 미정의 동작이 발생합니다.
// ❌ 크래시 코드
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec; // 빈 벡터 (size=0, capacity=0)
vec[0] = 42; // ❌ 범위 밖 접근 (미정의 동작)
std::cout << vec[0] << '\n'; // 쓰레기 값 또는 크래시
}
// 미정의 동작: 컴파일은 되지만 실행 시 문제 발생
해결법 1: 크기 지정 초기화
// ✅ 올바른 코드 - 크기 지정
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec(5); // 크기 5, 모든 요소 0으로 초기화
vec[0] = 42; // OK
std::cout << vec[0] << '\n'; // 42
// 크기와 초기값 지정
std::vector<int> vec2(5, 10); // 크기 5, 모든 요소 10
// vec2: [10, 10, 10, 10, 10]
}
해결법 2: push_back 사용
// ✅ 올바른 코드 - push_back으로 추가
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec; // 빈 벡터
vec.push_back(42); // 자동 확장 (size=1)
vec.push_back(10); // size=2
vec.push_back(20); // size=3
std::cout << vec[0] << '\n'; // 42
std::cout << vec.size() << '\n'; // 3
}
해결법 3: at() 사용 (범위 검사)
// ✅ 안전한 접근 - at()은 범위 검사
#include <vector>
#include <iostream>
#include <stdexcept>
int main() {
std::vector<int> vec = {1, 2, 3};
try {
std::cout << vec.at(0) << '\n'; // 1 (OK)
std::cout << vec.at(10) << '\n'; // 예외 발생
} catch (const std::out_of_range& e) {
std::cerr << "오류: " << e.what() << '\n';
// 오류: vector::_M_range_check: __n (which is 10) >= this->size() (which is 3)
}
}
operator[] vs at() 비교:
| 방법 | 범위 검사 | 예외 발생 | 성능 | 사용 시기 |
|---|---|---|---|---|
vec[i] | ❌ 없음 | ❌ 없음 | 빠름 | 인덱스가 확실히 유효할 때 |
vec.at(i) | ✅ 있음 | ✅ out_of_range | 약간 느림 | 안전성이 중요할 때 |
| 실전 패턴: |
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec;
// ✅ 패턴 1: 크기 확인 후 접근
if (vec.size() > 0) {
std::cout << vec[0] << '\n';
}
// ✅ 패턴 2: empty() 확인
if (!vec.empty()) {
std::cout << vec[0] << '\n';
}
// ✅ 패턴 3: 범위 기반 for (안전)
for (int x : vec) {
std::cout << x << '\n';
}
// ✅ 패턴 4: reserve로 용량 미리 확보
vec.reserve(100); // 100개 요소를 위한 메모리 할당
for (int i = 0; i < 100; ++i) {
vec.push_back(i); // 재할당 없이 추가
}
}
실수 18: switch문에서 break 누락
// ❌ 버그 (의도하지 않은 fall-through)
int main() {
int x = 1;
switch (x) {
case 1:
std::cout << "One\n";
// break 없음!
case 2:
std::cout << "Two\n";
break;
}
// 출력: One\nTwo\n (의도하지 않음)
}
해결:
// ✅ 올바른 코드
int main() {
int x = 1;
switch (x) {
case 1:
std::cout << "One\n";
break; // 필수
case 2:
std::cout << "Two\n";
break;
default:
std::cout << "Other\n";
break;
}
}
초보자를 위한 디버깅 가이드
디버깅 프로세스
1단계: 에러 메시지 읽기
에러 메시지 구조:
main.cpp:10:5: error: 'cout' was not declared in this scope
^^^^^^^ ^^ ^^ ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
파일 줄 열 타입 에러 내용
읽는 순서:
1. 파일과 줄 번호 확인 (main.cpp:10)
2. 에러 타입 확인 (error vs warning)
3. 에러 내용 이해 ('cout' was not declared)
4. 제안 확인 (note: suggested alternative: 'std::cout')
2단계: 첫 번째 에러만 집중
main.cpp:10:5: error: 'cout' was not declared in this scope
main.cpp:10:5: note: suggested alternative: 'std::cout'
main.cpp:11:5: error: 'endl' was not declared in this scope
main.cpp:12:5: error: expected ';' before 'return'
....(중략 50줄) ...
첫 번째 에러만 고치세요!
→ #include <iostream> 추가
→ 나머지 에러도 자동으로 해결될 가능성 높음
3단계: 이진 탐색으로 에러 위치 찾기
// 500줄 코드에서 에러가 나는데 위치를 모를 때
// 1. 코드를 절반으로 나누어 주석 처리
// 상반부 주석 처리 → 컴파일
// 에러 없으면 하반부에 문제
// 에러 있으면 상반부에 문제
// 2. 문제 있는 절반을 다시 절반으로
// 반복하여 에러 위치를 좁혀감
// 예제:
int main() {
// 코드 1-250줄
/* 주석 처리 */
// 코드 251-500줄
// ...
}
팁 1: 컴파일러 경고를 에러로 취급
컴파일러 경고는 잠재적 버그를 알려줍니다. 경고를 무시하지 말고 에러로 취급하세요.
# GCC/Clang - 권장 플래그
g++ -std=c++17 -Wall -Wextra -Werror -pedantic main.cpp -o main
# 플래그 설명:
# -Wall: 기본 경고 활성화
# -Wextra: 추가 경고 활성화
# -Werror: 경고를 에러로 취급 (컴파일 실패)
# -pedantic: 표준 준수 엄격 검사
# MSVC
cl /W4 /WX main.cpp
# /W4: 경고 레벨 4 (최대)
# /WX: 경고를 에러로 취급
경고 예제:
// 경고가 나는 코드
#include <iostream>
int main() {
int x = 10;
unsigned int y = 5;
if (x > y) { // warning: comparison of integer expressions of different signedness
std::cout << "x > y\n";
}
int arr[5];
for (int i = 0; i <= 5; ++i) { // warning: array subscript is above array bounds
arr[i] = i;
}
}
// 경고를 고친 코드
int main() {
int x = 10;
int y = 5; // unsigned 제거
if (x > y) {
std::cout << "x > y\n";
}
int arr[5];
for (int i = 0; i < 5; ++i) { // <= 를 < 로 수정
arr[i] = i;
}
}
팁 2: 최소 재현 코드 (MCVE) 작성
MCVE (Minimal, Complete, Verifiable Example): 에러를 재현하는 최소한의 코드
// ❌ 복잡한 코드 (500줄)
// 어디서 에러가 나는지 모름
// ✅ 최소 재현 코드 (10줄)
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec;
std::cout << vec[0] << '\n'; // 에러 발생!
return 0;
}
MCVE 작성 단계:
- 에러가 나는 부분만 추출
- 불필요한 코드 제거
- 필요한 헤더만 포함
- 독립적으로 컴파일 가능하게 작성
팁 3: 온라인 컴파일러 활용
온라인 컴파일러는 환경 설정 없이 빠르게 테스트할 수 있습니다. 추천 사이트:
- Compiler Explorer (godbolt.org)
- 여러 컴파일러 비교 (GCC, Clang, MSVC)
- 어셈블리 코드 확인
- 최적화 레벨별 결과 비교
- cpp.sh
- 간단한 테스트용
- 빠른 실행
- Wandbox
- 다양한 컴파일러 버전
- 빠른 응답 속도
- OnlineGDB
- 디버거 지원
- 단계별 실행 가능 사용 시나리오:
- 문법 확인 (예: C++17 기능 테스트)
- 컴파일러별 동작 차이 확인
- 에러 메시지 비교
- 최적화 효과 확인
팁 4: 디버거 사용법
GDB (Linux/Mac):
# 디버그 정보 포함하여 컴파일
g++ -g main.cpp -o main
# GDB 실행
gdb ./main
# GDB 명령어
(gdb) break main # main 함수에 중단점
(gdb) run # 프로그램 실행
(gdb) next # 다음 줄 (함수 호출 건너뜀)
(gdb) step # 다음 줄 (함수 내부로 진입)
(gdb) print x # 변수 x 값 출력
(gdb) continue # 다음 중단점까지 실행
(gdb) quit # 종료
Visual Studio (Windows):
1. F9: 중단점 설정/해제
2. F5: 디버깅 시작
3. F10: 다음 줄 (Step Over)
4. F11: 함수 내부로 (Step Into)
5. Shift+F11: 함수 밖으로 (Step Out)
6. 변수에 마우스 올리면 값 표시
VS Code (크로스 플랫폼):
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "C++ Debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/main",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"preLaunchTask": "build"
}
]
}
팁 5: 로깅으로 디버깅
printf 디버깅 (간단하지만 효과적):
#include <iostream>
int main() {
int x = 10;
std::cout << "DEBUG: x = " << x << '\n'; // 변수 값 확인
std::vector<int> vec = {1, 2, 3};
std::cout << "DEBUG: vec.size() = " << vec.size() << '\n'; // 크기 확인
for (size_t i = 0; i < vec.size(); ++i) {
std::cout << "DEBUG: vec[" << i << "] = " << vec[i] << '\n';
}
}
조건부 디버그 출력:
#include <iostream>
// 디버그 모드 플래그
#define DEBUG 1
#if DEBUG
#define LOG(x) std::cout << "[DEBUG] " << x << '\n'
#else
#define LOG(x) // 릴리스 빌드에서는 무시
#endif
int main() {
int x = 10;
LOG("x = " << x); // 디버그 모드에서만 출력
// 릴리스 빌드: g++ -DDEBUG=0 main.cpp
}
체크리스트
컴파일 전 체크리스트
- 모든 클래스/구조체 정의 끝에 세미콜론이 있는가?
- 사용하는 모든 기능의 헤더를 포함했는가?
-
std::접두사를 붙였는가? (또는 using 선언) - main 함수가
int main()인가? - 함수 선언과 정의의 시그니처가 일치하는가?
런타임 전 체크리스트
- 모든 포인터를 초기화했는가?
- 배열 인덱스가 범위 내인가?
- new/delete 짝이 맞는가?
- 문자열 비교를 == 로 하지 않았는가? (C 스타일)
- 지역 변수 주소를 반환하지 않았는가?
코드 리뷰 체크리스트
- 컴파일러 경고가 없는가? (-Wall -Wextra)
- 스마트 포인터를 사용하는가?
- const 참조를 적절히 사용하는가?
- 범위 기반 for문을 사용하는가? (배열 순회)
- switch문에 break가 있는가?
정리
실수 빈도 Top 5
- 세미콜론 누락 (클래스 정의 끝)
- 헤더 미포함 (
<iostream>,<string>등) - std:: 생략 (
cout→std::cout) - 포인터 초기화 안 함
- 배열 범위 초과
핵심 규칙
- 클래스/구조체 정의 끝에는 세미콜론
- 사용하는 모든 기능의 헤더를 포함
- std:: 접두사를 붙이거나 using 선언
- 포인터는 반드시 초기화 (
nullptr또는 유효한 주소) - 배열 인덱스는 0부터 size-1까지
- new/delete는 짝으로 (또는 스마트 포인터 사용)
- 컴파일러 경고를 무시하지 마세요 (-Wall -Wextra)
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 개요 | “처음 배우는” C++ 완벽 가이드
- C++ 포인터 | “어렵다는 포인터” 5분 만에 이해하기
- C++ 개발 환경 설정 | Visual Studio·GCC·Clang 설치
- C++ LNK2019 | “unresolved external symbol” 링커 에러 해결
자주 묻는 질문 (FAQ)
Q. C와 C++의 차이는 뭔가요?
A. C++는 C의 상위 집합이지만, 몇 가지 차이가 있습니다:
- C++는
<iostream>, C는<stdio.h> - C++는
std::string, C는char[] - C++는
new/delete, C는malloc/free - C++는 클래스·템플릿·예외 처리 지원
Q. void main()을 쓰면 안 되나요?
A. 표준이 아닙니다. 일부 컴파일러는 허용하지만, 이식성이 없습니다. 항상 int main()을 사용하세요.
Q. using namespace std를 쓰면 안 되나요?
A. 헤더 파일에는 절대 쓰지 마세요 (이름 충돌). .cpp 파일에서는 괜찮지만, std:: 접두사를 쓰는 것이 더 명확합니다.
Q. 포인터와 참조의 차이는 뭔가요?
A.
- 포인터: 주소를 저장,
nullptr가능, 재할당 가능 - 참조: 별칭,
nullptr불가, 재할당 불가 초보자는 참조를 먼저 익히는 것을 권장합니다.
마치며
C++ 초보자가 겪는 대부분의 에러는 패턴이 있습니다. 이 글에서 다룬 15가지 실수를 숙지하면, 에러 메시지를 보고 5분 안에 해결할 수 있게 됩니다. 학습 로드맵:
- 이 글의 실수들을 모두 경험해 보세요 (직접 에러를 만들고 고치기)
- 작은 프로그램을 많이 작성하세요 (계산기, 숫자 맞추기 게임 등)
- 컴파일러 경고를 읽는 습관을 들이세요
- 스마트 포인터와 STL을 익히세요 다음 단계: 기본 실수를 극복했다면, C++ 포인터 완벽 가이드와 C++ STL vector 가이드를 읽어보세요.
관련 글
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ 초보자가 자주 하는 실수 Top 15 | 컴파일 에러부터 런타임 크래시까지」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「C++ 초보자가 자주 하는 실수 Top 15 | 컴파일 에러부터 런타임 크래시까지」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
이 글에서 다루는 키워드 (관련 검색어)
C++, 초보자, 컴파일에러, 실수, 입문, 디버깅, 에러해결 등으로 검색하시면 이 글이 도움이 됩니다.