C++ 면접 질문 및 답변

C++Beginner
지금 연습하기

소개

C++ 인터뷰에서 탁월한 성과를 거두는 데 필요한 지식과 자신감을 갖추도록 설계된 이 포괄적인 가이드에 오신 것을 환영합니다. C++ 의 복잡성을 탐색하려면 핵심 원칙, 고급 기능 및 실제 적용에 대한 깊은 이해가 필요합니다. 이 문서는 기본 개념 및 객체 지향 프로그래밍 (Object-Oriented Programming) 패러다임부터 최신 C++ 의 복잡성, 자료 구조, 알고리즘 및 시스템 설계 원칙에 이르기까지 광범위한 주제를 꼼꼼하게 다룹니다. 신입 사원 직책을 준비하든 시니어 엔지니어 직책을 준비하든, 이 자료는 상세한 답변, 실용적인 문제 해결 전략 및 모범 사례에 대한 통찰력을 제공하여 어떤 도전 과제든 해결할 준비를 갖추도록 보장합니다. C++ 를 마스터하고 경력 잠재력을 발휘하기 위한 여정을 시작해 봅시다.

CPP

C++ 기초 및 핵심 개념

std::vectorstd::list의 차이점을 설명하세요.

답변:

std::vector는 동적 배열로, 연속적인 메모리 할당을 제공하여 빠른 임의 접근 (O(1)) 이 가능하지만, 중간에서의 삽입/삭제는 느립니다 (O(n)). std::list는 이중 연결 리스트로, 어디서든 효율적인 삽입/삭제 (O(1)) 를 제공하지만, 임의 접근은 느리고 (O(n)) 각 요소당 더 많은 메모리 오버헤드를 가집니다.


C++ 에서 virtual 키워드의 목적은 무엇인가요?

답변:

virtual 키워드는 파생 클래스의 멤버 함수 구현을 기본 클래스 포인터나 참조를 통해 호출할 수 있도록 하여 다형성 (polymorphism) 을 가능하게 합니다. 이는 포인터/참조 타입이 아닌 실제 객체 타입에 따라 런타임에 올바른 재정의된 함수가 호출되도록 보장합니다.


RAII(Resource Acquisition Is Initialization) 개념을 설명하세요.

답변:

RAII 는 C++ 프로그래밍 관용구로, 리소스 관리 (예: 메모리, 파일 핸들, 뮤텍스) 를 객체의 수명과 연결합니다. 리소스는 생성자에서 획득하고 소멸자에서 해제됩니다. 이는 예외가 발생하더라도 리소스 누수를 방지하며 리소스가 올바르게 해제되도록 보장합니다.


얕은 복사 (shallow copy) 와 깊은 복사 (deep copy) 의 차이점은 무엇인가요?

답변:

얕은 복사는 멤버 변수의 값만 복사합니다. 즉, 멤버가 포인터인 경우, 포인터가 가리키는 데이터가 아닌 포인터 자체만 복사됩니다. 그러면 두 객체는 동일한 기본 리소스를 공유하게 됩니다. 깊은 복사는 가리키는 데이터에 대한 새 메모리를 할당하고 내용을 복사하여 각 객체가 자체 독립적인 리소스 복사본을 갖도록 합니다.


C++ 에서 const를 언제 사용해야 하나요?

답변:

const는 값이 변경되지 않아야 하는 변수를 선언하고, 함수 매개변수가 수정되지 않을 것임을 명시하며, 객체의 상태를 수정하지 않는 멤버 함수를 표시하는 데 사용해야 합니다. 이는 코드의 명확성을 향상시키고, 컴파일러가 최적화하도록 돕고, 의도치 않은 수정을 방지합니다.


C++ 에서 nullptr, NULL, 0의 차이점을 설명하세요.

답변:

nullptr은 C++11 에서 도입된 키워드로, 널 포인터 값을 나타내기 위해 특별히 사용되며, 타입 안전성을 제공하고 정수 타입과의 모호성을 방지합니다. NULL은 일반적으로 0 또는 (void*)0으로 정의된 매크로입니다. 0은 널 포인터로 암묵적으로 변환될 수 있는 정수 리터럴이지만, 정수 값일 수도 있어 잠재적인 모호성을 야기할 수 있습니다.


스마트 포인터 (smart pointers) 란 무엇이며 왜 사용되나요?

답변:

스마트 포인터는 포인터처럼 작동하지만 자신이 가리키는 메모리를 자동으로 관리하여 메모리 누수를 방지하는 객체입니다. RAII 를 사용하여 동적으로 할당된 메모리가 스마트 포인터가 범위를 벗어날 때 해제되도록 합니다. 일반적인 유형으로는 std::unique_ptr(독점 소유권) 및 std::shared_ptr(참조 카운팅을 통한 공유 소유권) 이 있습니다.


C++ 에서 연산자 오버로딩 (operator overloading) 이란 무엇인가요?

답변:

연산자 오버로딩은 C++ 연산자 (+, -, ==, << 등) 를 사용자 정의 타입에 대해 재정의할 수 있도록 합니다. 이를 통해 연산자가 피연산자의 타입에 따라 다르게 동작하도록 하여, 복소수나 사용자 정의 컨테이너와 같은 사용자 정의 클래스를 다룰 때 코드를 더 직관적이고 읽기 쉽게 만들 수 있습니다.


C++11 의 이동 의미론 (move semantics) 개념을 설명하세요.

답변:

rvalue 참조와 함께 도입된 이동 의미론은 복사하는 대신 리소스 (예: 동적으로 할당된 메모리) 를 한 객체에서 다른 객체로 '이동'할 수 있도록 합니다. 이는 임시 객체의 리소스가 더 이상 필요하지 않을 때 비용이 많이 드는 깊은 복사를 피하여, 함수에서 큰 객체를 반환하거나 컨테이너의 크기를 조정하는 등의 작업 성능을 크게 향상시킵니다.


C++ 의 Rule of Three/Five/Zero란 무엇인가요?

답변:

Rule of Three 는 클래스가 소멸자, 복사 생성자 또는 복사 대입 연산자를 정의하는 경우, 이 세 가지 모두가 필요할 가능성이 높다고 말합니다. Rule of Five 는 C++11 부터 이동 생성자와 이동 대입 연산자를 추가합니다. 현대 C++ 에서 선호되는 Rule of Zero 는 클래스가 원시 리소스를 관리하지 않는다면, 스마트 포인터와 표준 라이브러리 컨테이너에 의존하여 이러한 특수 멤버 함수 중 어느 것도 필요하지 않아야 함을 제안합니다.


C++ 객체 지향 프로그래밍 (OOP)

객체 지향 프로그래밍 (OOP) 의 네 가지 주요 기둥은 무엇인가요? 각각 간략하게 설명하세요.

답변:

네 가지 주요 기둥은 캡슐화 (데이터와 메서드 번들링), 상속 (기존 클래스로부터 새 클래스 생성), 다형성 (객체가 여러 형태를 취하는 것), 추상화 (복잡한 구현 세부 정보 숨기기) 입니다.


C++ 에서 캡슐화 (Encapsulation) 개념과 그 중요성에 대해 설명하세요.

답변:

캡슐화는 데이터와 해당 데이터를 조작하는 메서드를 단일 단위 (클래스) 로 묶는 것입니다. 이는 데이터 은닉, 외부 접근으로부터 데이터를 보호하고, 공개 인터페이스를 통해 접근을 제어함으로써 모듈성과 유지보수성을 촉진하는 데 중요합니다.


C++ 에서 컴파일 타임 (정적) 다형성과 런타임 (동적) 다형성의 차이점은 무엇인가요?

답변:

컴파일 타임 다형성은 함수 오버로딩과 연산자 오버로딩을 통해 달성되며 컴파일 시점에 해결됩니다. 런타임 다형성은 가상 함수와 기본 클래스에 대한 포인터/참조를 통해 달성되며 런타임에 해결되어 동적 메서드 디스패치를 가능하게 합니다.


C++ 에서 추상 클래스 (abstract class) 와 인터페이스 (순수 가상 클래스) 를 언제 사용해야 하나요?

답변:

추상 클래스는 일부 공통 구현과 일부 순수 가상 함수를 가진 기본 클래스를 제공하고 싶을 때 사용됩니다. 인터페이스 (순수 가상 함수만 있는 클래스) 는 구현 세부 정보 없이 파생 클래스가 반드시 구현해야 하는 계약만 정의하고 싶을 때 사용됩니다.


C++ 에서 'virtual' 키워드의 목적을 설명하세요.

답변:

'virtual' 키워드는 런타임 다형성을 달성하는 데 사용됩니다. 기본 클래스에서 함수가 가상으로 선언되면, 기본 클래스 포인터나 참조를 통해 해당 함수의 파생 클래스 버전을 호출할 수 있게 되어 실제 객체 타입에 기반한 동적 메서드 디스패치를 가능하게 합니다.


C++ 에서 생성자 (constructor) 와 소멸자 (destructor) 는 무엇인가요? 언제 호출되나요?

답변:

생성자는 객체가 생성될 때 자동으로 호출되는 특별한 멤버 함수로, 객체의 상태를 초기화하는 데 사용됩니다. 소멸자는 객체가 파괴될 때 자동으로 호출되는 특별한 멤버 함수로, 객체가 획득한 리소스를 해제하는 데 사용됩니다.


C++ 에서 'this' 포인터란 무엇인가요?

답변:

'this' 포인터는 클래스의 모든 비정적 멤버 함수 내에서 사용할 수 있는 암시적 상수 포인터입니다. 이는 멤버 함수가 호출된 객체를 가리키며, 객체 자체의 멤버에 접근하고 동일한 이름을 가진 멤버 변수와 지역 변수를 구별하는 데 사용됩니다.


C++ 에서 public, private, protected 접근 지정자의 차이점을 설명하세요.

답변:

Public 멤버는 어디서든 접근 가능합니다. Private 멤버는 동일한 클래스 내부에서만 접근 가능합니다. Protected 멤버는 동일한 클래스 내부 및 파생 클래스에서 접근 가능하지만, 클래스 계층 구조 외부에서는 접근할 수 없습니다.


메서드 오버라이딩 (method overriding) 과 메서드 오버로딩 (method overloading) 은 무엇인가요?

답변:

메서드 오버라이딩은 파생 클래스가 기본 클래스에 이미 정의된 가상 함수에 대한 특정 구현을 제공할 때 발생합니다. 메서드 오버로딩은 동일한 범위 내의 여러 함수가 동일한 이름을 가지지만 매개변수 (개수, 타입 또는 순서) 가 다를 때 발생합니다.


C++ 에서 '인터페이스 (interface)' 개념을 설명하세요.

답변:

C++ 에서 인터페이스는 일반적으로 순수 가상 함수만 포함하는 추상 클래스로 구현됩니다. 이는 구현 세부 정보를 제공하지 않고 특정 동작 집합을 보장하기 위해 모든 순수 가상 함수를 구현함으로써 구체적인 클래스가 준수해야 하는 계약을 정의합니다.


C++ 의 Rule of Three/Five/Zero란 무엇인가요?

답변:

Rule of Three 는 소멸자, 복사 생성자 또는 복사 대입 연산자 중 하나라도 정의하면 세 가지 모두를 정의해야 한다고 말합니다. Rule of Five 는 이동 생성자와 이동 대입 연산자를 포함하도록 이를 확장합니다. Rule of Zero 는 원시 리소스를 관리하지 않는다면 컴파일러가 생성한 버전에 의존하여 이들 중 어느 것도 정의할 필요가 없다고 제안합니다.


고급 C++ 기능 및 모던 C++

C++11 이상에서 std::movestd::forward의 목적을 설명하세요.

답변:

std::move는 인수를 무조건 rvalue 참조로 캐스팅하여 이동 의미론 (리소스 소유권 이전) 을 가능하게 합니다. std::forward는 원본 인수가 rvalue 인지 여부에 따라 인수를 조건부로 rvalue 참조로 캐스팅하여, 완벽 전달 (perfect forwarding) 시나리오에서 값 범주를 보존합니다.


C++ 의 Rule of Zero, Three, Five 에 대해 설명하세요.

답변:

Rule of Three 는 소멸자, 복사 생성자 또는 복사 대입 연산자 중 하나라도 정의하면 세 가지 모두를 정의해야 한다고 말합니다. Rule of Five 는 이동 생성자와 이동 대입 연산자를 포함하도록 이를 확장합니다. Rule of Zero 는 클래스가 리소스를 직접 관리하지 않는다면, 이들 중 어느 것도 정의하지 않고 컴파일러가 생성한 기본값이나 스마트 포인터에 의존해야 함을 제안합니다.


'완벽 전달 (perfect forwarding)' 개념과 std::forward가 이를 어떻게 지원하는지 설명하세요.

답변:

완벽 전달은 함수 템플릿이 임의의 인수를 받아 원래의 값 범주 (lvalue 또는 rvalue) 및 const/volatile 한정자를 보존하면서 다른 함수로 전달할 수 있도록 합니다. std::forward는 이를 위해 중요하며, 원본 인수가 rvalue 인 경우에만 조건부로 인수를 rvalue 참조로 캐스팅하여 전달된 호출에 대한 올바른 오버로드 해결을 보장합니다.


스마트 포인터 (std::unique_ptr, std::shared_ptr, std::weak_ptr) 란 무엇이며 왜 원시 포인터보다 선호되나요?

답변:

스마트 포인터는 원시 포인터를 감싸는 RAII(Resource Acquisition Is Initialization) 래퍼로, 메모리를 자동으로 관리하여 메모리 누수 및 댕글링 포인터를 방지합니다. unique_ptr은 독점 소유권을 제공하고, shared_ptr은 참조 카운팅을 통해 공유 소유권을 가능하게 하며, weak_ptrshared_ptr 사이클에서 순환 참조를 끊습니다. 이들은 리소스 관리를 단순화하고 코드 안전성을 향상시킵니다.


C++ 에서 noexceptthrow()의 차이점을 설명하세요.

답변:

throw() (C++11 에서 deprecated) 는 런타임에 목록에 없는 예외가 발생했는지 확인하는 동적 예외 명세로, std::unexpected를 발생시켰습니다. noexcept (C++11 부터) 는 함수가 예외를 발생시키지 않음을 나타내는 컴파일 타임 명세입니다. noexcept 함수가 예외를 발생시키면 std::terminate가 호출되어 최적화를 위한 더 강력한 보장을 제공합니다.


C++11 의 람다 표현식 (lambda expression) 이란 무엇이며 주요 구성 요소는 무엇인가요?

답변:

람다 표현식은 인라인으로 정의할 수 있는 익명 함수 객체입니다. 주요 구성 요소는 캡처 절 ([]), 매개변수 목록 (()), mutable 지정자 (선택 사항), 예외 지정자 (선택 사항), 반환 타입 (선택 사항, 추론됨), 함수 본문 ({}) 입니다. 람다는 간결한 콜백 및 알고리즘에 유용합니다.


C++ 에서 constconstexpr는 어떻게 다른가요?

답변:

const는 변수의 값이 초기화 후 변경될 수 없음을 나타내거나, 멤버 함수가 객체의 상태를 수정하지 않음을 나타냅니다. constexpr (C++11 부터) 는 값이나 함수가 컴파일 타임에 평가될 수 있음을 나타냅니다. constexpr는 변수에 대해 const를 내포하지만, constconstexpr를 내포하지 않습니다.


SFINAE(Substitution Failure Is Not An Error) 란 무엇이며 어떻게 사용되나요?

답변:

SFINAE 는 C++ 템플릿 메타프로그래밍의 원칙으로, 템플릿 매개변수 치환 중에 템플릿 인스턴스화가 실패하면 오류가 아니라 해당 특정 오버로드 또는 특수화가 후보 집합에서 제거됩니다. 이는 일반적으로 std::enable_if와 함께 사용하여 타입 특성에 따라 템플릿 인스턴스화를 조건부로 활성화하거나 비활성화하는 데 사용됩니다.


C++11 의 '가변 템플릿 (variadic templates)' 개념을 설명하세요.

답변:

가변 템플릿은 가변 개수의 인수를 받을 수 있는 템플릿입니다. 매개변수 팩 (typename... Args 또는 Args...) 을 사용하여 0 개 이상의 템플릿 매개변수 또는 함수 인수의 시퀀스를 나타냅니다. 일반적으로 재귀적으로 처리되거나 폴드 표현식 (C++17) 을 사용하여 팩의 각 요소에 대해 연산합니다.


'rvalue 참조 (rvalue references)'란 무엇이며 '이동 의미론 (move semantics)'을 어떻게 가능하게 하나요?

답변:

rvalue 참조 (&&) 는 임시 객체 또는 파괴될 예정인 객체에만 바인딩되어 lvalue 참조 (&) 와 구별됩니다. 이 구별을 통해 컴파일러는 비싼 깊은 복사를 수행하는 대신 임시 객체에서 리소스를 '훔치는' 오버로드 (이동 생성자/대입 연산자) 를 선택할 수 있어 이동 의미론을 가능하게 하고 성능을 향상시킵니다.


C++17 의 std::optional, std::variant, std::any의 목적을 설명하세요.

답변:

std::optional은 값을 포함하거나 비어 있는 선택적 값을 나타내며, 결과를 반환하지 않을 수 있는 함수에 유용합니다. std::variant는 타입 안전한 union 으로, 주어진 시점에 지정된 타입 집합 중 하나를 보유합니다. std::any는 어떤 단일 타입의 값도 보유할 수 있으며, void 포인터와 유사하지만 타입 정보를 가진 타입 안전한 이기종 저장소를 제공합니다.


C++ 자료 구조 및 알고리즘

C++ 에서 std::vectorstd::list의 차이점을 설명하세요. 언제 하나를 다른 하나보다 선택해야 할까요?

답변:

std::vector는 연속적인 메모리를 제공하는 동적 배열로, 빠른 임의 접근 (O(1)) 을 제공하지만 중간에서의 삽입/삭제는 느립니다 (O(N)). std::list는 이중 연결 리스트로, 어디서든 O(1) 의 삽입/삭제를 제공하지만 임의 접근은 O(N) 입니다. 빈번한 임의 접근에는 vector를, 중간에서의 빈번한 삽입/삭제에는 list를 선택하세요.


해시 테이블 (또는 해시 맵) 이란 무엇이며, C++ 에서 std::unordered_map은 어떻게 작동하나요?

답변:

해시 테이블은 버킷 배열로의 인덱스를 계산하기 위해 해시 함수를 사용하여 키 - 값 쌍을 저장합니다. std::unordered_map은 C++ 의 해시 테이블 구현입니다. 해싱을 사용하여 키를 버킷 인덱스에 매핑하며, 일반적으로 별도 체이닝 (버킷 내 연결 리스트) 또는 개방 주소법을 사용하여 충돌을 처리하여 삽입, 삭제 및 조회를 평균 O(1) 시간 복잡도로 제공합니다.


Big O 표기법의 개념을 설명하세요. O(1), O(N), O(N^2) 에 대한 예시를 제공하세요.

답변:

Big O 표기법은 입력 크기가 증가함에 따라 알고리즘의 시간 또는 공간 복잡성의 상한선을 설명합니다. O(1) 은 상수 시간 (예: 배열 요소 접근) 입니다. O(N) 은 선형 시간 (예: 리스트 순회) 입니다. O(N^2) 은 이차 시간 (예: 버블 정렬의 중첩 루프) 입니다.


스택 (stack) 과 큐 (queue) 의 차이점을 설명하세요. 주요 연산은 무엇인가요?

답변:

스택은 LIFO(Last-In, First-Out) 자료 구조인 반면, 큐는 FIFO(First-In, First-Out) 자료 구조입니다. 주요 스택 연산은 push(상단에 추가) 및 pop(상단에서 제거) 입니다. 주요 큐 연산은 enqueue(후면에 추가) 및 dequeue(전면에서 제거) 입니다.


이진 탐색 트리 (BST) 란 무엇인가요? 장점과 단점은 무엇인가요?

답변:

BST 는 왼쪽 자식의 값이 부모보다 작고 오른쪽 자식의 값이 더 큰 트리 기반 자료 구조입니다. 장점으로는 효율적인 검색, 삽입 및 삭제 (평균 O(log N)) 가 있습니다. 단점으로는 편향된 트리 (최악의 경우 O(N)) 가 발생할 수 있으며 배열에 비해 메모리 오버헤드가 더 높다는 점이 있습니다.


퀵 정렬 (quicksort) 은 어떻게 작동하나요? 평균 및 최악의 경우 시간 복잡도는 어떻게 되나요?

답변:

퀵 정렬은 분할 정복 정렬 알고리즘입니다. 피벗을 요소로 선택하고 피벗을 중심으로 배열을 분할하여 작은 요소를 왼쪽에, 큰 요소를 오른쪽에 배치합니다. 그런 다음 하위 배열을 재귀적으로 정렬합니다. 평균 시간 복잡도는 O(N log N) 이지만, 피벗 선택이 일관되게 매우 불균형한 분할을 초래하는 경우 최악의 경우 시간 복잡도는 O(N^2) 입니다.


C++ 에서 std::map의 목적은 무엇인가요? std::unordered_map과는 어떻게 다른가요?

답변:

std::map은 키를 기준으로 정렬된 순서로 키 - 값 쌍을 저장하는 연관 컨테이너로, 일반적으로 자체 균형 이진 탐색 트리 (예: 레드 - 블랙 트리) 로 구현됩니다. 연산에 대해 O(log N) 시간 복잡도를 제공합니다. std::unordered_map은 해싱을 사용하며 평균 O(1) 복잡도를 제공하지만 정렬된 순서를 유지하지는 않습니다.


재귀 (recursion) 의 개념을 설명하세요. 간단한 예시를 제공하세요.

답변:

재귀는 함수가 문제를 해결하기 위해 자신을 호출하는 프로그래밍 기법입니다. 재귀를 중지하기 위한 기본 사례와 문제를 더 작고 유사한 하위 문제로 분해하는 재귀 단계를 포함합니다. 예시: 팩토리얼 (n!) 계산, 여기서 factorial(n) = n * factorial(n-1)이고 factorial(0) = 1입니다.


그래프 (graph) 자료 구조란 무엇인가요? 그래프를 표현하는 두 가지 일반적인 방법을 언급하세요.

답변:

그래프는 노드 (정점) 와 이를 연결하는 간선으로 구성된 비선형 자료 구조입니다. 개체 간의 관계를 나타낼 수 있습니다. 두 가지 일반적인 표현은 인접 행렬 (matrix[i][j]가 i 와 j 사이의 간선을 나타내는 2D 배열) 과 인접 리스트 (각 인덱스/키가 정점을 나타내고 해당 값이 이웃 목록인 배열 또는 맵) 입니다.


std::setstd::vector 또는 std::list보다 언제 사용해야 할까요?

답변:

std::set은 자체 균형 BST 로 일반적으로 구현되는 고유한 요소를 정렬된 순서로 저장하는 연관 컨테이너입니다. 고유한 요소를 저장하고, 정렬된 순서를 유지하며, 효율적인 조회, 삽입 및 삭제 (O(log N)) 를 수행해야 할 때 std::set을 사용하세요. vectorlist는 중복을 허용하며 본질적으로 정렬된 순서를 유지하지 않습니다.


System Design and Concurrency in C++

Explain the difference between a process and a thread. When would you choose one over the other?

Answer:

A process is an independent execution unit with its own memory space, while a thread is a lightweight execution unit within a process, sharing its memory. Choose processes for isolation and robustness (e.g., separate applications), and threads for concurrency within a single application to share data and reduce overhead.


What is a mutex in C++ and how is it used to prevent race conditions?

Answer:

A mutex (mutual exclusion) is a synchronization primitive that protects shared resources from simultaneous access by multiple threads. A thread acquires the mutex before accessing the shared resource and releases it afterward, ensuring only one thread can access the critical section at a time, thus preventing race conditions.


Describe a common scenario where a deadlock can occur. How can you prevent it?

Answer:

A deadlock occurs when two or more threads are blocked indefinitely, each waiting for the other to release a resource. A common scenario is two threads each holding one mutex and trying to acquire the other. Prevention strategies include consistent lock ordering, using std::lock, or employing std::unique_lock with std::defer_lock.


What is a condition variable in C++ and when is it useful?

Answer:

A condition variable allows threads to wait for a certain condition to become true. It's useful for producer-consumer patterns or when one thread needs to signal another that some event has occurred. Threads wait on the condition variable, and another thread notifies them when the condition is met, typically in conjunction with a mutex.


Explain the concept of atomicity. How can you achieve atomic operations in C++?

Answer:

Atomicity means an operation is indivisible and appears to occur instantaneously, either completing entirely or not at all. In C++, atomic operations can be achieved using std::atomic types for fundamental data types, or by protecting critical sections with mutexes for more complex operations.


What are std::future and std::promise used for in C++ concurrency?

Answer:

std::promise is used to set a value or an exception that will be retrieved by a std::future object. std::future provides a way to access the result of an asynchronous operation. Together, they enable asynchronous communication and retrieval of results from tasks running on separate threads.


How does std::async simplify asynchronous task execution compared to manually creating std::thread?

Answer:

std::async simplifies asynchronous execution by automatically managing thread creation (or reuse), execution, and result retrieval. It returns a std::future directly, handling potential exceptions and join/detach logic, whereas std::thread requires manual management of these aspects.


Discuss the trade-offs between using std::shared_ptr and raw pointers in a multi-threaded environment.

Answer:

std::shared_ptr provides automatic memory management and thread-safe reference counting, reducing memory leaks and dangling pointers. However, its reference count updates are atomic, incurring a performance overhead. Raw pointers are faster but require careful manual memory management and are prone to race conditions if not protected by mutexes in concurrent access.


What is a thread pool and why is it beneficial in system design?

Answer:

A thread pool is a collection of pre-initialized threads that can be reused to execute tasks. It's beneficial because it reduces the overhead of creating and destroying threads for each task, limits the number of concurrent threads to prevent resource exhaustion, and improves overall system responsiveness and throughput.


When designing a high-performance concurrent system, what are some key considerations regarding cache coherence and false sharing?

Answer:

Cache coherence ensures that all processors see a consistent view of memory. False sharing occurs when unrelated data items, accessed by different threads, reside in the same cache line, causing unnecessary cache line invalidations and performance degradation. Design considerations include careful data layout (padding) and avoiding shared mutable state where possible.


Practical Problem Solving and Coding Challenges

Given a sorted array and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order. Assume no duplicates.

Answer:

This is a classic binary search problem. Initialize low = 0, high = n-1. While low <= high, calculate mid. If nums[mid] == target, return mid. If nums[mid] < target, low = mid + 1. Else, high = mid - 1. Finally, return low.


Explain how to detect a cycle in a linked list and provide a high-level algorithm.

Answer:

Use Floyd's Cycle-Finding Algorithm (Tortoise and Hare). Initialize two pointers, slow and fast, both starting at the head. slow moves one step at a time, fast moves two steps. If they ever meet, a cycle exists. If fast reaches nullptr or fast->next is nullptr, there's no cycle.


How would you reverse a string in-place in C++?

Answer:

Use two pointers, left starting at the beginning and right at the end of the string. Swap characters at left and right, then increment left and decrement right. Continue until left crosses right. This modifies the string directly without extra space.


Describe the difference between std::vector and std::list in terms of memory layout and performance characteristics.

Answer:

std::vector stores elements contiguously in memory, allowing for O(1) random access and cache efficiency. Insertions/deletions in the middle are O(N). std::list is a doubly-linked list, storing elements non-contiguously. Insertions/deletions are O(1) once the iterator is found, but random access is O(N) due to traversal.


Implement a function to check if a given string is a palindrome, ignoring non-alphanumeric characters and case.

Answer:

Use two pointers, left and right. Move left forward and right backward, skipping non-alphanumeric characters. Convert valid characters to lowercase. Compare characters at left and right. If they don't match, it's not a palindrome. Continue until left >= right.


Given an array of integers, find the maximum sum of a contiguous subarray.

Answer:

This is Kadane's Algorithm. Maintain current_max and global_max. Iterate through the array: current_max = max(num, current_max + num). Update global_max = max(global_max, current_max) in each iteration. Initialize both to the first element or negative infinity.


Explain how to find the 'k'th smallest element in an unsorted array efficiently.

Answer:

The most efficient approach is Quickselect, which is a variation of Quicksort. It has an average time complexity of O(N). Alternatively, using a min-heap (priority queue) and extracting k elements would be O(N log K), or sorting the array first would be O(N log N).


How would you implement a basic LRU (Least Recently Used) cache?

Answer:

Use a std::list (or std::deque) to maintain the order of usage and a std::unordered_map to store key-value pairs along with iterators to their corresponding list nodes. On access, move the item to the front of the list. On insertion when full, remove the item at the back of the list.


Given two sorted arrays, merge them into a single sorted array.

Answer:

Use two pointers, one for each array, starting from their beginnings. Compare the elements pointed to and add the smaller one to the result array, advancing its pointer. If one array is exhausted, append the remaining elements of the other. This is O(M+N) time and O(M+N) space.


Describe a method to find all permutations of a given string.

Answer:

This can be solved using recursion and backtracking. For each character, swap it with every character to its right (including itself) and recursively find permutations for the remaining substring. Use a std::set or check for duplicates if the input string has repeated characters.


시스템 설계 및 C++ 동시성

프로세스와 스레드의 차이점을 설명하세요. 언제 하나를 다른 하나보다 선택해야 할까요?

답변:

프로세스는 자체 메모리 공간을 가진 독립적인 실행 단위인 반면, 스레드는 프로세스 내의 경량 실행 단위로 메모리를 공유합니다. 격리 및 안정성이 중요한 경우 (예: 별도의 애플리케이션) 에는 프로세스를 선택하고, 단일 애플리케이션 내에서 데이터를 공유하고 오버헤드를 줄이기 위해 동시성을 활용할 때는 스레드를 선택합니다.


C++ 에서 뮤텍스 (mutex) 란 무엇이며 레이스 컨디션 (race condition) 을 방지하기 위해 어떻게 사용되나요?

답변:

뮤텍스 (상호 배제) 는 여러 스레드가 공유 리소스에 동시에 접근하는 것을 방지하는 동기화 기본 요소입니다. 스레드는 공유 리소스에 접근하기 전에 뮤텍스를 획득하고 접근 후 해제하여, 한 번에 하나의 스레드만 임계 영역 (critical section) 에 접근할 수 있도록 보장함으로써 레이스 컨디션을 방지합니다.


교착 상태 (deadlock) 가 발생할 수 있는 일반적인 시나리오를 설명하세요. 어떻게 방지할 수 있나요?

답변:

교착 상태는 두 개 이상의 스레드가 무기한 차단되어 각자 다른 스레드가 리소스를 해제하기를 기다리는 상태입니다. 일반적인 시나리오는 두 개의 스레드가 각각 하나의 뮤텍스를 보유하고 다른 뮤텍스를 획득하려고 시도하는 경우입니다. 방지 전략에는 일관된 잠금 순서 지정, std::lock 사용 또는 std::defer_lock과 함께 std::unique_lock 사용 등이 있습니다.


C++ 에서 조건 변수 (condition variable) 란 무엇이며 언제 유용한가요?

답변:

조건 변수는 스레드가 특정 조건이 참이 될 때까지 대기할 수 있도록 합니다. 이는 생산자 - 소비자 패턴이나 한 스레드가 다른 스레드에게 어떤 이벤트가 발생했음을 신호해야 할 때 유용합니다. 스레드는 조건 변수에서 대기하고, 다른 스레드는 조건이 충족되면 일반적으로 뮤텍스와 함께 이들에게 알립니다.


원자성 (atomicity) 의 개념을 설명하세요. C++ 에서 원자적 연산을 어떻게 달성할 수 있나요?

답변:

원자성은 연산이 분할될 수 없으며 완전히 완료되거나 전혀 발생하지 않는 것처럼 보이는 것을 의미합니다. C++ 에서는 기본 데이터 타입에 대해 std::atomic 타입을 사용하거나, 더 복잡한 연산의 경우 뮤텍스로 임계 영역을 보호함으로써 원자적 연산을 달성할 수 있습니다.


C++ 동시성에서 std::futurestd::promise는 무엇에 사용되나요?

답변:

std::promisestd::future 객체에서 검색될 값이나 예외를 설정하는 데 사용됩니다. std::future는 비동기 작업의 결과에 접근하는 방법을 제공합니다. 이 둘은 비동기 통신과 별도의 스레드에서 실행되는 작업의 결과 검색을 가능하게 합니다.


std::asyncstd::thread를 수동으로 생성하는 것에 비해 비동기 작업 실행을 어떻게 단순화하나요?

답변:

std::async는 스레드 생성 (또는 재사용), 실행 및 결과 검색을 자동으로 관리하여 비동기 실행을 단순화합니다. 직접 std::future를 반환하며 잠재적 예외 및 join/detach 로직을 처리하는 반면, std::thread는 이러한 측면의 수동 관리가 필요합니다.


멀티스레드 환경에서 std::shared_ptr와 원시 포인터 사용의 절충점을 논의하세요.

답변:

std::shared_ptr는 자동 메모리 관리 및 스레드 안전 참조 카운팅을 제공하여 메모리 누수 및 댕글링 포인터를 줄입니다. 그러나 참조 카운트 업데이트는 원자적이므로 성능 오버헤드가 발생합니다. 원시 포인터는 더 빠르지만 신중한 수동 메모리 관리가 필요하며 동시 접근 시 뮤텍스로 보호되지 않으면 레이스 컨디션이 발생하기 쉽습니다.


스레드 풀 (thread pool) 이란 무엇이며 시스템 설계에서 왜 유익한가요?

답변:

스레드 풀은 작업을 실행하는 데 재사용될 수 있는 사전 초기화된 스레드 모음입니다. 각 작업에 대해 스레드를 생성하고 파괴하는 오버헤드를 줄이고, 리소스 고갈을 방지하기 위해 동시 스레드 수를 제한하며, 전반적인 시스템 응답성과 처리량을 개선하기 때문에 유익합니다.


고성능 동시 시스템을 설계할 때 캐시 일관성 (cache coherence) 및 잘못된 공유 (false sharing) 와 관련하여 고려해야 할 주요 사항은 무엇인가요?

답변:

캐시 일관성은 모든 프로세서가 메모리에 대한 일관된 뷰를 보도록 보장합니다. 잘못된 공유는 다른 스레드에서 액세스하는 관련 없는 데이터 항목이 동일한 캐시 라인에 있을 때 발생하여 불필요한 캐시 라인 무효화 및 성능 저하를 유발합니다. 설계 고려 사항에는 신중한 데이터 레이아웃 (패딩) 및 가능한 경우 공유 가변 상태 피하기가 포함됩니다.


시나리오 기반 및 디자인 패턴 질문

애플리케이션 전체에서 로거 인스턴스가 하나만 존재하도록 보장하고 쉽게 접근할 수 있도록 로깅 시스템을 설계하려면 어떻게 해야 합니까?

답변:

싱글턴 (Singleton) 디자인 패턴을 사용합니다. 이는 단일 인스턴스를 보장하고 전역 접근 지점을 제공합니다. 생성자를 private 으로 하고 인스턴스를 가져오는 static 메서드를 사용하는 것이 핵심 구성 요소입니다.


옵저버 (Observer) 디자인 패턴이 유익한 시나리오를 설명하세요. C++ 에서 어떻게 구현할 수 있습니까?

답변:

객체의 상태 변경이 여러 종속 객체에 영향을 주어야 하지만, 이들을 직접적으로 결합하지 않으면서 알림을 보내야 할 때 유용합니다. 예를 들어, 데이터 모델 변경에 따라 UI 요소가 업데이트되는 경우입니다. 추상 Subject(게시자) 와 Observer(구독자) 인터페이스를 사용하여 구현하며, Subject는 알림을 받을 Observer 목록을 유지합니다.


공통 데이터 소스에서 다양한 유형의 문서 (예: PDF, HTML, TXT) 를 생성해야 하지만, 각 문서 유형에 대한 생성 로직이 복잡하고 다릅니다. 어떤 디자인 패턴을 사용해야 합니까?

답변:

팩토리 메서드 (Factory Method) 패턴입니다. 객체를 생성하기 위한 인터페이스를 정의하지만, 어떤 클래스를 인스턴스화할지는 서브클래스가 결정하도록 합니다. 이는 클라이언트 코드를 인스턴스화하는 구체적인 클래스와 분리하여 새로운 문서 유형을 쉽게 추가할 수 있도록 합니다.


각 패킷 유형에 대해 특정 처리 로직이 필요한 다양한 유형의 네트워크 패킷 (예: TCP, UDP, ICMP) 을 처리하는 시스템을 어떻게 설계하시겠습니까?

답변:

전략 (Strategy) 패턴입니다. 패킷 처리를 위한 공통 인터페이스를 정의하고, 각 패킷 유형에 대한 구체적인 전략을 구현합니다. 그런 다음 기본 처리 로직은 패킷 유형에 따라 이러한 전략 간에 동적으로 전환하여 유연성과 확장성을 높일 수 있습니다.


현재 애플리케이션의 요구 사항과 일치하지 않는 인터페이스를 제공하는 기존 라이브러리 클래스가 있습니다. 소스 코드를 수정하지 않고 이 클래스를 어떻게 사용할 수 있습니까?

답변:

어댑터 (Adapter) 패턴을 사용합니다. 애플리케이션에서 기대하는 인터페이스를 구현하고 내부적으로 기존 라이브러리 클래스의 인스턴스를 사용하여 두 인터페이스 간의 호출을 변환하는 어댑터 클래스를 만듭니다.


기존 객체의 구조를 변경하지 않고 새로운 기능 (예: 로깅, 보안 검사, 캐싱) 을 추가해야 하는 시나리오를 고려해 보세요. 어떤 패턴이 적합합니까?

답변:

데코레이터 (Decorator) 패턴입니다. 이를 통해 개별 객체에 동작을 동적으로 추가할 수 있으며, 동일한 클래스의 다른 객체 동작에는 영향을 주지 않습니다. 원래 객체를 새 기능을 추가하는 데코레이터 객체로 감쌉니다.


복잡한 GUI 애플리케이션을 구축하고 있습니다. 애플리케이션의 데이터 (모델) 를 프레젠테이션 (뷰) 및 사용자 상호 작용 로직 (컨트롤러) 과 분리하려면 어떻게 해야 합니까?

답변:

모델 - 뷰 - 컨트롤러 (MVC) 패턴을 사용합니다. 모델은 데이터와 비즈니스 로직을 관리하고, 뷰는 데이터를 표시하며, 컨트롤러는 사용자 입력을 처리하고 모델과 뷰를 모두 업데이트합니다. 이러한 분리는 유지보수성과 테스트 용이성을 향상시킵니다.


다형적 동작을 구현하기 위해 함수 포인터보다 가상 함수를 사용하는 것을 선호하는 경우는 언제입니까?

답변:

가상 함수는 클래스 계층 구조 내에서 컴파일 타임 다형성에 선호되며, 객체의 실제 유형에 따라 동적 디스패치를 가능하게 합니다. 함수 포인터는 다른 함수를 호출하는 데 런타임 유연성을 제공하지만, 본질적으로 객체 지향 다형성이나 가상 테이블 조회를 지원하지는 않습니다.


관련 객체 제품군 (예: Windows, Mac 및 Linux 용 다양한 유형의 UI 위젯) 을 구체적인 클래스를 지정하지 않고 생성해야 합니다. 어떤 패턴을 사용하시겠습니까?

답변:

추상 팩토리 (Abstract Factory) 패턴입니다. 관련되거나 종속적인 객체 제품군을 구체적인 클래스를 지정하지 않고 생성하기 위한 인터페이스를 제공합니다. 이를 통해 다른 '팩토리'(예: WindowsWidgetFactory, MacWidgetFactory) 간에 전환하여 플랫폼별 위젯을 생성할 수 있습니다.


객체의 상태가 변경되고 해당 상태에 따라 다른 동작이 필요한 상황을 큰 조건문 없이 어떻게 처리하시겠습니까?

답변:

상태 (State) 패턴입니다. 이를 통해 객체는 내부 상태가 변경될 때 동작을 변경할 수 있습니다. 객체는 클래스가 변경된 것처럼 보입니다. 각 상태는 별도의 클래스로 캡슐화되며, 컨텍스트 객체는 현재 상태 객체로 동작을 위임합니다.


모범 사례, 관용구 및 코드 품질

C++ 의 Rule of Zero, Three, Five, 또는 Six 에 대해 설명하세요.

답변:

Rule of Zero 는 리소스를 직접 관리하지 않는다면 사용자 정의 소멸자, 복사/이동 생성자 또는 복사/이동 대입 연산자를 정의할 필요가 없다고 말합니다. Rule of Three/Five/Six는 리소스를 관리할 때 적용되며, 리소스 소유권을 올바르게 처리하고 이중 해제 또는 메모리 누수와 같은 문제를 방지하기 위해 이러한 특수 멤버 함수 (소멸자, 복사 생성자, 복사 대입, 이동 생성자, 이동 대입 및 선택적으로 기본 생성자) 를 정의해야 합니다.


RAII(Resource Acquisition Is Initialization) 관용구를 설명하고 예시를 제공하세요.

답변:

RAII 는 리소스 획득 (메모리 할당 또는 파일 열기와 같은) 이 객체 초기화에 연결되고, 리소스 해제가 객체 소멸에 연결되는 C++ 프로그래밍 관용구입니다. 이는 예외가 발생하더라도 객체가 범위를 벗어날 때 리소스가 올바르게 해제되도록 보장합니다. std::unique_ptrstd::lock_guard가 일반적인 예입니다.


C++ 에서 const 정확성이 중요한 이유는 무엇인가요?

답변:

const 정확성은 상수 (constant) 로 표시된 객체나 데이터를 수정할 수 없도록 보장하여 코드 안전성, 가독성 및 유지보수성을 향상시킵니다. 이를 통해 컴파일러는 불변성을 강제하고, 의도치 않은 부작용을 방지하며, 더 나은 최적화를 가능하게 합니다. 또한 const 객체를 const 참조를 기대하는 함수에 전달할 수 있도록 합니다.


std::movestd::forward를 사용하는 목적을 설명하세요.

답변:

std::move는 인수를 rvalue 참조로 캐스팅하여 객체의 리소스를 '이동'할 수 있음을 나타내어 이동 의미론을 가능하게 합니다. std::forward는 원본 인수가 rvalue 인지 여부에 따라 인수를 조건부로 rvalue 참조로 캐스팅하여, 일반적으로 템플릿 함수 내에서 완벽 전달 시나리오의 값 범주 (lvalue 또는 rvalue) 를 보존합니다.


std::unique_ptrstd::shared_ptr 중 언제 std::unique_ptr을 선호해야 합니까?

답변:

동적으로 할당된 객체에 대한 독점 소유권이 필요할 때는 std::unique_ptr을 선호하세요. 오버헤드가 최소화되고 단일 소유권을 명확하게 나타냅니다. 여러 소유자가 동일한 리소스를 공유해야 할 때만 std::shared_ptr을 사용하세요. 이는 참조 카운팅 오버헤드가 포함되기 때문입니다.


null 포인터에 대해 NULL 또는 0 대신 nullptr을 사용하는 몇 가지 이점은 무엇인가요?

답변:

nullptr은 모든 포인터 타입으로 암시적으로 변환될 수 있지만 정수 타입으로는 변환될 수 없는 고유한 타입 (std::nullptr_t) 입니다. 이는 포인터가 의도된 경우에 정수를 기대하는 오버로드된 함수를 실수로 호출하는 일반적인 오류를 방지하여, NULL(종종 0 또는 (void*)0임) 또는 0에 비해 타입 안전성과 코드 명확성을 향상시킵니다.


'PIMPL'(Pointer to IMPLementation) 관용구를 설명하세요.

답변:

PIMPL 관용구는 구현 세부 정보를 별도의 동적으로 할당된 객체로 이동시켜 클래스의 구현 세부 정보를 숨깁니다. 이 객체는 private 포인터로 가리킵니다. 이는 컴파일 종속성을 줄이고, 컴파일 시간을 개선하며, 클라이언트 코드를 다시 컴파일하지 않고도 private 구현 변경을 허용합니다. 또한 바이너리 호환성을 유지하는 데 도움이 됩니다.


헤더 파일에서 using namespace std;를 사용하는 것이 일반적으로 좋지 않은 이유는 무엇인가요?

답변:

헤더 파일에서 using namespace std;를 사용하면 해당 헤더를 포함하는 모든 파일에 대해 전역 네임스페이스를 오염시킵니다. 이는 특히 대규모 프로젝트나 라이브러리를 결합할 때 이름 충돌 및 모호성 오류를 유발할 수 있습니다. 이름을 명시적으로 한정하거나 (예: std::vector) 특정 범위 내에서 using 선언을 사용하는 것이 좋습니다 (예: .cpp 파일 또는 함수 내부).


생성자에 explicit 키워드를 사용하는 목적은 무엇인가요?

답변:

explicit 키워드는 단일 인수를 받는 생성자의 타입에서 클래스 타입으로의 암시적 변환을 방지합니다. 이는 의도치 않은 객체 생성 또는 타입 변환을 방지하여 코드를 더 안전하고 예측 가능하게 만듭니다. 예를 들어, explicit MyClass(int)MyClass obj = 5;를 방지하지만 MyClass obj(5);는 허용합니다.


클래스의 복사 또는 이동을 방지하려면 어떻게 해야 합니까?

답변:

클래스의 복사를 방지하려면 복사 생성자와 복사 대입 연산자를 delete로 선언하세요. 이동을 방지하려면 이동 생성자와 이동 대입 연산자를 delete로 선언하세요. 예를 들면 다음과 같습니다: MyClass(const MyClass&) = delete; MyClass& operator=(const MyClass&) = delete;.


요약

인터뷰를 위한 C++ 숙달은 성실한 준비가 보상하는 여정입니다. 이 문서는 일반적인 질문과 통찰력 있는 답변의 기초를 제공하여 핵심 개념, 고급 기능 및 문제 해결 접근 방식에 대해 자신 있게 논의할 수 있는 지식을 갖추도록 했습니다. 인터뷰에서의 성공은 올바른 답변을 아는 것뿐만 아니라 이해도, 열정, 비판적 사고 능력을 보여주는 것임을 기억하십시오.

C++ 의 환경은 끊임없이 진화하고 있으며, 지속적인 학습이 앞서 나가는 열쇠입니다. 이 가이드를 더 깊이 탐구하고, 연습하고, 실제 코딩을 위한 발판으로 삼으십시오. 새로운 도전을 받아들이고, 프로젝트에 기여하며, 기술을 연마하는 것을 멈추지 마십시오. 학습에 대한 헌신은 의심할 여지 없이 소프트웨어 개발에서 성공적이고 만족스러운 경력을 쌓는 길을 열어줄 것입니다.