C++ 자주 틀리는 C++ 기술 면접 질문 50선 | 출제 의도와 모범 답변 [#46-2]

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이 됨.

개념을 잡는 비유

이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.


목차

  1. 메모리·포인터·RAII (1~15)
  2. 동시성·성능 (16~30)
  3. 템플릿·타입·STL (31~40)
  4. 설계·실무 (41~50)
  5. 완전한 면접 Q&A 예시
  6. 자주 하는 실수
  7. 면접 준비 팁
  8. 프로덕션 인사이트

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문 빠른 참조표

번호키워드한 줄 요지
1shared_ptr vs unique_ptr기본 unique_ptr, 공유 필요 시 shared_ptr
2weak_ptr순환 참조 끊기, lock()으로 사용
3댕글링해제된 메모리 참조, 스마트 포인터로 방지
4RAII생성 시 획득, 소멸 시 해제
5shallow vs deep copy포인터만 vs 대상까지 복사
6소멸자 예외terminate, noexcept로
7메모리 누수Valgrind, ASan
8new vs malloc생성자/소멸자 호출 유무
9placement new지정 메모리에 객체 구성
10스마트 포인터 배열unique_ptr<T[]>, vector
11make_shared한 번 할당, 예외 안전
12가상 소멸자다형적 delete 시 필수
13멤버 초기화 순서선언 순서대로
14explicit암시적 변환 방지
15default/delete명시적 기본/삭제
16Data Race동기화 없이 쓰기+읽기 → UB
17Mutex vs Atomic여러 변수 vs 단일 변수
18데드락락 순서 통일, std::lock
19volatile vs atomic동기화용 아님 vs 원자성
20메모리 오더seq_cst, acquire, release
21false sharing같은 캐시 라인 수정
22스레드 풀생성 비용, 재사용
23lock-freeCAS, 복잡함
24condition_variable조건 대기·알림
25future/promise비동기 결과
26CPU vs I/O 바운드스레드 수 결정
27프로파일링perf, VTune
28캐시 친화연속 접근, 정렬
29false sharing 해결패딩, thread_local
30Asio Strand순차 실행 보장
31lvalue/rvalue이동 의미론 기반
32perfect forwardingforward, 값 카테고리 유지
33SFINAE vs concepts타입 제약
34CRTP정적 다형성
35vector eraseit = erase(it)
36map vs unordered_map정렬 vs O(1)
37iterator 무효화삽입/삭제 시
38emplace_back인자만 넘겨 구성
39move 후 사용moved-from 주의
40noexcept예외 없음 선언
41PIMPL컴파일 의존성, ABI
42예외 안전성basic, strong, nothrow
43ODR하나의 정의
44UB댕글링, overflow, Data Race
45빌드 시간PIMPL, 전방 선언
46ABI가상 함수, 멤버 레이아웃
47정적/동적 라이브러리링크 시점
48메모리 도구Valgrind, ASan, TSan
49코드 리뷰소유권, 동시성
50C++에서 조심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 무효화, emplacevector erase, emplace_back
설계PIMPL, 예외 안전성, ODRPIMPL 구조

3일 전: 꼬리 질문 대비

  • “그럼 weak_ptr은?” → Q2 답변
  • “Mutex 대신 Atomic을 쓸 수 있나요?” → Q17 답변
  • “return에 std::move를 붙여야 하나요?” → Q31 답변
  • “PIMPL의 단점은?” → 컴파일은 줄지만 힙 할당·간접 접근 비용

당일: 답변 구조

  1. 한 문장 요약 (예: “unique_ptr은 한 곳만 소유, shared_ptr은 공유”)
  2. 핵심 차이 2~3가지 (이동 vs 복사, 비용, 순환 참조)
  3. 선택 기준 (기본은 unique_ptr, 공유 필요 시 shared_ptr)
  4. 경험 있으면 한두 문장 추가

체크리스트

  • 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 매핑·에러 코드·상태 머신·커맨드 테이블 실전