목차

     

     


     

     

    Chapter 6 상속, 그리고 객체 지향 설계


     

     

    항목 32:  public 상속 모형은 반드시 "is-a(..는 ...의 일종이다)"를 따르도록 만들자

     

    Q. 다음 보기의 설명 중에 틀린 것을 고르십시오. 클래스 D("Derived)를 클래스 B("Base")로부터 public 상속을 통해 파생시켰다고 가정합니다.

    1.  public 상속은 "is-a(..는 ...의 일종이다)"를 의미한다.

    2.  D 타입으로 만들어진 모든 객체는 또한 B 타입의 객체이지만, 그 반대는 되지 않는다.

    3.  D 클래스는 B 클래스보다 더 큰 메모리 공간을 가진다.

    4.  private으로 상속받으면 public와 protected를 private 멤버로 바꿔준다.

    더보기

    답. 3번
    자식 클래스는 부모 클래스에서 상속받은 멤버 변수들을 포함하게 되므로, 부모 클래스의 멤버 변수와 추가로 정의된 자식 클래스의 멤버 변수들을 모두 가질 수 있습니다. 때로는 이로 인해 자식 클래스가 더 많은 메모리를 사용할 수 있습니다. 그러나 상속된 멤버 변수들을 제외하고 자식 클래스에서 새롭게 추가된 변수가 없거나 적은 경우, 자식 클래스의 인스턴스가 부모 클래스의 인스턴스보다 작을 수 있습니다. 

     

    Q. 아래 코드는 Rectangle 클래스와 Rectangle을 상속받는 Square 클래스의 코드입니다

    이때, ‘Square is a Rectangle’ 라는 명제가 들어맞을까요?!

    class Rectangle
    {
    public:
    	virtual void setHeight(int newHeight);
        virtual void setHeight(int newHeight);
        
        virtual int height() const;
        virtual int width() const;
        ...
    };
    
    void make Bigger(Rectangle& r)
    {
    	int oldHeight = r.height();
        
        r.setWidth(r.width() + 10);
        
        assert(r.height() == oldHeight);
    }
    
    class Square : public Rectangle { ... };
    
    Square s;
    ...
    assert(s.width() == s.height());
    
    makeBigger(s);
    
    assert(s.width() == s.height());
    더보기

    정사각형은 직사각형에 포함된다고 생각할 수 있지만

    직사각형 : 가로, 세로 각 두변의 길이가 같은 사각형

    정사각형 : 네 변의 길이가 모두 같은 사각형

    정사각형은 직사각형이다. (Square is a Rectangle)

    하지만!!

    makeBigger() 함수는 가로길이만 10 늘린다. 가로 길이만 바뀌기에 정사각형의 조건에 부합할 수 없고 assert(s.width() == s.height()); 단정문도 부합하지 않는다

    public 상속은 기본 클래스 객체가 가진 모든 것들이 파생 클래스 객체에도 그대로 적용되는 상속이므로 정사각형은 직사각형에게 상속을 받을 수 없다

     

    컴파일 의존성을 줄이는 방법 2가지를 말하고, 간단하게 설명해보아요


     

     

    항목 33:  상속된 이름을 숨기는 일은 피하자

     

    Q. 아래의 코드에서 에러가 발생하는 부분은 어딘지 찾고 설명해보세요.

    class Base{
    public:
    	virtual void mf1() = 0;
    	virtual void mf1(int);
    	virtual void mf2();
    	void mf3();
    	void mf3(double);
    
    private:
    	int x;
    };
    
    class Derived : public Base{
    public:
    	virtual void mfl();
    	void mf3();
    	void mf4();
    };
    
    int main(){
    	Derived d;
    	int x;
    
    	d.mf1();        
    	d.mf1(x);    
    	d.mf2();        
    	d.mf3();       
    	d.mf3(x);
    }
    더보기

    답:  d.mf1(x);  d.mf3(x);

    d.mf1(x);

    • Derived::mf1이 Base::mf1을 가립니다.

    d.mf3(x);

    • Derived::mf3이 Base::mf3을 가립니다.

    https://sig-product-docs.synopsys.com/ko-KR/bundle/coverity-docs/page/extend-sdk/topics/mangled_naming_scheme_cpp.html

    오버로드


     

     

    항목 34:  인터페이스 상속과 구현 상속의 차이를 파악하고 구별하자 

     

    Q. 다음 보기의 설명 중에 틀린 것을 고르십시오. 

    1.  순수 가상 함수를 선언하는 목적은 파생 클래스에게 함수의 인터페이스만을 물려주려는 것이다.

    2.  단순 가상 함수를 선언하는 목적은 파생 클래스로 하여금 함수의 인터페이스뿐만 아니라 그 함수의 기본 구현도 물려받게 하자는 것이다. 

    3.  비가상 함수는 클래스 파생에 상관없는 불변동작과 같기 때문에, 파생 클래스에서 재정의할 수 없다.

    4.  비가상 함수를 선언하는 목적은 파생 클래스가 함수 인터페이스와 더불어 그 함수의 필수적인 구현을 물려받게 하는 것이다.

    5.  가상 함수는 비가상 함수와 달리 비용이 들기 때문에 설계 시 비가상 함수를 우선시 하는게 좋다.

    더보기

    답.  5번

     

     

    Q. 순수 가상 함수를 선언하는 목적과 단순 가상 함수를 선언하는 목적은 어떻게 다를까요?

    더보기

    248~249쪽

    • 단순 가상 함수를 선언하는 목적 : 파생 클래스로 하여금 함수의 인터페이스뿐만 아니라 그 함수의 기본 구현도 물려주기
    • 순수 가상 함수를 선언하는 목적 : 파생 클래스에게 함수의 인터페이스만을 물려주기

     

     

    항목 35:  가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자

     

    Q.  아래 코드에서 사용된 디자인 패턴은 무엇인가요?

    class GameCharacter;
    
    // 체력치 계산에 대한 기본 알고리즘을 구현한 함수
    int defaultHealthCalc(const GameCharacter & gc);
    
    class GameCharacter {
    public:
    	typedef int (*HealthCalcFunc) (const GameCharacter&);
    	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) { }
    	int healthValue() const { return healthFunc(*this); }
    
    private:
    	HealthCalcFunc healthFunc;
    };
    
    class EvilBadGuy : public GameCharacter {
    public:
    	explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf) { /*...*/ }
    };
    
    int main(){
    	int loseHealthQuickly(const GameCharacter&);
    	int loseHealthSlowly(const GameCharacter&);
    
    	EvilBadGuy ebg1(loseHealthQuickly);
    	EvilBadGuy ebg2(loseHealthSlowly);
    };
    class GameCharacter;
    
    class HealthCalcFunc {
    public:
    	virtual int calc(const GameCharacter& gc) const { /* ... */ }
    	// ...
    };
    
    HealthCalcFunc defaultHealthCalc;
    
    class GameCharacter {
    public:
    	explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc) : pHealthCalc(phcf) {}
    	int healthValue() const { return pHealthCalc->calc(*this); }
    
    private:
    	HealthCalcFunc* pHealthCalc;
    };
    더보기

    답.  전략(Strategy) 패턴

    체력치 계산이 구태여 어떤 캐릭터의 일부일 필요가 없습니다.

    각 캐릭터의 생성자에 체력치 계산용 함수 포인터를 넘기게 만들고, 이 함수를 호출해서 실제 계산을 수행합니다.  이렇게하면 게임이 실행되는 도중에 특정 캐릭터에 대한 체력치 계산 함수를 바꿀수 있습니다. 

    ---------------------------------------------------------------------------------------------------------------------------------------

     

    객체의 행동을 바꾸고싶은 경우 직접 변경하는 것이 아닌 전략이라는 캡슐화한 알고리즘을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴입니다.

    • 장점으로는 같은 행동에 대해서 여러 다른 클래스들이 재사용할 수 있게 해준다는 점과, Runtime시 행동을 선택해야하는 일이 발생할 경우 쉽게 행동을 교체할 수 있다는 점입니다. 
    • 단점으로 행동을 일일히 클래스로 구현해야하므로 클래스가 길어질 수 있다는 점입니다.
    //Stategy Pattern의 예시입니다.
    //추상 키워드인 virtual 키워드를 사용해 상위 클래스 (캡슐화한 알고리즘을 컨텍스트 안에서 교체) 선언
    class IWaepon
    {
    	public:
    	virtual void Attack();
    };
    
    class Gun : public IWaepon
    {
    	public:
    	virtual void Attack() override;
    };
    
    class Sword : public IWaepon
    {
    	public:
    	virtual void Attack() override; 
    };
    
    //상속 관계에 있을 때 행위를 인터페이스로 구현해 줌으로써, 런타임 시 행동을 쉽게 교체할 수 있도록함 (Upcast)

     


     

     

     

    항목 36:  상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!

     

    Q. 아래 코드의 실행 결과로 옳은 것은?

    class Base{
    public:
    	virtual void mf() { cout << "Base Class" };
    };
    
    class Derived : public Base{
    public:
    	void mf() { cout << "Derived Class" };
    };
    
    int main(){
    	Derived x;
    
    	Base* pB = &x;
        pB->mf();
        
        Derived* pD = &x;
        pD->mf();
    }

     

    1.  Base Class, Derived Class

    2.  Derived Class, Derived Class

    3.  Derived Class, Base Class

    더보기

    답: 2번.

    pB 및 pD가 진짜로 가리키는 대상은 Derived 타입의 객체입니다.

    참고) 가상함수를 재정의할 때 override를 붙이지 않아도 됩니다. 하지만 override 키워드를 사용하는 것은 좋은 프로그래밍 습관입니다.  

     

     

    Q.  정적 바인딩과 동적 바인딩의 차이점에 대해 이야기 해보아요.

    더보기

    바인딩이란, 프로그램의 어떤 기본 단위가 가질 수 있는 구성요소의 구체적인 값 또는 성격을 확정하는 것을 의미한다. 예를 들어

    int num = 123; //의 경우 컴파일 시간에 어떠한 성격인지 확정할 수 있다.
    

     

    정적 바인딩: 컴파일 시간에 일어나며, 실행 중 변하지 않고 유지됨

    • 즉, 컴파일 시간에 호출될 해당 함수의 주소가 결정되어 바인딩 된다.
    • 실행 파일에 호출할 함수가 위치한 메모리 주소가 이미 확정된 것

    동적 바인딩: 런타임 시간에 일어나며, 프로그램 실행 도중 변경 가능하다.

    • 실행 파일을 만들때 호출할 함수의 메모리 주소가 확정되지 않은 상태를 의미
    • 주소를 저장할 공간을 미리 확보한다. (4byte/8byte) == 포인터…!

    바인딩이란, 프로그램 소스에 쓰인 각종 내부 요소, 이름, 식별지들에 대해 값 혹은 속성을 확정하는 과정을 일컫는다.

     

     


     

     

    항목 37:  어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정
    의하지 말자

     

    Q. 아래 코드의 실행 결과에 대해 설명해보세요. 

    pc->draw(Shape::Red) 와 pr->draw(Shape::Red)가 각각 어떤 함수를 호출하는지 설명해보세요.

    class Shape{
    public:
    	enum ShapeColor { Red, Green, Blue };
        
        virtual void draw(ShapeColor color = Red) const = 0;
    };
    
    class Rectangle : public Shape{
    public:
    	virtual void draw(ShapeColor color = Green) const;
    };
    
    class Circle : public Shape{
    public:
    	virtual void draw(ShapeColor color) const;
    };
    
    int main(){
    	Shape* ps;
        Shape* pc = new Circle;
        Shape* pr = new Rectangle;
        
        pc->draw(Shape::Red);
        pr->draw(Shape::Red);
    }

     

    더보기

    pc->draw(Shape::Red); // Circle::draw(Shape::Red)를 호출.
    pr->draw(Shape::Red); // Rectangle::draw(Shape::Red)를 호출.

     

     

    Q. 매개변수가 ___바인딩으로 설정된 이유는 무엇일까요?

    더보기

    답:  정적 바인딩.

    C++은 런타임 효율이 숨겨져 있습니다.

    함수의 기본 매개변수가 동적으로 바인딩된다면, 프로그램 실행 중에 가상 함수의 기본 매개변수 값을 결정하는 방법을 컴파일러 쪽에서 마련해 주어야합니다. 그럴 경우엔 **속도 유지와 구현 간편성에 무게를 둔 결과**로 정적바인딩으로 설정되게 된 것입니다.

     

     

     

     


     

     

    항목 38:  “has-a(...는 ...를 가짐)” 쪽은 “is-implemented-in-terms-of
    (...는 ...를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자

     

    Q. 다음 보기 중 틀린 것을 고르십시오.

    1.  합성(Composition)은 어떤 타입의 객체들이 그와 다른 타입의 객체들을 포함하고 있을 경우에 성립하는 그 타입들 사이의 관계를 일컫습니다.

    2.  프로그래밍에서 '레이어링(layering), 포함(Containment), 통합(Aggregation), 내장(Embedding)' 용어들을 '합성(Composition)' 대신에 사용하기도 합니다.

    3.  "has-a(...는 ..를 가짐)"과 "is-implemented-in-terms-of(...는 ...를써서 구현됨)"은 모두 합성에 속합니다.

    4.  버퍼, 뮤텍스, 탐색 트리와 같이 순수하게 시스템과 관련된 종류의 객체가 속한 부분은 소프트웨어의 응용 영역에 속합니다.

    5.  객체 합성이 응용 영역의 객체들 사이에서 일어나면 has-a 관계이고 구현 영역에서 일어나면 is-implemented-in-terms-of 관계입니다. 

    더보기

    답:  4번

    사람, 이동수단, 비디오 프레임과 같은 객체는 소프트웨어의 응용 영역(application domain)에 속합니다.

    버퍼, 뮤텍스, 탐색 트리와 같이 순수하게 시스템 구현과 관련된 종류의 객체가 속한 부분은 소프트웨어의 구현 영역에 속합니다.

     

     

     


     

     

     

    항목 39:  private 상속은 심사숙고해서 구사하자

     

    Q.  두 경우에서 알맞은 것을 고르시오 ( 32비트인 경우)

    class Empty
    {
    };
    

    Case A.

    class Base
    {
    private:
    	int x;
    	Empty e;
    }
    

    Case B.

    class Base : public Empty
    {
    private:
    	int x;
    }
    
    1. sizeof(Base) == sizeof(int)
    2. sizeof(Base) > sizeof(int)
    #include <iostream>
    using namespace std;
    
    class Empty
    {
    };
    
    class Base
    {
    private:
    	int x;
    	Empty e;
    };
    
    class Base_ : public Empty
    {
    private:
    	int x;
    };
    
    int main()
    {
    	Base b;
    	Base_ bb;
    
    	cout << "sizeof(Base) : " << sizeof(b) << endl;
    	cout << "sizeof(Base_) : " << sizeof(bb) << endl;
    
    	return 0;
    }
    더보기

    답:  읽기 횟수를 줄이고 싶으면, size

     

     

     

     

     

    Q. private 상속에 관련된 아래 내용 중 틀린 곳을 고르십시오.

    1.  private 상속은 is-a 를 뜻한다.

    2.  클래스 사이의 상속 관계가 private이면 컴파일러는 일반적으로 파생 클래스 객체를 기본 클래스 객체로 변환하지 않는다.

    3.  기본 클래스로부터 물려받은 멤버는 파생 클래스에서 모조리 private 멤버가 된다.

    4.  기본 클래스로부터 private 상속을 통해 파생 클래스를 파생시킨 것은, 기본 클래스에서 쓸 수 있는 기능들 몇 개를 활용할 목적이다. 이 자체로 구현 기법 중 하나이다.

    더보기

    답. 1번

    private 상속의 의미는 is-implemented-in-terms-of 입니다.

     

     

    Q. private 상속의 의미에 대해 설명하세요.

    private 상속을 써야하는 경우와 객체 합성을 써야하는 경우를 구별하여 설명해보세요. 

    더보기

    private 상속은 '구현만 물려받을 수 있다'는 의미를 가집니다. 그렇기 때문에 인터페이스로서의 사용은 할 수 없다는 의미입니다.

     

    할 수 있으면 객체 합성을 사용하고, 꼭 필요한 경우에 private 상속을 하는게 좋습니다. 

    private 상속을 써야하는 상황으로는 ' 비공개 멤버를 접근할 때 '와 ' 가상 함수를 재정의할 경우 '가 있습니다.

     

     


    Q.  private 상속 대신 public 상속에 객체 합성 조합을 사용하면 좋은 점은 무엇이 있을까요?

    더보기

    답:

    • (282쪽) 클래스를 설계하는 데 있어서 파생은 가능하게 하되, 파생 클래스에서 재정의할 수 없도록 설계 차원에서 막고 싶을 때 유용하다
    • (283쪽) 컴파일 의존성을 최소화하고 싶을 때, 규모가 큰 시스템을 만들 때

    ⇒ #include를 클래스 정의에서 선언하지 않아서 의존성 최소화

     

     


     

     

     

     

    항목 40:  다중 상속은 심사숙고해서 사용하자

     

    Q. 다중 상속 중 다이아몬드 상속 구조를 해결하는 방법으로 무엇이 있는지 설명해보세요.

    더보기

    방법#1:

    Base 클래스를 가상 기본 클래스(virtual base class)로 만들고 이를 상속받는 2개의 Derived 클래스에서 가상 상속(virtual inheritance)를 사용하는 방법이 있습니다. 이 방법은 가상 상속의 비용 문제 때문에 가상 기본 클래스에는 데이터를 두지 않는 것이 좋습니다.

     

    방법#2:

    인터페이스 클래스를 만들어 동작에 관련된 함수를 넣고 public 상속, 구현을 돕는(데이터를 넣을) 클래스를 받아 private 상속을 받는 방법이 있습니다.