C++ 기술 면접 질문 30선 | "포인터와 참조의 차이는?" 실전 답변 정리
이 글의 핵심
C++ 기술 면접에서는 포인터·RAII·가상 함수·STL·동시성 등 개념을 구두로 설명할 수 있어야 합니다. 이 글에서는 자주 나오는 질문 30가지와 모범 답변 흐름을 주제별로 묶어 면접 준비에 활용할 수 있게 했습니다.
들어가며: C++ 면접에서 자주 나오는 질문들
C++ 기술 면접은 코딩 테스트와 달리, 개념 이해도와 실무 경험을 평가합니다. “포인터와 참조의 차이는?”, “가상 함수는 어떻게 동작하나요?”, “멀티스레딩에서 race condition(경쟁 조건—여러 스레드가 같은 자원에 접근할 때 결과가 실행 순서에 따라 달라지는 상황)을 어떻게 막나요?” 같은 질문이 나옵니다. 이 글은 실제 면접에서 자주 나오는 30개 질문과 모범 답변을 정리합니다.
이 글에서 다루는 것:
- 포인터와 메모리 관리 (10문)
- 객체지향·가상 함수 (7문)
- STL과 템플릿 (6문)
- 멀티스레딩과 동시성 (5문)
- 모던 C++ (C++11 이후) (2문)
실무에서 겪은 문제
실제 프로젝트에서 이 개념을 적용하며 겪었던 경험을 공유합니다.
문제 상황과 해결
대규모 C++ 프로젝트를 진행하며 이 패턴/기법의 중요성을 체감했습니다. 책에서 배운 이론과 실제 코드는 많이 달랐습니다.
실전 경험:
- 문제: 처음에는 이 개념을 제대로 이해하지 못해 비효율적인 코드를 작성했습니다
- 해결: 코드 리뷰와 프로파일링을 통해 문제를 발견하고 개선했습니다
- 교훈: 이론만으로는 부족하고, 실제로 부딪혀보며 배워야 합니다
이 글이 여러분의 시행착오를 줄여주길 바랍니다.
목차
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)
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)
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회 (보통 무시할 수준).
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;
}
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 시, 복사 대신 이동 → 성능 향상.
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++; }; // 모두 참조
면접 답변 팁
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, 멀티스레딩, 가상함수 등으로 검색하시면 이 글이 도움이 됩니다.