기술면접 대비 CS 공부 - CPP
면접 대비 사전 QnA 정리 - CPP
언어 기초 & 메모리
1) 스택과 힙 메모리의 차이를 설명해주세요. (꼬리: 스택 오버플로우/힙 단편화)
🧠 핵심 요약
- 스택: 자동 관리, 빠름, 크기 제한 존재
- 힙: 동적 관리, 유연함, 단편화 가능
🔹 특징 및 상세설명
- 스택은 함수 호출 시 자동으로 공간이 할당되고 반환되며, 지역 변수와 매개변수, 반환 주소 등이 저장된다.
- 과도한 재귀나 대형 지역 배열로 인해 스택 한계를 넘으면 스택 오버플로우가 발생한다.
- 힙은 런타임에 동적으로 할당되며, 다양한 크기의 블록이 섞이면 외부 단편화가 생긴다.
- 단편화를 줄이기 위해 메모리 풀, 아레나 할당기, 오브젝트 풀 등을 사용한다.
💬 면접식 답변
스택과 힙은 메모리 할당 방식과 관리 주체에서 차이가 있습니다. 스택은 함수 호출 시 자동으로 할당되고 반환되는 메모리 영역으로, 컴파일 타임에 크기가 결정되며 할당/해제가 매우 빠릅니다. 하지만 크기 제한이 있어서 재귀 함수를 과도하게 호출하거나 큰 배열을 선언하면 스택 오버플로우가 발생할 수 있습니다. 반면 힙은 런타임에 동적으로 할당되는 메모리 영역으로, 필요한 만큼 자유롭게 할당할 수 있어 유연하지만 할당과 해제 비용이 상대적으로 높고, 메모리 단편화 문제가 발생할 수 있습니다. 단편화를 줄이기 위해서는 메모리 풀이나 아레나 할당기 같은 기법을 활용할 수 있습니다. 따라서 짧은 수명의 지역 변수는 스택에, 가변적이거나 큰 수명을 가진 데이터는 힙에 할당하는 것이 적합합니다.
2) RAII란 무엇인가요? (꼬리: 스마트 포인터와의 관계)
🧠 핵심 요약
- “자원 획득 = 객체 초기화”
- 생성자에서 획득, 소멸자에서 자동 해제
🔹 특징 및 상세설명
- RAII는 C++의 핵심 철학으로, 객체 수명에 자원 관리(파일, 소켓, 메모리 등)를 결합한다.
- 예외가 발생하더라도 소멸자가 호출되어 자원이 자동 해제된다.
- 대표 구현: unique_ptr (단독 소유), shared_ptr (참조 카운팅 공유), lock_guard (뮤텍스 자동 해제).
💬 면접식 답변
RAII는 Resource Acquisition Is Initialization의 약자로, 자원의 획득과 객체의 생명주기를 일치시키는 C++의 핵심 설계 철학입니다. 생성자에서 자원을 획득하고 소멸자에서 자동으로 해제되도록 설계하여, 수동으로 자원을 관리할 때 발생할 수 있는 누수나 중복 해제 문제를 방지합니다. 특히 예외가 발생하더라도 스택 언와인딩 과정에서 소멸자가 반드시 호출되기 때문에 자원이 안전하게 해제됩니다. 대표적인 예로 unique_ptr, shared_ptr 같은 스마트 포인터와 lock_guard 같은 뮤텍스 관리 클래스가 있으며, 이들은 모두 RAII 패턴을 따라 자동으로 자원을 관리합니다. 이러한 설계 덕분에 C++에서는 명시적인 자원 해제 코드 없이도 안전하고 간결한 코드를 작성할 수 있습니다.
3) 포인터와 참조의 차이는? (꼬리: nullptr vs NULL)
🧠 핵심 요약
- 포인터: 주소 저장, 재지정 가능, nullptr 가능
- 참조: 별칭, 재지정 불가, null 불가
🔹 특징 및 상세설명
- 포인터는 메모리 주소를 직접 다루며 연산 가능하지만, 안전하지 않다.
- 참조는 유효한 객체를 반드시 가리켜야 하고 null 상태가 될 수 없다.
- C++11의 nullptr은 타입 안전한 null 리터럴이며, NULL보다 명확하다.
💬 면접식 답변
포인터와 참조는 모두 다른 객체를 가리키는 방법이지만, 사용 방식과 안전성에 차이가 있습니다. 포인터는 메모리 주소를 저장하는 변수로, nullptr을 가질 수 있고 재지정이 가능하며 포인터 산술 연산도 가능합니다. 하지만 잘못 사용하면 댕글링 포인터나 null 역참조 같은 문제가 발생할 수 있습니다. 반면 참조는 객체의 별칭으로, 선언과 동시에 반드시 유효한 객체를 가리켜야 하며 null이 될 수 없고 한 번 바인딩되면 재지정도 불가능합니다. 따라서 포인터보다 안전하고 간결하게 사용할 수 있습니다. 또한 C++11 이후에는 NULL 대신 nullptr 키워드를 사용하여 포인터 타입 안전성을 확보하며, 함수 오버로딩 시 모호성을 제거할 수 있습니다.
4) 얕은 복사와 깊은 복사의 차이는 무엇인가요?
🧠 핵심 요약
- 얕은 복사: 포인터 주소만 복제 (리소스 공유)
- 깊은 복사: 리소스 자체를 새로 복사 (독립 소유)
🔹 특징 및 상세설명
- 얕은 복사는 두 객체가 동일한 리소스를 가리켜 하나가 해제되면 다른 쪽은 댕글링 포인터 위험이 있다.
- 깊은 복사는 메모리를 새로 할당해 데이터를 복제하므로 안전하다.
- 리소스를 관리하는 클래스는 Rule of 5(복사/이동 생성자, 복사/이동 대입, 소멸자)를 반드시 정의해야 한다.
💬 면접식 답변
얕은 복사는 포인터의 주소값만 복제하여 두 객체가 같은 메모리 영역을 공유하게 됩니다. 이 경우 한 객체가 메모리를 해제하면 다른 객체는 댕글링 포인터를 가지게 되어 중복 해제나 접근 오류가 발생할 수 있습니다. 반면 깊은 복사는 포인터가 가리키는 실제 데이터를 새로운 메모리 공간에 복제하여, 각 객체가 독립적인 리소스를 소유하도록 합니다. 따라서 한 객체의 변경이 다른 객체에 영향을 주지 않으며 안전하게 자원을 관리할 수 있습니다. 동적 메모리나 파일 핸들 같은 리소스를 관리하는 클래스에서는 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자, 소멸자를 모두 정의하는 Rule of 5를 따라야 하며, 이를 통해 깊은 복사와 이동 의미를 명확히 구현할 수 있습니다.
5) const의 역할과 mutable의 의미는?
🧠 핵심 요약
- const: 불변성 보장
- mutable: 예외적으로 수정 허용
🔹 특징 및 상세설명
- const 객체는 멤버를 변경할 수 없으며, const 멤버 함수는 외부에서 관찰 가능한 상태를 변경하지 않아야 한다.
- mutable은 캐시나 통계값 등 논리적 불변성을 깨지 않는 멤버에 사용된다.
- const 포인터에서는 ‘포인터 자체’와 ‘대상이 가리키는 값’의 불변성을 구분해야 한다.
💬 면접식 답변
const 키워드는 변수나 객체가 변경되지 않음을 컴파일러에게 명시하여 불변성을 보장하는 역할을 합니다. const 객체는 const 멤버 함수만 호출할 수 있으며, 이를 통해 의도하지 않은 상태 변경을 방지할 수 있습니다. 하지만 캐시 값이나 통계 정보처럼 외부에서 관찰 가능한 상태는 변하지 않지만 내부적으로 값을 갱신해야 하는 경우가 있습니다. 이때 mutable 키워드를 사용하여 const 멤버 함수 내에서도 특정 멤버 변수를 수정할 수 있도록 허용합니다. 예를 들어 데이터베이스 조회 결과를 캐싱하는 경우, 논리적으로는 객체 상태가 변하지 않았지만 내부 캐시는 업데이트되어야 하므로 mutable을 활용할 수 있습니다. 이는 논리적 불변성과 물리적 불변성을 구분하여 설계 유연성을 높여줍니다.
6) static 키워드의 의미는? (꼬리: 초기화 시점)
🧠 핵심 요약
- static: 정적 수명, 프로그램 전체 공유
- 함수 내부 static은 지연 초기화 가능
🔹 특징 및 상세설명
- 전역 static: 다른 번역 단위에서 접근 불가 (내부 링크).
- 함수 내부 static: 호출 간 값 유지, C++11부터 스레드 안전 보장.
- 클래스 static 멤버: 모든 인스턴스가 공유하는 단일 변수.
- 정적 초기화 순서 문제를 피하려면 함수 내부 static 사용 권장.
💬 면접식 답변
static 키워드는 변수나 함수의 생명주기와 접근 범위를 제어하는 다목적 키워드입니다. 전역 static 변수는 해당 번역 단위(.cpp 파일) 내부에서만 접근 가능하여 외부 링크를 차단하고, 함수 내부 static 변수는 함수 호출 간에도 값을 유지하며 프로그램 종료 시까지 생존합니다. C++11부터는 함수 내부 static 변수의 초기화가 스레드 안전하게 보장되어, 싱글턴 패턴 구현 시 추가적인 락 없이도 안전하게 사용할 수 있습니다. 클래스의 static 멤버 변수는 모든 인스턴스가 공유하는 단일 변수로, 객체 생성 없이도 접근할 수 있으며 클래스 전체의 상태를 표현하는 데 유용합니다. 다만 정적 초기화 순서 문제(Static Initialization Order Fiasco)를 피하기 위해서는 함수 내부 static을 활용하는 것이 권장됩니다.
7) inline 함수란 무엇인가요? (꼬리: 컴파일러 최적화와의 관계)
🧠 핵심 요약
- inline: 중복 정의 허용 + 치환 힌트
- 실제 인라인 여부는 컴파일러가 결정
🔹 특징 및 상세설명
- 호출 오버헤드를 줄일 수 있으나 코드 부피가 증가한다.
- 헤더에 정의된 템플릿 함수나 래퍼 함수는 자동 inline 취급된다.
- 최적화 결정은 컴파일러의 재량이며, LTO나 PGO 분석에 따라 다르다.
💬 면접식 답변
inline 키워드는 컴파일러에게 함수 호출을 함수 본문으로 치환하여 호출 오버헤드를 줄이도록 권장하는 힌트입니다. 함수 호출 시 스택 프레임을 생성하고 복귀 주소를 저장하는 비용을 제거할 수 있어, 작고 자주 호출되는 함수에서 성능 향상을 기대할 수 있습니다. 하지만 inline은 어디까지나 힌트일 뿐이며, 실제 인라인 여부는 컴파일러가 함수 크기, 복잡도, 최적화 수준 등을 고려하여 결정합니다. 과도하게 사용하면 코드 부피가 증가하여 인스트럭션 캐시 효율이 떨어질 수 있고, 가상 함수나 재귀 함수는 인라인화가 어렵습니다. 현대 컴파일러는 LTO(Link-Time Optimization)나 PGO(Profile-Guided Optimization)를 통해 자동으로 인라인 최적화를 수행하므로, 명시적인 inline 키워드는 제한적으로 사용하는 것이 좋습니다.
8) 메모리 정렬(alignment)과 패딩(padding)은 왜 필요한가요?
🧠 핵심 요약
- 정렬: CPU 접근 효율 향상
- 패딩: 정렬 기준을 맞추기 위한 공간
🔹 특징 및 상세설명
- CPU는 정렬된 주소에서 데이터를 읽을 때 가장 빠르다.
- 구조체는 각 멤버의 정렬 단위에 맞춰 패딩이 삽입된다.
- 낭비를 줄이려면 큰 타입부터 배치하거나
#pragma pack으로 정렬 단위를 조정한다. - 캐시라인 경계도 중요하며, false sharing 방지를 위해 주의해야 한다.
💬 면접식 답변
메모리 정렬은 CPU가 데이터를 효율적으로 읽고 쓰기 위해 특정 주소 경계에 맞춰 데이터를 배치하는 것을 의미합니다. 대부분의 CPU는 정렬되지 않은 메모리 접근 시 성능이 떨어지거나 추가적인 사이클이 필요하며, 일부 아키텍처에서는 정렬되지 않은 접근 자체가 금지되기도 합니다. 이를 위해 컴파일러는 구조체 멤버 사이에 패딩을 삽입하여 각 멤버가 정렬 기준을 만족하도록 조정합니다. 예를 들어 char(1바이트) 다음에 int(4바이트)가 오면 3바이트의 패딩이 추가될 수 있습니다. 이러한 공간 낭비를 줄이기 위해서는 큰 타입부터 선언하여 자연스럽게 정렬되도록 하거나, #pragma pack을 사용하여 패딩을 최소화할 수 있습니다. 또한 멀티스레드 환경에서는 캐시라인 경계를 고려하여 false sharing을 방지하는 것도 중요합니다.
9) volatile의 역할은 무엇이며 멀티스레드에서 사용 가능한가요?
🧠 핵심 요약
- volatile: 메모리에서 항상 읽게 함
- 동기화 보장은 없음
🔹 특징 및 상세설명
- volatile은 “외부 요인으로 값이 바뀔 수 있음”을 의미하며, 최적화 방지를 위해 사용된다.
- 하지만 atomic성, 가시성, 순서를 보장하지 않아 멀티스레드 동기화에는 부적절하다.
- 스레드 간 통신에는 std::atomic 또는 뮤텍스를 사용해야 한다.
💬 면접식 답변
volatile 키워드는 컴파일러에게 해당 변수가 외부 요인에 의해 예기치 않게 변경될 수 있음을 알려, 최적화 과정에서 값을 캐싱하지 말고 항상 메모리에서 읽도록 강제합니다. 주로 하드웨어 레지스터나 메모리 맵 I/O, 인터럽트 핸들러에서 사용되며, 컴파일러가 변수 접근을 제거하거나 순서를 바꾸는 것을 방지합니다. 하지만 volatile은 원자성, 메모리 가시성, 명령 재배치 방지를 보장하지 않기 때문에 멀티스레드 환경에서의 동기화 도구로는 적합하지 않습니다. 스레드 간 데이터 공유는 반드시 std::atomic이나 뮤텍스를 사용하여 처리해야 하며, volatile을 동기화 목적으로 사용하면 race condition이 발생할 수 있습니다. Java의 volatile과 달리 C++의 volatile은 메모리 모델과 무관하게 동작하므로 혼동하지 않아야 합니다.
10) new/delete와 malloc/free의 차이는? (꼬리: new가 malloc을 호출?)
🧠 핵심 요약
- new/delete: 객체 생성 + 생성자/소멸자 호출
- malloc/free: 단순 메모리 블록 할당/반납
🔹 특징 및 상세설명
- new는 operator new를 통해 메모리를 확보하고 생성자를 호출한다.
- malloc은 단순한 바이트 버퍼를 반환하며, 생성자 호출이 없다.
- 실패 시 new는 예외(std::bad_alloc)를, malloc은 nullptr을 반환한다.
- 내부 구현에서 new가 malloc을 호출할 수도 있지만 의미적으로는 다르다.
💬 면접식 답변
new와 delete는 C++의 객체 생성 및 소멸을 담당하는 연산자이고, malloc과 free는 C 스타일의 메모리 할당 함수입니다. new는 operator new를 통해 메모리를 할당한 후 해당 타입의 생성자를 자동으로 호출하여 객체를 완전히 초기화하며, 실패 시 std::bad_alloc 예외를 던집니다. 반면 malloc은 단순히 요청한 크기만큼의 메모리 블록을 반환할 뿐 생성자 호출이 없고, 실패 시 nullptr을 반환합니다. 따라서 C++ 클래스 객체는 반드시 new로 생성해야 하며, malloc으로 할당하면 생성자가 호출되지 않아 미정의 동작이 발생할 수 있습니다. 또한 new는 타입 안전성을 제공하여 반환 타입이 명확하지만, malloc은 void*를 반환하므로 캐스팅이 필요합니다. 내부적으로 new가 malloc을 호출할 수도 있지만, 의미적으로는 객체 생성이라는 상위 개념으로 구분됩니다.
객체지향 & 다형성
11) 객체지향(OOP)의 핵심 개념을 설명해주세요. (꼬리: 캡슐화 vs 추상화)
🧠 핵심 요약
- OOP의 4대 개념: 캡슐화, 상속, 다형성, 추상화
- 복잡한 시스템을 구조화하고 재사용성을 높임
🔹 특징 및 상세설명
- 캡슐화: 데이터와 함수를 하나로 묶고 외부 접근을 제한함 (정보 은닉).
- 상속: 기존 클래스를 기반으로 새로운 클래스를 정의, 중복 제거.
- 다형성: 동일 인터페이스로 다양한 객체 동작 가능.
- 추상화: 불필요한 세부를 감추고 본질만 드러냄.
예시: IRenderer 인터페이스를 통해 DirectX, Vulkan, Metal 렌더러를 교체 가능하게 설계.
💬 면접식 답변
객체지향 프로그래밍은 캡슐화, 상속, 다형성, 추상화라는 네 가지 핵심 개념을 기반으로 복잡한 시스템을 구조화하고 재사용성을 높이는 설계 패러다임입니다. 캡슐화는 데이터와 이를 조작하는 함수를 하나의 단위로 묶고 외부 접근을 제한하여, 내부 구현 변경이 외부에 영향을 주지 않도록 정보를 은닉합니다. 상속은 기존 클래스의 기능을 재사용하고 확장하여 코드 중복을 줄이며, 다형성은 동일한 인터페이스를 통해 다양한 타입의 객체를 일관되게 처리할 수 있게 합니다. 추상화는 불필요한 세부 구현을 감추고 본질적인 기능만 드러내어 시스템 복잡도를 낮춥니다. 예를 들어 게임 엔진에서 IRenderer 인터페이스를 정의하면 DirectX, Vulkan, Metal 등 다양한 렌더러를 교체 가능하게 설계할 수 있으며, 이는 객체지향의 다형성과 추상화를 활용한 대표적인 사례입니다.
12) 가상 함수는 어떻게 동작하나요? (꼬리: vtable은 어디에 존재하나요?)
🧠 핵심 요약
- 가상 함수는 런타임에 호출 대상이 결정됨.
- 각 클래스는 vtable을 하나 가지며, 객체는 vptr을 통해 접근.
🔹 특징 및 상세설명
- vtable(가상 함수 테이블): 각 클래스가 보유하는 함수 포인터 배열.
- vptr(가상 함수 포인터): 객체가 자신이 속한 vtable을 가리키는 숨겨진 포인터.
- 호출 시: 객체 → vptr → vtable → 함수 주소 순으로 호출.
- 오버헤드는 포인터 접근 1회 수준이며, 다형성 구현의 핵심이다.
💬 면접식 답변
가상 함수는 런타임 다형성을 구현하기 위한 메커니즘으로, 컴파일 타임이 아닌 실행 시점에 실제 객체 타입에 따라 호출할 함수를 결정합니다. 이를 위해 각 클래스는 가상 함수 테이블(vtable)이라는 함수 포인터 배열을 하나씩 가지며, 각 객체는 생성 시 자신이 속한 클래스의 vtable을 가리키는 숨겨진 포인터(vptr)를 내부에 포함하게 됩니다. 가상 함수 호출 시 컴파일러는 객체의 vptr을 통해 vtable에 접근하고, 해당 함수의 주소를 찾아 간접 호출합니다. 이 과정은 포인터 역참조 한 번 수준의 오버헤드만 발생하므로 성능 저하가 크지 않으며, 다형성 구현의 핵심 도구로 활용됩니다. vtable은 클래스 단위로 존재하므로 객체마다 추가되는 메모리는 vptr 하나(보통 8바이트)뿐입니다.
13) 순수 가상 함수와 추상 클래스의 차이는?
🧠 핵심 요약
- 순수 가상 함수: 구현이 없는 인터페이스 함수
- 추상 클래스: 순수 가상 함수를 하나 이상 포함한 클래스
🔹 특징 및 상세설명
- 순수 가상 함수는
<code>= 0</code>형태로 선언한다. - 추상 클래스는 인스턴스화할 수 없고, 파생 클래스가 반드시 구현해야 한다.
- C++에는
interface키워드가 없으며, 순수 가상 함수만 가진 클래스를 인터페이스로 사용한다. - 다형성 확장을 쉽게 하고, 의존성 역전을 돕는다.
💬 면접식 답변
순수 가상 함수는 선언부에 = 0을 붙여 구현이 없음을 명시한 함수로, 파생 클래스가 반드시 오버라이드해야 합니다. 순수 가상 함수를 하나 이상 포함한 클래스는 추상 클래스가 되며, 직접 인스턴스화할 수 없고 반드시 파생 클래스에서 구현을 제공해야 합니다. C++에는 Java나 C#처럼 별도의 interface 키워드가 없기 때문에, 순수 가상 함수만으로 구성된 클래스를 인터페이스로 사용합니다. 이를 통해 다형성을 구현하고, 의존성 역전 원칙을 적용하여 확장 가능하고 테스트하기 쉬운 구조를 만들 수 있습니다.
14) 오버로딩과 오버라이딩의 차이는?
🧠 핵심 요약
- 오버로딩: 함수 이름은 같지만 시그니처가 다름 (컴파일 타임)
- 오버라이딩: 상속 관계에서 가상 함수를 재정의 (런타임)
🔹 특징 및 상세설명
- 오버로딩은 정적 다형성으로, 컴파일 시점에 어떤 함수가 호출될지 결정된다.
- 오버라이딩은 런타임 다형성으로, 실제 객체 타입에 따라 함수 호출이 달라진다.
-
override키워드는 의도하지 않은 오타나 오버로딩 혼동을 방지한다.
💬 면접식 답변
오버로딩과 오버라이딩은 모두 다형성을 구현하는 방법이지만, 작동 시점과 목적이 다릅니다. 오버로딩은 같은 이름의 함수를 매개변수 타입이나 개수에 따라 여러 개 정의하는 정적 다형성으로, 컴파일 타임에 어떤 함수가 호출될지 결정됩니다. 주로 같은 동작을 다양한 타입에 대해 제공할 때 사용됩니다. 반면 오버라이딩은 상속 관계에서 부모 클래스의 가상 함수를 자식 클래스에서 재정의하는 런타임 다형성으로, 실제 객체 타입에 따라 실행 중에 호출될 함수가 결정됩니다. C++11 이후에는 override 키워드를 사용하여 오버라이딩 의도를 명확히 표현하고, 실수로 오버로딩이 되는 것을 방지할 수 있습니다.
15) 템플릿은 왜 사용하나요? (꼬리: 인스턴스화 시점)
🧠 핵심 요약
- 타입에 독립적인 코드 재사용
- 인스턴스화 시점: 실제 사용 시
🔹 특징 및 상세설명
- 템플릿은 코드 중복 없이 여러 타입을 처리할 수 있다.
- 인스턴스화는 컴파일 시 발생하며, 사용된 타입별로 별도 코드가 생성된다.
- 과도한 인스턴스화는 코드 부풀림과 빌드 시간 증가를 유발할 수 있다.
- C++20의
Concepts와if constexpr로 제약 조건을 명확히 표현할 수 있다.
💬 면접식 답변
템플릿은 타입에 독립적인 제너릭 코드를 작성하여 코드 재사용성을 높이는 C++의 핵심 기능입니다. 동일한 로직을 여러 타입에 대해 반복 작성할 필요 없이, 템플릿으로 한 번 정의하면 컴파일 시점에 사용된 타입별로 자동으로 인스턴스화됩니다. 예를 들어 std::vector
와 std::vector 은 같은 템플릿에서 생성된 별도의 코드입니다. 하지만 과도하게 사용하면 타입마다 코드가 생성되어 바이너리 크기가 증가하고 빌드 시간이 길어질 수 있습니다. C++20의 Concepts를 활용하면 템플릿 매개변수에 제약 조건을 명확히 표현하여 가독성과 오류 메시지를 개선할 수 있으며, if constexpr을 통해 컴파일 타임 분기도 가능합니다.
16) 다중 상속의 장단점은 무엇인가요? (꼬리: 다이아몬드 문제)
🧠 핵심 요약
- 장점: 다양한 인터페이스 구현 가능
- 단점: 모호성, 다이아몬드 상속 문제
🔹 특징 및 상세설명
- 다중 상속은 여러 베이스 클래스를 동시에 상속받을 수 있다.
- 그러나 동일한 조상 클래스가 여러 경로로 중복 상속되면 다이아몬드 문제가 생긴다.
- 이를 해결하기 위해 virtual 상속을 사용하여 중복 베이스를 하나로 공유한다.
- 설계 복잡도가 높아지므로, 대체로 합성(Composition)이 더 선호된다.
💬 면접식 답변
다중 상속은 하나의 클래스가 여러 부모 클래스를 동시에 상속받을 수 있는 기능으로, 여러 인터페이스를 조합하여 구현할 수 있다는 장점이 있습니다. 하지만 동일한 조상 클래스가 여러 경로로 상속되면 다이아몬드 문제가 발생하여 멤버가 중복되고 모호성이 생깁니다. 이를 해결하기 위해 virtual 상속을 사용하면 공통 베이스 클래스를 하나로 공유할 수 있지만, 구조가 복잡해지고 성능 오버헤드가 발생합니다. 실무에서는 다중 상속보다 합성(Composition)이나 인터페이스 분리를 통해 기능을 조합하는 방식이 더 선호되며, 필요한 경우에만 제한적으로 다중 상속을 사용하는 것이 권장됩니다.
17) 가상 소멸자가 필요한 이유는?
🧠 핵심 요약
- 다형적 베이스 클래스는 반드시 가상 소멸자 필요
- 이유: delete 시 파생 소멸자 미호출 방지
🔹 특징 및 상세설명
- 베이스 포인터로 파생 객체를 delete할 때, 소멸자가 가상이 아니면 베이스의 소멸자만 호출된다.
- 결과적으로 파생 클래스의 리소스가 해제되지 않아 누수가 발생한다.
- 규칙: “가상 함수가 하나라도 있다면 소멸자도 반드시 virtual”
💬 면접식 답변
다형성을 사용하여 베이스 클래스 포인터로 파생 클래스 객체를 다룰 때, 소멸자가 virtual이 아니면 delete 시 베이스 클래스의 소멸자만 호출되고 파생 클래스의 소멸자는 호출되지 않습니다. 이 경우 파생 클래스에서 동적 할당한 리소스가 해제되지 않아 메모리 누수가 발생하며, 파일 핸들이나 네트워크 연결 같은 자원도 제대로 정리되지 않습니다. 따라서 가상 함수를 하나라도 가진 클래스는 소멸자도 반드시 virtual로 선언해야 하며, 이는 다형적 베이스 클래스 설계의 필수 규칙입니다. 반대로 다형성을 사용하지 않는 클래스에서는 virtual 소멸자가 불필요한 vtable 오버헤드를 유발하므로 사용하지 않는 것이 좋습니다.
18) friend 키워드는 언제 사용하나요?
🧠 핵심 요약
- 클래스 외부에서 내부 멤버 접근 허용
- 접근 제한을 예외적으로 완화
🔹 특징 및 상세설명
- friend는 특정 함수나 클래스가 비공개(private) 멤버에 접근할 수 있게 한다.
- 연산자 오버로딩(
operator<<,operator==) 구현 시 자주 사용된다. - 그러나 남용 시 캡슐화가 깨지고 결합도가 높아지므로 최소한으로 사용해야 한다.
💬 면접식 답변
friend 키워드는 클래스의 캡슐화를 예외적으로 완화하여 특정 외부 함수나 클래스가 private 및 protected 멤버에 접근할 수 있도록 허용합니다. 주로 연산자 오버로딩(operator«, operator== 등)을 구현할 때 비멤버 함수로 정의하면서도 내부 데이터에 접근해야 하는 경우나, 팩토리 패턴이나 테스트 클래스에서 내부 상태를 검증해야 할 때 사용됩니다. 하지만 friend를 남용하면 캡슐화가 깨지고 클래스 간 결합도가 높아져 유지보수성이 떨어지므로, 반드시 필요한 경우에만 제한적으로 사용해야 합니다. 가능하면 public 인터페이스를 통해 기능을 제공하는 것이 바람직합니다.
19) 연산자 오버로딩의 장단점은?
🧠 핵심 요약
- 장점: 코드 가독성 향상
- 단점: 남용 시 의미 모호화
🔹 특징 및 상세설명
- 연산자 오버로딩은 타입에 맞는 자연스러운 연산 표현을 가능하게 한다.
- 멤버 vs 비멤버 선택:
- 멤버 함수로 구현 →
operator[],operator=,operator() - 비멤버 함수로 구현 →
operator+,operator==,operator<<
- 멤버 함수로 구현 →
- 의미적 일관성을 유지해야 하며, 부수 효과(side effect)를 최소화해야 한다.
💬 면접식 답변
연산자 오버로딩은 사용자 정의 타입에 대해 기본 연산자(+, -, ==, « 등)의 동작을 재정의하여 내장 타입처럼 자연스럽게 사용할 수 있도록 하는 기능입니다. 예를 들어 Vector 클래스에서 operator+를 정의하면 v1 + v2처럼 직관적으로 벡터 덧셈을 표현할 수 있으며, Complex 클래스에서 operator«를 정의하면 cout으로 간편하게 출력할 수 있습니다. 하지만 연산자의 본래 의미와 동떨어진 동작을 정의하거나 부수 효과가 예상되지 않는 연산자에 복잡한 로직을 넣으면 코드 가독성이 오히려 떨어지고 유지보수가 어려워집니다. 따라서 연산자 오버로딩은 수학적 연산, 비교 연산, 스트림 입출력처럼 의미가 명확하고 직관적인 경우에만 신중하게 사용해야 하며, 멤버 함수와 비멤버 함수 중 적절한 형태를 선택하는 것도 중요합니다.
20) 복사 생성자와 이동 생성자의 차이는 무엇인가요?
🧠 핵심 요약
- 복사: 리소스 복제
- 이동: 리소스 소유권 이전
🔹 특징 및 상세설명
- 복사 생성자는 새 객체를 만들고 원본의 데이터를 복제한다.
- 이동 생성자는 원본의 자원을 새 객체로 이전하고 원본을 안전한 상태로 만든다.
- 대용량 객체(버퍼, 컨테이너 등)에서는 이동이 복사보다 훨씬 효율적이다.
- 이동 생성자는 noexcept로 선언하면 STL 컨테이너에서 최적화가 적용된다.
💬 면접식 답변
복사 생성자는 원본 객체의 데이터를 새 메모리 공간에 복제하여 두 객체가 독립적인 리소스를 소유하도록 하는 반면, 이동 생성자는 원본 객체의 리소스 소유권을 새 객체로 이전하고 원본은 비어 있는 유효한 상태로 만듭니다. 큰 메모리 버퍼나 컨테이너를 다룰 때 복사는 전체 데이터를 복제해야 하므로 비용이 크지만, 이동은 포인터만 옮기면 되므로 훨씬 효율적입니다. 특히 임시 객체(rvalue)를 반환하는 경우 이동 의미론이 자동으로 적용되어 성능이 크게 향상됩니다. 이동 생성자를 noexcept로 선언하면 STL 컨테이너가 재할당 시 복사 대신 이동을 사용하여 예외 안전성과 성능을 모두 확보할 수 있습니다. 따라서 리소스를 관리하는 클래스에서는 이동 생성자와 이동 대입 연산자를 반드시 구현하는 것이 권장됩니다.
🔷 C++ 면접 예상 질문 50선 – 모범답변 (3/3: 21–30)
메모리, 컨테이너, STL
21) STL이란 무엇이며, 어떤 장점을 가지나요?
🧠 핵심 요약
- STL(Standard Template Library): C++ 표준 템플릿 기반의 컨테이너, 알고리즘, 반복자 라이브러리
- 재사용성, 안정성, 제너릭 프로그래밍 기반
🔹 특징 및 상세설명
- STL은 컨테이너(Container), 알고리즘(Algorithm), 반복자(Iterator)로 구성되어 있음.
- 컨테이너: vector, list, map, set 등 자료 저장 구조.
- 알고리즘: sort, find, count, accumulate 등 범용 연산 함수.
- 반복자: 컨테이너를 일관된 방식으로 순회하는 추상화 계층.
- 코드 재사용성과 효율성이 높으며, 템플릿 기반으로 다양한 타입에서 동작.
💬 면접식 답변
STL(Standard Template Library)은 C++ 표준 라이브러리의 핵심으로, 컨테이너, 알고리즘, 반복자라는 세 가지 주요 구성 요소를 제공하여 제너릭 프로그래밍을 지원합니다. 컨테이너는 vector, list, map, set 등 다양한 자료구조를 제공하며, 알고리즘은 sort, find, transform 같은 범용 연산을 템플릿 함수로 구현하여 모든 컨테이너에서 재사용할 수 있습니다. 반복자는 컨테이너와 알고리즘을 연결하는 추상화 계층으로, 포인터와 유사한 인터페이스를 통해 일관된 방식으로 데이터를 순회합니다. STL은 템플릿 기반으로 타입 안전성을 보장하면서도 성능 저하 없이 다양한 타입에 적용할 수 있으며, 잘 테스트된 표준 구현을 사용하여 개발 생산성과 코드 품질을 동시에 높일 수 있습니다.
22) vector와 list의 차이점은 무엇인가요?
🧠 핵심 요약
- vector: 연속 메모리, 랜덤 접근 빠름
- list: 비연속 메모리, 삽입/삭제 빠름
🔹 특징 및 상세설명
- vector는 동적 배열로, 인덱스 접근이 빠르지만 중간 삽입/삭제는 느리다.
- list는 이중 연결 리스트로, 삽입/삭제는 O(1)이지만 랜덤 접근이 불가능하다.
- 메모리 지역성(Locality)은 vector가 훨씬 우수하다.
- 대부분의 경우 vector가 더 효율적이며, 대용량 데이터에서도 캐시 효율이 높다.
💬 면접식 답변
vector는 연속된 메모리 공간에 요소를 저장하는 동적 배열로, 인덱스를 통한 랜덤 접근이 O(1)에 가능하고 캐시 지역성이 뛰어나 순회 성능이 우수합니다. 하지만 중간 삽입이나 삭제 시 뒤쪽 요소들을 모두 이동해야 하므로 O(N)의 비용이 발생합니다. 반면 list는 이중 연결 리스트로 각 노드가 힙에 개별적으로 할당되어 있어, 임의의 위치에서 삽입과 삭제가 O(1)에 가능하지만 랜덤 접근이 불가능하고 순회 시 포인터를 따라가야 하므로 캐시 미스가 자주 발생합니다. 실무에서는 대부분의 경우 메모리 지역성과 캐시 효율이 뛰어난 vector가 훨씬 빠르며, 삽입/삭제가 빈번한 경우에도 vector의 성능이 더 좋을 때가 많습니다. 따라서 특별한 이유가 없다면 vector를 기본으로 사용하고, 정말 필요한 경우에만 list를 선택하는 것이 권장됩니다.
23) vector의 capacity와 reserve의 차이는?
🧠 핵심 요약
- capacity: 실제 할당된 메모리 크기
- reserve: 미리 capacity를 확보하는 함수
🔹 특징 및 상세설명
- vector는 요소가 추가될 때 용량(capacity)이 가득 차면 2배씩 증가하며 새 메모리로 복사 이동된다.
- reserve(n)은 미리 n개의 공간을 확보해 재할당 비용을 줄인다.
- capacity는 할당된 공간의 크기이며, size는 실제 원소 개수다.
- shrink_to_fit()으로 남는 capacity를 줄일 수 있다.
💬 면접식 답변
vector의 capacity는 실제로 할당된 메모리 공간의 크기를 나타내고, size는 현재 저장되어 있는 요소의 개수를 의미합니다. vector에 요소를 추가하다가 capacity를 초과하면 vector는 더 큰 메모리 블록을 새로 할당하고(보통 2배), 기존 요소들을 모두 복사 또는 이동한 뒤 이전 메모리를 해제합니다. 이 과정은 비용이 크고 반복되면 성능이 크게 저하됩니다. reserve(n) 함수는 미리 n개의 요소를 저장할 수 있는 공간을 확보하여 재할당 횟수를 줄이고 성능을 향상시킵니다. 특히 요소 개수를 미리 알 수 있는 경우 reserve를 사용하면 불필요한 재할당을 완전히 방지할 수 있습니다. 반대로 shrink_to_fit()을 사용하면 사용하지 않는 여분의 capacity를 줄여 메모리를 절약할 수 있습니다.
24) push_back과 emplace_back의 차이는 무엇인가요?
🧠 핵심 요약
- push_back: 객체 복사 또는 이동 삽입
- emplace_back: 객체를 제자리에서 직접 생성
🔹 특징 및 상세설명
- push_back은 이미 생성된 객체를 복사하거나 이동하여 vector에 삽입한다.
- emplace_back은 전달된 인자로 vector 내부에서 직접 생성(생성자 호출).
- 복사나 이동이 생략되어 불필요한 임시 객체 생성이 없다.
- 따라서 복사 비용이 큰 객체에서는 emplace_back이 효율적이다.
💬 면접식 답변
push_back과 emplace_back은 모두 vector에 요소를 추가하지만, 객체 생성 방식에서 차이가 있습니다. push_back은 이미 생성된 객체를 인자로 받아 vector 내부로 복사하거나 이동시킵니다. 따라서 임시 객체를 생성한 뒤 복사/이동하는 두 단계를 거칩니다. 반면 emplace_back은 생성자 인자를 직접 전달받아 vector 내부의 메모리 위치에서 바로 객체를 생성합니다. 이를 통해 불필요한 임시 객체 생성과 복사/이동 과정을 생략하여 성능을 향상시킵니다. 특히 복사 비용이 큰 객체나 복잡한 생성자를 가진 객체에서 emplace_back의 성능 이점이 두드러지므로, 가능하면 emplace_back을 사용하는 것이 권장됩니다.
25) lvalue와 rvalue의 차이는 무엇인가요?
🧠 핵심 요약
- lvalue: 메모리에 이름이 존재, 수정 가능
- rvalue: 임시 값, 표현식 종료 시 소멸
🔹 특징 및 상세설명
- lvalue: 변수처럼 메모리 주소가 존재하고 재사용 가능.
- rvalue: 임시 객체, 즉시 소멸(리터럴, 연산 결과 등).
- 무브 시멘틱(
std::move)은 lvalue를 rvalue로 캐스팅하여 무브 시멘틱을 유도. - rvalue 참조(
T&&)는 성능 최적화(이동 생성자 등)에 핵심적 역할을 한다.
💬 면접식 답변
lvalue와 rvalue는 C++에서 값 카테고리를 구분하는 개념으로, 표현식의 특성에 따라 나뉩니다. lvalue는 메모리 주소를 가진 식별 가능한 객체로, 변수처럼 표현식이 끝나도 계속 존재하며 주소를 얻을 수 있습니다. 반면 rvalue는 임시 값으로 표현식이 끝나면 소멸하며 주소를 얻을 수 없습니다. C++11에서 도입된 rvalue 참조(T&&)는 임시 객체를 식별하여 이동 의미론을 활용할 수 있게 합니다. std::move를 사용하면 lvalue를 rvalue로 캐스팅하여 명시적으로 이동을 유도할 수 있습니다. 이를 통해 큰 객체를 복사하지 않고 소유권만 이전하여 성능을 크게 향상시킬 수 있으며, 이는 현대 C++의 핵심적인 최적화 기법입니다.
26) map과 unordered_map의 차이는?
🧠 핵심 요약
- map: 균형 이진 탐색 트리 기반 (정렬됨)
- unordered_map: 해시 기반 (정렬되지 않음)
🔹 특징 및 상세설명
- map: Red-Black Tree 기반, 키 정렬 유지, 탐색 O(log N).
- unordered_map: Hash Table 기반, 평균 탐색 O(1), 충돌 시 체이닝 사용.
- 정렬된 순회가 필요하면 map, 빠른 조회가 필요하면 unordered_map을 사용한다.
💬 면접식 답변
map과 unordered_map은 모두 키-값 쌍을 저장하는 연관 컨테이너이지만, 내부 구현과 성능 특성이 다릅니다. map은 Red-Black Tree(균형 이진 탐색 트리) 기반으로 키가 자동으로 정렬되어 저장되며, 탐색, 삽입, 삭제가 모두 O(log N)입니다. 정렬된 순서로 순회할 수 있어 범위 검색이나 순서가 중요한 경우에 유용합니다. 반면 unordered_map은 해시 테이블 기반으로 평균적으로 O(1)의 탐색, 삽입, 삭제 성능을 제공하지만, 최악의 경우 O(N)이 될 수 있습니다. 키의 순서는 유지되지 않으며 메모리 사용량이 다소 많습니다. 따라서 정렬된 순회나 범위 검색이 필요하면 map을, 단순히 빠른 조회가 목적이라면 unordered_map을 선택하는 것이 적절합니다.
27) 해시 충돌이 발생하면 어떻게 처리되나요?
🧠 핵심 요약
- 대표적 방식: 체이닝(Chaining), 개방 주소법(Open Addressing)
🔹 특징 및 상세설명
- 체이닝: 같은 해시 인덱스에 연결 리스트를 두어 충돌 원소들을 저장.
- 개방 주소법: 비어 있는 다음 슬롯을 찾아 순차적으로 삽입(선형/이차/이중 해싱).
- C++ STL의 unordered_map은 체이닝 방식을 사용한다.
- 해시 품질이 나쁘면 충돌이 잦아지고 O(N) 탐색이 될 수 있으므로 해시 함수를 신중히 선택해야 한다.
💬 면접식 답변
해시 충돌은 서로 다른 키가 동일한 해시 값을 가질 때 발생하며, 주로 체이닝과 개방 주소법으로 해결합니다. 체이닝(Chaining)은 각 해시 버킷에 연결 리스트를 두어 같은 해시 값을 가진 원소들을 순차적으로 저장하는 방식입니다. 구현이 간단하고 메모리가 동적으로 확장되지만, 캐시 효율이 낮고 충돌이 많으면 리스트 탐색으로 O(N)까지 느려질 수 있습니다. 개방 주소법(Open Addressing)은 충돌 발생 시 다른 빈 슬롯을 찾아 저장하는 방식으로, 선형 탐사, 이차 탐사, 이중 해싱 등의 전략이 있습니다. 캐시 효율이 좋지만 테이블이 가득 차면 성능이 급격히 저하됩니다. C++ STL의 unordered_map은 체이닝 방식을 사용하며, 해시 함수의 품질이 성능에 큰 영향을 미치므로 적절한 해시 함수를 선택하는 것이 중요합니다.
28) std::sort는 어떤 알고리즘을 사용하나요?
🧠 핵심 요약
- C++ 표준 sort는 Introsort 알고리즘을 사용함.
- 퀵정렬, 힙정렬, 삽입정렬을 혼합.
🔹 특징 및 상세설명
- Introsort는 기본적으로 퀵정렬을 사용하되,
재귀 깊이가 깊어지면 힙정렬로 전환해 최악의 O(N log N)을 보장. - 원소 개수가 작을 때는 삽입정렬로 전환 (캐시 효율 높음).
- 이 하이브리드 구조 덕분에 평균/최악 성능이 모두 우수하다.
💬 면접식 답변
std::sort는 Introsort(Introspective Sort)라는 하이브리드 정렬 알고리즘을 사용하여 평균과 최악 경우 모두 O(N log N) 성능을 보장합니다. 기본적으로 퀵정렬로 시작하지만, 재귀 깊이가 일정 수준(보통 log N)을 초과하면 힙정렬로 전환하여 퀵정렬의 최악 케이스(O(N²))를 방지합니다. 또한 부분 배열의 크기가 작아지면 삽입정렬로 전환하여 작은 데이터셋에서의 캐시 효율을 높입니다. 이러한 하이브리드 구조 덕분에 Introsort는 다양한 입력 패턴에서 안정적이고 빠른 성능을 제공하며, C++ 표준 라이브러리의 기본 정렬 알고리즘으로 채택되었습니다. 참고로 std::stable_sort는 요소 간 상대적 순서를 유지하는 안정 정렬이 필요할 때 사용되며, 일반적으로 병합정렬 기반으로 구현됩니다.
29) set과 unordered_set의 차이는?
🧠 핵심 요약
- set: 정렬된 트리 기반 (O(log N))
- unordered_set: 해시 기반 (O(1) 평균)
🔹 특징 및 상세설명
- set은 Red-Black Tree 기반으로 정렬 순서 유지, 중복 불가.
- unordered_set은 해시 기반으로 빠른 탐색 가능하지만 순서 보장 X.
- 메모리 사용량은 unordered_set이 다소 많다.
💬 면접식 답변
set과 unordered_set은 모두 중복을 허용하지 않는 컨테이너이지만, 내부 구조와 성능이 다릅니다. set은 Red-Black Tree 기반으로 원소가 정렬된 순서로 저장되며, 삽입, 삭제, 탐색이 모두 O(log N)입니다. 정렬된 순회가 가능하고 범위 검색이나 최소/최대 값을 찾는 데 유리합니다. 반면 unordered_set은 해시 테이블 기반으로 평균 O(1)의 탐색 성능을 제공하지만 순서를 보장하지 않으며, 최악의 경우 O(N)이 될 수 있습니다. 메모리 사용량도 set보다 다소 많습니다. 정렬이나 순서가 필요 없고 단순히 존재 여부를 빠르게 확인하려면 unordered_set을, 정렬된 데이터가 필요하거나 범위 검색이 필요하면 set을 사용하는 것이 적합합니다.
30) unique_ptr과 shared_ptr의 차이는?
🧠 핵심 요약
- unique_ptr: 단일 소유
- shared_ptr: 참조 카운트 기반 공유
🔹 특징 및 상세설명
- unique_ptr은 복사 불가, 이동만 가능 → 명확한 소유권 표현.
- shared_ptr은 참조 카운트를 통해 여러 포인터가 자원을 공유.
- 순환 참조 방지를 위해 weak_ptr 사용.
- unique_ptr이 기본 선택이며, shared_ptr은 필요할 때만 사용.
💬 면접식 답변
unique_ptr과 shared_ptr은 모두 RAII 기반의 스마트 포인터이지만, 소유권 모델이 다릅니다. unique_ptr은 단일 소유권을 표현하며 복사가 불가능하고 이동만 가능합니다. 메모리 오버헤드가 없고 성능이 raw 포인터와 동일하므로, 명확한 소유권 관계에서 기본 선택으로 사용됩니다. shared_ptr은 참조 카운팅을 통해 여러 포인터가 동일한 객체를 공유할 수 있도록 하며, 마지막 shared_ptr이 소멸될 때 자동으로 메모리를 해제합니다. 하지만 제어 블록을 위한 추가 메모리와 원자적 연산 비용이 발생합니다. shared_ptr 간 순환 참조가 발생하면 참조 카운트가 0이 되지 않아 메모리 누수가 발생하므로, 한쪽을 weak_ptr로 변경하여 순환을 차단해야 합니다. weak_ptr은 shared_ptr을 관찰하지만 참조 카운트를 증가시키지 않는 비소유 포인터입니다.
메모리 관리 & 동기화
31) 스마트 포인터는 내부적으로 어떻게 동작하나요?
🧠 핵심 요약
- 스마트 포인터는 RAII 기반 자원 자동 해제 도구
- shared_ptr은 참조 카운팅, unique_ptr은 소유권 이전
🔹 특징 및 상세설명
- unique_ptr: 복사 불가, 이동만 가능.
-
shared_ptr:
use_count로 참조 수를 관리, 마지막 참조 해제 시 자원 해제. - weak_ptr: 순환 참조 방지용 비소유 포인터.
- 참조 카운팅은 힙에 별도의 제어 블록을 두어 thread-safe하게 관리됨.
💬 면접식 답변
스마트 포인터는 RAII 원칙을 따라 포인터의 생명주기와 자원 관리를 결합하여 메모리 누수를 방지하는 도구입니다. unique_ptr은 복사를 금지하고 이동만 허용하여 단일 소유권을 명확히 표현하며, 소멸 시 자동으로 delete를 호출합니다. 추가 메모리 오버헤드가 없어 raw 포인터와 동일한 성능을 제공합니다. shared_ptr은 힙에 제어 블록을 할당하여 참조 카운트와 약한 참조 카운트를 관리하며, use_count가 0이 되면 자동으로 객체를 삭제합니다. 참조 카운팅은 스레드 안전하게 원자적 연산으로 수행되지만 그만큼 오버헤드가 발생합니다. weak_ptr은 shared_ptr과 함께 사용되어 순환 참조를 방지하며, 객체의 수명에 영향을 주지 않고 관찰만 합니다. lock() 메서드를 통해 안전하게 shared_ptr로 변환할 수 있습니다.
32) 메모리 누수가 발생하는 원인은 무엇인가요?
🧠 핵심 요약
- 할당 후 해제 누락
- 순환 참조(shared_ptr), 예외 누락, 포인터 관리 실패
🔹 특징 및 상세설명
- delete 누락: 동적 할당 후 해제하지 않음.
- shared_ptr 순환 참조: 서로를 shared_ptr로 참조하면 use_count가 0이 되지 않음.
- 예외 처리 누락: 예외 발생 시 delete 문 미도달.
- 해결책: 스마트 포인터 사용, weak_ptr로 순환 차단, RAII 설계.
💬 면접식 답변
메모리 누수는 동적으로 할당된 메모리가 해제되지 않고 계속 남아 있어 사용 가능한 메모리가 점점 줄어드는 현상입니다. 가장 흔한 원인은 new로 할당한 메모리를 delete하지 않는 것이며, 특히 예외가 발생하여 delete 문에 도달하지 못하는 경우 누수가 발생합니다. 또한 shared_ptr 간 순환 참조가 발생하면 참조 카운트가 0이 되지 않아 자동 해제되지 않습니다. 이를 방지하기 위해서는 스마트 포인터(unique_ptr, shared_ptr)를 사용하여 자동 메모리 관리를 활용하고, 순환 참조가 예상되는 경우 한쪽을 weak_ptr로 변경해야 합니다. 또한 RAII 패턴을 따라 모든 자원을 객체의 생명주기에 묶고, 예외 안전성을 고려하여 설계하면 메모리 누수를 크게 줄일 수 있습니다. Valgrind나 AddressSanitizer 같은 도구를 활용하여 누수를 탐지하는 것도 유용합니다.
33) RAII가 예외 안전성을 보장하는 이유는?
🧠 핵심 요약
- 생성자에서 자원 획득, 소멸자에서 자동 해제
- 예외 발생 시에도 소멸자는 반드시 호출됨
🔹 특징 및 상세설명
- try 블록 내에서 예외가 발생해도, 스택에 있는 RAII 객체의 소멸자가 자동 호출된다.
- 덕분에 자원 누수가 방지된다.
- 예외 안전성 수준은 Basic, Strong, Nothrow 세 단계로 구분된다.
💬 면접식 답변
RAII가 예외 안전성을 보장하는 핵심 이유는 C++의 스택 언와인딩(Stack Unwinding) 메커니즘 때문입니다. 예외가 발생하면 현재 스코프에서 catch 블록을 찾을 때까지 스택을 거슬러 올라가며 각 스코프의 지역 객체들의 소멸자를 자동으로 호출합니다. RAII 객체는 생성자에서 자원을 획득하고 소멸자에서 해제하므로, 예외가 발생하더라도 소멸자가 반드시 호출되어 자원이 안전하게 정리됩니다. 예를 들어 파일을 열고 작업 중 예외가 발생해도, RAII 기반의 파일 핸들 객체는 소멸자에서 자동으로 파일을 닫아 리소스 누수를 방지합니다. 이는 수동으로 try-finally 블록을 작성하는 것보다 훨씬 안전하고 간결합니다. 예외 안전성 수준은 Basic(자원 누수 없음), Strong(실패 시 원래 상태 복구), Nothrow(예외를 던지지 않음) 세 단계로 구분되며, RAII는 최소한 Basic 보장을 제공합니다.
34) 멀티스레드 환경에서 race condition이란 무엇인가요?
🧠 핵심 요약
- 여러 스레드가 동시에 공유 자원에 접근할 때 순서가 불확실한 상태
🔹 특징 및 상세설명
- 동시에 같은 변수를 읽거나 쓸 때 결과가 실행 순서에 따라 달라질 수 있다.
- 해결책: 뮤텍스, 스핀락, 세마포어 등 동기화 기법 사용.
- atomic 연산으로도 해결 가능하나, 논리적 불변성이 깨지지 않도록 주의 필요.
💬 면접식 답변
race condition(경쟁 상태)은 여러 스레드가 동시에 공유 자원에 접근하여 읽고 쓸 때, 실행 순서에 따라 결과가 달라지는 비결정적 상황을 말합니다. 예를 들어 두 스레드가 동시에 같은 변수를 증가시킬 때, 각 스레드가 읽기-수정-쓰기 과정을 거치는 동안 다른 스레드의 연산과 겹치면 일부 증가가 손실될 수 있습니다. 이는 데이터 무결성을 해치고 예측 불가능한 버그를 유발합니다. 이를 방지하기 위해 뮤텍스(mutex)를 사용하여 임계 구역(critical section)을 보호하거나, std::atomic을 사용하여 원자적 연산을 보장해야 합니다. 뮤텍스는 한 번에 하나의 스레드만 접근하도록 락을 걸고, atomic은 하드웨어 수준에서 분할 불가능한 연산을 제공합니다. 적절한 동기화 없이 공유 데이터를 접근하는 것은 미정의 동작(undefined behavior)을 유발하므로, 멀티스레드 환경에서는 반드시 동기화 메커니즘을 사용해야 합니다.
35) mutex와 spinlock의 차이는 무엇인가요?
🧠 핵심 요약
- mutex: 커널 개입, 스레드 블록
- spinlock: 커널 개입 없음, 바쁜 대기
🔹 특징 및 상세설명
- mutex는 잠금 실패 시 스레드를 대기 상태로 전환 → context switch 발생.
- spinlock은 잠금이 풀릴 때까지 반복 확인(루프) → 짧은 임계 구역에 유리.
- 멀티코어 환경에서 spinlock은 빠를 수 있으나, 긴 대기에는 비효율적.
💬 면접식 답변
mutex와 spinlock은 모두 동기화 도구이지만, 대기 방식과 적용 상황이 다릅니다. mutex는 잠금을 획득하지 못한 스레드를 블록 상태로 전환하여 CPU 자원을 양보합니다. 이 과정에서 컨텍스트 스위칭이 발생하므로 비용이 있지만, 대기 시간이 긴 경우 CPU를 효율적으로 사용할 수 있습니다. 반면 spinlock은 잠금을 획득할 때까지 루프를 돌며 계속 확인하는 바쁜 대기(busy-waiting) 방식입니다. 커널 개입이 없어 컨텍스트 스위칭 비용이 없지만, 대기 중에도 CPU를 계속 사용하므로 긴 대기에는 비효율적입니다. 따라서 임계 구역이 매우 짧고 경합이 적은 경우 spinlock이 유리하며, 대기 시간이 길거나 경합이 심한 경우 mutex가 적합합니다. 멀티코어 환경에서 spinlock의 효과가 더 크지만, 단일 코어에서는 spinlock이 오히려 성능을 저하시킬 수 있습니다.
36) deadlock이란 무엇이며, 발생 조건 4가지는?
🧠 핵심 요약
- 교착상태: 두 스레드가 서로 자원을 점유한 채 대기
- 발생 조건 4가지: 상호배제, 점유대기, 비선점, 순환대기
🔹 특징 및 상세설명
1️⃣ 상호배제(Mutual Exclusion) – 한 자원은 한 스레드만 사용 가능
2️⃣ 점유대기(Hold and Wait) – 하나 점유 후 다른 자원 대기
3️⃣ 비선점(No Preemption) – 자원 강제 회수 불가
4️⃣ 순환대기(Circular Wait) – 서로가 상대 자원을 대기
예방: 락 획득 순서 통일, 타임아웃, Lock Hierarchy 사용 등
💬 면접식 답변
deadlock(교착 상태)은 두 개 이상의 스레드가 서로가 점유한 자원을 기다리며 영원히 대기하여 진행할 수 없는 상태를 말합니다. 교착 상태는 네 가지 조건이 동시에 만족될 때 발생합니다. 첫째, 상호배제(Mutual Exclusion)는 자원을 한 번에 하나의 스레드만 사용할 수 있다는 조건입니다. 둘째, 점유대기(Hold and Wait)는 스레드가 최소 하나의 자원을 점유한 채 다른 자원을 기다리는 상황입니다. 셋째, 비선점(No Preemption)은 다른 스레드가 점유한 자원을 강제로 빼앗을 수 없다는 조건입니다. 넷째, 순환대기(Circular Wait)는 스레드들이 원형으로 서로의 자원을 기다리는 상태입니다. 교착 상태를 예방하려면 네 조건 중 하나라도 깨야 하며, 실무에서는 모든 락을 일정한 순서로 획득하거나 타임아웃을 설정하고, Lock Hierarchy를 도입하여 순환 대기를 방지합니다. 또한 std::lock()을 사용하면 여러 뮤텍스를 동시에 획득하여 교착 상태를 피할 수 있습니다.
37) condition_variable의 역할은?
🧠 핵심 요약
- 스레드 간 신호(wait/notify) 전달
- 뮤텍스와 함께 사용
🔹 특징 및 상세설명
- 하나의 스레드가 특정 조건을 만족할 때 다른 스레드에 알림을 보냄.
-
wait()는 조건이 만족될 때까지 블록,notify_one()또는notify_all()로 깨움. - CPU 낭비 없이 효율적인 스레드 동기화 가능.
💬 면접식 답변
condition_variable은 스레드 간 이벤트 기반 동기화를 위한 메커니즘으로, 특정 조건이 만족될 때까지 스레드를 대기시키고 조건이 충족되면 깨우는 역할을 합니다. wait() 함수는 조건이 만족될 때까지 스레드를 블록하며, 내부적으로 뮤텍스를 자동으로 해제하여 다른 스레드가 조건을 변경할 수 있도록 합니다. 조건이 만족되어 깨어나면 다시 뮤텍스를 획득한 후 실행을 재개합니다. notify_one()은 대기 중인 스레드 중 하나를 깨우고, notify_all()은 모든 대기 스레드를 깨웁니다. 이를 통해 바쁜 대기(busy-waiting) 없이 효율적으로 스레드 간 협력을 구현할 수 있습니다. 주의할 점은 spurious wakeup(허위 깨어남) 가능성이 있으므로, wait() 호출 시 조건을 검사하는 predicate를 함께 사용하여 조건이 실제로 만족될 때만 진행하도록 해야 합니다.
38) atomic은 어떤 상황에서 사용되나요?
🧠 핵심 요약
- 단일 명령어로 실행되어 중단 불가능한 연산
- lock-free 구현 가능
🔹 특징 및 상세설명
- atomic은 메모리 일관성을 보장하며, Lock-Free 자료구조 구현에 핵심적.
- 예시:
std::atomic<int> counter; counter++; - 내부적으로 CPU의 CAS(Compare-And-Swap) 명령을 사용.
- 단, 복잡한 연산은 여전히 뮤텍스가 필요할 수 있다.
💬 면접식 답변
atomic은 하나의 연산이 중단 없이 완전히 수행되거나 전혀 수행되지 않음을 보장하는 원자적 연산을 제공하는 타입입니다. std::atomic
는 CPU의 하드웨어 명령(CAS: Compare-And-Swap 등)을 활용하여 뮤텍스 없이도 스레드 안전한 연산을 수행합니다. 예를 들어 atomic 변수에 대한 증가 연산(++)은 읽기-수정-쓰기가 하나의 원자적 단위로 실행되어 race condition이 발생하지 않습니다. atomic은 Lock-Free 자료구조 구현의 핵심 도구로, 뮤텍스보다 가벼우며 컨텍스트 스위칭이 없어 성능이 우수합니다. 하지만 복잡한 연산이나 여러 변수를 동시에 업데이트해야 하는 경우에는 여전히 뮤텍스가 필요합니다. 또한 메모리 오더링(memory ordering)을 명시하여 명령 재배치를 제어할 수 있으며, 이를 통해 더 세밀한 동기화 제어가 가능합니다.
39) Lock-Free 자료구조란 무엇인가요?
🧠 핵심 요약
- 뮤텍스 없이 atomic 연산만으로 동시 접근을 제어하는 구조
🔹 특징 및 상세설명
- 예: Lock-Free Stack, Queue (CAS 기반).
- 장점: 컨텍스트 스위치 없음 → 빠름.
- 단점: 코드 복잡, ABA 문제 발생 가능.
- 해결책: tag/version counter, hazard pointer 사용.
💬 면접식 답변
Lock-Free 자료구조는 뮤텍스나 락을 사용하지 않고 atomic 연산만으로 동시 접근을 안전하게 처리하는 자료구조입니다. Lock-Free 알고리즘은 최소한 하나의 스레드가 유한한 단계 내에 진행할 수 있음을 보장하며, 컨텍스트 스위칭이 없어 성능이 뛰어나고 우선순위 역전(priority inversion)이나 데드락 문제가 발생하지 않습니다. 대표적인 예로 Lock-Free Stack과 Queue가 있으며, CAS(Compare-And-Swap) 연산을 활용하여 구현됩니다. 하지만 ABA 문제(값이 A → B → A로 변경되어 변화를 감지하지 못하는 문제)가 발생할 수 있어, 버전 카운터나 Hazard Pointer를 사용하여 해결해야 합니다. Lock-Free 자료구조는 설계와 디버깅이 매우 복잡하므로, 성능이 중요한 특정 상황에서만 사용하고 대부분의 경우 뮤텍스 기반 자료구조가 더 안전하고 유지보수하기 쉽습니다.
40) Memory Order(메모리 순서)란 무엇인가요?
🧠 핵심 요약
- 멀티코어 환경에서의 메모리 접근 순서 제어
- atomic 연산의 가시성·순서 보장 방식
🔹 특징 및 상세설명
- CPU와 컴파일러는 명령어를 재정렬할 수 있음 → 동기화 문제 발생.
- C++11의 atomic은 memory_order를 명시적으로 지정 가능:
- relaxed (순서 보장 없음)
- acquire/release (잠금 해제 시점 제어)
- seq_cst (가장 강력한 순서 보장)
- 올바른 선택이 Lock-Free 구조의 성능과 안전성 모두에 영향.
💬 면접식 답변
Memory Order(메모리 순서)는 멀티코어 환경에서 메모리 접근 순서와 가시성을 제어하는 개념으로, atomic 연산이 다른 스레드에게 어떻게 보이는지를 정의합니다. CPU와 컴파일러는 성능 최적화를 위해 명령을 재배치할 수 있는데, 이로 인해 멀티스레드 환경에서 예상치 못한 동작이 발생할 수 있습니다. Memory Order는 이러한 재배치를 제한하여 올바른 동기화를 보장합니다. C++11에서는 여러 메모리 순서 옵션을 제공합니다. relaxed는 순서 보장 없이 가장 빠르지만 동기화가 되지 않으며, acquire/release는 생산자-소비자 패턴에서 사용되어 release 이전의 모든 쓰기가 acquire 이후에 보이도록 보장합니다. seq_cst는 가장 강력한 순서 보장으로 모든 스레드에서 일관된 전역 순서를 제공하지만 성능 비용이 가장 큽니다. 올바른 Memory Order 선택은 Lock-Free 프로그래밍의 핵심이며, 잘못 사용하면 미묘한 동시성 버그가 발생할 수 있으므로 깊은 이해가 필요합니다.
🔷 C++ 면접 예상 질문 50선 – 모범답변 (5/5: 41–50)
컴파일, 최적화, 설계 원칙, 예외, RTTI
41) C++의 컴파일 과정은 어떻게 이루어지나요?
🧠 핵심 요약
1️⃣ 전처리 → 2️⃣ 컴파일 → 3️⃣ 어셈블 → 4️⃣ 링크
각 단계는 독립적이며 오류 발생 시점이 다름
🔹 특징 및 상세설명
-
전처리:
#include,#define처리 및 매크로 치환 - 컴파일: C++ 소스 → 어셈블리 코드 생성 (.obj, .o 파일)
- 어셈블: 어셈블리 → 기계어로 변환
- 링크: 여러 개의 오브젝트 파일을 결합하여 실행 파일 생성
- 링커는 심볼 테이블을 통해 함수/변수 참조를 해결함
💬 면접식 답변
C++ 컴파일 과정은 전처리, 컴파일, 어셈블, 링크의 네 단계로 이루어집니다. 첫째, 전처리 단계에서는 #include 지시문으로 헤더 파일을 삽입하고 #define 매크로를 치환하며 조건부 컴파일(#ifdef)을 처리합니다. 둘째, 컴파일 단계에서는 전처리된 C++ 소스 코드를 구문 분석하고 최적화하여 어셈블리 코드로 변환합니다. 셋째, 어셈블 단계에서는 어셈블리 코드를 기계어로 변환하여 오브젝트 파일(.obj 또는 .o)을 생성합니다. 넷째, 링크 단계에서는 여러 오브젝트 파일과 라이브러리를 결합하여 최종 실행 파일을 생성하며, 심볼 테이블을 통해 함수와 변수의 참조를 해결합니다. 각 단계는 독립적으로 오류를 발생시킬 수 있으며, 컴파일 오류는 구문 문제를, 링크 오류는 정의되지 않은 참조나 중복 정의 문제를 나타냅니다.
42) inline 함수는 언제 사용되며, 주의할 점은?
🧠 핵심 요약
- 작은 함수의 호출 오버헤드 제거용
- 컴파일러가 강제하지 않음
🔹 특징 및 상세설명
- inline은 함수 본문을 호출 지점에 삽입하여 호출 오버헤드를 줄인다.
- 단, 코드 크기 증가(인스트럭션 캐시 부담) 위험이 있음.
- 가상 함수나 루프 내부에서 사용 시 성능 저하 가능.
- modern compiler는 자동 인라인 최적화를 수행하므로 남용 금지.
💬 면접식 답변
inline 함수는 함수 호출 시 스택 프레임 생성과 점프 비용을 제거하기 위해 호출 지점에 함수 본문을 직접 삽입하는 최적화 기법입니다. 작고 자주 호출되는 함수에서 inline을 사용하면 호출 오버헤드를 제거하여 성능을 향상시킬 수 있습니다. 하지만 과도하게 사용하면 코드 크기가 증가하여 인스트럭션 캐시 효율이 떨어지고, 결과적으로 성능이 오히려 저하될 수 있습니다. inline 키워드는 어디까지나 컴파일러에 대한 힌트일 뿐, 실제 인라인 여부는 컴파일러가 함수 크기, 복잡도, 재귀 여부 등을 고려하여 결정합니다. 가상 함수나 재귀 함수는 인라인화되기 어렵습니다. 현대 컴파일러는 LTO(Link-Time Optimization), PGO(Profile-Guided Optimization) 같은 고급 최적화 기법으로 자동으로 인라인을 수행하므로, 명시적인 inline 키워드는 제한적으로 사용하는 것이 좋습니다.
43) const와 constexpr의 차이는?
🧠 핵심 요약
- const: 런타임 상수
- constexpr: 컴파일타임 상수
🔹 특징 및 상세설명
- const는 초기화 후 변경 불가지만, 컴파일 시간에 값이 확정될 필요는 없음.
- constexpr은 반드시 컴파일 시 계산 가능한 값이어야 함.
- constexpr은 함수, 생성자에도 적용 가능(C++11 이후).
- constexpr은 상수 표현식 최적화를 통해 실행 속도를 향상시킴.
💬 면접식 답변
const와 constexpr은 모두 불변성을 표현하지만, 평가 시점과 사용 범위에서 차이가 있습니다. const는 변수가 초기화 후 변경되지 않음을 보장하지만, 초기값이 컴파일 타임에 결정될 필요는 없습니다. 예를 들어 사용자 입력이나 함수 반환값으로 초기화된 const 변수는 런타임에 값이 결정됩니다. 반면 constexpr은 컴파일 타임에 값이 확정되어야 하며, 컴파일러가 컴파일 중에 계산을 완료합니다. constexpr 변수는 배열 크기나 템플릿 인자처럼 컴파일 타임 상수가 필요한 곳에 사용할 수 있습니다. C++11 이후 constexpr은 함수와 생성자에도 적용할 수 있어, 컴파일 타임에 복잡한 계산을 수행하고 결과를 상수로 사용할 수 있습니다. 이를 통해 런타임 오버헤드 없이 성능을 최적화하며, 메타프로그래밍의 핵심 도구로 활용됩니다.
44) RTTI(Run-Time Type Information)란 무엇인가요?
🧠 핵심 요약
- 런타임에 객체의 실제 타입 정보를 제공하는 시스템
- dynamic_cast, typeid에서 사용됨
🔹 특징 및 상세설명
- RTTI는 다형성 타입의 런타임 식별에 사용된다.
- typeid(obj): 객체의 실제 타입 반환
- dynamic_cast<T*>(): 안전한 다운캐스팅 수행
- 내부적으로 클래스마다 type_info 테이블(vtable과 별개)을 가진다.
- RTTI는 오버헤드가 있으므로 최소한으로 사용하는 것이 권장된다.
💬 면접식 답변
RTTI(Run-Time Type Information)는 프로그램 실행 중에 객체의 실제 타입 정보를 확인할 수 있게 해주는 C++의 메커니즘입니다. RTTI는 주로 두 가지 기능을 제공합니다. typeid 연산자는 객체의 타입 정보를 std::type_info 객체로 반환하여 타입을 비교하거나 이름을 확인할 수 있게 하고, dynamic_cast는 베이스 클래스 포인터를 파생 클래스 포인터로 안전하게 변환하며 실패 시 nullptr을 반환합니다. RTTI는 가상 함수를 가진 다형적 클래스에만 작동하며, 내부적으로 각 클래스는 type_info 테이블을 가지고 있습니다. 하지만 RTTI는 런타임 오버헤드가 있고 타입 정보를 저장하는 메모리 비용이 발생하므로, 일부 임베디드 시스템이나 성능 중시 환경에서는 컴파일러 옵션으로 비활성화하기도 합니다. 설계 측면에서 RTTI에 지나치게 의존하는 것은 다형성의 이점을 제대로 활용하지 못한다는 신호일 수 있으므로, 가상 함수를 통한 다형성 설계를 우선 고려하는 것이 좋습니다.
45) 예외 처리(exception handling)는 내부적으로 어떻게 작동하나요?
🧠 핵심 요약
- try/catch 블록과 스택 언와인딩(Stack Unwinding) 기반
🔹 특징 및 상세설명
- 예외 발생 시 스택 프레임을 하나씩 해제하며 catch 블록을 탐색.
- 이 과정에서 지역 객체의 소멸자가 자동 호출됨.
- 표준 라이브러리의 예외는
std::exception을 상속받음. - 예외는 성능 오버헤드가 있으므로, 성능 민감 구간에서는 반환값 기반 처리 선호.
💬 면접식 답변
C++의 예외 처리는 try-catch 블록과 스택 언와인딩(Stack Unwinding)을 기반으로 동작합니다. 예외가 발생하면 프로그램은 현재 실행 지점에서 호출 스택을 거슬러 올라가며 해당 예외 타입을 처리할 수 있는 catch 블록을 찾습니다. 이 과정에서 각 스택 프레임의 지역 객체들의 소멸자가 자동으로 호출되어 RAII 기반 자원 관리를 통해 메모리 누수를 방지합니다. 표준 라이브러리의 모든 예외는 std::exception을 상속받으며, what() 메서드로 오류 메시지를 제공합니다. 예외는 값으로 던지고 참조로 잡는 것이 권장되며, 포인터로 던지면 소유권 문제가 발생할 수 있습니다. 하지만 예외는 스택 언와인딩과 타입 정보 탐색으로 인한 런타임 오버헤드가 있어, 성능에 민감한 실시간 시스템이나 게임 엔진에서는 반환 코드나 std::optional, std::expected 같은 대안을 사용하기도 합니다.
46) noexcept 키워드는 어떤 역할을 하나요?
🧠 핵심 요약
- 함수가 예외를 던지지 않음을 보장
- 최적화 및 강제 종료 제어
🔹 특징 및 상세설명
- noexcept 지정 시 예외가 발생하면 프로그램이 즉시 종료(
std::terminate). - move 생성자에 noexcept를 붙이면 컨테이너 이동 시 불필요한 복사 방지.
- 런타임 비용은 없지만, 최적화 힌트로 컴파일러에 전달된다.
💬 면접식 답변
noexcept 키워드는 함수가 예외를 절대 던지지 않음을 컴파일러와 프로그래머에게 명시적으로 선언하는 지정자입니다. noexcept로 선언된 함수에서 예외가 발생하면 std::terminate가 즉시 호출되어 프로그램이 종료됩니다. 이는 예외 처리 메커니즘을 우회하여 오버헤드를 줄이고 최적화 기회를 제공합니다. 특히 이동 생성자와 이동 대입 연산자에 noexcept를 붙이는 것이 중요합니다. STL 컨테이너는 재할당 시 noexcept 이동 연산을 사용할 수 있으면 이동을 선택하고, 그렇지 않으면 강한 예외 보장(Strong Exception Guarantee)을 위해 복사를 선택합니다. noexcept는 런타임 비용이 없고 순수하게 컴파일 타임 정보이지만, 함수 인터페이스의 일부이므로 나중에 제거하기 어렵습니다. 따라서 정말 예외를 던지지 않는 함수에만 신중하게 사용해야 하며, 소멸자는 기본적으로 noexcept로 간주됩니다.
47) 컴파일 타임 다형성과 런타임 다형성의 차이는?
🧠 핵심 요약
- 컴파일 타임: 템플릿 기반(static)
- 런타임: virtual 함수 기반(dynamic)
🔹 특징 및 상세설명
- 컴파일 타임 다형성: 함수 오버로딩, 템플릿 인스턴스화 등.
- 런타임 다형성: 가상 함수, vtable을 통한 동적 바인딩.
- 전자는 인라인화 가능해 빠르지만 코드 크기 증가 가능.
- 후자는 유연하지만 약간의 런타임 오버헤드가 존재.
💬 면접식 답변
컴파일 타임 다형성과 런타임 다형성은 다형성을 구현하는 두 가지 근본적으로 다른 접근 방식입니다. 컴파일 타임 다형성은 템플릿과 함수 오버로딩을 통해 구현되며, 컴파일 시점에 타입이 결정되어 각 타입별로 별도의 코드가 생성됩니다. 인라인화가 가능하고 가상 함수 호출 오버헤드가 없어 성능이 우수하지만, 바이너리 크기가 증가하고 타입이 컴파일 시점에 확정되어야 합니다. 반면 런타임 다형성은 가상 함수와 vtable을 통해 구현되며, 실행 시점에 실제 객체 타입에 따라 호출할 함수가 결정됩니다. 간접 호출로 인한 약간의 오버헤드가 있지만, 타입을 런타임에 결정할 수 있어 플러그인 시스템이나 동적 객체 생성에 유리합니다. 실무에서는 두 방식을 적절히 조합하여 사용합니다. 예를 들어 STL 컨테이너는 컴파일 타임 다형성(템플릿)을 사용하고, GUI 프레임워크나 게임 엔티티 시스템은 런타임 다형성을 활용합니다.
48) SOLID 원칙을 설명해주세요.
🧠 핵심 요약
1️⃣ SRP – 단일 책임
2️⃣ OCP – 개방 폐쇄
3️⃣ LSP – 리스코프 치환
4️⃣ ISP – 인터페이스 분리
5️⃣ DIP – 의존성 역전
🔹 특징 및 상세설명
- SRP: 클래스는 하나의 책임만 가져야 함.
- OCP: 확장에는 열려 있고 수정에는 닫혀 있어야 함.
- LSP: 상위 타입의 객체를 하위 타입으로 치환 가능해야 함.
- ISP: 사용하지 않는 인터페이스에 의존하지 않도록 분리.
- DIP: 고수준 모듈은 저수준 구현이 아닌 추상에 의존해야 함.
💬 면접식 답변
SOLID 원칙은 객체지향 설계의 다섯 가지 핵심 원칙으로, 유지보수성, 확장성, 테스트 용이성을 높이는 설계 지침입니다. 첫째, 단일 책임 원칙(SRP)은 클래스가 하나의 책임만 가져야 하며 변경 이유도 하나여야 한다는 원칙입니다. 둘째, 개방-폐쇄 원칙(OCP)은 확장에는 열려 있고 수정에는 닫혀 있어야 한다는 원칙으로, 새 기능 추가 시 기존 코드를 변경하지 않고 확장할 수 있어야 합니다. 셋째, 리스코프 치환 원칙(LSP)은 부모 클래스 객체를 자식 클래스 객체로 치환해도 프로그램이 올바르게 동작해야 한다는 원칙입니다. 넷째, 인터페이스 분리 원칙(ISP)은 클라이언트가 사용하지 않는 인터페이스에 의존하지 않도록 인터페이스를 작게 분리해야 한다는 원칙입니다. 다섯째, 의존성 역전 원칙(DIP)은 고수준 모듈이 저수준 구현이 아닌 추상에 의존해야 한다는 원칙으로, 의존성 주입(Dependency Injection)의 이론적 기반이 됩니다. 실무에서는 특히 DIP와 OCP를 적용하여 테스트 가능하고 확장 가능한 아키텍처를 설계하는 것이 중요합니다.
49) static 키워드는 C++에서 어떤 용도로 쓰이나요?
🧠 핵심 요약
- 변수 수명과 범위 제어
- 함수/클래스 단위에서도 활용
🔹 특징 및 상세설명
- 정적 변수: 함수 내부에서 한 번만 초기화되고, 호출 간 값 유지.
- 정적 함수: 해당 번역 단위(.cpp) 내에서만 접근 가능.
- 클래스 정적 멤버: 모든 객체가 공유하는 공용 데이터.
- 프로그램 시작 시 초기화, 종료 시 소멸.
💬 면접식 답변
static 키워드는 C++에서 변수와 함수의 생명주기, 접근 범위, 링크를 제어하는 다목적 키워드로 여러 컨텍스트에서 다르게 사용됩니다. 첫째, 전역 static 변수와 함수는 해당 번역 단위(.cpp) 내에서만 접근 가능하여 내부 링크를 가지며, 이름 충돌을 방지하고 캡슐화를 강화합니다. 둘째, 함수 내부 static 변수는 함수 호출 간에도 값을 유지하며 프로그램 시작이 아닌 처음 실행 시 초기화됩니다. C++11부터는 이 초기화가 스레드 안전하게 보장되어 싱글턴 패턴 구현에 안전하게 사용할 수 있습니다. 셋째, 클래스 static 멤버 변수는 모든 인스턴스가 공유하는 단일 변수로, 객체 생성 없이 클래스 이름으로 접근 가능하며 클래스 전체의 상태나 설정을 저장하는 데 유용합니다. 넷째, static 멤버 함수는 this 포인터가 없어 인스턴스 멤버에 접근할 수 없지만 static 멤버만 사용할 수 있습니다. static 변수는 프로그램 시작 시 초기화되고 종료 시 소멸되므로, 정적 초기화 순서 문제를 피하려면 함수 내부 static을 활용하는 것이 권장됩니다.
50) C++에서 메모리 최적화를 위해 고려할 점은?
🧠 핵심 요약
- 불필요한 복사 제거
- 캐시 지역성 향상
- 객체 수명 최소화
🔹 특징 및 상세설명
-
무브 시멘틱 활용 (
std::move,emplace_back) - reserve() / shrink_to_fit()로 vector 재할당 최소화
- 메모리 풀, 커스텀 알로케이터로 동적 할당 최적화
- 구조체 정렬(padding) 최소화로 메모리 낭비 방지
- 캐시 친화적 데이터 레이아웃 설계 (AoS → SoA 변환)
💬 면접식 답변
C++에서 메모리 최적화는 성능을 결정짓는 핵심 요소로, 불필요한 복사 제거, 캐시 지역성 향상, 동적 할당 최소화, 메모리 레이아웃 최적화가 중요합니다. 첫째, 무브 시멘틱(std::move, emplace_back)을 활용하여 불필요한 복사를 제거하고, 함수 반환 시 RVO(Return Value Optimization)를 활용합니다. 둘째, vector의 reserve()로 재할당을 방지하고 shrink_to_fit()으로 불필요한 메모리를 해제하며, 컨테이너 선택 시 캐시 효율을 고려합니다. 셋째, 자주 할당/해제되는 객체에는 메모리 풀이나 커스텀 알로케이터를 사용하여 동적 할당 비용을 줄입니다. 넷째, 구조체 멤버를 큰 타입부터 배치하여 패딩을 최소화하고, 데이터 지향 설계(Data-Oriented Design)를 통해 AoS(Array of Structures)를 SoA(Structure of Arrays)로 변환하여 SIMD와 캐시 효율을 높입니다. 다섯째, 멀티스레드 환경에서는 false sharing을 방지하기 위해 자주 수정되는 변수들을 다른 캐시라인에 배치합니다. 실무에서는 프로파일링 도구(Valgrind, perf, VTune)를 사용하여 병목을 식별하고 최적화하는 것이 중요합니다.
51) C++에서 enum class란 무엇인가요?
🧠 핵심 요약
- C++11부터 도입된 타입 안전한 열거형(enum)
- 기존 enum의 스코프 충돌 및 암묵적 변환 문제 해결
- 명시적 범위 지정(Color::Red) 과 형변환 제한이 특징
🔹 특징 및 상세설명
- 기존
enum은 전역 스코프에 노출되어 이름 충돌 위험이 높고,int로 암묵 변환되어 타입 안정성이 부족함 -
enum class는 자체 스코프를 가져서 이름 충돌을 방지하고, 암묵적 형변환이 불가능함 - 서로 다른 enum class 간 비교 불가능 → 타입 안전성 보장
- 필요 시 명시적 캐스팅 사용 가능
1
int n = static_cast<int>(Color::Green);
- 기본 자료형 지정으로 메모리 절약 가능
1
enum class ErrorCode : uint8_t { OK = 0, NotFound = 1, Unknown = 255 };
💬 코드 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;
enum class Color { Red, Green, Blue };
enum class Fruit { Apple, Banana, Orange };
int main() {
Color c = Color::Red;
Fruit f = Fruit::Apple;
// if (c == f) ❌ 오류: 타입이 다름
// int x = c; ❌ 암묵적 변환 불가
int x = static_cast<int>(Color::Green); // ✅ 명시적 변환
cout << "x = " << x << endl;
}
💬 면접식 답변
enum class는 C++11부터 도입된 타입 안전한 열거형입니다.
기존 enum은 전역 스코프와 암묵적 형변환 문제로 안전하지 않았지만,
enum class는 자체 스코프를 가져 이름 충돌을 막고 암묵 변환을 제한해
명확하고 안전한 코드 작성이 가능합니다.