14. Graphics Class Design

14.1. Design Principles

그래픽 클래스를 어떻게 설계할 것인가?

14.1.1. Types

먼저 필요한 그래픽의 타입을 정의해야 한다.

14.1.2. Operations

그 뒤에는 그에 적용할 수 있는 동작들을 정의해야 한다.

14.1.3. Naming

그 동작들을 함수로서 적절하게 이름지어야 한다.

14.1.4. Mutability

오브젝트의 변형 가능성은 함수에서 중요한 고려 요소이다.

14.2. Shape

Shape는 그 자체로는 추상적인 개념이지만 모든 도형들의 기본이 되는 기반 클래스이다.

class Shape {       // deals with color and style and holds sequence of lines
public:
    void draw() const;                // deal with color and draw lines
    virtual void move(int dx, int dy);         // move the shape +=dx and +=dy

    void set_color(Color col);
    Color color() const;

    void set_style(Line_style sty);
    Line_style style() const;

    void set_fill_color(Color col);
    Color fill_color() const;

    Point point(int i) const;              // read-only access to points
    int number_of_points() const;

    Shape(const Shape&) = delete;    // prevent copying
    Shape& operator=(const Shape&) = delete;

    virtual ~Shape() { }
protected:
    Shape() { }
    Shape(std::initializer_list<Point> lst);        // add() the Points to this Shape

    virtual void draw_lines() const;     // draw the appropriate lines
    void add(Point p);                // add p to points
    void set_point(int i, Point p);         // points[i]=p;
private:
    std::vector<Point> points;                // not used by all shapes
    Color lcolor {fl_color()};         // color for lines and characters (with default)
    Line_style ls {0};
    Color fcolor {Color::invisible};     // fill color
};

Shape::Shape(std::initializer_list<Point> lst) {
    for (Point p : list) add(p);
}

14.2.1. An abstract class

Shape의 생성자를 protected:로 정의하면 Shape는 파생 클래스를 통해서만 생성될 수 있게 할 수 있다. 기반 클래스의 소멸자는 virtual로 정의하라.

14.2.2. Access control

기반 클래스의 데이터 멤버들은 기본적으로 private로 두고, 공개 범위는 파생 클래스에서 정한다. 이를 접근하고 수정하는 getter와 setter는 헤더 내에 구현해서 인라이닝되게 해야 한다.

void Shape::set_color(Color col) {
    lcolor = col;
}

Color Shape::color() const {
    return lcolor;
}

void Shape::set_point(int i, Point p) {      // not used; not necessary so far
    points[i] = p;
}

Point Shape::point(int i) const {
    return points[i];
}

int Shape::number_of_points() const {
    return points.size();
}

14.2.3. Drawing shapes

도형을 그릴 때에는 Shape의 draw를 상속해서 각각의 도형에 맞게 함수를 구현한다.

struct Shape {
    // . . .
    virtual void draw_lines() const;        // let each derived class define its
                                // own draw_lines() if it so chooses
    // . . .
};

struct Circle : Shape {
    // . . .
    void draw_lines() const;        // “override” Shape::draw_lines()
    // . . .
};

14.2.4. Copying and mutability

Shape 클래스는 기반 클래스이므로 복사 생성자와 복사 대입 연산자를 제거한다.

Shape(const Shape&) = delete;      // prevent copying
Shape& operator=(const Shape&) = delete;

14.3. Base and derived classes

C++ 객체지향 프로그래밍에서는 3가지 핵심 개념이 있다.

  • 파생 : 클래스로부터 다른 클래스를 생성하는 것.
  • 가상 함수 : 기반 클래스의 같은 이름을 가진 함수를 호출함으로써 실제로는 파생 클래스의 함수를 호출하는 것.
  • 프라이빗, 프로텍티드 멤버 : 세부 구현을 노출시키지 않고 캡슐화하는 것.

이런 클래스 간 파생 관계를 클래스 계층이라 한다.

14.3.1. Object layout

클래스가 파생될 때에는 메모리상에서는 기반 클래스의 멤버들 뒤에 파생 클래스의 멤버들이 위치한다. 가상 함수 호출을 수행할 때에는 가상 함수들이 할당된 가상 함수 테이블을 거쳐서 호출된다(오버라이딩). 가상 함수 테이블은 오브젝트마다 생성되는 것이 아니라 클래스별당 하나씩 생성되므로 메모리 소모가 크게 더 커지지는 않는다.

14.3.2. Deriving classes and defining virtual functions

보통은 public:으로 기반 클래스로부터 파생한다. private 파생도 있지만 지금은 다루지 않는다. 가상 함수는 클래스 선언부에서 virtual로 선언되어야 하지만 그 밖에서는 virtual 키워드는 필요 없고 쓸 수도 없다.

class Circle : public Shape { public: /* . . . */ };

14.3.3. Overriding

가상 함수를 오버라이딩할 때에는 이름과 타입이 전부 일치해야 한다.

struct B {
    virtual void f() const { cout << "B::f "; }
    void g() const { cout << "B::g "; }         // not virtual
};

struct D : B {
    void f() const { cout << "D::f "; }         // overrides B::f
    void g() { cout << "D::g "; }
};

struct DD : D {
    void f() { cout << "DD::f "; }          // doesn’t override D::f (not const)
    void g() const { cout << "DD::g "; }
};

override 키워드를 붙여서 오버라이딩을 검사할 수 있다.

struct B {
    virtual void f() const { cout << "B::f "; }
    void g() const { cout << "B::g "; }          // not virtual
};

struct D : B {
    void f() const override { cout << "D::f "; }      // overrides B::f
    void g() override { cout << "D::g "; }    // error: no virtual B::g to override
};

struct DD : D {
    void f() override { cout << "DD::f "; }        // error: doesn’t override
                                            // D::f (not const)
    void g() const override { cout << "DD::g "; }       // error: no virtual D::g
                                           // to override
};

14.3.4. Access

C++에서는 클래스 멤버에 대한 3가지 접근 범위가 존재한다.

  • private : 선언된 클래스의 멤버를 통해서만 접근 가능
  • public : 모든 함수를 통해 접근 가능
  • protected : 선언된 클래스 및 파생된 클래스의 멤버를 통해서만 접근 가능

14.3.5. Pure virtual functions

클래스가 파생 클래스를 제외한 방식으로 아예 생성되지 않게 하려면 순수 가상 함수를 두면 된다. 이를 추상 클래스라 한다. 순수 가상 함수를 오버라이딩하지 않은 파생 클래스는 여전히 추상 클래스이다.

class B {                    // abstract base class
public:
    virtual void f() =0;         // pure virtual function
    virtual void g() =0;
};

B b;                   // error: B is abstract

class D2 : public B {
public:
    void f() override;
    // no g()
};

D2 d2;          // error: D2 is (still) abstract

class D3 : public D2 {
public:
    void g() override;
};

D3 d3;          // OK

14.4. Benefits of object-oriented programming

클래스 계층을 둠으로써 우리는 다음의 이점을 얻는다.

  • 인터페이스 상속 : 기반 클래스를 받는 함수가 파생 클래스의 객체를 받을 수 있음.
  • 구현 상속 : 파생 클래스의 함수를 호출할 때 기반 클래스 함수에서 만들어 둔 것들을 실행할 수 있음.

구현 상속을 할 경우 기반 클래스를 바꾸면 해당되는 파생 클래스가 전부 재컴파일되어야 한다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중