[C/C++] 모의 면접 질문 리스트

_ dfdeer·2025년 8월 28일

모의 면접

목록 보기
2/5

1. C / C++

C: 절차지향 언어, 함수 중심, 구조체로 데이터 구조화 가능.
C++: 객체지향 언어, 클래스, 상속, 다형성, 캡슐화, 템플릿 제공, C의 기능 대부분 포함.

C와 C++ 은 각자 절차 중심, 객체 중심으로 지향하는 방향이 다르다.
추가로 C++은 함수/연산자 오버로딩, 네임스페이스, 예외 처리 등 더욱 많은 기능이 있다.

또한 메모리 관리 방식에서도 C는 malloc/free, C++은 new/delete 와 같은 기능으로 관리한다.

2. [C++] 클래스 / 구조체

C++ 에서 클래스와 구조체는 둘 다 변수와 함수 멤버를 포함하고 있다.

하지만 차이점으로는 기본적으로 접근 제어가 클래스는 private, 구조체는 public 이다. 게다가 클래스는 상속 다형성 지원이 일반적이기 때문에 구조체와는 다른점이 많다.

즉, 작은 데이터 집합과 단순 구조는 구조체, 캡슐화 및 객체지향은 클래스이다.

3. 포인터와 레퍼런스의 차이점

구분포인터레퍼런스
선언int* pint& p
NULL 가능OX
재할당 가능OX
연산O, 포인터 연산X, 항상 참조 대상 고정
사용*p = 10r = 10

댕글링 포인터

이미 해제된 메모리를 가리키는포인터이다.

ex)

int* p = new int(5);
delete p;
// p는 존재하지만 메모리는 해제됨 -> 댕글링

void 포인터

타입이 없는 포인터로, 어떤 타입이든 가리킬 수 있다.
대신 사용 전에 반드시 타입 캐스팅을 해주어야 한다.

ex)

void* vp;
int x = 10;
vp = &x
int* ip = static_cast<int*>(vp);

4. 동적 할당

런타임에 메모리 크기를 결정하고 할당하는 것이다.
아까 말했던 C와 C++ 의 동적 메모리 관리에서 말했듯이 C는 malloc/free, C++ 은 new/delete 를 통해 관리한다.

상세한 내용은 5번에 적혀있다.

깊은 복사

포인터가 가리키는 실제 데이터까지 새로 복사하는 것이다.

클래스 내 동적 배열 멤버가 있는 경우 필요하다.

얕은 복사

포인터 값만 복사하여 같은 메모리를 참조하는 것이다.

5. malloc/free & new/delete

구분malloc/freenew/delete
타입 검사XO
생성자/소멸자 호출XO
연산자함수연산자
반환 타입void*실제 타입 포인터

6. C++ 캐스트 연산자

static_cast<T>(expr) -> 컴파일 타임 타입 변환
dynamic_cast<T*>(expr) -> 런타임 안전 형변환. 주로 상속 구조
const_cast<T>(expr) -> const 제거/추가
reinterpret_cast<T>(expr) -> 포인터 타입의 비트 단위 변환

캐스트 연산자 사용 경험

static_cast

AActor* 를 특정 캐릭터 클래스 포인터로 변환해야 할 때 같은 계층 구조에서 변환할 때는 static_cast 가 더 명확하고 안전하다는 점 때문에 사용했다.

dynamic_cast

AI 시스템에서 UObject* 또는 AActor* 를 다룰 때, 런타임에 해당 AI 가 Enemy 인지 NPC 인지 구분해야하는 경우가 있었기에, dynamic_cast 를 사용해 잘못된 캐스팅을 방지하고 nullptr 체크로 안전하게 처리했다.

dynamic_cast의 연산 조건

반드시 다형성 클래스에서 사용해야 한다.
캐스팅 실패 시 포인터는 nullptr 을 반환하고, 참조 시에는 예외가 발생한다.

RTTI(Run-Time Type Information)

런타임에 객체의 실제 타입을 확인하는 기능으로, dynamic_cast 와 typeid 사용 시 필요하다.

7. static

전역/지역 변수에 붙이면 프로그램 전체에서 공유되는 저장소를 사용한다.

클래스 멤버 함수/변수에 붙이면 객체마다가 아닌 클래스 단위로 존재한다.

함수 내부에서 붙으면 정적 지역 변수로 함수 호출 시 그 함수가 종료되어도 프로그램이 끝날 때까지 정적 지역 변수는 계속해서 유지된다.

함수가 재호출 되어도 static 변수는 초기화되지 않는다는 뜻이다.

8. const

constant. 상수라는 뜻이다.
변하지않는 수, 즉 수정을 할 수 없게 만들 변수에 붙이는 키워드이다.
포인터와 조합해 사용할 수도 있다.

ex)

const int* p 		// 값 변경 불가
int* const p 		// 포인터 자체 변경 불가
const int* const p  // 둘 다 불가

9. 가상 함수

부모 클래스에서 virtual 키워드를 붇인 멤버 함수를 가상 함수라고 한다.
다형성을 구현하고 런타임에 호출할 함수를 결정하기 위해 C++ 에서 제공하는 기능이다.

런타임에 동일 타입의 포인터나 참조라도 실제 객체의 타입에 따라 다른 함수를 호출하기 위해서 사용된다.

상속 받은 클래스에서 함수를 오버라이드하여 유연하게 동작 변경이 가능해 자주 쓰인다.

동작 원리

동작 원리는 클래스에 vtavle(virtual table) 이라는 가상 테이블이 만들어지고, 가상 함수 호출 시 이 테이블을 통해 다른 함수를 호출하는 방식이다.

소멸자에 virtual 키워드를 붙이는 이유

가상 소멸자는 상속 구조에서 delete 시 자식 객체도 안전하게 소멸하기 위해 사용하는 기능이다.

C++ 에서 기본 소멸자는 정적 바인딩이 되기 때문에 컴파일러가 포인터 타입만 보고 어떤 소멸자를 호출할지 결정한다.
따라서 delete 시 실제 객체가 자식 클래스여도 타입 포인터가 부모 클래스면 그것만 보고 부모 클래스 소멸자만 호출하게 되는 것이다.

그러므로 부모 클래스의 소멸자에 virtual 키워드를 붙여 가상 소멸자로 만들어주면 컴파일러가 자식 소멸자를 호출하고 부모 소멸자를 호출하기 때문에 부모 포인터로 자식 객체를 관리해도 안전하게 메모리가 해제되게 할 수 있다.

순수 가상 함수

함수 선언부 끝에 '= 0' 을 붙여 만든 가상 함수이다.
자식 클래스에서 이 함수를 반드시 구현하도록 강제하려는 목적이다.
즉, 인터페이스 역할을 한다.

자식 클래스가 이 함수를 반드시 오버라이드해야 구체적 객체가 생성이 가능하다.

추상 클래스

만약 이 순수 가상 함수를 가지고 있는 클래스는 추상 클래스가 된다.

추상 클래스는 인스턴스 생성이 불가하고, 포인터나 참조로만 사용이 가능하다.

10. 바인딩

정적 바인딩

컴파일 시점에 함수 호출을 결정한다.

동적 바인딩

런타임 시점에 함수 호출을 결정한다.

ex) 가상 함수

11. 오버로딩과 오버라이딩

오버로딩

같은 이름, 다른 매개변수의 함수를 여러개 만드는 것이다.
컴파일 시점에 어떤 함수를 호출할지 결정하는 정적 바인딩을 사용한다.

ex)

#include <iostream>

using namespace std;

class Print {
public:
	void Show(int x) { cout << int: " << x << endl; }
    void Show(double x) { cout << double: " << x << endl; }
    void Show(int x, int y) { cout << int, int: " << x << ", " << y << endl; }
};

int main() {
	Print p;
    p.Show(5);	   // int: 5
    p.Show(3.14);  // double: 3.14
    p.Show(1, 2);  // int, int: 1, 2
    
    return 0;
}

위의 예시처럼 같은 이름의 함수이지만 다른 매개변수들로 이루어져있다면 그에 맞춰서 컴파일 타임에 어떤 함수를 호출할지 결정하는 것이다.

상속이 필요 없고 정적 바인딩이기에 런타임 비용이 없다.

오버라이딩

상속 구조에서 부모 클래스의 가상 함수를 자식 클래스에서 재정의하는 것이다.
런타임에서 어떤 함수를 호출할지 결정하는 동적 바인딩을 사용한다.

ex)

#include <iostream>

using namespace std;

clss Base {
public:
	virtual void Say() { cout << "Base Say" << endl; }
};

class Derived : public Base {
public:
	void Say() override { cout << "Derived Say" << endl; }
};

int main() {
	Base* b = new Derived();
    b->Say(); // Derived Say (런타임 결정)
    delete b;
    
    return 0;
}

위의 예시를 보면 알겠지만 부모 클래스인 Base 에서 가상 함수로 Say() 를 정의해두었고, 자식 클래스인 Derived 에서 Say() 함수를 재정의하였는데, 이걸 오버라이딩이라고 한다.

override 를 붙이는 것은 컴파일러에게 이 함수를 재정의했다고 알려주는 역할을 할 뿐, 필수는 아니다.

키워드를 붙이지 않아도 오버라이딩은 진행된다.

12. C++ STL

STL 은 재사용 가능한 자료구조와 알고리즘을 제공하는 C++ 라이브러리이다.

주요 구성기능
컨테이너 (Containers)자료를 저장하는 구조
알고리즘 (Algorithms)정렬, 탐색 등
이터레이터 (Iterators)컨테이너 탐색

STL 컨테이너

종류예시특징
연속형vector, array, string메모리 연속, 임의 접근 O
연결형list, forward_list포인터 연결, 삽입/삭제 O, 임의 접근 X
연관형set, map, multiset, multimap키 기반, 자동 정렬, 이진 검색 트리(BST) 기반
해시 기반unordered_set, unordered_map키 기반, 해시 테이블 기반, 평균 O(1) 탐색

Vector / List

항목VectorList
저장 방식메모리 연속노드 + 포인터 연결
임의 접근O(1)X, 순차적 탐색 필요, O(n)
삽입/삭제끝: O(1) amortized, 중간: O(n)어느 위치든 O(1) (노드만 연결 변경)
메모리 사용상대적으로 적음노드 포인터 추가 시 메모리 더 사용

Vector가 임의 접근이 가능한 이유

  • 메모리가 연속적으로 할당되어 있음
  • 포인터 연산으로 v[i] 위치에 바로 접근 가능 -> O(1)
  • 반면 List 는 각 노드가 분리된 메모리 -> 순차적 접근 -> O(n)

vector::push_back() / vector::emplace_back()

구분push_backemplace_back
동작객체를 만들어 복사/이동 후 삽입객체를 바로 생성 후 삽입 (in-place)
효율이동/복사 비용 발생이동/복사 비용 없음 -> 생성자 직접 호출
사용 시점이미 존재하는 객체 삽입생성자 매개변수를 전달하여 직접 삽입

즉, push_back 은 객체 생성 > 복사/이동 > 삽입 순이고
emplace_back 은 백터 내부에서 객체 생성 및 삽입을 하기 때문에 더 효율적이다.

하지만 만약에 이미 생성된 객체를 넣어야 한다면 emplace_back 은 사용할 수 없다.

따라서 상황에 맞게 사용해주어야 한다.

Set / Map

  • Set: 중복 없는 키 저장, 정렬된 상태 유지
  • Map: 키-값 쌍 저장, 키 중복 X, 자동 정렬
  • 구현: Red-Black Tree (Self-balancing BST) -> 삽입/삭제/검색 O(log n)

map / unordered_map

항목mapunordered_map
내부 구조Red-Black Tree(BST)해시 테이블
정렬 여부자동 오름차순 정렬없음
검색/삽입/삭제O(log n)평균 O(1), 최악 O(n)
메모리 사용량상대적으로 적음해시 테이블 + 버킷 (많음)
해시 함수

키를 버킷 인덱스로 변환한다.
좋은 해시 함수는 키가 균등하게 분포되어 충돌을 최소화한다.

탐색 시간 또한 평균적으로 O(1) 을 유지한다.

해시 충돌

두 개 이상의 키가 동일한 버킷에 매핑되는 현상이다.

이를 해결하기 위해서는 동일 버킷에 연결 리스트로 저장하는 체이닝(Chaining) 이나 다른 빈 버킷을 탐색하는 오픈 어드레싱(Open Addressing) 을 사용해야한다.

STL 구현은 보통 체이닝을 사용한다.

벡터 / 리스트 / 맵의 임의 접근, 검색, 중간 삽입 삭제의 각각 시간 복잡도

주요 컨테이너들의 시간 복잡도 표이다.

컨테이너임의 접근검색중간 삽입/삭제
vectorO(1)O(n)O(n)
listX (임의 접근 불가)O(n)O(1)
mapO(log n)O(log n)O(log n)
unordered_map평균 O(1), 최악 O(n)평균 O(1), 최악 O(n)평균 O(1), 최악 O(n)

13. L Value / R Value

C 언어

L-value: 메모리 위치가 있는 값 -> 주소(&)를 가질 수 있음.
R-value: 임시로 계산된 값 -> 주소 없음.

C++ 에서의 값 범주

범주예시특징
lvalue변수 x, *ptr, 배열 요소메모리 주소 있음, 재사용 가능
prvalue42, "hi", 1 + 2임시 값, 메모리 없음
xvaluestd::move(x), std::unique_ptr().release()객체는 있지만 곧 버려질 값
glvaluelvalue + xvalue실제 객체가 있는 값
rvalueprvalue + xvalue오른쪽에만 올 수 있는 값

무브 시맨틱 / std::move()

이전에는 객체를 다른 객체에 대입할 때 복사 말고는 방법이 없었다.
하지만 C++ 11 로 넘어가면서 해결책이 생겨났다.

무브 시맨틱

복사대신 자원의 소유권을 이전하는 최적화 방식이다.
큰 버퍼/핸들(파일, 소켓, 힙 메모리 등)을 복사 없이 포인터만 넘기고 원본은 빈 껍데기 상태로 만든다.

std::move()

std::move(x) 는 이동을 수행하지 않는다.
단지 x 를 rvalue 참조로 캐스팅(xvalue)하여 컴파일러에게 이동이 가능하다고 알리기만 한다.

실제 이동은 무브 생성자/무브 대입 연산자가 수행한다.

즉, std::move() 는 캐스트일 뿐이고, 이동이 가능하다는 신호만 줄 뿐이다.

이동 후 객체의 상태

유효한 상태(소멸자 호출 가능)이지만 값은 미정이다.
컨테이너의 크기를 0, 포인터는 nullptr 등 비어있는 상태로 만들어둔다.

이동된 객체를 파괴, 재할당, 재사용(재초기화) 하는 건 괜찮지만, 내용을 기대하고 사용하면 위험하다.

대입 연산자와 복사 생성자의 호출 시점

객체를 새로 생성하면서 다른 객체로 초기화할 때, 복사 생성자와 무브 생성자를 통해 자원을 옮긴다.

이미 존재하는 객체에 값을 대입할 때는 대입 연산자를 사용한다.

또한 함수 반환과 같이 임시가 쓰이는 경우에는 컴파일러 최적화가 개입할 수 있다.

14. 매크로 / inline

구분매크로 함수inline 함수
처리 시점전처리기 단계 (컴파일 전)컴파일러 단계
동작 방식단순 문자열 치환함수 호출 대신 코드 삽입
타입 검사XO
디버깅어려움용이함
확장성오버로딩, 템플릿 불가오버로딩, 템플릿 가능
최적화항상 치환됨컴파일러가 결정

inline 함수 최적화 부분은 코드가 간단하다면 함수 호출 과정이 사라져서 더 빠르지만,
길고 복잡하다면 컴파일러가 inline 을 무시하고 일반 함수처럼 처리한다.

15. C++ 스마트 포인터

스마트 포인터는 RAII 를 편리하게 적용한 도구이다.

항목특징
unique_ptr단독 소유
shared_ptr공유 소유
weak_ptr비소유 관찰자

기본 원칙으로는 unique_ptr 를 우선 사용하고, 여러 주체가 소유해야 하면 shared_ptr 을 쓰되 순환 참조를 weak_ptr 로 끊는 것이다.

RAII

자원(메모리, 파일, 핸들, 락, GPU 리소스 등)을 객체의 생성자에서 획득하고 소멸자에서 반환하는 패턴이다.

스마트 포인터는 포인터를 RAII 객체로 감싸 예외가 발생해도 자원 해제를 자동으로 해제해 누수를 방지한다.

하지만 직접 쓰기보다는 unique_ptr + 커스텀 삭제자나 std::ifstream 등 표준 RAII 타입을 사용하는 걸 권장하는 듯하다.

unique_ptr

  • 특징

    • 단독 소유(owner). 복사 불가. move만 가능.
    • 가장 가볍고 오버헤드 적음.
    • unique_ptr<T[]]> 로 배열 관리 가능
  • 사용 시기

    • 객체 소유권이 오직 하나일 때.
    • ex) 게임 오브젝트, 컴포넌트 RAII 자원 관리.
  • 예시

auto ptr = std::make_unique<Player>(100, "User");
std::unique_ptr<Player> ptr2 = std::move(ptr); // 소유권 이전

shared_ptr

  • 특징

    • 여러 개가 같은 객체 공유 가능 (참조 카운팅).
    • 마지막 하나가 사라질 때 자동으로 delete.
    • weak_ptr 와 같이 써야 순환 참조 문제 방지 가능.
  • 사용 시기

    • 여러 객체가 같은 데이터를 공유해야 할 때.
    • ex) 그래프, 씬 트리, 캐싱된 리소스 관리.
  • 예시

std::shared_ptr<Texture> tex = std::make_shared<Texture>("grass.png");
std::shared_ptr<Texture> tex2 = tex; // 참조 카운트 +1

weak_ptr

  • 특징

    • shared_ptr 를 쓸 때만 사용 가능.
    • 참조 카운트 증가시키지 않음 (순환 참조 방지).
    • 사용 전 반드시 .lock() 을 통해 살아있는지 확인 필요.
  • 사용 시기

    • 관찰자 패턴(Observer), 캐시, 순환 구조가 생길 수 있는 경우.
  • 예시

struct Node {
    std::shared_ptr<Node> child;
    std::weak_ptr<Node> parent;  // weak_ptr로 선언
};

std::shared_ptr<Node> parent = std::make_shared<Node>();
std::shared_ptr<Node> child = std::make_shared<Node>();

parent->child = child;
child->parent = parent; // 만약 그냥 썼을 경우 순환 참조 → 메모리 해제 안 됨

0개의 댓글