C++ 기술 면접 질문 30선 | '포인터와 참조의 차이는?' 실전 답변 정리
이 글의 핵심
C++ 기술 면접에서는 포인터·RAII·가상 함수·STL·동시성 등 개념을 구두로 설명할 수 있어야 합니다. 이 글에서는 자주 나오는 질문 30가지와 모범 답변 흐름에 더해, vtable·스마트 포인터 제어 블록·이동/RVO·템플릿 인스턴스화·프로덕션 면접 프레임까지 심층 보충으로 정리했습니다.
들어가며: C++ 면접에서 자주 나오는 질문들
C++ 기술 면접은 코딩 테스트와 달리, 개념 이해도와 실무 경험을 평가합니다. “포인터와 참조의 차이는?”, “가상 함수는 어떻게 동작하나요?”, “멀티스레딩에서 race condition(경쟁 조건—여러 스레드가 같은 자원에 접근할 때 결과가 실행 순서에 따라 달라지는 상황)을 어떻게 막나요?” 같은 질문이 나옵니다. 이 글은 실제 면접에서 자주 나오는 30개 질문과 모범 답변을 정리합니다.
이 글에서 다루는 것:
- 포인터와 메모리 관리 (10문)
- 객체지향·가상 함수 (7문)
- STL과 템플릿 (6문)
- 멀티스레딩과 동시성 (5문)
- 모던 C++ (C++11 이후) (2문)
- 심층 보충(면접 심화용): vtable·ABI와 디스패치 비용,
shared_ptr제어 블록, 이동·RVO·NRVO·noexcept, 템플릿 인스턴스화·ODR·extern template, 프로덕션/시니어 면접 답변 프레임·요약 표
1. 포인터와 메모리 관리 (10문)
Q1. 포인터와 참조의 차이는?
답변:
| 특징 | 포인터 | 참조 |
|---|---|---|
| 재할당 | 가능 (ptr = &other;) | 불가능 (초기화 후 고정) |
| nullptr | 가능 | 불가능 (반드시 유효한 객체) |
| 문법 | *ptr, ptr-> | 일반 변수처럼 사용 |
| 크기 | 포인터 자체 크기 (8바이트, 64비트) | 별칭이므로 추가 메모리 없음 |
예시:
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o ptr_ref ptr_ref.cpp && ./ptr_ref
#include <iostream>
int main() {
int x = 10;
int* ptr = &x;
ptr = nullptr; // ✅ 가능
int& ref = x;
std::cout << ref << "\n"; // 10
return 0;
}
실행 결과: 10 이 한 줄 출력됩니다.
언제 쓰나요?
- 포인터: 동적 할당, 옵셔널 값 (nullptr 가능), 배열
- 참조: 함수 인자 (복사 방지), 반환값 (lvalue 반환)
실무 가이드라인:
- 함수 인자로 객체를 받을 때: const 참조 사용 (복사 비용 절약)
- 함수가 객체를 수정해야 할 때: 비const 참조 사용
- 옵셔널 값 (없을 수도 있음): 포인터 또는
std::optional사용 - 배열·자료구조: 포인터 사용
예시:
// ✅ 좋은 코드
void print(const std::string& str) { // 복사 안 함, 수정 안 함
std::cout << str << '\n';
}
void modify(std::string& str) { // 복사 안 함, 수정 가능
str += " modified";
}
std::string* findUser(int id) { // 없을 수도 있음
if (id == 0) return nullptr;
return new std::string("User");
}
Q2. 스택과 힙의 차이는?
답변:
| 특징 | 스택 (Stack) | 힙 (Heap) |
|---|---|---|
| 할당 방법 | 자동 (지역 변수) | 수동 (new, malloc) |
| 해제 방법 | 자동 (스코프 벗어나면) | 수동 (delete, free) |
| 속도 | 빠름 (포인터 이동만) | 느림 (메모리 관리 오버헤드) |
| 크기 | 제한적 (보통 8MB) | 큼 (시스템 메모리) |
| 생명주기 | 함수 종료 시 해제 | 명시적 해제 전까지 유지 |
예시:
void func() {
int a = 10; // 스택
int* p = new int(20); // 힙
delete p; // 명시적 해제 필요
} // a는 자동 해제
스택이 빠른 이유:
스택 할당은 스택 포인터(SP)를 이동하는 것만으로 끝납니다. CPU 명령어 12개면 됩니다. 반면 힙 할당은 메모리 관리자(Allocator)가 “어디에 빈 공간이 있나?” 찾아야 하므로, 수십수백 개의 명령어가 필요합니다.
언제 힙을 써야 하나요?:
- 크기가 큰 데이터 (스택 크기 제한 8MB)
- 생명주기가 함수를 넘어서는 데이터 (함수 밖에서도 써야 함)
- 크기를 런타임에 결정 (예: 사용자 입력에 따라 배열 크기 결정)
Q3. 메모리 누수(Memory Leak)란? 어떻게 방지하나요?
답변:
메모리 누수: new로 할당한 메모리를 delete하지 않아서, 프로그램이 종료될 때까지 메모리가 해제되지 않는 현상입니다.
예시:
void leak() {
int* p = new int(42);
// delete p; 를 안 함 ❌
} // p는 사라지지만, 힙 메모리는 남음
방지법:
1. 스마트 포인터 사용 (권장):
// 실행 예제
void noLeak() {
std::unique_ptr<int> p = std::make_unique<int>(42);
// 자동 해제 ✅
}
2. RAII (Resource Acquisition Is Initialization):
class FileHandle {
FILE* file;
public:
FileHandle(const char* path) : file(fopen(path, "r")) {}
~FileHandle() { if (file) fclose(file); } // 소멸자에서 해제
};
3. Valgrind로 탐지 (Linux):
valgrind --leak-check=full ./myapp
Q4. 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)의 차이는?
답변:
얕은 복사: 포인터 주소만 복사 → 두 객체가 같은 메모리를 가리킴
깊은 복사: 포인터가 가리키는 데이터까지 복사 → 독립적인 메모리
예시:
class MyClass {
int* data;
public:
MyClass(int val) : data(new int(val)) {}
// ❌ 기본 복사 생성자 (얕은 복사)
// MyClass(const MyClass& other) : data(other.data) {}
// ✅ 깊은 복사
MyClass(const MyClass& other) : data(new int(*other.data)) {}
~MyClass() { delete data; }
};
문제 (얕은 복사):
MyClass a(10);
MyClass b = a; // 얕은 복사 → a.data와 b.data가 같은 주소
// 소멸 시 double free ❌
해결: 복사 생성자·복사 대입 연산자를 명시적으로 정의하거나, std::unique_ptr 사용 (복사 불가, 이동만 가능).
Q5. 스마트 포인터 종류와 차이는?
답변:
| 종류 | 소유권 | 복사 | 사용 예 |
|---|---|---|---|
| unique_ptr | 단독 소유 | 불가 (이동만) | 단일 소유자, RAII |
| shared_ptr | 공유 소유 (참조 카운트) | 가능 | 여러 객체가 공유 |
| weak_ptr | 소유 안 함 (관찰만) | - | 순환 참조 방지 |
예시:
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // ❌ 복사 불가
std::unique_ptr<int> p2 = std::move(p1); // ✅ 이동 (p1은 nullptr)
std::shared_ptr<int> s1 = std::make_shared<int>(42);
std::shared_ptr<int> s2 = s1; // ✅ 복사 (참조 카운트 2)
심층: 스마트 포인터와 제어 블록(control block) 내부
1) unique_ptr: 제어 블록이 없는 이유와 비용 모델
std::unique_ptr은 일반적으로 별도의 힙 “제어 블록”을 두지 않습니다. 스택에 있는 작은 핸들(포인터 + 옵션으로 상태를 가진 deleter)이 유일한 소유자이며, 소멸 시 deleter를 통해 리소스를 해제합니다. 커스텀 deleter가 상태를 많이 들고 있으면 unique_ptr 객체 자체 크기가 커질 수 있다는 점이 면접 포인트입니다(“제어 블록”이 아니라 값 타입 안에 deleter가 박힘).
2) shared_ptr: 제어 블록에 들어가는 정보(개념적 모델)
shared_ptr은 관리 대상 객체(또는 그 메모리)와는 별도로, 거의 항상 제어 블록(control block)을 둡니다. 개념적으로 제어 블록은 최소 다음을 포함합니다.
- 강한 참조 카운트(strong count):
shared_ptr인스턴스 수. 0이 되면 관리 객체를 파괴·해제할 수 있습니다. - 약한 참조 카운트(weak count):
weak_ptr관련 bookkeeping + (구현에 따라) 제어 블록 자체 수명. 강한 참조가 0이 되어도 약한 참조가 남아 있으면 제어 블록은 유지될 수 있고,weak_ptr::lock()이 성공하면 strong이 다시 증가합니다. - Deleter / allocator의 타입 소거: 커스텀 삭제자를 쓰면 제어 블록 내부에 타입이 지워진 저장소 + 호출 가능한 삭제 함수가 붙습니다(함수 포인터, 또는 이에 준하는 메커니즘). 이 때문에
shared_ptr<Base>로 서로 다른 deleter를 가진shared_ptr들이 공존할 수 있습니다(동일 제어 블록을 공유한다는 전제 하에).
3) make_shared vs shared_ptr(new T)의 메모리·수명 트레이드오프
std::make_shared<T>(...)는 흔히T객체와 제어 블록을 한 번에 할당해 할당 횟수 감소·지역성 향상을 기대할 수 있습니다.- 반면
shared_ptr<T>(new T)는 두 번 할당될 수 있고, 캐시 지역성이 나빠질 여지가 있습니다. - 주의(시니어 질문으로 나오는 함정):
make_shared로 붙은 배치에서는 객체가 파괴된 뒤에도 제어 블록 메모리가 weak 참조 때문에 남는 동안, 큰 객체의 메인 페이로드 공간이 회수되지 않을 수 있습니다. 즉 “큰 객체 + 오래 살아남는weak_ptr” 조합은 RSS 관점에서 불리할 수 있어, 별도 할당으로 분리하는 설계가 논의되기도 합니다.
4) weak_ptr의 역할: 순환 참조와 관측자 패턴
weak_ptr은 소유권을 늘리지 않습니다. 그래서 트리/그래프에서 부모–자식이 서로 shared_ptr로 잡히는 순환을 끊을 때 한쪽을 weak_ptr로 두는 패턴이 표준적입니다. expired()/lock()은 제어 블록을 통해 객체 생존 여부를 조회하는 연산이며, lock() 성공 시 새 shared_ptr 생성으로 strong count가 증가합니다.
5) 원자성(atomicity)과 비용
shared_ptr의 참조 카운트 갱신은 구현상 원자적(atomic) 연산을 사용하는 경우가 많아, unique_ptr 대비 무조건 비쌉니다. 멀티스레드에서 동일 인스턴스에 대한 복사/소멸이 빈번하면 캐시 라인 바운싱까지 겹칠 수 있습니다. 면접에서는 “공유가 진짜 필요한지”를 먼저 묻는 것이 성숙한 답입니다.
6) enable_shared_from_this가 제어 블록과 맞물리는 이유
객체가 이미 shared_ptr로 관리되는지 알기 어려운 상황에서 shared_from_this()는 현재 객체가 속한 제어 블록과 동일한 수명 규칙을 전제로 합니다. 스택 객체에 대해 shared_from_this()를 잘못 쓰면 UB로 이어질 수 있어, 면접에서는 “제어 블록과 객체 생명이 엮이는 API”로 설명하면 깊이가 나옵니다.
면접 한 줄 요약: shared_ptr의 핵심은 제어 블록(strong/weak + deleter 소거)이고, make_shared는 할당·지역성 이점과 수명·메모리 회수 패턴의 트레이드오프를 이해해야 한다.
Q6. 댕글링 포인터(Dangling Pointer)란?
답변:
이미 해제된 메모리를 가리키는 포인터입니다.
예시:
int* ptr = new int(42);
delete ptr;
*ptr = 10; // ❌ 댕글링 포인터 사용 (Undefined Behavior)
방지법:
delete후ptr = nullptr;설정- 스마트 포인터 사용 (자동 해제)
Q7. new와 malloc의 차이는?
답변:
| 특징 | new | malloc |
|---|---|---|
| 언어 | C++ | C |
| 타입 안전 | O (타입 체크) | X (void* 반환) |
| 생성자 호출 | O | X |
| 실패 시 | 예외 (std::bad_alloc) | nullptr 반환 |
| 해제 | delete | free |
예시:
// new
MyClass* obj = new MyClass(10); // 생성자 호출 ✅
delete obj;
// malloc
MyClass* obj2 = (MyClass*)malloc(sizeof(MyClass)); // 생성자 호출 X ❌
free(obj2);
권장: C++에서는 new/delete 또는 스마트 포인터 사용.
Q8. RAII란?
답변:
RAII (Resource Acquisition Is Initialization): 리소스(메모리, 파일, 락 등)를 객체의 생명주기에 묶는 기법입니다. 생성자에서 획득, 소멸자에서 해제하므로, 예외가 발생해도 자동으로 정리됩니다.
예시:
class FileLock {
std::mutex& mtx;
public:
FileLock(std::mutex& m) : mtx(m) { mtx.lock(); }
~FileLock() { mtx.unlock(); }
};
void process() {
std::mutex mtx;
FileLock lock(mtx); // 생성 시 lock
// 예외가 나도 소멸자에서 unlock ✅
}
STL 예시: std::unique_ptr, std::lock_guard, std::ifstream (파일 자동 닫기)
Q9. 가상 소멸자는 왜 필요한가요?
답변:
다형성을 쓸 때, 베이스 클래스 포인터로 파생 클래스 객체를 delete하면, 베이스 클래스 소멸자만 호출됩니다. 파생 클래스의 리소스가 해제되지 않아 메모리 누수가 발생합니다.
문제 코드:
class Base {
public:
~Base() { std::cout << "~Base\n"; } // ❌ 가상 아님
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() { delete[] data; std::cout << "~Derived\n"; }
};
int main() {
Base* ptr = new Derived();
delete ptr; // ❌ ~Base만 호출, ~Derived는 호출 안 됨 → 메모리 누수
return 0;
}
해결:
class Base {
public:
virtual ~Base() { std::cout << "~Base\n"; } // ✅ 가상 소멸자
};
이제 delete ptr;이 ~Derived → ~Base 순서로 호출됩니다.
Q10. 스택 오버플로우는 언제 발생하나요?
답변:
스택 메모리가 고갈될 때 발생합니다. 주요 원인:
1. 무한 재귀:
void recursive() {
recursive(); // ❌ 종료 조건 없음
}
2. 큰 배열을 스택에 할당:
// 변수 선언 및 초기화
int main() {
int arr[10000000]; // ❌ 40MB (스택 기본 크기 8MB 초과)
return 0;
}
해결:
- 재귀에 종료 조건 추가
- 큰 배열은 힙에 할당 (
std::vector또는new)
일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.
2. 객체지향·가상 함수 (7문)
Q11. 가상 함수(Virtual Function)는 어떻게 동작하나요?
답변:
가상 함수 테이블(vtable)을 통해 런타임에 호출할 함수를 결정합니다.
동작 원리:
- 가상 함수가 있는 클래스는 vtable (함수 포인터 배열)을 가집니다.
- 각 객체는 vptr (vtable 포인터)을 멤버로 가집니다.
- 가상 함수 호출 시, vptr → vtable → 실제 함수 순서로 간접 호출합니다.
구체적인 메모리 레이아웃:
// 실행 예제
Base 객체:
[vptr: 8바이트][멤버 변수들...]
Base의 vtable:
[0] → Base::show 주소
[1] → Base::other 주소
Derived의 vtable:
[0] → Derived::show 주소 (오버라이드)
[1] → Base::other 주소 (오버라이드 안 함)
호출 과정:
Base* ptr = new Derived();
ptr->show();
ptr->show()호출ptr의 vptr을 읽음 → Derived의 vtable 주소- vtable의 0번 슬롯 읽음 →
Derived::show주소 - 해당 주소로 점프
예시:
class Base {
public:
virtual void show() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void show() override { std::cout << "Derived\n"; }
};
int main() {
Base* ptr = new Derived();
ptr->show(); // "Derived" 출력 (런타임에 결정)
delete ptr;
return 0;
}
오버헤드: 간접 호출 1회 (보통 무시할 수준).
심층: vtable 레이아웃과 디스패치 비용(ABI·CPU 관점)
1) 객체 레이아웃과 vptr 위치
가상 함수가 하나라도 있는 폴리모픽 클래스의 인스턴스는, 구현체에 따라 가장 앞쪽(또는 지정된 오프셋)에 vptr(가상 테이블 포인터)가 놓이는 경우가 많습니다(Itanium C++ ABI 계열에서 흔한 형태). 이 포인터는 런타임 타입에 맞는 vtable을 가리키며, 파생 클래스로 슬라이스 복사·슬라이스 대입을 하면 vptr이 부분적으로만 바뀌어 부분 슬라이싱 문제로 이어질 수 있다는 점이 면접에서 자주 이어집니다.
2) vtable이 “함수 포인터 배열”을 넘어서는 이유
vtable은 논리적으로는 가상 함수 슬롯의 배열이지만, 링커·ABI 관점에서는 RTTI(type_info) 포인터, 오프셋·썽크 정보 등이 같은 가상 테이블 그룹 안에 함께 배치되는 경우가 있습니다. 슬롯 인덱스는 선언 순서·숨겨진 엔트리(예: 삭제자, 코어 ABI별 특수 슬롯)에 따라 달라질 수 있으므로, “내가 본 예제의 [0]번이 항상 첫 가상 함수”라고 단정하기보다 “구현·버전·ABI에 종속”이라고 말하는 편이 안전합니다.
3) 다중 상속(MI)과 “조정 썽크(adjustment thunk)”
다중 상속에서는 파생 객체가 여러 베이스 서브객체를 포함하고, 각 베이스 관점에서의 this 포인터 값이 서로 다릅니다. 그래서 Base2*로 캐스팅된 채 가상 함수를 호출하면, 실제 구현 함수의 시그니처가 기대하는 this가 Derived*일 수 있어 호출 전에 this를 보정하는 작은 코드 조각(썽크)이 끼어들 수 있습니다. 면접 답변으로는 “MI가 있으면 vptr가 여러 개일 수 있고, 가상 호출 한 번이 포인터 조정+간접 점프로 이어질 수 있다”까지 말하면 충분히 심화입니다.
4) 가상 상속(virtual inheritance)
다이아몬드를 풀기 위해 가상 베이스를 쓰면, 공유 서브객체 배치·생성자 호출 순서·포인터 연산이 더 복잡해집니다. 성능 논쟁에서 중요한 포인트는 “가상 상속은 레이아웃과 생성·파괴 비용이 커질 수 있다”는 사실과, 디버깅·ABI 안정성 이슈가 함께 따라온다는 점입니다.
5) 디스패치 비용을 숫자로 말하기 전에: 병목 분리
가상 호출의 미시적 비용은 대략 vptr 로드(종속 로드) → 슬롯 로드 → 간접 분기로 요약됩니다. 나노초 단위로는 작아 보여도, 핫 루프에서 호출 빈도가 극단적으로 높을 때는 명령 캐시·분기 예측 실패·로드 포트 경합이 누적됩니다. 반대로 I/O, 락 경합, 캐시 미스, 할당이 지배적인 워크로드에서는 가상 함수 몇 번이 전체 지연의 1%도 안 되는 경우가 많습니다. 시니어 답변은 “프로파일로 증명하기 전엔 단정하지 않는다”가 핵심입니다.
6) 컴파일러가 정적 호출로 접는 경우(devirtualization)
동적 타입이 컴파일 타임에 확정되면 가상 호출이 직접 호출로 바뀔 수 있습니다. 예: 지역 변수 타입이 Derived로 고정, final/final 클래스 계층, LTO·PGO·모듈화된 전체 프로그램 정보. 면접에서는 “가상 함수 = 항상 느리다”가 아니라 “추가 정보가 있으면 정적화 여지가 있다”고 말하면 됩니다.
7) 대안과 트레이드오프
- CRTP: 런타임 다형성 대신 컴파일 타임 다형성—인라인·최적화는 좋아지지만 컴파일 시간·바이너리 크기 비용이 생깁니다.
std::variant+std::visit: 제한된 대안 타입 집합에서 분기 명시—가독성과 안전성은 좋아질 수 있으나 설계 제약이 큽니다.
면접 한 줄 요약: vtable은 ABI에 묶인 메타데이터 구조이고, 가상 호출 비용은 간접 분기 + (필요 시) this 조정까지 포함해 말해야 정확하며, 실제 병목은 프로파일로 확인한다.
Q12. 순수 가상 함수(Pure Virtual Function)란?
답변:
구현이 없는 가상 함수로, = 0으로 선언합니다. 순수 가상 함수가 있는 클래스는 추상 클래스(Abstract Class)가 되어, 인스턴스화할 수 없습니다.
예시:
class Shape {
public:
virtual double area() const = 0; // 순수 가상 함수
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14 * radius * radius; }
};
int main() {
// Shape s; // ❌ 컴파일 에러 (추상 클래스)
Circle c(5.0);
std::cout << c.area() << '\n'; // ✅
return 0;
}
용도: 인터페이스 정의 (Java의 interface와 유사).
Q13. override 키워드는 왜 쓰나요?
답변:
override는 C++11에서 추가된 키워드로, 실수로 오버라이드를 안 하는 것을 방지합니다.
문제 코드:
class Base {
public:
virtual void show() const {}
};
class Derived : public Base {
public:
void show() {} // ❌ const가 없어서 오버라이드 안 됨 (새로운 함수)
};
해결:
class Derived : public Base {
public:
void show() override {} // ❌ 컴파일 에러: const가 없어서 오버라이드 실패
};
컴파일러가 오버라이드가 안 되면 에러를 내므로, 실수를 미리 잡을 수 있습니다.
Q14. 다중 상속의 문제점은?
답변:
다이아몬드 문제(Diamond Problem): 두 부모 클래스가 같은 조상을 상속하면, 조상의 멤버가 중복됩니다.
예시:
class A { public: int value; };
class B : public A {};
class C : public A {};
class D : public B, public C {}; // A가 두 번 상속됨
int main() {
D d;
// d.value = 10; // ❌ 모호함: B::value인가 C::value인가?
d.B::value = 10; // ✅ 명시적 지정
return 0;
}
해결: 가상 상속(Virtual Inheritance):
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {}; // A는 한 번만 상속됨
int main() {
D d;
d.value = 10; // ✅ 모호하지 않음
return 0;
}
실무: 다중 상속은 복잡하므로, 인터페이스(순수 가상 함수만 있는 클래스)를 다중 상속하는 것이 일반적입니다.
Q15. static 멤버 변수는 언제 쓰나요?
답변:
모든 객체가 공유하는 변수입니다. 클래스당 하나만 존재합니다.
예시:
class Counter {
static int count; // 선언
public:
Counter() { count++; }
static int getCount() { return count; }
};
int Counter::count = 0; // ✅ 정의 (클래스 외부)
int main() {
Counter c1, c2, c3;
std::cout << Counter::getCount() << '\n'; // 3
return 0;
}
용도: 싱글톤, 객체 개수 추적, 공유 설정값.
Q16. const 멤버 함수는 무엇인가요?
답변:
객체의 상태를 변경하지 않는 함수입니다. const 객체에서도 호출할 수 있습니다.
예시:
class Point {
int x, y;
public:
Point(int x, int y) : x(x), y(y) {}
int getX() const { return x; } // ✅ const 멤버 함수
void setX(int val) { x = val; } // 비const
};
int main() {
const Point p(10, 20);
std::cout << p.getX() << '\n'; // ✅ const 함수 호출 가능
// p.setX(30); // ❌ 컴파일 에러: const 객체에서 비const 함수 호출 불가
return 0;
}
Q17. friend 키워드는 언제 쓰나요?
답변:
외부 함수·클래스가 private 멤버에 접근할 수 있게 합니다.
예시:
class Box {
int width;
friend void printWidth(const Box& b); // friend 선언
public:
Box(int w) : width(w) {}
};
void printWidth(const Box& b) {
std::cout << b.width << '\n'; // ✅ private 접근 가능
}
용도: 연산자 오버로딩 (operator<<), 테스트 코드.
주의: 캡슐화를 깨므로 남용하지 말 것.
3. STL과 템플릿 (6문)
Q18. vector와 list의 차이는?
답변:
| 특징 | vector | list |
|---|---|---|
| 구조 | 동적 배열 | 이중 연결 리스트 |
| 임의 접근 | O(1) | O(N) |
| 삽입/삭제 (중간) | O(N) | O(1) (이터레이터 있으면) |
| 메모리 | 연속 | 비연속 (노드별 할당) |
| 캐시 효율 | 높음 | 낮음 |
언제 쓰나요?
- vector: 대부분의 경우 (임의 접근, 순회 빠름)
- list: 중간 삽입·삭제가 매우 빈번한 경우
Q19. map과 unordered_map의 차이는?
답변:
| 특징 | map | unordered_map |
|---|---|---|
| 구조 | 레드-블랙 트리 | 해시 테이블 |
| 정렬 | O (키 순서) | X |
| 접근 시간 | O(log N) | 평균 O(1), 최악 O(N) |
| 메모리 | 적음 | 많음 (해시 테이블) |
언제 쓰나요?
- map: 키 순서가 필요한 경우
- unordered_map: 빠른 접근이 필요한 경우 (코딩 테스트 대부분)
Q20. 템플릿 특수화(Template Specialization)란?
답변:
특정 타입에 대해 다른 구현을 제공하는 기법입니다.
예시:
template <typename T>
T add(T a, T b) {
return a + b;
}
// ✅ std::string에 대한 특수화
template <>
std::string add<std::string>(std::string a, std::string b) {
return a + " " + b; // 공백 추가
}
int main() {
std::cout << add(3, 5) << '\n'; // 8
std::cout << add<std::string>("Hello", "World") << '\n'; // "Hello World"
return 0;
}
Q21. iterator가 무효화되는 경우는?
답변:
vector:
push_back,insert,erase시 재할당이 일어나면 모든 이터레이터 무효화.erase호출 시 삭제된 위치 이후 이터레이터 무효화.
예시:
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 3) {
v.erase(it); // ❌ it 무효화 → 이후 ++it는 Undefined Behavior
}
}
해결:
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 3) {
it = v.erase(it); // ✅ erase는 다음 이터레이터 반환
} else {
++it;
}
}
Q22. emplace_back과 push_back의 차이는?
답변:
push_back: 객체를 생성한 뒤 복사/이동
emplace_back: 컨테이너 내부에서 직접 생성 (in-place construction)
예시:
std::vector<std::pair<int, std::string>> v;
v.push_back({1, "apple"}); // 임시 객체 생성 → 이동
v.emplace_back(2, "banana"); // ✅ 직접 생성 (더 효율적)
효과: 복사·이동 생성자 호출 횟수 감소 → 성능 향상 (특히 무거운 객체).
Q23. 템플릿 메타프로그래밍이란?
답변:
컴파일 타임에 계산을 수행하는 기법입니다. 런타임 오버헤드가 없습니다.
예시 (컴파일 타임 팩토리얼):
template <int N>
struct Factorial {
static const int value = N * Factorial<N-1>::value;
};
template <>
struct Factorial<0> {
static const int value = 1;
};
int main() {
std::cout << Factorial<5>::value << '\n'; // 120 (컴파일 타임에 계산)
return 0;
}
C++11 이후: constexpr로 더 간단하게:
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int main() {
constexpr int result = factorial(5); // 컴파일 타임
return 0;
}
심층: 템플릿 인스턴스화와 컴파일 모델
1) 템플릿은 “미리 컴파일된 함수”가 아니라 “생성 규칙”
함수·클래스 템플릿은 타입 인자가 주어지기 전까지 완전한 엔티티가 아닙니다. 번역 단위 어딘가에서 foo<int>() 같은 사용 지점이 나타나면, 컴파일러는 그 조합에 대해 정의를 실체화(instantiate)합니다. 같은 vector<int>::push_back이 여러 .cpp에서 쓰이면 동일 인스턴스가 여러 오브젝트에 중복 생성될 수 있고, 링커는 COMDAT/weak symbol 같은 메커니즘으로 하나로 합치는 경우가 많습니다. 면접에서는 “인스턴스당 비용이 누적된다”는 점을 말하면 됩니다.
2) 암시적 vs 명시적 인스턴스화
- 암시적: 사용이 보이면 컴파일러가 자동 생성.
- 명시적 인스턴스화:
template class std::vector<MyType>;처럼 한 번에 강제 생성해 다른 번역 단위의 중복 작업을 줄이거나, 구현을.cpp에 두는 패턴(제한적)과 연결됩니다. extern template: “이 조합은 여기서 만들지 말고 외부 정의를 링크해라”는 선언으로, 빌드 시간 단축에 쓰입니다(프로젝트 규약과 함께 써야 함).
3) 두 단계 이름 탐색(two-phase lookup)과 typename/template
템플릿 정의 안의 이름은 첫째 단계(템플릿 정의 시점)와 둘째 단계(인스턴스화 시점)에 나뉘어 해석됩니다. 종속 이름(dependent name)은 인스턴스화될 때까지 최종 의미가 고정되지 않아, T::value가 타입인지 값인지 모호할 때 typename, 템플릿 멤버 함수 호출이 template 키워드를 요구하는 이유가 여기에 있습니다. “템플릿 에러가 지저분한 이유”는 인스턴스화 시점에 터지는 진단이 많기 때문이기도 합니다.
4) ODR·일반화된 “하나의 정의”
일반 함수와 마찬가지로, 같은 인스턴스에 대한 정의는 프로그램 전체에서 동일해야 합니다. 헤더에 템플릿 정의를 두는 관행은 ODR을 만족시키기 위해 정의가 포함되어야 한다는 제약과 맞닿아 있습니다. 반대로 템플릿이 아닌 inline 함수와 섞어 생각하면 혼동이 생깁니다—템플릿은 “헤더에 정의가 있어야 인스턴스화 가능”이 일반적입니다.
5) 컴파일 시간·모듈(C++20)과 concepts
템플릿 메타프로그래밍이 커질수록 컴파일 시간이 폭증합니다. C++20 concepts는 조기 실패·에러 메시지 개선·의도 명시에 도움이 되고, 모듈은 전처리기 포함 폭발을 완화하는 방향으로 논의됩니다. 면접에서는 “템플릿은 강력하지만 빌드 비용이 있다”는 균형 잡힌 문장이 좋습니다.
면접 한 줄 요약: 템플릿은 사용 지점마다 인스턴스가 생기며(중복은 링커가 정리), 2-phase lookup·ODR·명시적/extern 인스턴스화가 빌드·링크 비용과 직결된다.
4. 멀티스레딩과 동시성
일상 비유로 이해하기: 동시성은 주방에서 여러 요리를 동시에 하는 것과 비슷합니다. 한 명의 요리사(싱글 스레드)가 국을 끓이다가 불을 줄이고, 그 사이에 야채를 썰고, 다시 국을 확인하는 식이죠. 반면 병렬성은 요리사 여러 명(멀티 스레드)이 각자 다른 요리를 동시에 만드는 겁니다.
(5문)
Q24. Race Condition이란? 어떻게 방지하나요?
답변:
여러 스레드가 동시에 같은 메모리를 읽고 쓸 때, 실행 순서에 따라 결과가 달라지는 현상입니다.
왜 문제인가요?
counter++는 원자적 연산이 아닙니다. 실제로는 세 단계로 나뉩니다:
- 메모리에서
counter값 읽기 (예: 100) - 1 증가 (101)
- 메모리에 쓰기 (101)
두 스레드가 동시에 실행하면:
- 스레드 1: 100 읽음 → 101 계산 → (아직 안 씀)
- 스레드 2: 100 읽음 → 101 계산 → 101 씀
- 스레드 1: 101 씀
- 결과: 101 (200이어야 하는데!)
문제 코드:
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // ❌ Race condition
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join(); t2.join();
std::cout << counter << '\n'; // 200000이 아닐 수 있음
return 0;
}
해결: 뮤텍스(Mutex)로 보호:
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // ✅ 락 획득
counter++;
} // 자동 unlock
}
Q25. Deadlock은 언제 발생하나요?
답변:
두 개 이상의 스레드가 서로의 락을 기다리며 무한 대기하는 상황입니다.
예시:
std::mutex mtx1, mtx2;
void thread1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lock2(mtx2); // ❌ thread2가 mtx2를 잡고 있음
}
void thread2() {
std::lock_guard<std::mutex> lock2(mtx2);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lock1(mtx1); // ❌ thread1이 mtx1을 잡고 있음
}
해결: 락 순서 통일 또는 std::lock 사용:
void thread1() {
std::lock(mtx1, mtx2); // ✅ 두 락을 동시에 획득 (데드락 방지)
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
}
Q26. atomic이란?
답변:
원자적 연산(Atomic Operation)을 제공하는 타입입니다. 락 없이 스레드 안전한 읽기·쓰기가 가능합니다.
예시:
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // ✅ 원자적 증가 (락 불필요)
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join(); t2.join();
std::cout << counter << '\n'; // 항상 200000
return 0;
}
주의: 복잡한 연산(여러 변수 동시 수정)은 뮤텍스를 써야 합니다.
Q27. condition_variable은 언제 쓰나요?
답변:
특정 조건이 만족될 때까지 스레드를 대기시키는 동기화 도구입니다. 생산자-소비자 패턴에서 자주 씁니다.
예시:
std::mutex mtx;
std::condition_variable cv;
std::queue<int> q;
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
q.push(i);
}
cv.notify_one(); // 소비자에게 알림
}
}
void consumer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !q.empty(); }); // 큐가 비면 대기
int val = q.front();
q.pop();
std::cout << val << '\n';
}
}
Q28. thread_local이란?
답변:
각 스레드마다 독립적인 복사본을 가지는 변수입니다.
예시:
thread_local int counter = 0;
void increment() {
for (int i = 0; i < 100; ++i) {
counter++; // 각 스레드가 독립적인 counter를 가짐
}
std::cout << std::this_thread::get_id() << ": " << counter << '\n';
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join(); t2.join();
// 각 스레드가 100 출력 (공유 안 됨)
return 0;
}
5. 모던 C++ (C++11 이후) (2문)
Q29. 이동 시맨틱(Move Semantics)이란?
답변:
복사 대신 리소스를 이동해서 성능을 높이는 기법입니다. rvalue 참조(&&)를 사용합니다.
왜 필요한가요?
큰 객체(예: std::vector<int>(1000000))를 복사하면 메모리 할당 + 데이터 복사로 느립니다. 하지만 임시 객체(rvalue)는 곧 사라질 것이므로, 복사할 필요 없이 소유권만 이전하면 됩니다.
비유:
- 복사: 친구 집에 가서 책을 복사기로 복사해 옴 (느림)
- 이동: 친구가 “이 책 너 가져”라고 소유권을 넘김 (빠름)
rvalue vs lvalue:
- lvalue: 이름이 있는 변수 (예:
x,arr[0]) - rvalue: 임시 값 (예:
42,x + y,std::move(x))
예시:
class MyString {
char* data;
public:
MyString(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 복사 생성자
MyString(const MyString& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data); // 깊은 복사
}
// ✅ 이동 생성자
MyString(MyString&& other) noexcept : data(other.data) {
other.data = nullptr; // 소유권 이전
}
~MyString() { delete[] data; }
};
효과: std::vector<MyString>에 push_back 시, 복사 대신 이동 → 성능 향상.
심층: 이동 시맨틱·RVO·NRVO와 컴파일러 최적화
1) 우선순위: 복사 생략 > 이동 > 복사
가장 빠른 것은 이동도 없이 객체를 최종 저장소에 직접 만들기입니다. C++17 보장된 복사 생략(guaranteed copy elision)은 특정 prvalue 반환에서 복사/이동 생성자 호출 자체가 발생하지 않아도 된다는 의미로 이해하면 면접에 충분합니다. 그 다음이 이동, 최후가 깊은 복사입니다.
2) RVO / NRVO의 차이(개념)
- RVO: 이름 없는 임시(prvalue)를 반환할 때 호출자 쪽에 직접 구성하는 최적화.
- NRVO: 지역 변수(이름 있음)를 반환할 때, 중간 임시 없이 호출자의 메모리에 직접 구성하려는 최적화.
NRVO는 컴파일러·상황에 따라 달라질 수 있어 언어가 항상 보장하지는 않습니다. 면접에서 “NRVO는 보통 되지만 표준이 매번 보장하는 것은 아니다”라고 말하면 정확합니다.
3) std::move의 정체: 캐스팅
std::move는 이동 연산 자체가 아니라 static_cast<T&&>에 가까운 rvalue 캐스팅입니다. 이후 이동 생성자/이동 대입이 선택되려면 해당 오버로드가 존재해야 하고, 없으면 복사로 떨어질 수 있습니다. 이동 후 객체는 파괴해도 되는 유효 상태를 만족해야 하며(표준 타입은 비움을 보장하는 경우가 많음), 사용자 정의 타입은 반드시 그렇지 않습니다.
4) noexcept와 컨테이너 재할당
std::vector가 재할당할 때 요소를 이동할지 복사할지는 강한 예외 안전 보장과 연결됩니다. 이동 연산이 noexcept가 아니면 예외 전파 가능성 때문에 구현이 복사를 선택할 수 있습니다. 즉 이동 성능 문제가 “예외 명세” 문제로 이어지는 대표 사례입니다.
5) 반환값 최적화와 API 설계
큰 값 타입을 반환할 때 “참조로 빼돌리기”보다 값 반환 + 생략/이동을 우선 고려하는 스타일이 모던 C++에서 흔합니다. 다만 반환 경로가 여러 개이거나 부분적으로만 초기화되는 패턴은 NRVO가 깨지기 쉬워, 성능이 크리티컬하면 측정이 필요합니다.
면접 한 줄 요약: 복사 생략이 최우선, std::move는 캐스팅, 이동은 noexcept·컨테이너 정책과 연결, NRVO는 “흔하지만 항상 보장은 아니다”로 말한다.
Q30. Lambda 표현식의 캡처 방식은?
답변:
[캡처]로 외부 변수를 람다 내부에서 사용할 수 있습니다.
| 캡처 | 의미 |
|---|---|
[] | 아무것도 캡처 안 함 |
[x] | x를 값으로 캡처 (복사) |
[&x] | x를 참조로 캡처 |
[=] | 모든 외부 변수를 값으로 캡처 |
[&] | 모든 외부 변수를 참조로 캡처 |
[this] | 클래스 멤버 접근 |
예시:
int x = 10, y = 20;
auto lambda1 = [x]() { return x + 1; }; // x 복사
auto lambda2 = [&x]() { x++; }; // x 참조 (수정 가능)
auto lambda3 = [=]() { return x + y; }; // 모두 복사
auto lambda4 = [&]() { x++; y++; }; // 모두 참조
프로덕션·시니어 면접에서 통하는 패턴
코어 C++ 문법을 넘어, 운영 환경과 트레이드오프를 연결하면 시니어·프로덕션 포지션에서 신뢰도가 크게 오릅니다. 아래는 질문 유형별로 답변의 “깊이”를 끌어올리는 프레임입니다.
1. 성능·지연 시간: 측정과 병목의 분리
“가상 함수가 느리다/스마트 포인터가 무겁다”처럼 단정하기보다, 프로파일에서 CPU·캐시 미스·할당·동기화 중 무엇이 지배적인지 구분합니다. 예: 가상 호출 오버헤드는 나노초 단위인데 캐시 미스나 락 경합이 밀리초를 쓰는 경우가 흔합니다. micro-benchmark의 함정(최적화로 사라지는 코드, 현실 입력과 다른 분포)을 언급하면 성숙도가 드러납니다.
2. 예외·안전성: 기본 보장 vs 강 보장
RAII·스마트 포인터 질문은 예외 안전성 보장 수준(기본/강)과 연결하면 좋습니다. 예: 컨테이너 재할당이 복사/이동 중 무엇을 택하는지는 이동 연산의 noexcept와 연결됩니다. “예외를 안 쓴다”는 팀이라도 ABI·서드파티·표준 라이브러리 관점에서 예외는 남아 있습니다.
3. 동시성: 데이터 경계와 불변 조건
mutex·atomic을 외운 뒤 한 걸음 더 가면 불변 조건(invariant)입니다. “어떤 필드 조합이 항상 동시에 참일 것인가?”를 정의하고, 락의 경계를 그 불변 조건에 맞춥니다. 데이터 경합을 ‘연산 한 줄’이 아니라 ‘불변 조건 깨짐’으로 설명하면 리뷰·장애 분석 언어와 통합니다.
4. API·진화: ABI와 호환성
라이브러리/서비스 경계에서는 바이너리 호환성(헤더만 바꿔 재컴파일해도 되는가)이나 버전링 질문이 나올 수 있습니다. std::string이나 템플릿 인스턴스를 모듈 경계에서 노출할 때의 비용을 아는지가 갈립니다. Pimpl로 구현 세부를 숨기는 동기 중 하나가 여기에 있습니다.
5. 장애·운영: 재현·관측·완화
“프로덕션에서 메모리 급증을 봤다” 같은 질문에 Valgrind/ASan만 말하기보다, 할당 프로파일러·샘플링 프로파일러·메트릭(할당률, RSS)로 가설–검증 루프를 설명합니다. C++ 서비스라면 코어 덤프·심볼·빌드 아이디까지 연결되면 만점에 가깝습니다.
6. 답변 구조: 주장–근거–한계
한 문단 안에 (1) 결론 (2) 이유·메커니즘 (3) 언제 틀릴 수 있는지(한계)를 넣으면 “시니어 답변” 형태가 됩니다. 예: “make_shared가 유리하다 → 할당·지역성 → 다만 weak만 남는 패턴에서는 회수 시점이 달라질 수 있다.”
7. 프로덕션 경험 질문: STAR로 압축하기
상황(Situation)–과제(Task)–행동(Action)–결과(Result) 순으로 말하면 설득력이 생깁니다. C++ 서비스라면 지표(지연 p99, 오류율, RSS, CPU)와 재현 조건(트래픽 패턴, 배포 버전)을 한 문장씩 넣습니다. 행동에는 도구(프로파일러, ASan, 코어 덤프)와 코드/설계 변경을 분리해 말하면 리뷰어 톤이 납니다.
8. 설계 질문: 요구사항을 먼저 고정하기
“가상 함수를 없애면 더 빠르지 않나요?”처럼 열린 질문에는 목표 지연·호환성·팀 빌드 시간을 먼저 확인하는 답이 좋습니다. 그다음 대안(CRTP, variant, 정적 다형성)과 비용(컴파일 시간, 바이너리, 유지보수)을 나란히 놓고 트레이드오프 표로 마무리하면 시니어 인상이 강해집니다.
9. 코너 케이스를 스스로 제시하기
nullptr, 빈 컨테이너, 동시 호출, 예외 경로를 스스로 꼽으면 “운영 경험이 있다”는 신호가 됩니다. 특히 동시성 질문에서는 락 순서·데드락·우아한 종료까지 한 단어씩 언급하면 가산점입니다.
면접 심화 요약: 비용·ABI·답변 프레임
아래 표는 이 글의 심층 보충을 한눈에 붙잡기 위한 것입니다. 세부는 각각 Q5·Q11·Q23·Q29와 프로덕션·시니어 면접 패턴 절을 참고하면 됩니다.
| 주제 | 핵심 메커니즘 | 비용·주의 | 면접에서 한 줄로 |
|---|---|---|---|
| vtable·가상 디스패치 | vptr → vtable 슬롯 → 간접 분기; MI·가상 상속 시 조정·썽크 | 나노초대지만 핫루프·분기 예측과 결합; 병목은 프로파일로 | ABI 종속, devirtualization 가능 |
shared_ptr 제어 블록 | strong/weak, deleter 소거, (often) atomic ref-count | unique_ptr보다 무겁고, make_shared는 할당·수명 트레이드오프 | 공유가 필요한지 먼저 |
| 이동·RVO·NRVO | 복사 생략 > 이동 > 복사; C++17 guaranteed elision | NRVO는 항상 보장 아님; noexcept 이동은 컨테이너와 연결 | std::move는 캐스팅 |
| 템플릿 인스턴스화 | 사용 지점마다 인스턴스, COMDAT 병합, 2-phase lookup | 컴파일 시간·헤더 의존성; extern template로 관리 가능 | ODR·명시적 인스턴스화 이해 |
| 프로덕션 답변 | 주장–근거–한계, STAR, 측정 우선 | 단정 대신 가설–검증 | 트레이드오프와 운영 지표 |
마지막으로: 내부 구조를 길게 말한 뒤에는 반드시 “그래서 프로젝트에서는 어떻게 결정했/겠다”로 연결하면 면접 답변이 끊기지 않습니다.
면접 답변 팁
1. 구조화된 답변
나쁜 답변:
“포인터는…주소를 가리키고…참조는…별칭이고…”
좋은 답변:
“포인터와 참조의 주요 차이는 세 가지입니다. 첫째, 포인터는 재할당이 가능하지만 참조는 초기화 후 고정됩니다. 둘째, 포인터는 nullptr이 가능하지만 참조는 반드시 유효한 객체를 가리켜야 합니다. 셋째, 문법적으로 포인터는
*와->를 쓰고, 참조는 일반 변수처럼 씁니다.”
2. 예시 코드 제시
개념 설명 후 짧은 코드 예시를 보여 주면 이해도가 높아 보입니다.
3. 실무 경험 연결
“실무에서는 메모리 누수를 방지하기 위해
unique_ptr을 기본으로 쓰고, 공유가 필요한 경우만shared_ptr을 씁니다.”
4. 모르면 솔직히
“정확히는 모르겠지만, 제 생각에는…맞나요?”
추측이라도 사고 과정을 보여 주는 것이 좋습니다.
한 줄 요약: 포인터·메모리·가상 함수·STL·멀티스레딩 등 30문으로 C++ 기술 면접 핵심을 정리해 두었습니다. 다음으로 신입 개발자 면접 준비나 코딩테스트 준비를 읽어보면 좋습니다.
자주 묻는 질문 (FAQ)
Q. 면접에서 코드를 완벽하게 외워야 하나요?
A: 아닙니다. 개념과 원리를 이해하고, 대략적인 코드 구조를 설명할 수 있으면 됩니다. 면접관이 “정확한 문법은 나중에 찾아보면 되니까, 의사 코드(pseudocode)로 설명해 보세요”라고 할 수도 있습니다.
Q. 신입인데 멀티스레딩 질문이 나오면?
A: 기본 개념(race condition, mutex)만 알아도 됩니다. “실무 경험은 없지만, 학교 프로젝트에서 thread와 mutex를 써 봤습니다”라고 솔직히 말하세요. 신입에게 고급 동시성 패턴을 기대하지 않습니다.
Q. 모던 C++ (C++11 이후)를 모르면 불리한가요?
A: 요즘은 거의 필수입니다. 최소한 auto, 람다, 스마트 포인터, 이동 시맨틱은 알아야 합니다. 면접관이 “C++11 이후 변화를 아나요?”라고 물으면, 위 4가지만 언급해도 충분합니다.
Q. “이 코드의 문제점은?”이라는 질문이 나오면?
A:
- 메모리 누수 확인 (new 있는데 delete 없음)
- 예외 안전성 (예외 발생 시 리소스 해제되는지)
- 효율성 (불필요한 복사, O(N²) 알고리즘)
- 엣지 케이스 (빈 입력, nullptr, 오버플로우)
관련 글
- C++ 메모리 누수: 메모리 관리 기초
- C++ 스마트 포인터: unique_ptr, shared_ptr
- C++ 가상 함수와 다형성: vtable, override
- C++ 멀티스레딩: thread, mutex, condition_variable
- C++ 이동 시맨틱: rvalue 참조, std::move
C++ 기술 면접은 개념 암기보다 이해와 적용을 중시합니다. 위 30개 질문은 실제 면접에서 90% 이상 커버되는 핵심 주제입니다. 각 질문에 대해 짧은 코드 예시와 함께 설명할 수 있도록 준비하고, 실무에서 어떻게 쓰는지 한 문장씩 덧붙이면 좋은 인상을 줄 수 있습니다. 모르는 질문이 나와도 당황하지 말고, 아는 범위 내에서 논리적으로 추론하는 모습을 보여 주세요.
검색 시 참고 키워드: C++ 기술 면접, 포인터 참조 차이, 가상 함수 vtable, 스마트 포인터, race condition, 이동 시맨틱
아키텍처 다이어그램
graph TD
A[시작] --> B{조건 확인}
B -->|예| C[처리 1]
B -->|아니오| D[처리 2]
C --> E[완료]
D --> E
설명: 위 다이어그램은 전체 흐름을 보여줍니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 메모리 누수 | 서버 다운시킨 실제 사례와 Valgrind로 찾는 5가지 패턴
- C++ 신입 개발자 면접 | “프로젝트 경험 없어요” 포트폴리오·답변 전략
- C++ 코딩 테스트 | “백준·프로그래머스” 알고리즘 유형별 STL 활용법
이 글에서 다루는 키워드 (관련 검색어)
C++, 기술면접, 면접질문, 포인터, 메모리관리, STL, 멀티스레딩, 가상함수 등으로 검색하시면 이 글이 도움이 됩니다.
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ 기술 면접 질문 30선 | ‘포인터와 참조의 차이는?’ 실전 답변 정리」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(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++ 기술 면접 질문 30선 | ‘포인터와 참조의 차이는?’ 실전 답변 정리」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 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 순서를 권장합니다.