[말랑 퀴즈] 26/10/02 해답

게시:     수정

카테고리:

태그:

제 말랑말랑 퀴즈 생성기는 이곳에서 확인하실 수 있습니다.

말랑말랑 퀴즈 — 해답지 ✅

날짜: 2026-10-02 문제 수: 6문제 총점: 50.2/60 (84점) ✅ 잘했어요

📊 채점 결과

문항 유형 난이도 결과 점수
Q1 객관식 🔴 어려움 ⭕ 정답 10 / 10
Q2 빈칸 채우기 🟡 보통 🔺 부분 정답 6.7 / 10
Q3 OX 🟡 보통 ⭕ 정답 10 / 10
Q4 객관식 🟡 보통 ⭕ 정답 10 / 10
Q5 서술형 🔴 어려움 🔺 부분 정답 8.5 / 10
Q6 서술형 🟡 보통 · 📌 오답노트 🔺 부분 정답 5 / 10
합계       50.2 / 60

총평

객관식과 OX는 모두 정답이었고, 특히 Q4에서 오답 보기마다 근거를 직접 짚은 점이 좋았습니다. Q5의 make_shared 할당 횟수와 예외 케이스도 정확했습니다. 반면 Q2에서 C++98 auto_ptr과 STL 컨테이너 관계를 놓쳤고, 오답노트로 재출제된 Q6은 이름 가림의 “설계 의도”와 private 상속에서 using이 안 되는 이유를 아직 정확히 잡지 못했습니다.


Q1. 🔴 어려움 — ⭕ 10/10

문제: operator new/operator delete를 직접 작성할 때 지켜야 할 관례에 대한 설명으로 가장 올바른 것은?

  • A. operator new는 가용 메모리가 부족하면 즉시 bad_alloc 예외를 던져야 하며, new 처리자 함수를 호출하는 것은 표준에 어긋난다.
  • B. operator new는 0바이트를 요청받아도 적법한 포인터를 반환해야 하고, 파생 클래스 전용 메모리 할당 시 기본 클래스의 operator new가 호출되어 크기가 안 맞는(더 큰) 요청이 들어올 수 있으므로, 요청 크기가 자신이 예정한 크기와 다르면 표준 operator new를 호출하도록 만들어야 한다. 또한 operator new[]는 원시 메모리 덩어리만 할당해야 하는데, 그 안에 들어갈 객체가 몇 개인지·크기가 얼마인지 알 수 없기 때문이다.
  • C. operator delete는 항상 매개변수로 받은 포인터가 0이 아님을 보장받으므로, null 포인터 검사를 할 필요가 없다.
  • D. 클래스 전용 operator delete를 안전하게 쓰려면, 기본 클래스에 가상 소멸자를 두지 않는 편이 좋다 — 그래야 크기 정보가 더 정확히 전달된다.

📝 내 선택: B. 이 부분은 잘 모르겠다.

정답: B

해설:

operator new가 지켜야 할 네 가지: 반환값이 제대로 되어있을 것 / 메모리 부족 시 new 처리자 호출 / 0바이트 대비책 / 기본 new가 가려지지 않도록 할 것.

  • 메모리 할당 실패 시 바로 예외를 던지지 않는다 — 현재 등록된 new 처리자가 있으면 그것을 호출하고, 처리자가 없을 때만 bad_alloc을 던진다.
  • 0바이트 요청에도 적법한 포인터를 반환해야 한다.
  • 특정 클래스(Base) 전용 operator new는 그 클래스 하나만을 위한 것이라, Derived* p = new Derived;처럼 파생 클래스 객체를 만들 때 크기가 다른(더 큰) 요청이 들어올 수 있다. 그래서 if(size != sizeof(Base)) return ::operator new(size);처럼 “틀린” 크기는 표준 operator new로 넘겨야 한다.
  • operator new[]는 배열 안에 들어갈 객체 개수·타입을 알 수 없고, 요청된 size_t가 실제 객체들을 담기에 필요한 양보다 더 클 수도 있어(배열 원소 개수 정보 저장 공간 등) 원시 메모리 덩어리만 할당해야 한다.
  • operator delete는 null 포인터 검사가 필요하다(if(rawMemory == 0) return;).
  • 가상 소멸자가 없는 기본 클래스에서 파생된 객체를 삭제하면 operator delete에 넘어오는 size_t 크기가 잘못될 수 있다 — 그래서 기본 클래스에 가상 소멸자를 두어야 크기가 정확히 전달된다.

  • A — 반대다. new 처리자를 먼저 호출하고, 처리자가 없을 때만 예외를 던진다.
  • C — operator delete도 null 포인터 검사를 해야 한다.
  • D — 반대다. 가상 소멸자를 두어야 크기 정보가 정확해진다.

🔍 피드백: B를 골랐고 정답입니다. “잘 모르겠다”고 적었지만 명시적인 포기가 아니라 선택 자체는 했으므로 만점입니다. 확신을 가져도 좋습니다. 핵심은 ①0바이트 요청에도 적법한 포인터 반환 ②파생 클래스 때문에 크기가 달라지면 표준 operator new로 위임 ③operator new[]는 원시 메모리만 다룬다는 세 가지입니다. A·C·D는 모두 반대로 뒤집힌 함정입니다.

출처: game_dev/cpp/chapter8/2025-11-09-cpp_8_51.md


Q2. 🟡 보통 — 🔺 6.7/10

문제: 빈칸을 채우시오.

자원 해제를 사람이 직접 delete로 하지 않고, 객체의 ① ___에서 자원을 해제하게 만드는 패턴을 ② ___(이)라고 한다. C++98의 ③ ___는 복사 시 원본을 null로 만들어 유일 소유권을 보장하지만, 이 때문에 내부적으로 복사를 요구하는 ④ ___에는 쓸 수 없다. 참조 카운팅 방식의 ⑤ ___는 이 문제를 해결했지만, ⑥ ___ 참조(예: A가 B를, B가 다시 A를 참조)는 해결하지 못하며, 배열에는 delete []가 필요하므로 이 두 스마트 포인터를 배열에 쓰면 안 된다.

📝 내 답: 소멸자, RAII, unique_ptr, 복사 함수, shared_ptr, 순환

정답:

① 소멸자 ② RAII(Resource Acquisition Is Initialization) ③ auto_ptr ④ STL 컨테이너 ⑤ shared_ptr ⑥ 순환

해설:

자원 해제를 소멸자에서 자동으로 일어나게 만드는 패턴이 RAII다. delete는 사람이 직접 하지 말고 스마트 포인터 같은 객체가 하게 한다.

auto_ptr은 복사할 때 원본을 null로 만들어 유일 소유권을 보장하지만, 이 특이한 복사 동작 때문에 내부적으로 복사를 요구하는 STL 컨테이너에는 쓸 수 없다.

shared_ptr은 참조 카운팅 방식으로, 마지막 포인터가 소멸될 때 자원을 해제한다. 가비지 컬렉션과 비슷하게 동작하지만 순환 참조(A↔B가 서로를 참조)는 해결하지 못한다.

두 스마트 포인터 모두 기본적으로 delete 연산자를 쓰므로, delete []가 필요한 배열에는 쓰면 안 된다.

🔍 피드백: ①②⑤⑥ 4개 정답(각 1.67점)입니다. ③은 unique_ptr가 아니라 auto_ptr입니다 — 문제에 “C++98의”라는 단서가 있었고, unique_ptr는 C++11에서 auto_ptr을 대체한 것이라 복사가 아예 막혀 있어 시기가 맞지 않습니다. ④는 “복사 함수”가 아니라 복사를 요구하는 STL 컨테이너입니다. 문장의 “복사를 요구하는 ○○에는 쓸 수 없다”에서 ○○는 auto_ptr을 넣으려는 대상이어야 한다는 점을 떠올리면 됩니다.

출처: game_dev/cpp/chapter3/2025-06-09-cpp_3_13.md


Q3. 🟡 보통 — ⭕ 10/10

문제: 다음 명제가 참(O)인지 거짓(X)인지 판단하라.

멤버 함수는 클래스의 public 인터페이스라 캡슐화에 영향을 주지 않으므로, clearCache()·clearHistory()·removeCookies()를 순서대로 호출하는 단순 편의 함수는 비멤버 함수보다 멤버 함수로 만드는 것이 캡슐화 측면에서 더 낫다. 다만 프렌드 함수는 private 멤버에 접근할 수 있어 이런 편의 함수로 쓰기에 특히 안전하다.

📝 내 답: X. 멤버 함수는 public 인터페이스면 캡슐화에 영향을 주며, 비멤버 함수로 만드는 것이 캡슐화 측면에서 더 낫다. 프렌드 함수는 private 멤버에 접근할 수 있지만, 안전하진 않다.

정답: X

해설:

멤버 함수는 클래스의 public 인터페이스이므로, 오히려 변경 시 영향 범위가 크고 캡슐화에 영향을 준다. 데이터에 접근할 수 있는 코드가 많아질수록 캡슐화 정도는 떨어진다. clearCache/clearHistory/removeCookies처럼 이미 공개된 인터페이스를 그냥 조합만 하는 기능이라면, 멤버 함수로 둘 이유가 없다 — 비멤버 비프렌드 함수가 캡슐화를 더 잘 지킨다.

프렌드 함수는 안전하지 않다. 프렌드는 private 멤버에도 접근할 수 있어 사실상 멤버 함수와 다를 바 없으며, 캡슐화를 깨기 때문에 최소화해야 할 대상이다.

🔍 피드백: 판단(X, 7점)과 근거(3점) 모두 정확합니다. 명제의 두 군데 오류(멤버 함수가 캡슐화에 무관하다는 주장, 프렌드가 안전하다는 주장)를 각각 짚었습니다. 하나 덧붙이면, 비멤버 비프렌드 함수가 가장 좋은 이유는 private 멤버에 접근할 수 없어서 캡슐화를 해칠 여지가 없기 때문입니다.

출처: game_dev/cpp/chapter4/2025-06-24-cpp_4_23.md


Q4. 🟡 보통 — ⭕ 10/10

문제: TCP 소켓의 송신 버퍼에 대한 설명으로 가장 올바른 것은?

  • A. 송신 버퍼는 크기가 완전히 고정되어 있어 프로그래머가 바꿀 수 없으며, 운영체제가 데이터를 push하고 사용자가 pop한다.
  • B. 송신 버퍼는 FIFO 방식으로 동작하며 사용자가 push(), 운영체제가 pop()한다. 버퍼가 꽉 차면 빈 공간이 생길 때까지 send()가 블로킹되지만, 일반적으로 버퍼가 수천 바이트를 담을 수 있어 s.send("hello")처럼 작은 데이터는 즉시 리턴된다.
  • C. send() 함수는 데이터를 상대방에게 실제로 전달한 뒤에야 리턴되므로, 상대방의 수신 상태와 무관하게 항상 블로킹된다.
  • D. 송신 버퍼가 가득 차도 send()는 즉시 리턴되며, 대신 전송이 실패했다는 에러 코드를 반환한다.

📝 내 답: B

A: 크기가 고정되어있지 않다. C: 수신 상태에 따라 블로킹되는 것이 아니라, 버퍼 용량에 따라 블로킹되었던것으로 기억한다. D: 버퍼가 가득차면 즉시 리턴되지 않고, 블로킹된다.

정답: B

해설:

소켓의 송신 버퍼는 바이트 배열이고, FIFO로 동작하며 크기는 고정이지만 조절 가능하다. 사용자가 push(), 운영체제가 pop()한다. 버퍼가 꽉 차면 빈 공간이 생길 때까지 블로킹되지만, 버퍼가 보통 수천 바이트를 담을 수 있어 s.send("hello") 같은 작은 호출은 즉시 리턴된다.

  • A — 크기는 조절 가능하고, push/pop 주체가 반대다(사용자가 push, OS가 pop).
  • C — send()는 데이터를 송신 버퍼에 넣는 것이며, 버퍼에 공간이 있으면 상대방의 수신 여부와 무관하게 즉시 리턴된다.
  • D — 버퍼가 가득 차면 에러가 아니라 블로킹된다.

🔍 피드백: 정답 B이고, 오답 보기마다 근거를 직접 적은 점이 훌륭합니다. C는 “블로킹 여부는 상대 수신 상태가 아니라 송신 버퍼의 빈 공간에 달렸다”는 설명이 정확합니다. A는 표현을 보강하면 “크기가 고정이 아니다”보다 “기본 크기는 정해져 있지만 조절은 가능하다, 그리고 push/pop 주체가 뒤바뀌었다”가 더 정확합니다. 점수에는 영향이 없습니다.

출처: server/game_server/3/2026-02-03-game_server_3_3.md


Q5. 🔴 어려움 — 🔺 8.5/10

문제: std::make_unique/std::make_shared에 대해 답하시오.

(1) new를 직접 쓰는 것보다 make 함수를 쓰면 좋은 이유 세 가지(코드 중복, 예외 안전성, 효율성)를 각각 한 줄로 설명하시오. (효율성은 make_shared에 한정된 이야기다)

(2) 아래 코드는 왜 자원 누수 위험이 있는가? 컴파일러가 어떤 순서로 코드를 배치할 수 있는지 설명하시오.

processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

(3) make_shared가 메모리 할당을 한 번만 하는 이유는 무엇인가? (new로 직접 만들면 몇 번 할당되는지도 함께 쓰시오.)

(4) make 함수를 쓸 수 없거나 피해야 하는 경우를 두 가지 이상 쓰시오.

📝 내 풀이:

(1) 첫째. new를 사용하면 객체 생성에 타입을 두번 작성해야하지만, make 함수를 쓰면 auto를 통해 한번 작성하면 된다. 둘째. new는 메모리 누수 문제가 터질수 있지만, make_shared는 그렇지 않다. 셋째. 할당 횟수가 줄어든다.

(2) 첫번째 인자 처리 후, 두번째 인자에서 예외가 터지면 누수가 생긴다.

(3) 제어 블록과 객체 생성을 한번에 할당하기 때문이다. new로 만들면 2번 할당된다.

(4) 커스텀 삭제자가 필요한 경우, 중괄호 초기화 리스트가 필요한 경우.

정답:

(1) 코드 중복 방지 / 예외 안전성 / 효율성(make_shared)

  • 코드 중복 방지: new 버전은 타입을 두 번(std::unique_ptr<Widget>과 new Widget) 적지만, make_unique<Widget>()은 한 번만 적으면 된다.
  • 예외 안전성: new로 만든 원시 포인터를 스마트 포인터 생성자에 넘기는 문장은, 그 사이에 다른 인수 평가에서 예외가 나면 원시 포인터가 스마트 포인터로 감싸이기 전에 누수될 수 있다. make 함수는 할당과 스마트 포인터 생성이 한 호출로 묶여 이 위험이 없다.
  • 효율성(make_shared만): new로 직접 만들면 객체와 제어 블록에 대해 메모리 할당이 두 번 일어나지만, make_shared는 한 번에 할당한다.

(2) 인수 평가 순서가 컴파일러 재량이라, new 직후 예외가 나면 스마트 포인터가 아직 없어 누수된다

컴파일러는 "new Widget" 실행 → computePriority() 실행 → shared_ptr 생성자 실행의 순서로 코드를 생성할 수도 있다. 이때 computePriority()(2단계)에서 예외가 발생하면, 이미 할당된 Widget(1단계)은 아직 shared_ptr로 감싸이기 전이라 자원이 샌다. make_shared를 쓰면 할당과 감싸기가 한 호출에서 일어나 이 문제가 없다.

(3) 객체와 제어 블록을 같은 메모리 조각에 한 번에 할당하기 때문

new를 직접 쓰면 Widget 객체를 위한 할당과 shared_ptr의 제어 블록(참조 카운트 등)을 위한 할당이 따로 일어나 총 두 번이다. make_shared는 객체와 제어 블록을 담을 만큼의 메모리를 한 조각으로 한 번에 할당한다.

(4) 사용 불가/비권장 경우 (두 가지 이상)

  • 커스텀 삭제자를 지정해야 할 때 — make 함수는 커스텀 삭제자를 받지 않는다.
  • 중괄호 초기화 리스트를 전달해야 할 때 — make 함수 내부는 괄호로 완벽 전달하므로 중괄호 리스트를 직접 못 넘긴다(우회책: auto + initializer_list 변수를 만들어 넘김).
  • (shared_ptr 한정) 클래스 고유 operator new/delete가 있는 타입 — allocate_shared가 요구하는 크기가 객체 크기와 다르기(제어 블록 포함) 때문에 부적합.
  • (shared_ptr 한정) 큰 객체 + weak_ptr가 오래 살아남는 상황 — 객체와 제어 블록이 한 메모리 조각이라, 마지막 weak_ptr가 없어질 때까지 객체의 메모리도 해제되지 않아 지연이 생길 수 있다.

🔍 피드백: 4개 논점(각 2.5점) 중 (3), (4)는 만점, (1)은 2.0, (2)는 1.5점입니다.

  • (1) 코드 중복과 효율성은 정확합니다. 예외 안전성은 “메모리 누수가 터질 수 있다”에서 한 걸음 더 나가, 어떤 상황에서(인수 평가 중 다른 예외로 스마트 포인터가 만들어지기 전) 누수되는지까지 한 줄로 적어야 완전합니다.
  • (2) 방향은 맞지만 “첫번째 인자 처리 후”가 모호합니다. 정확히는 new Widget 실행 → computePriority() 실행 → shared_ptr 생성자 순으로 컴파일러가 배치할 수 있고(인수 평가 순서는 컴파일러 재량), 2단계에서 예외가 나면 이미 할당된 Widget이 아직 shared_ptr에 담기기 전이라 샌다는 점을 명시해야 합니다.

출처: game_dev/moderncpp/4/2026-09-08-moderncpp_4_21.md


Q6. 🟡 보통 · 📌 오답노트 — 🔺 5/10

문제: 아래 두 클래스를 보고 답하시오.

class Base {
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
};

class Derived : public Base {
public:
    virtual void mf1();
    void mf3();
    void mf4();
};

(1) Derived d; d.mf1(10);과 d.mf3(3.14);는 각각 컴파일되는가? 안 된다면 정확히 왜 안 되는지, 그리고 d.mf2();는 왜 문제없이 호출되는지 설명하시오.

(2) 이런 “이름 가림” 현상이 매개변수 타입이나 가상/비가상 여부와 무관하게 일어나는 이유(설계 의도)는 무엇인가?

(3) Derived가 Base를 public 상속한다고 할 때, mf1과 mf3의 모든 오버로드 버전을 다시 쓸 수 있게 하려면 어떻게 해야 하는가?

(4) 만약 Derived가 Base를 private 상속하고, mf1의 매개변수 없는 버전만 상속받고 싶다면 (3)의 방법을 쓸 수 없다. 왜 안 되며, 대신 무엇을 써야 하는가?

출처: game_dev/cpp/chapter6/2025-10-19-cpp_6_33.md 오답노트: wn-29fd719b · 2026-09-14 최초 오답 · 이번이 2번째 복습

📝 내 풀이:

(1) 이름 가림으로 인해 컴파일되지 않는다. 이는 접근 지시자, 함수 인자, 가상/비가상에 상관하지 않는다. mf2는 부모에만 있는 가상함수이기 때문이다.

(2) vtable에서 인자 정보는 포함되지 않은 채 덮어씌워지기 때문으로 아는데, 설계 의도까지는 모르겠다.

(3) using을 통해 사용할 클래스의 함수를 명시한다.

(4) private이기 때문이며, 전달 함수를 사용한다.

정답:

(1) d.mf1(10), d.mf3(3.14) 둘 다 컴파일 에러 — d.mf2()는 정상

Derived가 mf1, mf3이라는 이름을 선언하는 순간, Base에 있던 같은 이름의 모든 오버로드(mf1(int), mf3(double) 포함)가 통째로 가려진다. 그래서 d.mf1(10), d.mf3(3.14)는 Derived의 유효범위에서 그 이름을 찾자마자 탐색이 멈춰 에러가 난다. 반면 mf2는 Derived에 같은 이름이 없으므로 탐색이 Base까지 이어져 Base::mf2가 정상 호출된다.

(2) 멀리 떨어진 기본 클래스의 오버로드가 의도치 않게 딸려 오는 것을 막기 위해서

라이브러리나 프레임워크를 이용해 파생 클래스를 만들 때, 멀리 떨어진 기본 클래스로부터 오버로드 버전이 뜻하지 않게 상속되는 상황을 막기 위한 설계다. 그래서 매개변수 타입이나 가상/비가상 여부와 무관하게, 이름이 같으면 무조건 가려진다.

(3) using Base::mf1;, using Base::mf3;를 public 영역에 선언

class Derived : public Base {
public:
    using Base::mf1;
    using Base::mf3;

    virtual void mf1();
    void mf3();
    void mf4();
};

using 선언은 그 이름의 모든 오버로드를 한 번에 끌어온다. public 영역에 쓰는 이유는, 기본 클래스의 public 이름은 파생 클래스에서도 public이어야 하기 때문이다.

(4) using 선언은 “이름 전체”를 끌어오므로 일부만 고를 수 없다 — 전달 함수(forwarding function)를 쓴다

using 선언은 그 이름에 해당하는 모든 오버로드를 통째로 끌어오기 때문에, “매개변수 없는 mf1만” 골라서 상속받을 수 없다. 이럴 때는 전달 함수를 직접 작성한다.

class Derived : private Base {
public:
    virtual void mf1() { Base::mf1(); }   // 매개변수 없는 버전만 전달
};

d.mf1()은 되지만 d.mf1(x)는 여전히 에러다 — 딱 필요한 버전만 상속받은 것이다.

🔍 피드백: 4개 논점(각 2.5점) 중 (1) 2.0, (2) 0, (3) 2.0, (4) 1.0점입니다.

  • (1) 컴파일 에러와 “접근 지시자·인자·가상 여부 무관”은 정확합니다. 다만 mf2가 되는 이유는 “가상함수라서”가 아니라 Derived에 mf2라는 이름이 없어 이름 탐색이 Base까지 이어지기 때문입니다. 가상/비가상은 관계없습니다.
  • (2) vtable 설명은 틀렸습니다. 이름 가림은 가상 함수 테이블이 아니라 이름 탐색(유효범위) 규칙이며, 비가상인 mf3에도 똑같이 적용됩니다. 설계 의도는 “멀리 떨어진 기본 클래스의 오버로드가 뜻하지 않게 파생 클래스로 딸려 오는 것을 막기 위해서”입니다.
  • (3) using은 맞습니다. 더 완전하려면 using Base::mf1; using Base::mf3;를 public 영역에 쓴다는 것까지 적어야 합니다.
  • (4) 전달 함수는 맞지만 이유가 다릅니다. private이어서가 아니라, using은 이름 전체(모든 오버로드)를 통째로 끌어오므로 “매개변수 없는 mf1만” 고를 수 없기 때문입니다.

출처: game_dev/cpp/chapter6/2025-10-19-cpp_6_33.md 오답노트: wn-29fd719b · 2026-09-14 최초 오답 · 이번이 2번째 복습


📌 복습 포인트

  • Q2 RAII와 스마트 포인터 — auto_ptr(C++98)는 복사 시 원본이 null이 되어 STL 컨테이너에 못 쓴다는 점 → game_dev/cpp/chapter3/2025-06-09-cpp_3_13.md
  • Q5 make 함수와 예외 안전성 — new Widget → computePriority() → shared_ptr 생성자 순 배치 시 누수되는 과정을 순서대로 설명하기 → game_dev/moderncpp/4/2026-09-08-moderncpp_4_21.md
  • Q6 이름 가림 — vtable이 아니라 이름 탐색 규칙이며, 설계 의도는 먼 기본 클래스의 오버로드 유입 방지, using은 이름 전체를 가져오므로 일부만 고르려면 전달 함수 → game_dev/cpp/chapter6/2025-10-19-cpp_6_33.md

MallangQuiz 카테고리 내 다른 글 보러가기

댓글남기기