C++ 개발자 기술 면접 | 자주 나오는 질문과 답변 스크립트(30초·90초)·꼬리 질문 대비

이 글의 핵심

같은 내용을 알아도 결론, 메커니즘, 실무, 한계의 순서로 말하느냐에 따라 면접 평가가 달라집니다. 네 블록 답변 프레임을 먼저 잡고, 12개 주제별 스크립트와 "이 코드 문제점은?" 유형에 쓸 체크리스트로 실제 면접에서 말할 문장을 준비할 수 있습니다.

들어가며: 같은 답변도 “순서”가 다르면 점수가 갈린다

C++ 기술 면접에서 자주 지는 패턴은 지식 부족이 아니라 말하는 순서입니다. 정의를 길게 늘어놓다 시간이 지나면, 면접관은 “이 사람이 핵심을 짚는 사람인가?”를 확인하지 못한 채 끝나 버립니다.

이 글은 질문을 수십 개 나열하는 대신 답변 골격과 말로 바로 쓸 수 있는 스크립트를 드립니다. 면접 전에 소리 내어 읽어 보며, 본인 경험 한 문장만 끼워 넣으면 그대로 현장에서 쓸 수 있습니다.

먼저 네 블록짜리 답변 프레임을 잡고, 고빈도 주제 12개에 대해 30초·90초 스크립트와 이어질 꼬리 질문을 정리하고, 짧게 답하면 되는 단골 질문도 한 문장 답으로 모았습니다. 마지막에는 코드 리뷰형 질문에 쓰는 점검 순서와, 면접에서 자주 부정확하게 말하는 문장을 짚습니다.


답변 프레임: 네 블록만 기억하면 된다

면접관이 듣고 싶은 정보는 대부분 아래 네 덩어리 안에 있습니다.

결론 10초, 메커니즘 30초, 실무 20초, 한계 10초로 이어지는 면접 답변 네 블록

그림: 본문의 답변 프레임을 그림으로 옮긴 것이다. 각 블록에 쓰는 시간은 본문이 제시한 기준이며, 모르는 질문일 때는 2번을 추측 대신 확인 방법으로 바꾼다.

  1. 결론(10초): “한 문장으로 무엇인가 / 언제 쓰는가”
  2. 메커니즘(30초): 컴파일러·런타임·메모리 모델 중 관련된 층만
  3. 실무(20초): 팀 코드에서의 선택 기준, 본인이 겪은 증상
  4. 한계(10초): 비용, UB, ABI, 예외 상황 중 하나만 짚고 마무리

모르는 질문이면 1번을 “확실히 아는 범위에서만” 짧게 말하며, 2번을 추측 대신 확인 방법(cppreference, 컴파일러 경고, Sanitizer, 최소 재현)으로 바꿉니다.


주제별 스크립트

각 항목은 30초는 거의 그대로 외워도 되고, 90초는 숫자·경험만 바꿔 넣으면 됩니다. 꼬리는 면접관이 이어서 묻기 좋은 방향입니다.

RAII와 스마트 포인터(소유권)

30초: “RAII는 생성자에서 자원을 잡고 소멸자에서 푸는 패턴이라서, 스코프를 벗어나면 해제 시점이 보장됩니다. C++에서는 기본이 unique_ptr로 단일 소유를 표현하며, 정말로 공유가 필요할 때만 shared_ptr을 씁니다.”

90초: “수동 new/delete는 예외 경로에서 누락되기 쉬워서 RAII로 감쌉니다. unique_ptr은 복사 없이 이동만 되니 소유권이 명확하며, shared_ptr은 참조 카운트 비용과 순환 참조 이슈가 있어서 기본값으로 두지 않습니다. weak_ptr은 캐시나 옵저버처럼 소유하지 않고 관찰만 할 때 씁니다.”

꼬리: “make_shared가 왜 shared_ptr 생성에 유리한가요?” → “제어 블록과 객체를 한 번에 할당해 할당 횟수와 캐시 친화를 노린다” 정도면 충분합니다. 한 단계 더 들어오면 “대신 weak_ptr이 남아 있는 동안에는 제어 블록과 함께 객체 메모리도 해제되지 않는다”는 단점까지 말할 수 있으면 좋습니다. 큰 객체를 weak_ptr 캐시로 관찰하는 구조에서는 이 차이 때문에 new로 따로 할당하는 쪽을 고르기도 합니다.


이동 의미론·복사·RVO

30초: “이동은 ‘깊은 복사’ 대신 내부 버퍼 포인터 같은 자원 핸들만 넘겨서 O(1)에 가깝게 옮기는 것입니다. 복사 생성자가 무거운 타입에서 특히 중요합니다.”

90초: “rvalue는 보통 임시값이며, 이동 생성자·이동 대입이 있으면 그 임시의 자원을 빼앗아 옵니다. 반환값 최적화(RVO)가 되는 형태에서는 return에 std::move를 붙이면 오히려 NRVO를 막을 수 있어서 조심합니다. std::move는 캐스트에 가깝으며, 실제 이동은 이동 연산이 있을 때만 일어난다고 말합니다.”

꼬리: “다섯 특수 멤버에서 Rule of 0/3/5는?” → “직접 자원을 잡으면 규칙을 맞추고, 스마트 포인터로 위임하면 0으로 둔다” 수준.


가상 함수·vtable·디스패치 비용

30초: “가상 함수는 동적 디스패치입니다. 객체에 vptr이 있고 vtable을 통해 실제 구현을 찾습니다. 정적 디스패치보다 간접 호출 비용이 있지만, 인터페이스 치환과 확장에 씁니다.”

90초: “컴파일 타임에 타입이 고정되면 인라인·디버추얼라이제이션(devirtualization)이 가능하지만, 인터페이스 뒤에 구현이 바뀌면 런타임에 함수 포인터 테이블을 탑니다. 따라서 핫 루프 안에서 가상 호출을 남발하면 캐시 미스와 분기 예측 실패가 아프다는 식으로 트레이드오프를 말합니다.”

꼬리: “소멸자는 왜 virtual이어야 하나요?” → “기본 클래스 포인터로 파생 객체를 지울 때 파생 소멸자까지 타려면 필요하다”.


const correctness

30초: “const는 컴파일 타임 계약입니다. 인자를 const T&로 받으면 호출 측에 수정 안 한다는 신호를 주고, 멤버 함수에 const를 붙이면 그 객체 상태를 바꾸지 않겠다는 약속입니다.”

90초: “mutable은 논리적 const와 물리적 const가 갈릴 때 쓰며, const_cast는 상수성 제거라 UB 위험이 있어서 면접에서는 ‘정말 필요한가’를 같이 말합니다.”

꼬리: “const 반복자와 반복자 차이?” → “가리키는 요소를 바꿀 수 있느냐”로 한 줄.


Data race·mutex·atomic

30초: “Data race는 둘 이상의 스레드가 동기화 없이 같은 메모리에 쓰기·읽기를 겹치는 것이며, C++에서는 정의되지 않은 동작입니다. 공유 상태는 mutex로 임계 구역을 보호하거나, 단순 카운터처럼 조건이 맞으면 atomic을 씁니다.”

90초: “mutex는 여러 필드를 한 번에 일관되게 바꿀 때 유리하며, atomic은 단일 변수·단순 연산에 맞을 때 고릅니다. false sharing, memory order는 포지션에 따라 깊이를 조절하면 됩니다.”

꼬리: “lock_guard vs unique_lock?” → “지연 잠금(defer_lock), 조건 변수 대기, try_lock, 락 소유권 이동이 필요하면 unique_lock”. 여기서 “재진입(같은 스레드가 두 번 잠금)”을 unique_lock의 기능으로 말하는 실수가 흔한데, 그건 recursive_mutex의 영역입니다.


예외 안전성(기본기)

30초: “예외가 나도 리소스가 새지 않고 불변식이 깨지지 않아야 합니다. RAII가 기본 도구고, 강한 보장까지 못 가더라도 기본 보장은 지키려고 설계합니다.”

90초: “복사 대입에서 자기 대입·부분 실패 시 상태를 말할 수 있으면 좋습니다. 실무에서는 noexcept가 이동 컨테이너 최적화에 영향을 준다는 정도까지 연결하면 플러스입니다.”

꼬리: “소멸자에서 예외?” → “C++11부터 소멸자는 기본적으로 noexcept라서, 예외가 소멸자 밖으로 빠져나가면 std::terminate가 호출됩니다. 그래서 소멸자 안에서는 예외를 잡아 로그만 남기고, 실패를 호출자에게 알려야 하는 정리 작업은 close()·flush() 같은 별도 함수로 노출합니다.”


깊은 복사 vs 얕은 복사

30초: “얕은 복사는 포인터 값만 복사해서 같은 힙을 가리키고, 깊은 복사는 새 메모리를 잡아 독립된 내용을 만듭니다. 기본 복사가 위험한 이유를 예로 들 수 있어야 합니다.”

90초: “Rule of Three/Five 시절에는 직접 자원을 잡으면 복사·이동·소멸을 같이 설계했으며, 지금은 스마트 포인터와 컨테이너로 위임하는 방법이 흔합니다.”

꼬리: “복사 대입 연산자에서 자기 대입?” → “먼저 검사하거나 copy-and-swap”.


std::vector와 반복자 무효화

30초: “vector에 삽입하다 capacity를 넘으면 저장 공간이 통째로 옮겨져 모든 반복자·참조가 무효화됩니다. 재할당이 없어도 삽입·삭제 지점 이후의 반복자는 무효화되므로, erase는 반환된 반복자로 이어 받아야 합니다.”

90초: “범위 for 안에서 erase하면 UB로 이어지기 쉬워서, 클래식 for 루프나 remove-erase 관용구를 말합니다.”

꼬리: “reserve 후 push_back?” → “reserve한 용량 안에서 push_back하면 재할당이 일어나지 않으므로 기존 반복자·참조가 유지됩니다(end()만 무효화). 단 용량을 넘는 순간 다시 모두 무효화된다”. 더 들어오면 재할당 시 원소의 이동 생성자가 noexcept가 아니면 강한 예외 보장을 위해 복사가 선택된다는 점(move_if_noexcept)까지 연결할 수 있습니다.


템플릿 인스턴스화·컴파일 시간

30초: “템플릿은 사용되는 타입마다 코드가 생성됩니다. 인스턴스화하는 번역 단위마다 정의가 보여야 해서 보통 헤더에 정의를 두고, 인스턴스가 많으면 바이너리와 빌드 시간이 늘 수 있습니다.”

90초: “명시적 인스턴스화나 모듈(C++20)로 완화할 수 있다는 정도까지 말하면 충분합니다.”

꼬리: “typename이 필요한 이유?” → “의존 이름 해석” 한 줄.


PIMPL

30초: “구현 세부를 포인터 뒤로 숨겨 헤더 의존을 줄이는 관용구입니다. 바이너리 호환과 컴파일 시간에 도움이 됩니다.”

90초: “헤더에는 전방 선언만 두며, 실제 멤버는 cpp의 impl 클래스에 둡니다. unique_ptr 특수성 때문에 소멸자를 cpp에 두는 패턴도 같이 말하면 좋습니다.”

꼬리: “비용은?” → “간접 접근 한 번, 힙 할당”.


undefined behavior 대표 사례

30초: “컴파일러는 프로그램에 UB가 없다고 가정하고 최적화하므로, UB가 있으면 소스만 봐서는 예상하기 어려운 기계어가 나올 수 있습니다. 대표적으로 null 역참조, 배열 벗어남, 미초기화 읽기, data race가 있습니다.”

90초: “UB를 줄이려면 경계 검사·Sanitizer·정적 분석·코드 리뷰를 조합한다고 말합니다.”

꼬리: “implementation-defined vs UB?” → “구현이 동작을 정하고 문서화해야 하는 것 vs 표준이 아무 요구도 하지 않는 것”.


링크 단계·ODR·inline

30초: “ODR은 하나의 정의 규칙입니다. 같은 함수가 여러 번 정의되면 링크 에러나 UB가 될 수 있어서, 헤더에는 inline 함수나 템플릿을 두는 식으로 관리합니다.”

90초: “inline은 ‘인라인 확장’보다 ODR 예외에 가깝다는 면접용 한 마디가 있으면 좋습니다.”

꼬리: “static vs anonymous namespace?” → “내부 링크·번역 단위 한정” 수준.


짧게 답하면 되는 단골 질문

위 12개 주제만큼 길게 끌 필요는 없지만 자주 나오는 질문들입니다. 30초 블록 하나로 답하고, 면접관이 더 물을 때만 넓히면 됩니다.

  • 포인터와 참조: “참조는 반드시 초기화되고 다른 대상으로 다시 묶을 수 없으며 null이 없습니다. 그래서 ‘없을 수도 있음’을 표현해야 하면 포인터나 std::optional을 씁니다.”
  • new/delete와 malloc/free: “new는 메모리를 잡고 생성자를 호출하며 실패하면 std::bad_alloc을 던지고, malloc은 바이트만 잡고 실패하면 nullptr을 돌려줍니다. 섞어서 해제하면 미정의 동작입니다.” 꼬리로 placement new가 나오면 “이미 확보한 메모리에 생성자만 호출하는 것이고, 소멸자는 직접 호출해야 한다”까지 말합니다.
  • 멤버 초기화 순서: “초기화 리스트에 적은 순서가 아니라 클래스에 선언된 순서대로 초기화됩니다. 앞 멤버가 뒤 멤버 값을 쓰도록 리스트를 짜면 미초기화 값을 읽으므로, -Wreorder 경고를 켜 둡니다.”
  • explicit: “단일 인자 생성자가 암시적 변환으로 쓰이는 것을 막습니다. void send(Buffer b)에 정수를 넘겼는데 Buffer(size_t)가 호출되는 식의 사고를 막는 용도입니다.”
  • 데드락 회피: “모든 스레드가 같은 순서로 락을 잡게 하거나, 여러 락을 한 번에 잡아야 하면 C++17의 std::scoped_lock lk(a, b);처럼 교착을 피하는 알고리즘으로 동시에 잠급니다. 락을 쥔 채 외부 콜백을 부르지 않는 것도 중요합니다.”
  • 메모리 오더: “기본값 seq_cst는 모든 스레드가 같은 전체 순서를 보게 하고, release 쓰기와 그 값을 읽는 acquire 읽기를 짝지으면 쓰기 이전의 작업이 읽은 쪽에 보이는 것까지만 보장해 비용을 줄입니다. 단순 통계 카운터는 relaxed로 충분합니다.”
  • perfect forwarding: “template <class T> void f(T&& x)의 전달 참조와 std::forward<T>(x)로, 받은 인자가 lvalue면 lvalue로, rvalue면 rvalue로 다음 함수에 넘겨 불필요한 복사를 막습니다.”
  • map과 unordered_map: “map은 균형 이진 트리라 정렬 순서와 범위 검색(lower_bound)이 되고 연산이 O(log n)입니다. unordered_map은 해시 테이블이라 평균 O(1)이지만 순서가 없고, 재해시 때 모든 반복자가 무효화됩니다.”
  • emplace_back과 push_back: “emplace_back은 인자를 받아 컨테이너 안에서 바로 생성해 임시 객체를 줄입니다. 다만 explicit 생성자도 호출되므로 v.emplace_back(10)이 의도치 않은 생성을 할 수 있습니다.”
  • moved-from 객체: “표준 타입은 이동된 뒤 ‘유효하지만 지정되지 않은 상태’라 소멸과 재대입은 안전하지만 값을 읽는 것에 의존하면 안 됩니다.”
  • ABI가 깨지는 변경: “가상 함수 추가·순서 변경, 데이터 멤버 추가·삭제·순서 변경처럼 객체 레이아웃이나 vtable이 바뀌는 변경입니다. 공개 라이브러리는 PIMPL로 레이아웃을 숨겨 이 위험을 줄입니다.”
  • 정적·동적 라이브러리: “정적 라이브러리는 링크 시 실행 파일에 복사되고, 동적 라이브러리는 실행 시 로드되어 여러 프로세스가 공유하며 파일 교체만으로 갱신할 수 있습니다. 대신 동적 쪽은 ABI 호환과 배포 시 경로 문제를 관리해야 합니다.”

“이 코드 문제점은?” 스타일 답변 체크리스트

코드를 주고 토론하는 라운드에서는 아래 순서로 훑으면 빈칸이 줄어듭니다.

순서볼 것
1수명: 댕글링 참조, 반환 지역 변수 주소, 캡처된 포인터
2소유권: 누가 delete하는지, 이중 해제, 스마트 포인터 혼용
3예외 경로: 중간 return에서 누수 없는지
4동시성: 읽기/쓰기 겹침, 락 순서(데드락)
5성능: 불필요한 복사, O(n²) 삽입, 시스템 콜 반복
6API 계약: nullptr, 빈 컨테이너, 오버플로

한 가지를 짚을 때는 “증상 → 원인 → 수정 → 검증 방법” 순으로 말하면 신뢰가 쌓입니다.


면접에서 자주 틀리게 말하는 문장

스크립트를 외워도 한 문장이 틀리면 꼬리 질문에서 무너집니다. 아래는 실제로 자주 나오는, 반쯤 맞지만 정확하지 않은 표현들입니다.

  • “std::move가 값을 옮깁니다.” std::move는 rvalue 참조로 바꾸는 캐스트일 뿐이고, 이동은 그 결과를 받은 이동 생성자·이동 대입이 합니다. const 객체에 std::move를 쓰면 이동 생성자가 선택되지 않아 조용히 복사가 일어난다는 예를 들 수 있으면 이해도가 드러납니다.
  • “shared_ptr은 스레드 안전합니다.” 참조 카운트 증감이 원자적이라는 것이지, 가리키는 객체에 대한 접근이나 같은 shared_ptr 변수를 여러 스레드가 동시에 대입하는 것까지 안전하다는 뜻이 아닙니다. 후자는 C++20의 std::atomic<std::shared_ptr<T>>(그 이전에는 std::atomic_load/std::atomic_store 자유 함수)가 필요합니다.
  • “volatile을 붙이면 스레드 간 공유가 안전합니다.” C++에서 volatile은 메모리 매핑 I/O처럼 컴파일러가 접근을 생략하면 안 되는 경우를 위한 것이고, 원자성이나 순서를 보장하지 않습니다. 스레드 간 공유는 std::atomic이나 mutex입니다.
  • “inline을 붙이면 인라인됩니다.” 인라인 여부는 컴파일러가 정하고, inline 키워드의 실질적 의미는 여러 번역 단위에 같은 정의가 있어도 ODR 위반이 아니게 해 주는 것입니다.
  • “가상 함수 호출은 항상 느립니다.” 객체를 값으로 직접 호출하거나 final로 동적 타입이 확정되면 컴파일러가 직접 호출로 바꿉니다. 비용이 문제 되는 곳은 타입이 섞인 컨테이너를 도는 핫 루프 정도입니다.

면접 준비를 하면서 흔히 겪는 일은, 모범 답안을 문장 그대로 외웠다가 꼬리 질문 한 번에 막히는 것입니다. 위 문장들처럼 “어디까지가 사실이고 어디부터 과장인가”를 스스로 설명할 수 있는지 점검해 보면, 외운 답과 이해한 답의 차이가 드러납니다. 각 주제마다 직접 돌려 본 최소 예제 하나(예: const 객체에 std::move를 쓰고 복사 생성자에 로그를 찍어 보기)를 준비해 두면 90초 답변에 “직접 확인해 보니”라는 근거를 붙일 수 있습니다.