C++ 자주 틀리는 C++ 기술 면접 질문 50선 | 출제 의도와 모범 답변 [#46-2]
이 글의 핵심
C++ 자주 틀리는 C++ 기술 면접 질문 50선에 대한 실전 가이드입니다. 출제 의도와 모범 답변 [#46-2] 등을 예제와 함께 설명합니다.
들어가며: “알 것 같은데 말로 못 꾸미는” 순간
출제 의도가 보이면 답이 보인다
C++ 기술 면접에서 이론은 아는데 “왜 이걸 물어보지?”, “어디까지 말해야 하지?”에서 막히는 경우가 많습니다. 이 글은 자주 나오는 질문 50선을 출제자 의도와 모범 답변 뼈대로 정리합니다. 단순 나열이 아니라 “이걸 왜 묻는지”를 알면, 짧게 요점만 말할지, 경험과 함께 풀어 말할지 선택하기 쉬워집니다.
이 글에서 다루는 것:
- 메모리·포인터·RAII (1~15번): 스마트 포인터, 소유권, 댕글링
- 동시성·성능 (16~30번): Data Race, Mutex vs Atomic, 캐시
- 템플릿·타입·STL (31~40번): 이동 의미론, perfect forwarding, iterator
- 설계·실무 (41~50번): PIMPL, 예외 안전성, 빌드·ABI
각 항목은 질문 → 출제 의도 → 모범 답변 요지 형태로 압축했습니다. 상세 이론은 시리즈 해당 번호를 참고하세요.
실행 가능 예제 (Q1 unique_ptr 예 — 면접에서 말할 수 있는 최소 코드):
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o interview_demo interview_demo.cpp && ./interview_demo
#include <memory>
#include <iostream>
int main() {
std::unique_ptr<int> p = std::make_unique<int>(42);
std::cout << *p << "\n"; // 소유권은 p 하나만
return 0;
}
관련 글: #33~#34 면접, #34-1 Data Race·Mutex·Atomic.
면접 문제 시나리오: “이런 상황에서 막혔다”
시나리오 1: 스마트 포인터 질문에서 멈춤
상황: “shared_ptr과 unique_ptr 차이를 아시나요?”에 “shared_ptr은 참조 카운팅, unique_ptr은…”까지 말하다가 “그럼 언제 뭘 쓰나요?”에서 막힘. 이론은 아는데 실무 선택 기준을 말 못함.
해결 포인트: “기본은 unique_ptr, 공유가 꼭 필요할 때만 shared_ptr(비용·순환 참조 주의)” 한 문장으로 요약하면 됩니다.
시나리오 2: Data Race 설명에서 “동기화”만 반복
상황: “Data Race가 뭔가요?”에 “동기화 없이…”라고 하다가 “그럼 어떻게 제거하나요?”에 “Mutex로 락을 잡고…”라고만 함. Mutex vs Atomic 선택 기준을 못 말함.
해결 포인트: “단일 변수·단순 연산 → Atomic, 여러 변수·복잡 로직 → Mutex”로 구분해서 말하면 됩니다.
시나리오 3: 이동 의미론에서 std::move 남발
상황: “이동 의미론이 뭔가요?”에 “std::move로 객체를 넘긴다”고 설명. “그럼 return에서 std::move를 붙여야 하나요?”에 “네”라고 잘못 답함.
해결 포인트: RVO가 있으므로 return 시 std::move는 붙이지 않는다. 오히려 복사가 발생할 수 있음.
시나리오 4: PIMPL을 “포인터로 숨긴다”에 그침
상황: “PIMPL이 뭔가요?”에 “구현을 포인터로 숨긴다”고만 함. “왜 쓰나요?”에 “캡슐화?”라고 애매하게 답함.
해결 포인트: 컴파일 의존성 감소, ABI 안정성, 바이너리 호환이 핵심. 헤더 변경 없이 구현만 수정 가능.
시나리오 5: iterator 무효화로 크래시
상황: vector를 순회하면서 조건에 맞는 요소를 erase하는 코드를 작성했는데, “가끔” 크래시가 발생. 범위 for문 안에서 erase를 호출함.
해결 포인트: 범위 for는 iterator 기반이라 erase 시 무효화됨. for (auto it = vec.begin(); it != vec.end(); ) 형태로 바꾸고 it = vec.erase(it) 사용.
시나리오 6: “로컬에선 되는데 서버에서만” 버그
상황: 멀티스레드 카운터가 로컬에서는 정상인데, 프로덕션 서버에서 수치가 어긋남. Data Race를 의심하지 못함.
해결 포인트: Data Race는 UB라 타이밍에 따라 다르게 나타남. TSan(ThreadSanitizer)으로 빌드해 보면 발견 가능. Atomic 또는 Mutex로 동기화.
시나리오 7: 헤더 한 줄 수정에 5분 컴파일
상황: 자주 쓰는 헤더를 수정했더니 전 프로젝트가 재컴파일됨. 빌드 시간이 너무 김.
해결 포인트: PIMPL, 전방 선언으로 구현을 .cpp로 옮기면 헤더가 안정됨. include 최소화, 모듈(C++20) 검토.
시나리오 8: shared_ptr 순환 참조로 메모리 누수
상황: 노드 간 부모-자식 참조를 shared_ptr로 했는데, 트리 해제 후에도 메모리가 안 내려감.
해결 포인트: 부모→자식은 shared_ptr, 자식→부모는 weak_ptr. 순환 구간을 끊어야 참조 카운트가 0이 됨.
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
목차
- 메모리·포인터·RAII (1~15)
- 동시성·성능 (16~30)
- 템플릿·타입·STL (31~40)
- 설계·실무 (41~50)
- 완전한 면접 Q&A 예시
- 자주 하는 실수
- 면접 준비 팁
- 프로덕션 인사이트
1. 메모리·포인터·RAII (1~15)
Q1. shared_ptr과 unique_ptr의 차이, 언제 무엇을 쓰나요?
- 출제 의도: 소유권·공유 vs 독점, 라이프사이클 설계를 이해하는지.
- 모범 답변 요지: unique_ptr은 한 곳만 소유, 이동만 가능. shared_ptr은 공유 소유, 참조 카운트. 공유가 꼭 필요할 때만 shared_ptr(비용·순환 참조 주의). 기본은 unique_ptr.
코드 예시:
// unique_ptr: 소유권이 한 곳에만
std::unique_ptr<Widget> w1 = std::make_unique<Widget>();
// std::unique_ptr<Widget> w2 = w1; // ❌ 복사 불가
std::unique_ptr<Widget> w2 = std::move(w1); // ✅ 이동만 가능
// shared_ptr: 여러 곳에서 공유
std::shared_ptr<Widget> s1 = std::make_shared<Widget>();
std::shared_ptr<Widget> s2 = s1; // ✅ 참조 카운트 증가
Q2. weak_ptr은 왜 필요하나요?
- 출제 의도: 순환 참조와 참조 카운트 한계를 아는지.
- 모범 답변 요지: shared_ptr만 쓰면 A→B→A처럼 순환 시 카운트가 0이 안 되어 누수. weak_ptr은 소유하지 않고 관찰만 하며, lock()으로 shared_ptr을 얻어 사용. 순환 구간에 weak_ptr을 두면 해제됨.
순환 참조 다이어그램:
flowchart LR
subgraph 순환["순환 참조 (shared_ptr만)"]
A1[A] -->|shared_ptr| B1[B]
B1 -->|shared_ptr| A1
N1["카운트 0 안 됨 → 누수"]
end
subgraph 해결["weak_ptr로 해결"]
A2[A] -->|shared_ptr| B2[B]
B2 -->|weak_ptr| A2
N2["카운트 0 → 정상 해제"]
end
Q3. 댕글링 포인터(Dangling Pointer)란? 어떻게 방지하나요?
- 출제 의도: 메모리 안전, 이미 해제된 메모리 참조 위험.
- 모범 답변 요지: 객체가 파괴된 뒤 그 주소를 가리키는 포인터. 사용 시 UB. 방지: raw 포인터 대신 스마트 포인터, 소유권을 한 곳에서만 관리, 사용 후 즉시 null 등.
댕글링 포인터 예시:
// ❌ 댕글링 포인터
int* p = new int(42);
delete p;
*p = 10; // UB: 이미 해제된 메모리 접근
// ✅ 스마트 포인터로 방지
std::unique_ptr<int> p = std::make_unique<int>(42);
// p가 스코프를 벗어나면 자동 해제, 접근 불가
Q4. RAII란 무엇인가요?
- 출제 의도: 리소스(메모리, 파일, 락)를 생성자에서 획득·소멸자에서 해제하는 패턴 이해.
- 모범 답변 요지: “Resource Acquisition Is Initialization”. 생성 시 리소스 획득, 소멸 시 자동 해제. 예외가 나도 스택 언와인딩으로 소멸자가 호출되어 누수·미해제를 막음. lock_guard, unique_ptr이 대표 예.
RAII 패턴 예시:
// lock_guard: 생성 시 락 획득, 소멸 시 자동 해제
{
std::lock_guard<std::mutex> lock(mtx);
// 크리티컬 섹션
} // lock 소멸 → 락 자동 해제 (예외 발생해도)
Q5. shallow copy와 deep copy의 차이는?
- 출제 의도: 복사 시 포인터가 가리키는 대상까지 복사하는지 이해.
- 모범 답변 요지: shallow copy는 포인터 값만 복사해 두 객체가 같은 메모리를 가리킴. deep copy는 가리키는 대상까지 새로 할당해 복사. 리소스 소유 시 deep copy 필요.
// shallow copy: 포인터만 복사 → 두 객체가 같은 버퍼 공유
class Bad {
int* data;
public:
Bad(const Bad& other) : data(other.data) {} // 위험!
};
// deep copy: 대상까지 복사
class Good {
int* data;
public:
Good(const Good& other) : data(new int[*other.data]) {}
};
Q6. 소멸자에서 예외를 던지면 안 되는 이유는?
- 출제 의도: 스택 언와인딩 중 예외 처리 메커니즘 이해.
- 모범 답변 요지: 소멸자는 객체 파괴·스택 언와인딩 중 호출됨. 이때 예외가 나면 “예외 처리 중 또 예외”가 되어
std::terminate호출. 소멸자는noexcept로 두고, 실패 시 로깅 등으로 처리.
Q7. 메모리 누수를 어떻게 발견하나요?
- 출제 의도: Valgrind, ASan 등 도구 사용 경험.
- 모범 답변 요지: Valgrind (memcheck), AddressSanitizer (-fsanitize=address), LeakSanitizer. 스마트 포인터 사용으로 예방. CI에 통합해 조기 발견.
Q8. new/delete와 malloc/free의 차이는?
- 출제 의도: C++ 객체 라이프사이클 이해.
- 모범 답변 요지: new는 생성자 호출, delete는 소멸자 호출. malloc/free는 메모리만 할당/해제. C++ 객체는 new/delete. 타입 안전성, 배열 new[]/delete[] 쌍 맞춤 주의.
Q9. placement new의 용도는?
- 출제 의도: 지정 메모리에 객체 구성 능력.
- 모범 답변 요지: 이미 할당된 메모리 버퍼에 객체를 “구성”할 때. 풀 할당자, 공유 메모리, 버퍼 재사용 시 사용.
new (ptr) T(args)형태.
alignas(Widget) char buf[sizeof(Widget)];
Widget* p = new (buf) Widget(42); // buf에 Widget 구성
p->~Widget(); // 명시적 소멸자 호출 (delete는 안 함)
Q10. 스마트 포인터로 배열을 다루려면?
- 출제 의도: unique_ptr<T[]>, vector 선택.
- 모범 답변 요지:
unique_ptr<T[]>또는 vector 권장.shared_ptr<T[]>는 C++17부터.make_unique<T[]>(n)사용.
Q11. make_shared vs shared_ptr(new T) 차이는?
- 출제 의도: 할당 횟수, 예외 안전성.
- 모범 답변 요지: make_shared는 객체+제어 블록을 한 번에 할당해 효율적.
shared_ptr(new T)는 new 실패 시 누수 가능(예외 안전). 가능하면 make_shared.
Q12. 소멸자가 가상이어야 하는 경우는?
- 출제 의도: 다형적 delete 시 파생 클래스 소멸자 호출.
- 모범 답변 요지: 기반 클래스 포인터로 delete할 때. 가상이 아니면 기반 소멸자만 호출되어 파생 멤버 누수. “다형적으로 사용하는 클래스는 가상 소멸자”.
Base* p = new Derived();
delete p; // Base 소멸자가 가상이어야 Derived::~Derived() 호출
Q13. 멤버 초기화 순서는?
- 출제 의도: 선언 순서 vs 초기화 리스트 순서.
- 모범 답변 요지: 선언 순서대로 초기화됨. 생성자 초기화 리스트 순서와 무관. b가 a를 쓰면 선언 순서가 a→b여야 함.
Q14. explicit 생성자는 왜 쓰나요?
- 출제 의도: 암시적 변환 방지.
- 모범 답변 요지: 인자 하나 받는 생성자는 암시적 변환을 허용.
explicit으로 막아 의도치 않은 변환 방지.std::vector<int> v = 10;같은 실수 방지.
Q15. default/delete 생성자·대입의 용도는?
- 출제 의도: 명시적 기본/삭제 지정.
- 모범 답변 요지:
= default로 컴파일러 생성 유도.= delete로 복사·이동 등 사용 금지.unique_ptr처럼 복사 삭제, 이동만 허용할 때 사용.
2. 동시성·성능 (16~30)
Q16. Data Race란? 어떻게 제거하나요?
- 출제 의도: 동기화 없이 한 스레드 쓰기·다른 스레드 읽기/쓰기 시 UB 인지.
- 모범 답변 요지: 같은 메모리 위치에 동기화 없이 한쪽은 쓰기, 다른 쪽은 읽기/쓰기 → UB. Mutex, Atomic, 동기화 프리미티브로 제거. #34-1 참고.
Data Race 제거 예시:
// ❌ Data Race
int counter = 0;
void inc() { counter++; } // 여러 스레드에서 호출 시 UB
// ✅ Atomic으로 제거
std::atomic<int> counter{0};
void inc() { counter++; } // 원자적
Data Race 발생 시퀀스:
sequenceDiagram
participant T1 as 스레드 1
participant Mem as 메모리
participant T2 as 스레드 2
Mem->>T1: counter 읽기: 0
Mem->>T2: counter 읽기: 0
T1->>T1: +1 → 1
T2->>T2: +1 → 1
T1->>Mem: 쓰기: 1
T2->>Mem: 쓰기: 1
Note over Mem: 결과 1 (기대값 2)
Q17. Mutex와 Atomic, 언제 무엇을 쓰나요?
- 출제 의도: 보호 단위(한 변수 vs 여러 변수·복잡 로직)에 따른 선택.
- 모범 답변 요지: 단일 변수·단순 연산(증가, 플래그) → Atomic. 여러 변수·복잡한 조건·자료구조 → Mutex.
Q18. 데드락을 피하는 방법은?
- 출제 의도: 락 순서 통일, std::lock 사용.
- 모범 답변 요지: 모든 스레드가 같은 순서로 락을 잡거나, std::lock으로 여러 락을 한꺼번에 잡기.
데드락 방지 코드:
// ✅ std::lock으로 데드락 방지
std::lock(mutex_a, mutex_b);
std::lock_guard<std::mutex> lock_a(mutex_a, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(mutex_b, std::adopt_lock);
Q19. volatile과 atomic의 차이는?
- 출제 의도: volatile이 스레드 안전이 아니라는 점.
- 모범 답변 요지: volatile은 컴파일러 최적화 방지용(하드웨어 매핑 등). 스레드 동기화·원자성 보장 아님. atomic은 원자 연산·메모리 순서 보장. 스레드 안전에는 atomic.
Q20. 메모리 오더(seq_cst, acquire, release)는?
- 출제 의도: 동기화 범위·성능 조절 이해.
- 모범 답변 요지: seq_cst는 순차 일관성(가장 강함, 기본값). acquire는 읽기 동기화, release는 쓰기 동기화. 성능이 중요하면 acquire/release 조합으로 완화 가능.
Q21. false sharing이란? 어떻게 해결하나요?
- 출제 의도: 캐시 라인·멀티스레드 성능.
- 모범 답변 요지: 서로 다른 스레드가 같은 캐시 라인에 있는 변수를 수정하면 캐시 무효화가 반복되어 성능 저하. 변수 분리, 패딩,
thread_local로 해결.
// ❌ false sharing: 같은 캐시 라인
struct Bad {
std::atomic<int> counter1; // 스레드 1이 수정
std::atomic<int> counter2; // 스레드 2가 수정
};
// ✅ 패딩으로 분리
struct Good {
std::atomic<int> counter1;
char padding[64]; // 캐시 라인 크기
std::atomic<int> counter2;
};
Q22. 스레드 풀 vs 스레드 매번 생성?
- 출제 의도: 생성/파괴 비용, 재사용.
- 모범 답변 요지: 스레드 생성은 비용이 큼. 풀로 미리 생성해 두고 작업만 할당하면 효율적. I/O 대기·작업 큐에 적합.
Q23. lock-free란?
- 출제 의도: 락 없이 동기화할 수 있는지.
- 모범 답변 요지: 락 대신 CAS(compare_exchange) 등으로 대기 없이 진행. 복잡도·ABA 문제 등 주의. 단순한 경우에만 사용, 대부분 Mutex가 충분.
Q24~30 요약
- Q24 condition_variable 용도 → 조건 만족 시까지 대기·알림.
- Q25 future/promise 용도 → 한 번만 전달되는 값·비동기 결과.
- Q26 CPU 바운드 vs I/O 바운드 → 스레드 수·이벤트 루프 선택에 영향.
- Q27 프로파일링 경험 → 도구(perf, VTune 등)로 병목 구간 파악.
- Q28 캐시 친화적 코드 → 연속 접근, 구조체 정렬·패딩.
- Q29 false sharing 해결 → 변수 분리, 패딩, thread_local.
- Q30 Asio Strand 역할 → 락 없이 핸들러 순차 실행 보장.
3. 템플릿·타입·STL (31~40)
Q31. lvalue와 rvalue, 이동 의미론이 뭔가요?
- 출제 의도: 불필요한 복사 제거, std::move 사용 시점.
- 모범 답변 요지: lvalue는 주소 취급 가능한 식, rvalue는 임시·이동 대상. 이동 의미론은 리소스를 “복사하지 않고 넘김”. std::move는 rvalue로 캐스팅해 이동 생성자/대입 호출 유도.
복사 vs 이동:
flowchart LR
subgraph copy["복사"]
C1[원본] --> C2[데이터 복제]
C2 --> C3[대상]
C1 -.->|유지| C1
end
subgraph move["이동"]
M1[원본] --> M2[포인터/핸들만 이전]
M2 --> M3[대상]
M1 -.->|빈 상태| M1
end
Q32. perfect forwarding이 뭔가요?
- 출제 의도: 템플릿에서 인자의 값 카테고리·const 유지.
- 모범 답변 요지: T&&(유니버설 참조)와 std::forward로, 넘겨받은 인자를 내부 함수에 lvalue/rvalue 그대로 전달. 래퍼·팩토리에서 사용.
perfect forwarding 예시:
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
Q33~40 요약
- Q33 SFINAE vs concepts → 타입 제약 표현 방식, C++20 concepts가 더 읽기 쉬움.
- Q34 CRTP 용도 → 정적 다형성, 인터페이스 없이 파생 클래스 타입 활용.
- Q35 vector에서 erase 시 iterator 무효화 → erase는 다음 유효 iterator 반환, 범위 for 중 erase 주의.
- Q36 map vs unordered_map 선택 → 정렬 필요·순서 보장 vs O(1) 해시.
- Q37 반복자 무효화 조건 → vector 삽입/삭제, map 삭제 등.
- Q38 emplace_back vs push_back → 생성 인자만 넘겨 한 번에 구성, 불필요한 복사/이동 감소.
- Q39 std::move 후 객체 사용 → “moved-from” 상태만 보장, 재사용 시 주의.
- Q40 noexcept의 의미 → 예외를 던지지 않음을 선언, 이동 등 최적화에 활용.
4. 설계·실무 (41~50)
Q41. PIMPL이 뭔가요? 왜 쓰나요?
- 출제 의도: 컴파일 의존성 감소, ABI 안정성, 바이너리 호환.
- 모범 답변 요지: 구현을 불완전 타입 포인터로 숨김. 헤더 변경 없이 구현만 수정 가능, 컴파일 시간·ABI 안정에 유리.
PIMPL 구조:
// Widget.h - 사용자에게 노출
class Widget {
public:
Widget();
~Widget();
void doSomething();
private:
struct Impl; // 불완전 타입
std::unique_ptr<Impl> pImpl;
};
// Widget.cpp - 구현만 여기서
struct Widget::Impl {
HeavyDependency dep; // 헤더에 노출 안 함
};
Q42. 예외 안전성 보장 수준 (basic/strong/nothrow)?
- 출제 의도: 예외 발생 시 리소스·불변식 관리 이해.
- 모범 답변 요지: basic: 누수 없음. strong: 실패 시 원래 상태로 롤백. nothrow: 예외 없음. swap·RAII로 strong 보장 구현.
strong 보장 예시 (copy-and-swap):
// strong 보장: 실패 시 원래 상태 유지
Widget& Widget::operator=(const Widget& other) {
Widget temp(other); // 예외 시 *this 변경 없음
swap(*this, temp); // swap은 보통 noexcept
return *this;
}
Q43. ODR(One Definition Rule)이란?
- 출제 의도: 헤더·inline·템플릿에서 정의가 한 번만 있거나 동일해야 함을 아는지.
- 모범 답변 요지: 변수·함수·클래스 등은 전역적으로 하나의 정의만 가져야 함. inline, 템플릿, 헤더-only는 조건부로 여러 번 정의 가능(동일해야 함).
Q44. undefined behavior 예시는?
- 출제 의도: UB 인지, 회피 의식.
- 모범 답변 요지: 댕글링 포인터 접근, signed 정수 overflow, Data Race, null 역참조, 배열 범위 초과, ODR 위반 등. UB는 “가끔만” 틀릴 수 있어 디버깅이 어렵다.
Q45. 빌드 시간을 줄이는 방법은?
- 출제 의도: 대형 프로젝트 경험.
- 모범 답변 요지: PIMPL, 전방 선언으로 불필요한 include 제거. 모듈(C++20), 병렬 빌드(-j), ccache 등. 헤더 의존성이 빌드 시간의 주원인.
Q46. ABI 호환이 깨지는 변경은?
- 출제 의도: 라이브러리 배포·버전 관리.
- 모범 답변 요지: 가상 함수 추가/순서 변경, 멤버 레이아웃 변경, vtable 크기 변경, 이름 맹글링 변경. PIMPL로 구현을 숨기면 헤더가 안 바뀌어 ABI 호환 유지에 유리.
Q47. 정적/동적 라이브러리 차이는?
- 출제 의도: 링크·배포 단위 이해.
- 모범 답변 요지: 정적(.a): 링크 시 코드가 실행 파일에 포함. 동적(.so): 런타임에 로드. 동적은 여러 프로세스가 공유 가능, 업데이트 시 .so만 교체 가능.
Q48~50 요약
- Q48 메모리 누수·오류 찾는 도구 → Valgrind, ASan, MSan, TSan.
- Q49 코드 리뷰에서 C++에서 자주 보는 포인트 → 소유권, 예외 안전성, 동시성, 스마트 포인터 사용.
- Q50 C++에서 가장 조심하는 것 → UB, 메모리 안전, 스레드 안전, ABI·헤더 의존성.
50문 빠른 참조표
| 번호 | 키워드 | 한 줄 요지 |
|---|---|---|
| 1 | shared_ptr vs unique_ptr | 기본 unique_ptr, 공유 필요 시 shared_ptr |
| 2 | weak_ptr | 순환 참조 끊기, lock()으로 사용 |
| 3 | 댕글링 | 해제된 메모리 참조, 스마트 포인터로 방지 |
| 4 | RAII | 생성 시 획득, 소멸 시 해제 |
| 5 | shallow vs deep copy | 포인터만 vs 대상까지 복사 |
| 6 | 소멸자 예외 | terminate, noexcept로 |
| 7 | 메모리 누수 | Valgrind, ASan |
| 8 | new vs malloc | 생성자/소멸자 호출 유무 |
| 9 | placement new | 지정 메모리에 객체 구성 |
| 10 | 스마트 포인터 배열 | unique_ptr<T[]>, vector |
| 11 | make_shared | 한 번 할당, 예외 안전 |
| 12 | 가상 소멸자 | 다형적 delete 시 필수 |
| 13 | 멤버 초기화 순서 | 선언 순서대로 |
| 14 | explicit | 암시적 변환 방지 |
| 15 | default/delete | 명시적 기본/삭제 |
| 16 | Data Race | 동기화 없이 쓰기+읽기 → UB |
| 17 | Mutex vs Atomic | 여러 변수 vs 단일 변수 |
| 18 | 데드락 | 락 순서 통일, std::lock |
| 19 | volatile vs atomic | 동기화용 아님 vs 원자성 |
| 20 | 메모리 오더 | seq_cst, acquire, release |
| 21 | false sharing | 같은 캐시 라인 수정 |
| 22 | 스레드 풀 | 생성 비용, 재사용 |
| 23 | lock-free | CAS, 복잡함 |
| 24 | condition_variable | 조건 대기·알림 |
| 25 | future/promise | 비동기 결과 |
| 26 | CPU vs I/O 바운드 | 스레드 수 결정 |
| 27 | 프로파일링 | perf, VTune |
| 28 | 캐시 친화 | 연속 접근, 정렬 |
| 29 | false sharing 해결 | 패딩, thread_local |
| 30 | Asio Strand | 순차 실행 보장 |
| 31 | lvalue/rvalue | 이동 의미론 기반 |
| 32 | perfect forwarding | forward, 값 카테고리 유지 |
| 33 | SFINAE vs concepts | 타입 제약 |
| 34 | CRTP | 정적 다형성 |
| 35 | vector erase | it = erase(it) |
| 36 | map vs unordered_map | 정렬 vs O(1) |
| 37 | iterator 무효화 | 삽입/삭제 시 |
| 38 | emplace_back | 인자만 넘겨 구성 |
| 39 | move 후 사용 | moved-from 주의 |
| 40 | noexcept | 예외 없음 선언 |
| 41 | PIMPL | 컴파일 의존성, ABI |
| 42 | 예외 안전성 | basic, strong, nothrow |
| 43 | ODR | 하나의 정의 |
| 44 | UB | 댕글링, overflow, Data Race |
| 45 | 빌드 시간 | PIMPL, 전방 선언 |
| 46 | ABI | 가상 함수, 멤버 레이아웃 |
| 47 | 정적/동적 라이브러리 | 링크 시점 |
| 48 | 메모리 도구 | Valgrind, ASan, TSan |
| 49 | 코드 리뷰 | 소유권, 동시성 |
| 50 | C++에서 조심 | UB, 메모리, 스레드 |
5. 완전한 면접 Q&A 예시
예시 1: shared_ptr vs unique_ptr (실제 대화형)
면접관: “shared_ptr과 unique_ptr의 차이를 설명해 주시고, 언제 무엇을 쓰시나요?”
지원자 (모범 답변):
unique_ptr은 한 곳에서만 소유권을 갖고, 복사는 안 되고 이동만 가능합니다. shared_ptr은 여러 곳에서 공유하며 참조 카운팅으로 마지막 참조가 사라질 때 해제합니다.
기본적으로는 unique_ptr을 쓰고, 정말 여러 곳에서 소유권을 나눠 가져야 할 때만 shared_ptr을 씁니다. shared_ptr은 참조 카운트·제어 블록 할당 비용이 있고, 순환 참조 시 weak_ptr이 필요하므로, 꼭 필요할 때만 사용하는 게 좋습니다.
면접관 (꼬리 질문): “그럼 weak_ptr은 왜 필요한가요?”
지원자:
shared_ptr만 쓰면 A가 B를 가리키고, B가 다시 A를 가리키는 순환이 생길 수 있습니다. 그러면 참조 카운트가 0이 안 되어 메모리 누수가 납니다. 순환 구간 중 한쪽을 weak_ptr로 바꾸면, weak_ptr은 소유하지 않으므로 카운트에 포함되지 않아 정상적으로 해제됩니다. lock()으로 shared_ptr을 얻어 사용할 때만 유효한지 확인하면 됩니다.
예시 2: Data Race와 Mutex vs Atomic
면접관: “Data Race가 뭔지 설명하고, Mutex와 Atomic 중 뭘 쓸지 어떻게 결정하시나요?”
지원자 (모범 답변):
Data Race는 같은 메모리 위치에 동기화 없이 한 스레드는 쓰기, 다른 스레드는 읽기나 쓰기를 할 때 발생합니다. C++ 표준상 undefined behavior입니다.
제거 방법으로 Mutex와 Atomic이 있는데, 선택 기준은 보호 대상입니다. 단일 변수이고 증가·플래그 같은 단순 연산이면 Atomic이 적합합니다. 여러 변수를 한 번에 수정하거나, 복잡한 조건·자료구조를 보호해야 하면 Mutex를 씁니다. Atomic은 그 변수 하나만 원자적으로 보호하고, 여러 변수 간의 일관성은 보장하지 못합니다.
예시 3: 이동 의미론과 std::move
면접관: “함수에서 vector를 반환할 때 std::move를 붙여야 하나요?”
지원자 (모범 답변):
붙이지 않습니다. return 시 지역 객체는 컴파일러가 자동으로 rvalue로 취급해 이동 생성자가 선택됩니다. RVO(Return Value Optimization)도 적용될 수 있어서, std::move를 붙이면 오히려 RVO가 막혀 복사가 발생할 수 있습니다. 그래서
return vec;처럼 그냥 반환하는 게 맞습니다.std::move는 “이 lvalue를 더 이상 쓰지 않으니 이동해도 된다”고 알릴 때, 예를 들어 인자로 넘기거나 멤버에 저장할 때 사용합니다.
예시 4: RAII 설명
면접관: “RAII가 뭔가요? 왜 중요한가요?”
지원자 (모범 답변):
“Resource Acquisition Is Initialization”의 약자로, 생성자에서 리소스를 획득하고 소멸자에서 해제하는 패턴입니다. C++에서는 스택에 올라간 객체가 스코프를 벗어날 때 반드시 소멸자가 호출되므로, 예외가 발생해도 스택 언와인딩 과정에서 소멸자가 호출되어 리소스가 해제됩니다.
lock_guard, unique_ptr, fstream 등이 대표 예입니다. new/delete를 직접 쓰지 않고 이런 RAII 래퍼를 쓰면 누수와 미해제를 방지할 수 있습니다.
예시 5: vector erase와 iterator 무효화
면접관: “vector에서 erase할 때 iterator가 무효화된다고 하는데, 어떻게 안전하게 삭제하나요?”
지원자 (모범 답변):
vector는 연속 메모리라서, erase 시 그 위치 이후의 iterator들이 모두 무효화됩니다. erase는 삭제된 요소의 다음 유효 iterator를 반환하므로,
it = vec.erase(it)처럼 반환값을 받아 사용하면 됩니다. 범위 for문 안에서는 erase를 직접 호출하면 안 됩니다.C++20부터는
std::erase_if(vec, pred)를 쓰면 내부에서 안전하게 처리해 줍니다.
예시 6: PIMPL과 ABI
면접관: “PIMPL을 쓰는 이유가 뭔가요? 캡슐화 말고요.”
지원자 (모범 답변):
캡슐화도 있지만, 실무에서는 컴파일 의존성 감소와 ABI 안정성이 더 중요합니다. 구현 디테일을 헤더에 넣으면, 그 헤더를 include하는 모든 파일이 구현 변경 시 재컴파일됩니다. PIMPL로 구현을 .cpp로 옮기면 헤더는 바뀌지 않아 컴파일 시간이 줄어듭니다.
공개 라이브러리에서는 ABI가 깨지면 사용자가 재컴파일 없이 새 .so로 교체할 수 없습니다. PIMPL로 구현을 숨기면 헤더 레이아웃이 안 바뀌어 ABI 호환을 유지하기 쉽습니다.
예시 7: 예외 안전성 보장 수준
면접관: “예외 안전성의 basic, strong, nothrow 보장이 뭔가요?”
지원자 (모범 답변):
basic 보장은 예외가 나도 리소스 누수가 없고, 프로그램이 유효한 상태를 유지하는 것입니다. strong 보장은 실패 시 연산 전 상태로 완전히 롤백되는 것입니다. nothrow는 예외를 던지지 않음을 보장합니다.
strong 보장은 copy-and-swap 패턴으로 구현할 수 있습니다. 임시 복사본을 만들고, 성공하면 swap으로 교체합니다. swap이 noexcept이면 strong 보장이 됩니다. push_back 같은 연산에서 “실패 시 원래 상태”가 중요할 때 적용합니다.
실전 연습: 다음 코드의 문제점
연습 1 — 이 코드의 문제는?
std::vector<int> getFiltered(const std::vector<int>& src) {
std::vector<int> result;
for (size_t i = 0; i < src.size(); ++i) {
if (src[i] > 0) result.push_back(src[i]);
}
return std::move(result); // 문제?
}
답: return std::move(result)는 불필요하고 RVO를 방해할 수 있음. return result;가 맞음.
연습 2 — 이 코드의 문제는?
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 0) vec.erase(it); // 문제?
}
답: erase 후 it가 무효화되는데 ++it로 다음으로 넘어감. it = vec.erase(it)로 반환값을 받아야 함.
6. 자주 하는 실수
실수 1: return에서 std::move 남발
// ❌ 잘못된 사용
std::vector<int> getData() {
std::vector<int> v = {1, 2, 3};
return std::move(v); // RVO 방해, 불필요
}
// ✅ 올바른 사용
std::vector<int> getData() {
std::vector<int> v = {1, 2, 3};
return v; // 컴파일러가 이동 또는 RVO 적용
}
실수 2: shared_ptr을 기본으로 사용
// ❌ 공유가 필요 없는데 shared_ptr 사용
std::shared_ptr<Config> config = std::make_shared<Config>(); // 한 곳에서만 사용
// ✅ 소유권이 한 곳이면 unique_ptr
std::unique_ptr<Config> config = std::make_unique<Config>();
실수 3: 범위 for 루프에서 erase
// ❌ iterator 무효화
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 0) vec.erase(it); // it 무효화!
}
// ✅ erase가 반환하는 다음 iterator 사용
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it == 0) it = vec.erase(it);
else ++it;
}
실수 4: volatile을 스레드 동기화에 사용
// ❌ volatile은 원자성 보장 안 함
volatile int flag = 0; // Data Race 가능
// ✅ atomic 사용
std::atomic<int> flag{0};
실수 5: 소멸자에서 예외를 던짐
// ❌ 스택 언와인딩 중 예외 → terminate
~Widget() {
if (error) throw std::runtime_error("error"); // 위험!
}
// ✅ 소멸자는 noexcept, 로깅 등으로 처리
~Widget() noexcept {
if (error) {
log::error("cleanup failed");
// 예외 던지지 않음
}
}
실수 6: 기반 클래스 소멸자를 가상으로 안 함
// ❌ 다형적 delete 시 파생 소멸자 호출 안 됨
class Base { /* ... */ };
class Derived : public Base { /* ... */ };
Base* p = new Derived();
delete p; // Derived 소멸자 호출 안 됨 → 누수
// ✅ 가상 소멸자
class Base {
public:
virtual ~Base() = default;
};
실수 7: emplace_back 대신 push_back으로 임시 객체 생성
// ❌ 불필요한 복사/이동
vec.push_back(MyType(1, 2, 3)); // 임시 생성 → 이동
// ✅ emplace_back: 인자만 넘겨 한 번에 구성
vec.emplace_back(1, 2, 3);
실수 8: std::move 후 객체 재사용
// ❌ moved-from 상태는 "유효하지만 unspecified"
auto v2 = std::move(v1);
v1.push_back(1); // 동작 가능하지만 보장 안 됨
// ✅ 이동 후에는 사용하지 않거나, clear() 후 재사용
v1.clear();
v1.push_back(1);
실수 9: 데드락 — 락 순서 불일치
// ❌ 스레드 A: lock(a) → lock(b), 스레드 B: lock(b) → lock(a) → 데드락
// ✅ 모든 스레드가 같은 순서: lock(a) → lock(b)
// 또는 std::lock(a, b)로 한 번에
실수 10: PIMPL에서 소멸자 정의 누락
// ❌ unique_ptr<Impl>은 완전 타입이 소멸 시 필요
// Widget.cpp에 ~Widget() 정의가 없으면 기본 소멸자가 인라인 생성되고,
// Impl이 불완전 타입이라 컴파일 에러
// ✅ Widget.cpp에 명시적 소멸자 정의
Widget::~Widget() = default;
7. 면접 준비 팁
시간별 준비 로드맵
| 시점 | 할 일 |
|---|---|
| 2주 전 | 50문 전체 훑기, 모르는 부분 표시 |
| 1주 전 | 핵심 개념 정리, 코드 5개 이상 손으로 작성 |
| 3일 전 | 꼬리 질문 대비, “모르면” 대답 연습 |
| 1일 전 | 50문 빠른 참조표만 복습, 충분히 휴식 |
| 당일 | 한 문장 요약 + 핵심 차이 + 선택 기준 구조로 답변 |
1주 전: 핵심 개념 정리
| 영역 | 필수 암기 | 연습할 코드 |
|---|---|---|
| 메모리 | unique_ptr vs shared_ptr, RAII, 댕글링 | 스마트 포인터 예제 |
| 동시성 | Data Race, Mutex vs Atomic, 데드락 | 락 순서, std::lock |
| STL | 이동 의미론, iterator 무효화, emplace | vector erase, emplace_back |
| 설계 | PIMPL, 예외 안전성, ODR | PIMPL 구조 |
3일 전: 꼬리 질문 대비
- “그럼 weak_ptr은?” → Q2 답변
- “Mutex 대신 Atomic을 쓸 수 있나요?” → Q17 답변
- “return에 std::move를 붙여야 하나요?” → Q31 답변
- “PIMPL의 단점은?” → 컴파일은 줄지만 힙 할당·간접 접근 비용
당일: 답변 구조
- 한 문장 요약 (예: “unique_ptr은 한 곳만 소유, shared_ptr은 공유”)
- 핵심 차이 2~3가지 (이동 vs 복사, 비용, 순환 참조)
- 선택 기준 (기본은 unique_ptr, 공유 필요 시 shared_ptr)
- 경험 있으면 한두 문장 추가
체크리스트
- 50문 요지 암기 (한 문장씩)
- 각 질문에 대한 꼬리 질문 1개씩 예상
- 코드 예제 5개 이상 손으로 쓸 수 있는지
- “모르면” 대답 연습 (“그 부분은 아직 경험이 없어서, 공부해 보겠습니다”)
면접관이 자주 꼬리 질문하는 패턴
| 첫 질문 | 자주 나오는 꼬리 질문 |
|---|---|
| shared_ptr vs unique_ptr | ”그럼 weak_ptr은?” |
| Data Race | ”Mutex vs Atomic 선택 기준은?” |
| 이동 의미론 | ”return에 std::move 붙여야 하나요?” |
| RAII | ”예외가 나도 해제되나요?” |
| PIMPL | ”단점은? ABI 말고” |
| vector erase | ”범위 for에서 erase하면?” |
| 가상 소멸자 | ”가상이 아니면 어떻게 되나요?“ |
| make_shared | ”shared_ptr(new T)와 차이는?“ |
8. 프로덕션 인사이트
인사이트 1: shared_ptr 비용은 생각보다 큼
프로덕션에서 shared_ptr을 남용하면 참조 카운트 원자 연산·제어 블록 할당이 누적됩니다. 기본은 unique_ptr, 공유가 꼭 필요할 때만 shared_ptr을 쓰는 습관이 성능에 도움이 됩니다.
인사이트 2: Data Race는 “가끔만” 틀린다
Data Race가 있으면 UB라서, 같은 코드가 환경에 따라 다르게 동작할 수 있습니다. “로컬에선 되는데 서버에서만 터진다”는 패턴이 자주 Data Race입니다. TSan(ThreadSanitizer)으로 빌드해 보는 것이 좋습니다.
인사이트 3: PIMPL은 ABI 안정에 필수
공개 라이브러리·SDK를 만들 때, 헤더에 구현 디테일을 노출하면 사용자가 새 버전으로 링크할 때 ABI가 깨질 수 있습니다. PIMPL로 구현을 숨기면 헤더 변경 없이 구현만 수정해 배포할 수 있습니다.
인사이트 4: 빌드 시간은 PIMPL·전방 선언으로
대형 프로젝트에서 헤더 의존성이 많으면 한 줄 수정에 수 분씩 컴파일됩니다. PIMPL, 전방 선언, 모듈(C++20)로 불필요한 include를 줄이는 것이 실무에서 중요합니다.
인사이트 5: Valgrind/ASan은 CI에 넣자
메모리 누수·버퍼 오버플로우는 로컬에서 안 나오다가 프로덕션에서 터질 수 있습니다. CI 파이프라인에 Valgrind, AddressSanitizer를 넣어 두면 조기에 발견할 수 있습니다.
인사이트 6: 이동은 “기본”으로 적용되게 하자
반환값, 컨테이너에 넣을 때 등 컴파일러가 자동으로 이동을 적용하는 경우가 많습니다. return vec;처럼 그냥 두고, std::move는 “이 lvalue를 더 이상 쓰지 않는다”는 명시적 신호가 필요할 때만 사용하는 게 좋습니다.
인사이트 7: 예외 안전성은 “기본”부터
operator=, push_back 등에서 복사 시 예외가 나면? basic 보장(누수 없음)은 최소한이고, strong 보장은 copy-and-swap 패턴으로 구현할 수 있습니다. “실패 시 원래 상태”가 중요한 연산에 적용합니다.
인사이트 8: 도구 조합
- Valgrind (memcheck): 누수, 잘못된 접근
- ASan (AddressSanitizer): 버퍼 오버플로우, use-after-free
- TSan (ThreadSanitizer): Data Race
- MSan (MemorySanitizer): 초기화 안 된 메모리
프로덕션 빌드에는 보통 비활성화하지만, CI·nightly 빌드에서는 이 조합으로 검증하는 것이 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 기술 면접 질문 30선 | “포인터와 참조의 차이는?” 실전 답변 정리
- C++ 신입 개발자 면접 | “프로젝트 경험 없어요” 포트폴리오·답변 전략
- C++ 스마트 포인터와 순환 참조(Circular Reference) 해결법 [#33-3]
이 글에서 다루는 키워드 (관련 검색어)
C++ 면접, 면접 50문, C++ 기술 면접, shared_ptr unique_ptr, Data Race, PIMPL 등으로 검색하시면 이 글이 도움이 됩니다.
정리
- 출제 의도를 알면 “어디까지 말할지”가 잡힙니다. 요지만 짧게 말한 뒤, “경험 있으면” 한두 문장 보강하는 식으로 활용할 수 있습니다.
- 문제 시나리오를 미리 생각해 두면, “이런 상황에서 막혔다”는 질문에도 대응할 수 있습니다.
- 완전한 Q&A 예시처럼 구조화해서 답하면 면접관이 이해하기 쉽습니다.
- 자주 하는 실수를 피하고, 준비 팁으로 마무리하면 실전에서 활용도가 높습니다.
- 상세 이론은 #33·#34·#38 등 해당 번호를 참고하세요.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 스마트 포인터 선택, 동시성 설계, STL 사용 시 위의 가이드를 참고하면 됩니다. 프로덕션에서는 소유권·예외 안전성·ABI 안정성을 특히 신경 쓰는 것이 좋습니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
Q. 면접에서 모르는 질문이 나오면?
A. “그 부분은 아직 경험이 없어서, 면접 후 공부해 보겠습니다”라고 솔직히 말하는 것이 좋습니다. 아는 것과 모르는 것을 구분하는 것도 역량입니다.
한 줄 요약: 자주 틀리는 C++ 면접 질문과 출제 의도·모범 답변으로 준비할 수 있습니다. 문제 시나리오, 완전한 Q&A 예시, 자주 하는 실수, 준비 팁, 프로덕션 인사이트까지 담았습니다. 다음으로 도메인별 요구 역량(#46-3)를 읽어보면 좋습니다.
다음 글: [C++ 면접·시스템 설계 #46-3] 회사·도메인별 C++ 요구 역량 차이: 네카라쿠배, 금융/HFT, 게임사
이전 글: [C++ 면접·시스템 설계 #46-1] 백엔드·게임 서버 시스템 디자인: 대규모 동시 접속과 메모리 풀
관련 글
- C++ 백엔드·게임 서버 시스템 디자인 | 대규모 동시 접속과 메모리 풀 [#46-1]
- C++ 도메인별 요구 역량 | 네카라쿠배·금융·게임 [#46-3]
- C++ 함수 객체(Functor) 완벽 가이드 | operator·상태 보유
- C++ 오픈소스 기여: 유명 라이브러리 분석부터 첫 Pull Request까지 [#45-1]
- C++ X-Macro 완벽 가이드 | enum-string 매핑·에러 코드·상태 머신·커맨드 테이블 실전