객체지향·언어 기본
```
- C와 C++의 차이는 무엇인가요?
기존 답변
C++는 기존 C언어의 절차적 기능에 더하여 객체 지향 및 제네릭 프로그래밍을 결합한 프로그래밍 언어입니다. C++은 C언어의 모든 기능을 사용할 수 있고, 객체 지향 프로그래밍 특성에 따라 코드의 재사용성과 대규모 프로젝트의 확장 및 유지보수에 유리한 기능들을 제공합니다.
평가: C++을 C의 완전한 상위 호환 언어라고 표현하면 엄밀하게는 틀립니다. 객체지향뿐 아니라 템플릿, RAII, 함수 오버로딩 등의 특징도 언급하면 좋습니다. C에서도 대규모 프로젝트 개발이 가능하므로, C++이 무조건 유지보수에 유리하다고 단정하기보다 이를 지원하는 언어 기능이 많다고 표현하는 편이 정확합니다.
모범답안
C는 함수와 데이터를 중심으로 프로그램을 구성하는 절차적 프로그래밍 언어입니다. 반면 C++는 C의 절차적 프로그래밍 방식을 대부분 지원하면서 클래스와 상속을 이용한 객체지향 프로그래밍, 템플릿을 이용한 제네릭 프로그래밍, RAII와 함수 오버로딩 같은 기능을 추가로 제공합니다. 이를 통해 자원의 수명 관리와 타입 추상화, 코드 재사용을 언어 차원에서 지원한다는 차이가 있습니다. 다만 C++은 C와 높은 호환성을 가지지만 완전한 상위 집합은 아닙니다.
2. 객체지향 프로그래밍의 특징을 설명해주세요.
기존 답변
객체 지향 프로그래밍이란 프로그램의 데이터를 객체/클래스 단위로 묶어 객체 간의 관계를 바탕으로 프로그램을 설계해 나가는 방법론입니다. 이런 객체지향의 특징으로는 상속, 다형성, 캡슐화, 추상화가 있습니다. 이러한 특징들을 통해 기능의 책임과 범위를 객체로 제한하기 때문에 기능의 추가나 확장 및 수정에 유리하다는 특징을 가지고 있습니다.
평가: 객체가 데이터뿐 아니라 그 데이터를 다루는 행위까지 함께 가진다는 점을 추가하면 좋습니다. 객체지향이 항상 확장성과 유지보수를 보장하는 것은 아니므로, 적절한 책임 분리가 전제된다고 표현하는 편이 정확합니다.
모범답안
객체지향 프로그래밍은 데이터와 그 데이터를 처리하는 기능을 객체 단위로 묶고, 객체 간의 협력 관계를 통해 프로그램을 구성하는 방법론입니다. 대표적인 특징으로 추상화, 캡슐화, 상속, 다형성이 있습니다. 객체마다 명확한 책임을 부여하고 내부 구현을 감추면 변경의 영향을 제한할 수 있으며, 공통 인터페이스를 통해 구체 타입에 대한 의존성을 줄일 수 있습니다. 다만 이러한 장점은 객체의 책임과 관계를 적절하게 설계했을 때 얻을 수 있습니다.
3. 캡슐화, 상속, 다형성을 각각 설명해주세요.
기존 답변
캡슐화는 클래스 내부의 기능과 데이터를 외부에서 직접적으로 접근하는 것을 제한하고, 객체 상태에 대한 안정성을 보장하는 것입니다. 상속은 기반 클래스의 기능과 데이터를 파생 클래스에서 재사용하는 것이며 다형성은 동일한 인터페이스로 각 객체의 구체 타입에 의존하지 않고 동일하게 다루는 것을 뜻합니다.
평가: 캡슐화는 단순히 접근 제한만이 아니라, 유효한 인터페이스를 통해 객체 상태를 통제하는 개념입니다. 상속은 코드 재사용보다 is-a 관계와 타입 확장에 초점을 맞춰 답하는 편이 좋습니다.
모범답안
캡슐화는 객체의 데이터와 구현을 외부에서 직접 변경하지 못하도록 숨기고, 공개된 인터페이스를 통해서만 상태를 변경하도록 제한하는 것입니다. 이를 통해 객체의 불변 조건을 유지할 수 있습니다.
상속은 기존 타입을 기반으로 새로운 타입을 정의하는 기능으로, 기반 클래스와 파생 클래스 사이의 is-a 관계를 표현합니다.
다형성은 동일한 인터페이스를 통해 서로 다른 구체 타입을 동일한 방식으로 다루면서, 실제 동작은 각 타입에 맞게 수행되도록 하는 특성입니다.
4. 오버로딩과 오버라이딩의 차이는 무엇인가요?
기존 답변
오버로딩과 오버라이딩은 정적 다형성과 동적 다형성을 구현하는 데 사용된다는 차이점이 있습니다. 오버로딩은 동일한 이름의 함수를 다른 매개인자로 선언함으로써 동일한 함수를 다른 방식으로 부르는 컴파일 타임에 이루어지는 정적 다형성입니다. 반대로 오버라이딩은 런타임 간에 기반 클래스의 선언된 함수의 인터페이스로 다루되, 동적 디스패치를 통해 각 파생 클래스에 재정의된 기능을 수행하는 동적 다형성이라고 할 수 있습니다.
평가: 오버라이딩이 항상 동적 다형성인 것은 아니며, 기반 함수가 virtual이고 기반 클래스 포인터나 참조를 통해 호출할 때 동적 디스패치가 발생합니다. 매개변수뿐 아니라 const, 참조 한정자 등도 오버로드 판별에 영향을 줄 수 있습니다.
모범답안
오버로딩은 같은 범위에서 동일한 이름의 함수를 매개변수 타입이나 개수가 다르게 여러 개 선언하는 것입니다. 호출할 함수는 컴파일 타임에 결정되므로 정적 다형성에 해당합니다.
오버라이딩은 파생 클래스가 기반 클래스의 가상 함수를 동일한 시그니처로 재정의하는 것입니다. 이를 기반 클래스 포인터나 참조를 통해 호출하면 실제 객체 타입에 따라 런타임에 호출 함수가 결정되므로 동적 다형성을 구현할 수 있습니다.
5. 정적 다형성과 동적 다형성의 차이는 무엇인가요?
기존 답변
정적 다형성은 컴파일 타임에 이루어지는 다형성으로 대표적으로 오버로딩이 있으며 동적 다형성은 런타임 중에 동적 바인딩을 통해 이루어지는 다형성으로 오버라이딩이 대표적입니다.
평가: 정적 다형성의 예로 함수 오버로딩뿐 아니라 템플릿도 추가할 수 있습니다. 동적 다형성은 정확히는 가상 함수 오버라이딩과 기반 클래스 포인터 또는 참조를 함께 사용해야 합니다. 성능과 유연성 차이도 짧게 언급하면 좋습니다.
모범답안
정적 다형성은 어떤 함수를 호출할지 컴파일 타임에 결정되는 방식입니다. 함수 오버로딩과 템플릿이 대표적이며, 컴파일러가 호출 대상을 알고 있어 인라인 최적화 등에 유리합니다.
동적 다형성은 기반 클래스의 가상 함수를 파생 클래스가 오버라이딩하고, 기반 클래스 포인터나 참조를 통해 호출할 때 실제 객체 타입에 따라 런타임에 호출 대상을 결정하는 방식입니다. 런타임 유연성이 높지만 간접 호출 비용이 발생할 수 있습니다.
6. 가상 함수는 어떻게 동작하나요?
기존 답변
가상함수는 기반 클래서에서 정의된 기능을 파생 클래서스에서 재정의하여 같은 인터페이스를 통해 다른 기능을 구현할 수 있게 해주는 함수입니다. 컴파일 타임 시 가상함수가 선언된 클래스 단위로 생성되는 버추얼 테이블과 객체를 생성할 시 멤버로 할당되는 버추얼 포인터를 통해 런타임 간에 동적 디스패치를 통해 호출할 함수가 바인딩되어 동작합니다.
평가: 개념은 대체로 맞습니다. vtable과 vptr은 C++ 표준이 강제하는 구현 방식은 아니지만 대부분의 컴파일러가 사용하는 일반적인 구현입니다. “런타임에 함수가 바인딩된다”보다는 객체의 vptr을 통해 해당 슬롯의 함수 주소를 간접 호출한다고 설명하면 더 정확합니다. 가상 호출은 기반 클래스 포인터 또는 참조를 통해 호출할 때 의미가 있습니다.
모범답안
가상 함수는 기반 클래스가 공통 인터페이스를 제공하고 파생 클래스가 이를 재정의할 수 있도록 하는 함수입니다. 기반 클래스 포인터나 참조로 가상 함수를 호출하면 실제 객체의 동적 타입에 맞는 함수가 실행됩니다.
일반적인 컴파일러 구현에서는 가상 함수를 가진 클래스마다 가상 함수 주소를 저장한 vtable이 생성되고, 객체 내부에는 해당 테이블을 가리키는 vptr이 저장됩니다. 호출 시 객체의 vptr을 따라 vtable에서 함수 주소를 조회한 뒤 간접 호출하여 동적 디스패치를 수행합니다. 다만 vtable과 vptr은 표준이 직접 규정한 방식이 아니라 일반적인 구현 방식입니다.
7. 가상 함수 테이블과 가상 함수 포인터를 설명해주세요.
기존 답변
가상함수 테이블은 컴파일 타임에 가상함수를 선언한 클래스 별로 가상함수에 대한 정보를 담고 있는 데이터 테이블입니다. 해당 테이블을 통하여 바인딩될 함수를 조회하게 되는데 이때 조회할 테이블의 주소를 가리키고 있는 포인터를 가상함수 포인터라고 합니다.
평가: “가상 함수에 대한 정보”보다 가상 함수의 주소가 슬롯 형태로 저장된 테이블이라고 답하면 명확합니다. 객체마다 vtable 전체를 가지는 것이 아니라, 보통 vtable을 가리키는 vptr만 가집니다. 다중 상속에서는 객체에 vptr이 여러 개 존재할 수 있습니다.
모범답안
vtable은 특정 다형적 클래스의 가상 함수 주소를 슬롯 형태로 저장한 테이블입니다. 파생 클래스가 가상 함수를 오버라이딩하면 해당 클래스의 vtable 슬롯에는 파생 클래스 함수의 주소가 들어갑니다.
vptr은 객체 내부에 저장되어 해당 객체가 사용해야 할 vtable을 가리키는 포인터입니다. 가상 함수 호출 시 vptr을 통해 vtable에 접근하고, 대응되는 슬롯의 함수 주소를 간접 호출합니다. 일반적으로 단일 상속에서는 하나의 vptr이 사용되지만 다중 상속 구조에서는 여러 개가 존재할 수 있습니다.
8. 가상 함수 호출에는 어떤 비용이 발생하나요?
기존 답변
가상 함수 호출을 하게 되면 일반 함수 호출과 달리 가상함수 포인터를 통한 간접 참조와 간접 조회 비용이 발생하게 됩니다. 더하여 각 함수의 멤버에 보이지 않는 가상함수 포인터가 추가가 되기 때문에 메모리의 크기 또한 증가하게 됩니다.
평가: “각 함수의 멤버에 vptr이 추가된다”는 표현은 틀렸습니다. vptr은 함수가 아니라 다형적 객체에 추가됩니다. 직접 호출보다 vptr과 vtable을 통한 간접 호출이 필요합니다. 컴파일러가 실제 호출 대상을 확정하기 어려워 인라인화나 분기 예측 측면에서 불리할 수 있습니다.
모범답안
가상 함수 호출은 일반적인 직접 함수 호출과 달리 객체의 vptr을 통해 vtable에 접근하고, 함수 주소를 조회한 뒤 간접 호출하는 과정이 필요합니다. 따라서 추가적인 메모리 접근과 간접 분기 비용이 발생할 수 있으며, 컴파일러가 호출 대상을 확정하기 어려우면 인라인 최적화가 제한될 수 있습니다.
또한 가상 함수를 가진 다형적 객체에는 일반적으로 vptr이 추가되므로 객체 크기가 포인터 크기만큼 증가할 수 있습니다. 다중 상속 구조에서는 vptr이 여러 개 생길 수도 있습니다.
9. 순수 가상 함수와 추상 클래스는 무엇인가요?
기존 답변
순수 가상 함수는 파생 클래스가 반드시 재정의해야만 하는 가상함수를 뜻합니다. 추상 클래스는 순수 가상 함수가 1개 이상 선언된 클래스를 뜻하며 추상 클래스는 독립적으로 객체화될 수 없으며 반드시 파생클래스의 구체 타입을 통해 생성되어야 합니다. 이러한 추상 클래스를 통해 협업 시 특정 인터페이스를 강제하고, 해당 인터페이스의 호출을 보장할 수 있게 됩니다.
평가: 순수 가상 함수가 있다고 해서 모든 직계 파생 클래스가 반드시 재정의해야 하는 것은 아닙니다. 재정의하지 않은 파생 클래스도 추상 클래스로 남을 뿐입니다. 순수 가상 함수도 별도의 정의를 가질 수 있습니다. “인터페이스의 호출을 보장한다”보다 “구체 클래스가 구현해야 할 계약을 정의한다”가 정확합니다.
모범답안
순수 가상 함수는 virtual 함수() = 0; 형태로 선언하며, 기반 클래스가 인터페이스만 정의하고 구체적인 구현 책임을 파생 클래스에 넘기기 위해 사용합니다.
순수 가상 함수를 하나 이상 가지고 있거나 상속받은 순수 가상 함수를 구현하지 않은 클래스는 추상 클래스가 되며 직접 객체를 생성할 수 없습니다. 파생 클래스가 모든 순수 가상 함수를 구현해야 비로소 구체 클래스로 객체화할 수 있습니다. 이를 통해 여러 구현체가 따라야 할 공통 계약을 정의할 수 있습니다.
10. 인터페이스 역할을 하는 클래스를 어떻게 설계하나요?
기존 답변
C++의 문법 상으론 인터페이스라고 정의되어 있는 문법이 존재하지 않지만, 인터페이스는 추상 클래스를 통해 설계됩니다. 추상 클래스를 통해 파생 클래스에게 동일한 인터페이스를 강요하여 일종의 설계도의 역할을 하게 됩니다. 통상적으로 인터페이스는 내부 멤버 변수를 선언하지 않는 것이 일반적입니다.
평가: 인터페이스 클래스에는 순수 가상 함수와 가상 소멸자를 두는 것이 중요합니다. 복사나 이동을 제한할 필요가 있는지도 고려할 수 있습니다. “인터페이스를 강요한다”보다 “계약을 정의한다”가 면접 표현으로 자연스럽습니다.
모범답안
C++에는 interface 전용 문법이 없으므로 일반적으로 추상 클래스를 이용해 인터페이스를 설계합니다. 공개된 순수 가상 함수만 선언해 파생 클래스가 구현해야 할 계약을 정의하고, 기반 클래스 포인터로 삭제될 가능성을 고려해 가상 소멸자를 선언합니다.
인터페이스는 구현 상태를 가지지 않도록 멤버 변수를 두지 않는 경우가 많으며, 생성자는 외부에서 직접 호출하지 못하도록 protected로 제한할 수도 있습니다.
11. 가상 소멸자가 필요한 이유는 무엇인가요?
기존 답변
가상 소멸자가 필요한 이유는 해당 클래스를 상속받은 파생 클래스의 소멸자가 호출이 되어야 하기 때문입니다. 가상 소멸자를 선언하지 않게 되면, 파생 클래스를 삭제하는 과정에서 기반 클래스의 소멸자가 호출되지 않거나 또는 기반 클래스의 소멸자만 호출되는 등의 상황이 발생하기 때문에 객체 내부에서 관리하고 있는 자원이 안정적으로 소멸되지 않을 위험이 있습니다.
평가: “기반 클래스의 소멸자가 호출되지 않는다”는 부분은 틀렸습니다. 기반 클래스 포인터로 파생 객체를 delete할 때 기반 클래스 소멸자가 가상이 아니면 정의되지 않은 동작입니다. 일반적인 결과로 기반 클래스 소멸자만 호출되어 파생 클래스 자원이 해제되지 않을 수 있습니다. 모든 기반 클래스에 반드시 필요한 것은 아니고, 다형적으로 삭제될 가능성이 있는 기반 클래스에 필요합니다.
모범답안
기반 클래스 포인터를 통해 파생 클래스 객체를 삭제할 가능성이 있다면 기반 클래스의 소멸자를 가상 함수로 선언해야 합니다.
가상 소멸자가 없는데 기반 클래스 포인터로 파생 객체를 delete하면 정의되지 않은 동작이 발생합니다. 일반적으로는 파생 클래스 소멸자가 호출되지 않아 파생 클래스가 관리하던 자원이 해제되지 않을 수 있습니다.
12. 생성자와 소멸자의 호출 순서를 설명해주세요.
기존 답변
기반 클래스의 생성자, 파생 클래스의 생성자, 파생 클래스의 소멸자, 기반 클래스의 소멸자 순으로 진행이 되며 만약 다중 상속을 받은 상황이라면 먼저 선언된 기반 클래스의 생성자가 먼저 호출이 되고, 이후 먼저 선언된 기반 클래스의 소멸자가 나중에 호출이 됩니다.
평가: 멤버 객체의 생성 및 소멸 순서까지 포함하면 더 완성도 높은 답변입니다. 멤버는 생성자 초기화 리스트에 작성된 순서가 아니라 클래스에 선언된 순서대로 생성됩니다. 소멸은 생성의 역순입니다.
모범답안
객체를 생성할 때는 먼저 기반 클래스가 생성되고, 그다음 멤버 객체가 선언된 순서대로 생성된 뒤, 마지막으로 파생 클래스 자신의 생성자 본문이 실행됩니다.
소멸할 때는 반대로 파생 클래스 소멸자 본문이 먼저 실행되고, 멤버 객체가 선언의 역순으로 소멸된 뒤 기반 클래스가 역순으로 소멸됩니다.
다중 상속에서는 상속 목록에 선언된 기반 클래스 순서대로 생성되고 역순으로 소멸됩니다. 생성자 초기화 리스트에 작성한 순서가 아니라 실제 선언 순서가 기준입니다.
13. 생성자에서 가상 함수를 호출하면 어떻게 되나요?
기존 답변
생성자에서 가상함수를 호출하는 것은 지양되어야 하는 형태의 프로그래밍입니다. 이유는 가상 함수를 동적 바인딩하게 해주는 버추얼 포인터의 획득 시점 때문입니다. 만약 기반 클래스의 생성자에서 파생 클래스가 재정의했을 것으로 예상되는 가상함수를 호출하게 된다면 해당 시점에는 기반 클래스의 버추얼 포인터를 기반으로 디스패치가 이루어지기 때문에 기반 클래스의 함수가 호출되고 파생 클래스의 함수가 호출되지 않습니다. 때문에 프로그래머가 의도하지 않은 동작이 일어날 수 있습니다.
평가: 결론은 맞습니다. vptr의 획득 시점보다, 생성 중에는 아직 파생 클래스 부분이 완성되지 않았기 때문에 현재 생성 중인 클래스 기준으로 가상 호출이 제한된다고 설명하는 편이 정확합니다. 순수 가상 함수 호출로 이어지면 심각한 오류가 발생할 수 있습니다.
모범답안
기반 클래스 생성자에서 가상 함수를 호출하더라도 파생 클래스의 오버라이딩 함수는 호출되지 않고, 현재 생성 중인 기반 클래스의 구현이 호출됩니다.
기반 클래스 생성자가 실행되는 시점에는 파생 클래스 부분이 아직 생성되지 않았기 때문에, C++은 완성되지 않은 파생 객체의 함수가 호출되지 않도록 가상 디스패치를 현재 생성 중인 클래스 범위로 제한합니다.
따라서 생성자에서 파생 클래스의 동작을 기대하며 가상 함수를 호출하면 의도와 다른 결과가 발생할 수 있어 지양하는 것이 좋습니다.
14. 소멸자에서 가상 함수를 호출하면 어떻게 되나요?
기존 답변
문제 없습니다.
평가: 틀린 답변입니다. 호출 자체는 가능하지만 생성자와 유사한 문제가 있습니다. 기반 클래스 소멸자가 실행될 때는 파생 클래스 부분이 이미 소멸된 상태이므로 파생 클래스의 오버라이딩 함수가 호출되지 않습니다. 소멸자에서도 가상 함수를 통한 파생 타입 동작을 기대하면 안 됩니다.
모범답안
소멸자에서 가상 함수를 호출하는 것도 가능하지만 주의해야 합니다.
파생 클래스의 소멸자가 먼저 실행된 뒤 기반 클래스 소멸자가 실행되므로, 기반 클래스 소멸자 시점에는 파생 클래스 부분이 이미 소멸한 상태입니다. 따라서 기반 클래스 소멸자에서 가상 함수를 호출해도 파생 클래스의 오버라이딩 함수가 아니라 현재 소멸 중인 기반 클래스의 함수가 호출됩니다.
그러므로 소멸자에서도 파생 클래스의 동작을 기대하고 가상 함수를 호출하는 설계는 피하는 것이 좋습니다.
15. 다중 상속의 장단점은 무엇인가요?
기존 답변
다중 상속을 통해서 여러 형태의 인터페이스를 조합하여 구체 타입의 기능을 다양하게 사용하거나 다양한 형태로 객체를 활용할 수 있게 됩니다. 다만 다중 상속은 동일한 기반 클래스를 상속받은 두개의 파생 클래스를 상속받는, 소위 다이아몬드 상속 등의 행위가 일어나며 불필요한 메모리를 중복으로 소유하거나, 의도하지 않은 동작이 일어날 수 있기 때문에 지양해야 합니다.
평가: 장단점의 방향은 맞습니다. 다중 상속 자체를 무조건 지양한다고 하기보다, 상태를 가진 구현 클래스의 다중 상속은 복잡성이 커지고 순수 인터페이스 다중 상속은 비교적 안전하다고 구분하면 좋습니다. 이름 충돌과 기반 클래스 변환 모호성도 주요 단점입니다.
모범답안
다중 상속은 하나의 파생 클래스가 여러 기반 클래스의 인터페이스나 기능을 동시에 상속받는 방식입니다. 여러 역할을 하나의 타입에 조합할 수 있다는 장점이 있으며, 특히 상태가 없는 순수 인터페이스를 조합할 때 유용합니다.
반면 동일한 이름의 멤버가 여러 기반 클래스에 존재하면 호출이 모호해질 수 있고, 다이아몬드 상속에서는 동일한 최상위 기반 클래스가 중복 포함될 수 있습니다. 또한 객체 구조와 포인터 변환, 생성과 소멸 순서가 복잡해집니다.
따라서 순수 인터페이스의 다중 상속은 사용할 수 있지만, 상태와 구현을 가진 클래스의 다중 상속은 신중하게 사용하는 편이 좋습니다.
16. 가상 상속은 무엇인가요?
기존 답변
모르겠습니다. 아마 중간 상속인 것 같은데?
평가: 가상 함수와는 직접적인 관련이 없습니다. 다이아몬드 상속에서 최상위 기반 클래스가 중복 생성되는 문제를 해결하기 위한 상속 방식입니다. virtual 키워드를 상속 지정자에 사용합니다.
모범답안
가상 상속은 다이아몬드 상속 구조에서 동일한 최상위 기반 클래스가 객체 안에 여러 번 포함되는 문제를 해결하기 위한 기능입니다.
예를 들어 Warrior와 Mage가 모두 Character를 상속하고, BattleMage가 Warrior와 Mage를 다중 상속하면 일반 상속에서는 BattleMage 내부에 Character 부분 객체가 두 개 존재합니다. 이 때문에 멤버 접근이 모호해지고 데이터도 중복됩니다.
중간 클래스들이 Character를 가상 상속하면 최종 객체에는 Character 부분 객체가 하나만 존재합니다.
가상 상속에서는 최종 파생 클래스가 가상 기반 클래스의 생성을 담당하며, 객체 구조와 접근 비용이 복잡해질 수 있으므로 필요한 경우에만 사용합니다.
17. 상속과 컴포지션 중 어떤 것을 선택해야 하나요?
기존 답변
제가 프로젝트를 진행할 때에는 상속보다 컴포지션이 프로그램의 확장성에 더욱 유리한 것을 느꼈습니다. 상속의 경우 파생클래스의 기능이 기반 클래스의 기능에 의존적인 경우가 많았습니다. 반대로 컴포지션은 구현에 의존하지 않고, 인터페이스에 의존하기 때문에 내부 구현을 독립적으로 채워나가며 필요한 상황에 필요한 기능을 구현하는 것이 용이 했습니다. 따라서 저는 컴포지션을 지향하고 상속을 지양하는 프로그래밍을 진행할 것 같습니다.
평가: 프로젝트 경험과 연결한 점은 좋습니다. 컴포지션이 반드시 인터페이스에 의존하는 것은 아닙니다. 구체 객체를 멤버로 포함할 수도 있습니다. 상속은 무조건 지양하는 대상이 아니라 안정적인 is-a 관계와 공통 인터페이스가 있을 때 적절합니다. “상속보다 컴포지션을 우선 고려한다” 정도가 균형 잡힌 답변입니다.
모범답안
두 타입 사이에 명확하고 안정적인 is-a 관계가 있으며, 기반 클래스 인터페이스를 통해 파생 객체를 다형적으로 다뤄야 한다면 상속을 사용합니다.
반면 어떤 객체가 다른 기능을 소유하거나 조합하는 has-a 관계라면 컴포지션을 사용합니다. 컴포지션은 기능을 독립적인 객체로 분리하고 필요에 따라 교체하거나 조합할 수 있어 결합도를 낮추는 데 유리합니다.
프로젝트에서는 상속 계층이 깊어질수록 기반 클래스 변경의 영향이 파생 클래스 전체로 퍼지는 문제를 경험했기 때문에, 기본적으로 컴포지션을 우선 고려하되 명확한 타입 관계와 다형성이 필요한 경우에는 상속을 사용하겠습니다.
18. 객체 슬라이싱이란 무엇인가요?
기존 답변
객체 슬라이싱이란 파생클래스 타입의 객체를 부모 클래스 타입의 변수에 저장하며, 파생 클래스에 선언된 데이터들이 잘려나가는 현상을 뜻합니다. 이를 통해 다형성 구현이 용이하게 실행되지 않을 수 있기 때문에 부모 타입의 변수로 파생클래스를 받기 보다, 일관된 크기의 포인터 타입으로 다형성을 구현하는 것이 권장됩니다.
평가: 객체 슬라이싱은 값 복사 과정에서 파생 클래스 부분이 복사 대상에서 제외되는 현상입니다. 단순히 데이터만 잘리는 것이 아니라 동적 타입 정보도 유지되지 않아 가상 함수 호출도 기반 타입 기준으로 동작합니다. 포인터뿐 아니라 참조도 사용할 수 있습니다.
모범답안
객체 슬라이싱은 파생 클래스 객체를 기반 클래스 객체에 값으로 복사할 때 파생 클래스에 해당하는 데이터와 동작이 잘려 나가고 기반 클래스 부분만 복사되는 현상입니다.
파생 객체를 기반 클래스 객체에 값으로 복사한 경우, 기반 클래스 객체는 독립적인 Base 객체이므로 원래 Derived 객체의 추가 멤버를 가지지 않으며, 가상 함수를 호출해도 Derived의 동적 타입이 유지되지 않습니다.
따라서 다형성을 유지해야 할 때는 기반 클래스 객체를 값으로 저장하지 않고 기반 클래스 포인터나 참조를 사용해야 합니다.
19. this 포인터는 무엇인가요?
기존 답변
this포인터는 현재 기능을 수행하고 있는 객체를 가리키는 포인터입니다. 매개 변수와 동일한 멤버 변수를 선언하여도 컴파일러가 이를 헷갈리지 않는 이유는 this가 생략되어 있기 때문입니다.
평가: 기본 개념은 맞습니다. this는 비정적 멤버 함수에 암묵적으로 전달됩니다. 정적 멤버 함수에는 특정 객체가 없으므로 this 포인터가 없습니다. const 멤버 함수에서는 this의 타입도 const 객체를 가리키는 형태가 됩니다.
모범답안
this 포인터는 비정적 멤버 함수가 호출된 현재 객체의 주소를 가리키는 암묵적인 포인터입니다. 멤버 함수 내부에서 멤버 변수나 다른 멤버 함수를 사용할 때 실제로는 this->멤버 형태로 접근합니다.
매개변수 이름과 멤버 변수 이름이 같은 경우에도 this->member처럼 구분할 수 있습니다.
일반적인 멤버 함수에서 this는 현재 클래스 객체를 가리키는 포인터이며, const 멤버 함수에서는 const 객체를 가리키는 포인터로 취급됩니다. 정적 멤버 함수는 특정 객체에 속해 호출되지 않으므로 this 포인터를 가지지 않습니다.
# 1. 메모리와 객체 수명
1. 스택과 힙의 차이는 무엇인가요?
스택은 지역변수나 매개변수가 저장되는 메모리 공간으로, 실제 작업의 흐름을 보장하는 최소 메모리 영역입니다. 반면에 힙은 사용자가 런타임 중에 원하는 크기의 데이터 공간을 확보하는 메모리 영역으로 동적할당한 데이터들이 올라가게 됩니다. 스택은 함수가 종료되면 메모리가 반환이 되지만 동적할당은 프로그래머가 직접 메모리 해제를 해주어야 메모리 공간이 반환이 된다는 특징이 있습니다.
평가: 핵심 차이는 맞지만, 스택을 “작업의 흐름을 보장하는 최소 메모리 영역”이라고 표현한 부분은 불명확합니다.
모범답안:
스택은 함수 호출 정보, 매개변수, 지역 변수 등이 저장되는 메모리 영역입니다. 함수가 호출되면 필요한 공간이 할당되고 함수가 종료되면 자동으로 반환되므로 할당과 해제가 빠르지만, 사용할 수 있는 크기가 비교적 제한적입니다.
힙은 프로그램 실행 중 필요한 크기의 메모리를 동적으로 할당하는 영역입니다. 스택보다 유연하게 사용할 수 있지만 메모리 할당과 해제 비용이 상대적으로 크며, 직접 수명을 관리하는 경우 해제를 누락하면 메모리 누수가 발생할 수 있습니다.
2. 전역 변수, 정적 변수, 지역 변수는 각각 어디에 저장되나요?
전역 변수와 정적 변수는 데이터 영역에 저장이 되며 지역변수는 스택에 저장됩니다.
평가: 기본 개념은 맞지만, 초기화 여부와 지역 정적 변수의 저장 위치를 구분하면 더 정확합니다.
모범답안:
전역 변수와 정적 변수는 프로그램의 정적 저장 영역에 위치하며 프로그램 시작부터 종료까지 수명이 유지됩니다. 명시적으로 초기화된 변수는 일반적으로 데이터 영역에, 0으로 초기화되거나 초기화되지 않은 변수는 BSS 영역에 저장됩니다.
일반적인 지역 변수와 함수 매개변수는 스택에 저장됩니다. 다만 함수 내부에 선언된 static 지역 변수는 지역 범위를 가지지만 저장 위치와 수명은 전역 변수와 마찬가지로 정적 저장 영역에 속합니다.
3. 메모리 누수는 무엇이며 왜 발생하나요?
메모리 누수는 보통 동적할당이 발생하는 힙 영역에서 주로 발생하게 됩니다. 동적할당을 통해 메모리 공간을 할당하여 데이터를 올려두었으나, 프로그래머가 해당 메모리 공간을 반환하지 않은채로 프로그램을 종료했을 때 발생합니다. 현재 들어서는 OS가 프로그램 종료후 누수를 잡아주고 있지만, 그럼에도 런타임 간에 메모리 공간 낭비를 방지하기 위하여 메모리 누수가 나지 않도록 신경써야 합니다.
평가: 발생 원인은 대체로 맞지만, 메모리 누수는 프로그램 종료 시점이 아니라 실행 중 해제할 수 없는 메모리가 계속 남는 현상입니다.
모범답안:
메모리 누수는 동적으로 할당한 메모리를 더 이상 사용하지 않으면서도 해제하지 못해, 프로그램이 실행되는 동안 해당 메모리가 계속 점유되는 현상입니다.
대표적으로 new로 할당한 메모리를 delete하지 않거나, 할당된 메모리의 주소를 잃어버렸을 때 발생합니다. 운영체제가 프로세스 종료 시 메모리를 회수하더라도, 게임처럼 장시간 실행되는 프로그램에서는 누수가 반복되면 메모리 사용량이 계속 증가해 성능 저하나 종료로 이어질 수 있습니다. 따라서 RAII와 스마트 포인터를 통해 자원 수명을 자동으로 관리하는 것이 좋습니다.
4. 댕글링 포인터란 무엇인가요?
댕글링 포인터란 참조에서 발생하는 상황입니다. 하나의 자원의 주소를 담고 있는 포인터가, 해당 자원이 해제되어 반환되었음에도 메모리 주소를 지속적으로 가리키고 있는 상황을 뜻합니다. 이것을 댕글링 포인터라고 하는데, 이 댕글링 포인터를 통해 메모리에 접근하면 접근 권한이 없는 메모리에 접근했기에 오류가 발생하게 됩니다.
평가: 핵심은 맞지만, 반드시 접근 권한 오류가 발생하는 것은 아니며 정의되지 않은 동작이 발생한다고 표현해야 합니다.
모범답안:
댕글링 포인터는 포인터가 가리키던 객체나 메모리가 이미 소멸하거나 해제됐지만, 포인터 자체는 여전히 기존 주소를 가지고 있는 상태를 말합니다.
예를 들어 동적 메모리를 delete한 뒤 포인터를 그대로 두거나, 함수가 종료된 뒤 지역 변수의 주소를 반환하면 댕글링 포인터가 발생할 수 있습니다. 이 포인터를 역참조하면 이미 유효하지 않은 객체에 접근하므로 정의되지 않은 동작이 발생합니다. 즉시 오류가 날 수도 있지만, 우연히 정상처럼 동작할 수도 있어 더 위험합니다.
5. 와일드 포인터란 무엇인가요?
와일드 포인터는 선언 후에 명시적으로 초기화되지 않아, 쓰레기값을 가리키고 있는 포인터를 뜻합니다. 이떄 와일드 포인터를 통해 메모리에 접근하면 예기치 못한 동작이 발생합니다. 프로그래밍을 처음 시작했을 때, 초기화가 익숙하지 않아 와일드 포인터를 통한 오류를 자주 내었고, 초기화를 습관적으로 하게되는 버릇을 들인 기억이 있습니다.
평가: 정확한 설명이며, 개인 경험까지 연결한 점도 면접에서 활용할 수 있습니다.
모범답안:
와일드 포인터는 선언 후 초기화되지 않아 어떤 주소를 가리키는지 알 수 없는 포인터입니다. 이런 포인터를 역참조하면 임의의 메모리에 접근하게 되어 정의되지 않은 동작이 발생합니다.
따라서 포인터는 선언과 동시에 유효한 주소나 nullptr로 초기화하는 습관이 중요합니다. 저도 초기화하지 않은 포인터로 인해 오류를 겪은 뒤부터 포인터를 선언할 때 항상 초기값을 지정하는 습관을 들였습니다.
6. 얕은 복사와 깊은 복사의 차이는 무엇인가요?
얕은 복사는 객체의 메모리 자체를 복사하는 과정입니다. 이러한 얕은 복사를 수행했을 때 만약 복사하고 있는 객체가 내부에 할당받은 자원을 관리하고 있다면 얕은 복사는 자원을 새로 할당하는 것이 아니라 해당 자원의 주소를 함께 가리키게 됩니다. 이는 두 자원 중 하나가 삭제될 경우 이중해제와 댕글링 포인터를 유발할 수 있습니다. 반면 깊은 복사는 할당받은 자원을 새로 할당받아, 서로 다른 자원을 소유하기 때문에 이중해제의 위험에서 안전한 복사힙니다.
평가: 전체적으로 잘 설명했지만, 얕은 복사를 “객체의 메모리 자체를 복사한다”기보다 멤버 값을 그대로 복사한다고 표현하는 편이 정확합니다.
모범답안:
얕은 복사는 객체의 각 멤버 값을 그대로 복사하는 방식입니다. 포인터 멤버가 있다면 포인터가 가리키는 자원 자체를 복사하는 것이 아니라 주소값만 복사하기 때문에 원본 객체와 복사 객체가 같은 자원을 공유하게 됩니다.
이 상태에서 두 객체가 모두 자원의 소유권을 가진다고 생각하고 해제하면 이중 해제가 발생할 수 있고, 한쪽이 먼저 자원을 해제하면 다른 쪽 포인터가 댕글링 포인터가 될 수 있습니다.
깊은 복사는 복사 대상 객체를 위해 새로운 자원을 별도로 할당하고, 원본 자원의 내용을 복사하는 방식입니다. 따라서 두 객체가 독립적인 자원을 관리할 수 있습니다.
7. 복사 생성자는 언제 호출되나요?
복사 생성자는 새로운 객체를 기존 객체로 초기화활 때 호출됩니다. 대표적으로 기존 객체를 이용해 새로운 객체를 생성하거나, 객체를 함수 인자에 값으로 전달하거나 함수에서 객체를 값으로 반환할 때 호출될 수 있습니다. 다반 함수 반환 과정에서는 복사 생략이나 이동이 적용되어 실제 복사 생성자가 호출되지 않을 수도 있습니다.
평가: 내용은 정확하며, 오탈자만 정리하면 면접 답변으로 충분합니다.
모범답안:
복사 생성자는 새로운 객체를 기존 객체로 초기화할 때 호출됩니다.
대표적으로 기존 객체를 이용해 새로운 객체를 생성하거나, 객체를 함수 인자로 값 전달하거나, 함수에서 객체를 값으로 반환할 때 호출될 수 있습니다. 다만 함수 반환 과정에서는 복사 생략이나 이동이 적용되어 실제 복사 생성자가 호출되지 않을 수도 있습니다.
Object source;
Object copied = source;
Object copied2(source);위 두 경우 모두 새로운 객체를 기존 객체로 초기화하므로 복사 생성자가 호출됩니다.
8. 복사 생성자와 복사 대입 연산자의 차이는 무엇인가요?
복사 생성자는 아직 생성되지 않은 새로운 객체를 초기화할 때 호출되고, 복사 대입 연산자는 생성이 완료된 객체에 다른 객체의 값을 복사할 때 호출됩니다. 따라서 복사 대입 연산자의 경우 복사 생성자와 달리 기존에 보유하고 있던 내부자원을 안전하게 정리한 후에 복사하는 과정이 요구됩니다.
평가: 핵심 차이와 자원 정리 필요성까지 정확하게 설명했습니다.
모범답안:
복사 생성자는 아직 생성되지 않은 새로운 객체를 기존 객체의 값으로 초기화할 때 호출됩니다.
반면 복사 대입 연산자는 이미 생성이 완료된 객체에 다른 객체의 값을 복사할 때 호출됩니다. 따라서 복사 대입 연산자는 대상 객체가 이미 자원을 보유하고 있을 수 있으므로 기존 자원을 안전하게 정리한 뒤 새로운 값을 복사해야 하며, 자기 자신을 대입하는 경우도 고려해야 합니다.
Object source;
Object copied = source; // 복사 생성자
Object target;
target = source; // 복사 대입 연산자9. RAII란 무엇인가요?
RAII는 Resource Accquisition is Initialization의 약자로 자원의 획득과 소멸을 객체의 생성자와 소멸자에 묶는다는 뜻입니다. 이를 통해 객체가 보유한 자원이 객체의 소멸과 함께 안전하게 해제될 수 있도록하는 자원관리 철학입니다. 대표적으로는 스마트 포인터가 있으며, 이전에 언리얼 엔진 소스 코드를 잠깐 공부한 경험이 있는데, 이때 임시 객체를 통해 프로파일링을 진행하는 모습을 보았습니다. 스코프를 두고, 원하는 순간 동안 프로파일링을 하는 것 또한 이러한 RAII를 기반으로 진행한 것이라고 생각합니다.
평가: 개념과 실제 엔진 사례를 잘 연결했으며, 약자 철자와 “획득 시점” 설명만 보완하면 좋습니다.
모범답안:
RAII는 Resource Acquisition Is Initialization의 약자로, 자원의 획득과 해제를 객체의 생성자와 소멸자에 결합하는 C++의 자원 관리 방식입니다.
객체가 생성될 때 메모리, 파일, 락 같은 자원을 획득하고, 객체가 스코프를 벗어나 소멸할 때 해당 자원을 자동으로 해제하도록 설계합니다. 이를 통해 함수가 중간에 반환되거나 예외가 발생하더라도 자원을 안정적으로 정리할 수 있습니다.
대표적인 예로 스마트 포인터와 lock_guard가 있습니다. 언리얼 엔진에서도 스코프에 임시 객체를 생성해 특정 구간의 프로파일링이나 정리 작업을 자동으로 수행하는 형태를 볼 수 있는데, 이 역시 RAII를 활용한 사례라고 볼 수 있습니다.
10. new/delete와 malloc/free의 차이는 무엇인가요?
가장 큰 차이는 생성자와 소멸자 호출에 있습니다. 뉴와 딜리트는 객체지향 프로그래밍 언어인 C++의 연산자이기 때문에 객체의 생성자와 소멸자를 호출하며 말록과 프리는 C언어의 동적할당 함수이기에 생성자와 소멸자를 호출하지 않습니다.
평가: 가장 중요한 차이는 맞지만, 반환 타입과 실패 처리 방식도 함께 언급하면 답변이 더 완성됩니다.
모범답안:
new와 delete는 C++의 연산자로, 메모리 할당과 함께 생성자를 호출하고 해제할 때 소멸자를 호출합니다.
반면 malloc과 free는 C 표준 라이브러리 함수로, 단순히 메모리 공간만 할당하고 해제하며 생성자와 소멸자를 호출하지 않습니다.
또한 new는 요청한 타입의 포인터를 반환하지만 malloc은 void*를 반환하므로 C++에서는 명시적 형 변환이 필요합니다. 기본적으로 new가 실패하면 std::bad_alloc 예외를 발생시키고, malloc이 실패하면 nullptr을 반환합니다.
C++에서는 특별한 이유가 없다면 직접 new와 delete를 사용하는 것보다 컨테이너나 스마트 포인터를 우선 사용하는 것이 좋습니다.
11. 게임에서 매 프레임 동적 할당을 피해야 하는 이유는 무엇인가요?
프레임 떨어짐
평가: 방향은 맞지만, 프레임 저하가 발생하는 구체적인 원인을 설명해야 합니다.
모범답안:
게임에서 매 프레임 동적 할당을 반복하면 메모리 할당자에서 사용 가능한 블록을 탐색하고 관리하는 비용이 계속 발생합니다. 또한 여러 스레드가 동일한 할당자를 사용하면 내부 동기화로 인한 경합이 발생할 수 있습니다.
반복적인 할당과 해제는 메모리 단편화를 유발할 수 있으며, 프레임마다 비용이 일정하지 않아 순간적인 프레임 드롭이나 끊김을 만들 수 있습니다. 게임은 평균 성능뿐 아니라 프레임 시간의 안정성이 중요하기 때문에, 자주 생성되고 제거되는 객체는 오브젝트 풀이나 메모리 풀을 사용하고 필요한 메모리를 미리 확보하는 방식이 유리합니다.
12. 메모리 단편화란 무엇인가요?
메모리 단편화는 크게 내부 단편화와 외부 단편화가 있습니다. 내부 단편화는 메모리 블록을 할당함으로써 생기는 비효율로, 실제 사용하고자 하는 메모리 크기보다 큰 크기의 메모리 블록이 할당되어서 빈 공간이 생기는 것을 뜻합니다. 외부 단편화는 사용하고자 하는 메모리의 공간이 충분히 있음에도 메모리를 할당하지 못하는 것을 뜻하는데요. 단편화라는 말답게 메모리가 이곳저곳에 쪼개져 있어서, 실질적으로 할당하고자 하는 만큼의 메모리를 사용하지 못하게 되는 상황을 뜻합니다.
평가: 내부 단편화와 외부 단편화의 차이를 정확하게 설명했습니다.
모범답안:
메모리 단편화는 메모리의 할당과 해제가 반복되면서 사용 가능한 공간이 비효율적인 형태로 남는 현상입니다.
내부 단편화는 할당자가 요청한 크기보다 큰 메모리 블록을 제공해, 할당된 블록 내부에 사용되지 않는 공간이 생기는 현상입니다.
외부 단편화는 사용 가능한 전체 메모리의 합은 충분하지만 빈 공간이 여러 위치에 작은 조각으로 흩어져 있어, 연속된 큰 메모리 블록을 할당하지 못하는 현상입니다.
게임에서는 다양한 크기의 객체를 지속적으로 할당하고 해제할 때 외부 단편화가 누적될 수 있으므로 메모리 풀이나 고정 크기 블록 할당자를 사용해 완화할 수 있습니다.
13. Rule of Three란 무엇인가요?
룰오브스리는 자원을 관리하는 클래스를 만들 때 반드시 염두에 두어야하는 3가지 멤버함수를 말합니다. 소멸자, 복사 생성자, 이동 생성자를 지칭하며, 이에 각 대입 연산자를 더하여 룰 오브 파이브라고 하기도 합니다. 이는 내부에서 자원을 안전하게 관리하기 위하여 클래스의 위 함수들을 구현할 때 꼭 자원에 대해 염두에 두라는 의미로 사용됩니다.
평가: Rule of Three의 구성 요소를 잘못 설명했으며, 이동 생성자가 아니라 복사 대입 연산자가 포함됩니다.
모범답안:
Rule of Three는 클래스가 직접 자원을 관리하기 때문에 다음 세 가지 특수 멤버 함수 중 하나를 직접 구현해야 한다면, 나머지 함수들도 함께 구현할 필요가 있을 가능성이 높다는 원칙입니다.
세 가지 함수는 다음과 같습니다.
-
소멸자
-
복사 생성자
-
복사 대입 연산자
예를 들어 클래스가 동적 메모리를 직접 소유한다면 소멸자에서 자원을 해제해야 하고, 복사 생성자와 복사 대입 연산자에서는 얕은 복사로 인한 이중 해제를 방지하도록 깊은 복사를 구현해야 합니다.
C++11 이후에는 여기에 이동 생성자와 이동 대입 연산자를 더해 Rule of Five라고 합니다.
14. Rule of Zero란 무엇인가요?
모루겠어요…
평가: 스마트 포인터와 표준 컨테이너를 사용해 자원 관리를 멤버에게 위임한다는 개념을 준비하면 됩니다.
모범답안:
Rule of Zero는 클래스가 자원을 직접 관리하지 않고 스마트 포인터나 표준 컨테이너처럼 이미 RAII를 적용한 타입에 자원 관리를 맡겨, 소멸자와 복사·이동 생성자 및 대입 연산자를 직접 구현하지 않는 설계 원칙입니다.
예를 들어 원시 포인터와 new, delete를 직접 사용하는 대신 std::vector, std::string, std::unique_ptr 등을 멤버로 사용하면 해당 타입들이 자원의 생성과 해제를 관리합니다. 그러면 클래스는 컴파일러가 자동으로 생성하는 특수 멤버 함수만으로도 안전하게 동작할 수 있습니다.
가능하다면 Rule of Three나 Rule of Five를 직접 구현하는 것보다 Rule of Zero를 따르는 것이 코드의 복잡성과 오류 가능성을 줄이는 데 유리합니다.
15. 메모리 정렬이란 무엇인가요?
메모리 정렬이란 cpu가 메모리를 효율적으로 읽을 수 있도록 메모리를 워드 단위로 배치하기 위해 발생하는 정렬입니다. 각 타입에 따른 정렬 규칙이 있으며 정렬 크기의 배수에 해당하는 메모리에 위치함으로써 메모리의 경계에 걸쳐서 데이터가 기록되지 않도록 도와줍니다. 구조체의 경우 가장 큰 멤버 변수를 기준으로 정렬 규칙이 설정됩니다.
평가: 전반적으로 정확하지만, 구조체 정렬 기준은 가장 큰 멤버의 크기가 아니라 가장 엄격한 정렬 요구사항입니다.
모범답안:
메모리 정렬은 CPU가 데이터를 효율적으로 읽고 쓸 수 있도록 각 타입의 데이터를 특정 주소 배수에 배치하는 규칙입니다.
예를 들어 정렬 요구사항이 4바이트인 타입은 일반적으로 4의 배수 주소에 배치됩니다. 데이터가 적절하게 정렬되어 있으면 CPU가 한 번의 메모리 접근으로 읽을 가능성이 높아지고, 일부 아키텍처에서는 정렬되지 않은 접근으로 인한 성능 저하나 오류를 피할 수 있습니다.
구조체의 정렬 요구사항은 일반적으로 멤버 중 가장 엄격한 정렬 요구사항을 기준으로 결정되며, 이를 지키기 위해 멤버 사이와 구조체 끝에 패딩이 삽입될 수 있습니다.
16. 패딩이 발생하는 이유는 무엇인가요?
메모리의 정렬 규칙을 지키기 위하여 구조체의 멤버 사이나 끝에 빈 공간의 바이트가 할당되는 것입니다. 메모리의 정렬 규칙은 구조체 내부의 가장 큰 변수를 기준으로 설정됩니다. 이떄 내부 멤버의 선언 순서가 정렬되지 않았다면 할당된 메모리 블록이 비효율적으로 배치될 수 있습니다.
평가: 핵심은 맞으며, 배열에서 각 구조체 원소의 정렬을 유지하기 위해 끝에도 패딩이 생긴다는 점을 추가하면 좋습니다.
모범답안:
패딩은 구조체의 각 멤버가 자신의 정렬 요구사항에 맞는 주소에 배치되도록 컴파일러가 멤버 사이 또는 구조체 끝에 삽입하는 빈 바이트입니다.
멤버 선언 순서가 정렬 요구사항과 맞지 않으면 중간 패딩이 많이 발생해 구조체 크기가 커질 수 있습니다. 또한 구조체 배열에서 다음 원소도 올바른 정렬 주소에서 시작해야 하므로 구조체 끝에도 패딩이 추가될 수 있습니다.
따라서 작은 타입과 큰 타입을 무작위로 배치하기보다, 일반적으로 정렬 요구사항이 큰 멤버부터 배치하면 패딩을 줄일 수 있습니다. 다만 가독성과 직렬화 규격 등도 함께 고려해야 합니다.
17. 캐시 친화적인 데이터 구조란 무엇인가요?
캐시는 다시 사용할 확률이 높은 데이터를 미리 가져오는 메모리 영역입니다. 따라서 캐시에 사용하고자 하는 데이터가 있는 것을 캐시 히트라고 하며, 이 캐시 히트를 일으킬 가능성이 많은 프로그래밍이 캐시 친화적인 프로그래밍입니다. 메모리의 시간적 지역성과 공간적 지역성을 지켜서 프로그래밍 하는 것이 가장 대표적인 캐시 친화적인 프로그래밍입니다.
평가: 시간적·공간적 지역성을 언급한 점은 정확하지만, 캐시는 데이터를 예측해 가져오기보다 캐시 라인 단위로 주 메모리 데이터를 보관한다고 설명하는 편이 좋습니다.
모범답안:
캐시 친화적인 데이터 구조는 CPU가 처리할 데이터를 캐시에서 높은 확률로 찾을 수 있도록 연속적이고 예측 가능한 형태로 배치한 구조입니다.
CPU는 메모리에서 필요한 데이터만 한 바이트씩 가져오는 것이 아니라 캐시 라인 단위로 주변 데이터까지 함께 가져옵니다. 따라서 배열이나 vector처럼 연속된 메모리를 순차적으로 접근하면 공간적 지역성을 활용할 수 있습니다.
또한 최근에 사용한 데이터를 가까운 시점에 다시 사용하는 방식은 시간적 지역성을 높입니다. 반대로 연결 리스트처럼 노드가 힙의 여러 위치에 흩어져 있으면 포인터를 따라갈 때마다 캐시 미스가 발생할 가능성이 높습니다.
게임에서는 많은 객체의 위치나 상태를 반복적으로 갱신하므로, 필요한 데이터를 연속된 메모리에 배치하고 불필요한 데이터를 함께 읽지 않도록 구성하는 것이 캐시 효율에 중요합니다.
2. 포인터·참조·const
-
포인터와 참조의 차이는 무엇인가요?
-
nullptr과NULL의 차이는 무엇인가요? -
const int*,int* const,const int* const의 차이는 무엇인가요? -
const 멤버 함수란 무엇인가요?
-
포인터나 참조를 반환할 때 무엇을 주의해야 하나요?
-
지역 변수의 주소나 참조를 반환하면 왜 안 되나요?
-
다운캐스팅이 위험한 이유는 무엇인가요?
-
static_cast와dynamic_cast의 차이는 무엇인가요? -
이중 포인터는 언제 사용하나요?
-
mutable키워드는 무엇인가요? -
reinterpret_cast는 어떤 경우에 사용하나요? -
const_cast는 어떤 경우에 사용하나요? -
dynamic_cast를 사용하기 위한 조건은 무엇인가요?
캐스팅 질문은 네 가지를 모두 암기하기보다 다음 정도를 확실히 구분해야 합니다.
static_cast: 컴파일 타임에 명시적 타입 변환dynamic_cast: 다형적 타입의 런타임 안전 검사reinterpret_cast: 비트나 주소 관점의 저수준 변환const_cast: const 속성 추가 또는 제거
3. 스마트 포인터
최우선
- 스마트 포인터를 사용하는 이유는 무엇인가요?
unique_ptr의 특징은 무엇인가요?shared_ptr은 어떻게 객체 수명을 관리하나요?weak_ptr은 왜 필요한가요?shared_ptr의 순환 참조 문제를 설명해주세요.make_unique와make_shared를 사용하는 이유는 무엇인가요?- 게임 엔진에서 모든 객체를
shared_ptr로 관리하면 어떤 문제가 발생하나요?
추가 준비
shared_ptr의 참조 카운트는 스레드 안전한가요?shared_ptr이 관리하는 객체 자체도 스레드 안전한가요?enable_shared_from_this는 왜 사용하나요?
신입 기준으로는 enable_shared_from_this까지 깊게 물을 가능성은 높지 않습니다. 다만 shared_ptr(this)를 새로 만들면 별도의 제어 블록이 생겨 이중 해제가 발생할 수 있다는 정도를 알면 충분합니다.
4. 복사와 이동 의미론
최우선
- lvalue와 rvalue의 차이는 무엇인가요?
- 이동 생성자는 무엇인가요?
- 이동 대입 연산자는 무엇인가요?
- 복사와 이동의 차이는 무엇인가요?
std::move는 실제로 객체를 이동시키나요?- 이동된 객체는 어떤 상태가 되나요?
- vector에 객체를 넣을 때 복사와 이동은 언제 발생하나요?
추가 준비
- 복사 생략이란 무엇인가요?
- RVO란 무엇인가요?
우선순위 낮음
std::forward- 완벽한 전달
완벽한 전달은 템플릿 라이브러리나 엔진 코어 직무에서는 나올 수 있지만 일반 신입 클라이언트 면접에서는 우선순위가 낮습니다.
5. 템플릿
최우선
- 템플릿이란 무엇인가요?
- 함수 템플릿과 클래스 템플릿의 차이는 무엇인가요?
- 템플릿의 장단점은 무엇인가요?
- 템플릿을 이용한 컴파일 타임 다형성이란 무엇인가요?
추가 준비
- 템플릿 특수화란 무엇인가요?
constexpr란 무엇인가요?auto와decltype의 차이는 무엇인가요?
제외 권장
- CRTP
- SFINAE
해당 항목은 신입 게임 클라이언트 면접의 기본 질문이라기보다 고급 C++ 질문에 가깝습니다. 본인이 실제 프로젝트에서 사용하지 않았다면 우선 제외해도 됩니다.
6. 컴파일과 링크
최우선
- C++ 소스 코드가 실행 파일이 되는 과정을 설명해주세요.
- 전처리, 컴파일, 링크 단계는 각각 무엇을 하나요?
- 헤더 파일과 소스 파일을 분리하는 이유는 무엇인가요?
- 헤더 가드가 필요한 이유는 무엇인가요?
#pragma once는 무엇인가요?- 전방 선언을 사용하는 이유는 무엇인가요?
- 전방 선언만으로 해결할 수 없는 경우는 언제인가요?
- 컴파일 오류와 링커 오류의 차이는 무엇인가요?
unresolved external symbol오류는 왜 발생하나요?- 정적 라이브러리와 동적 라이브러리의 차이는 무엇인가요?
- DLL이란 무엇인가요?
추가 준비
- ODR이란 무엇인가요?
inline함수란 무엇인가요?inline키워드를 붙이면 반드시 인라인화되나요?__declspec(dllexport)와__declspec(dllimport)는 왜 필요한가요?