책정리/혼자 연구하는 C,C++ 2

29장 상속

GONII 2015. 2. 28. 20:15

29-1 상속

가. 클래스 확장

상속은 캡슐화, 추상화와 함께 객체 지향 프로그래밍의 중요한 특징 중 하나이다. 캡슐화와 추상화는 객체가 온전한 부품이 될 수 있는 방법을 제공하는데 비해 상속은 클래스를 좀 더 쉽게 만들 수 있는 고수준의 재사용성을 확보하고 클래스간의 계층적인 관계를 구성함으로써 객체 지향의 또 다른 큰 특징인 다형성의 문법적 토대가 된다. 상속을 하는 목적 또는 상속에 의한 효과는 다음 세 가지로 간략하게 요약할 수 있다.

  1. 기존의 클래스를 재활용한다. 가장 기본적인 효과이다.
  2. 공통되는 부분을 상위 클래스에 통합하여 반복을 제거하고 유지, 보수를 편리하게 한다.
  3. 공동의 조상을 가지는 계층을 만듬으로써 객체의 집합에 다형성을 부여한다.

   

상속을 할 때 원본 클래스가 어떤 것이라는 것을 밝히고 이 외에 더 필요한 멤버를 추가로 선언한다. 그러면 컴파일러는 원본 클래스의 모든 멤버에 대한 선언문을 가져오고 추가로 선언한 멤버도 클래스 안에 같이 포함시킨다. 전통적인 방법에 비해 복사해서 붙여 넣고 기존 멤버에 대한 선언문을 가져오는 동작을 컴파일러가 대신하는 것이다.

기존의 클래스의 재활용만을 목적으로 한다면 사실 복사한 후 뜯어 고치는 전통적인 방법과 상속을 하는 방법에 차이점이 없다. 그러나 코드의 유지, 보수 측면에서는 엄청난 차이가 있는데 우너본을 변경해야 할 때 복사한 경우는 양쪽을 다 직접 고쳐야 하지만 상속의 경우는 원본 클래스마 고치면 상속 받은 클래스까지 한꺼번에 같이 수정되어 편리하며 불일치의 위험도 없다.

나. 상속의 예

  • 예제 InheritPoint

#include <iostream>

#include <windows.h>

using namespace std;

   

void gotoxy(int x, int y);

   

class coord

{

protected:

int x, y;

public:

coord(int ax, int ay) : x(ax), y(ay) {}

void GetXY(int &rx, int &ry) const { rx = x; ry =y; }

void SetXY(int ax, int ay) { x = ax; y = ay; }

};

   

class point : public coord

{

protected:

char ch;

public:

point( int ax, int ay, char ach ) : coord(ax, ay) { ch = ach; }

void show()

{

gotoxy(x,y);

cout << ch;

}

void hide()

{

gotoxy(x,y);

cout << ' ' ;

}

};

   

void main()

{

point p( 10, 10, '@');

p.show();

}

void gotoxy(int x, int y)

{

COORD Pos={x,y};

SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),Pos);

}

point 클래스가 coord 클래스로부터 상속을 받은 것이다. 클래스끼리 상속될 때 상위 클래스를 기반 클래스(Base Class)라고 하며 상속을 받는 클래스를 파생 클래스(Derived Class)라고 한다. 이 경우 coord 기반 클래스로부터 point 클래스가 파생되었다고 표현한다. 기반, 파생 이라는 용어 대신 부모, 자식이라는 용어를 대신 사용하기도 하고 상위 클래스(Super Class), 하위 클래스(Sub Class)라는 용어를 쓰기도 한다.

생성자, 소멸자 등의 특수한 몇 가지를 제외하고 파생 클래스는 기반 클래스의 모든 멤버를 상속받는다.

다. 상속과 정보 은폐

클래스가 상속될 때 기반 클래스의 멤버에 대한 액세스 속성이 파생 클래스에게 어떻게 상속되는지 다음 예제를 통해 테스트해보자.

  • 예제 InheritAccess

#include <iostream>

using namespace std;

   

class B

{

private:

int b_pri;

void b_fpri() { cout << "기반클래스 private함수"<< endl; }

protected:

int b_pro;

void b_fpro() { cout << "기반클래스 protected함수"<< endl; }

public:

int b_pub;

void b_fpub() { cout << "기반클래스 public함수"<< endl; }

};

   

class D : public B

{

private:

int d_pri;

void d_fpri() { cout << "파생 클래스 private함수"<< endl; }

public :

void d_fpub()

{

// 자신의 모든 멤버 액세스 가능

d_pri = 0 ;

d_fpri();

   

// 에러 : 부모의 private 멤버는 액세스 할수 없음

/*b_pri = 1;

b_fpri();*/

   

// 부모의 protected 멤버는 액세스 가능

b_pro = 2;

b_fpro();

   

// 부모의 public 멤버 액세스 가능

b_pub = 3;

b_fpub();

}

};

   

void main()

{

D d;

// 자신의 멤버 함수 호출

d.d_fpub();

// 부모의 public 멤버 함수 호출

d.b_fpub();

}

protected 액세스 속성은 상속 관계에 있지 않은 클래스나 외부에 대해 private과 같으면 파생 크래스에 대해서는 public과 같다.

파생 클래스는 기반 클래스와 아주 밀접한 관계에 있음에도 불구하고 기반 클래스의 private 멤버를 참조하지 못한다는 것은 선뜻 이해하기 어려울 수도 있다. 쓰지도 못할 멤버를 왜 상속받아야 하는지 직관적으로 이해되지 않는다. 그러나 부모 클래스가 스스로의 정보 은폐를 위해 자식에게조차 멤버를 숨겨야 할 필요는 분명히 있으며 이렇게 해야 파생 클래스가 영향을 받지 않는다.

라. 상속 액세스 지정

파생 클래스를 정의하는 일반적인 문법, 즉 C++의 상속 구문은 다음과 같다.

클래스 선언문 다음에 : 이 오고 상속 받을 기반 클래스의 이름이 온다. 그리고 : 과 기반 클래스 이름 사이에 상속 액세스 지정자라는 것이 위치하는데 이 지정자는 기반 클래스의 멤버들이 파생 클래스로 상속될 때 액세스 속성이 어떻게 변할 것인가를 지정한다.

   

29-2 상속의 특성

가. C++의 상속 특성

  1. 하나의 기반 클래스로부터 여러 개의 클래스를 파생시킬 수 있다.

  2. 하나의 클래스로부터 파생될 수 있는 클래스의 개수에 제한이 없을 뿐만 아니라 파생의 깊이에도 제한이 없다. 파생된 클래스로부터 새로운 클래스를 얼마든지 파생시킬 수 있다.

    각 파생 관계의 아래쪽으로 내려올수록 더 많은 속성과 동작이 정의될 것이다. 파생 관계의 위쪽에 있는 클래스는 속성을 몇 개 가지지 않는 일반적인 사물을 표현하며 포괄하는 범위가 넓은 반면 아래쪽에 있는 클래스일수록 점점 더 특수하고 구체적인 사물을 표현한다.

    부모 자식 클랫의 관계를 IS A관계라고 하는데 이는 자식 클래스가 일종의 부모 클래스라는 뜻이다. "동물은 일종의 생물이다"를 animal is a creature라고 표현할 수 있다.

  3. 자주 사용되지는 않지만 C++은 두 개 이상의 클래스로부터 새로운 클래스를 파생시킬 수 있는데 이를 다중 상속이라고 한다.
  4. 기본 타입으로부터는 상속이 허가되지 않는다.

나. 이차 상속

상속의 깊이에 제한이 없어 파생된 클래스로부터 또 다른 클래스를 파생시킬 수 있다.

  • 예제 InheritCircle

#include <iostream>

#include <windows.h>

#include <cmath>

using namespace std;

   

void gotoxy(int x, int y);

   

class coord

{

protected:

int x, y;

public :

coord(int ax, int ay) { x = ax; y = ay; }

void GetXY(int &rx, int &ry) { x = rx; y = ry; }

};

   

class point : public coord

{

protected:

char ch;

public :

point( int ax, int ay, char ach) : coord(ax,ay) { ch = ach; }

void show()

{

gotoxy(x,y); cout << ch;

}

void hide()

{

gotoxy(x,y); cout << ' ';

}

};

   

class circle : public point

{

protected:

int rad;

public:

circle(int ax, int ay, char ach, int aRad) : point(ax,ay,ach) { rad = aRad; }

void show()

{

for( double a = 0 ; a < 360 ; a += 15 )

{

gotoxy(int(x+sin(a*3.14/180)*rad), int(y-cos(a*3.14/180)*rad));

cout << ch ;

}

}

void hide()

{

for( double a = 0 ; a < 360 ; a += 15 )

{

gotoxy(x+sin(a*3.14/180)*rad, int(y-cos(a*3.14/180)*rad));

cout << ' ';

}

}

};

   

void main()

{

point p (10, 10, '@');

p.show();

circle c(40, 10, '*', 8);

c.show();

}

   

void gotoxy(int x, int y)

{

COORD Pos={x,y};

SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),Pos);

}

상속받은 멤버 중 show, hide는 다시 재정의하는데 점을 그리는 방법과 원을 그리는 방법이 다르기 때문에 코드를 다시 작성했다. 상속받은 함수의 본체를 수정하는 것을 오버라이딩이라고 한다.

다. 객체의 생성 및 파괴

상속받은 멤버는 파생 클래스에서 직접 초기화할 수 없으며 기반 클래스에게 초기화를 부탁해야 한다. 파생 클래스는 기반 클래스의 모든 멤버를 상속받기는 하지만 이 멤버를 어떻게 초기화해야 하는지는 정확하게 알지 못한다. 또한 멤버 중 일부는 private 액세스 속성을 가질 수도 있으므로 파생 클래스가 이 멤버를 초기화할 권한이 없다. 대신 기반 클래스의 public생성자를 호출하여 상속받은 멤버를 초기화해야 한다. 생성자는 항상 public이므로 누구나 호출할 수 있다.

상속받은 멤버의 의미와 초기화 방법에 대해서 가장 정호악하게 알고 있는 주체는 이 멤버를 정의한 클래스이므로 기반 클래스의 생성자를 이용하는 것이 합리적이다. 파생 클래스가 기반 클래스의 생성자를 호출할 때는 초기화 리스트를 사용해야 한다.

  1. main에서 circle객체 c를생성할 때 circle의 생성자가 호출된다. circle(40,10,'*',8)이 호출되며 생성자로 원 객체 생성에 필요한 인수들이 전달될 것이다.
  2. 생성자의 본체가 실행되기 전에 초기화 리스트가 먼저 실행된다. 초기화 리스트에서 기반 클래스인 point의 생성자를 호출하며 이 생성자로 ax, ay, ach 인수를 전달한다.
  3. point의 생성자는 다시 자신의 초기화 리스트에 있는 coord의 생성자를 호출하며 이 생성자로 ax, ay 인수를 전달한다. 이런식으로 파생 클래스는 항상 기반 클래스의 생성자를 통해 상속받은 멤버를 초기화해야 한다.
  4. coord의 생성자에서 x, y 멤버를 인수로 전달된 ax, ay로 초기화한다. 이때 단순타입이므로 초기화 리스트를 쓰지 않아도 상관없다.
  5. coord의 생성자가 리턴되면 point의 생성자 본체에서 ch 멤버에 인수로 전달된 ach의 값 '*'을 대입한다. point는 자신의 고유 멤버를 초기화한 후 리턴한다.
  6. circle 생성자는 초기화 리스트를 통해 상속받은 멤버의 초기화를 마치고 본체에서 자신의 고유 멤버인 Rad을 aRad 인수로 초기화한다. circle 생성자가 자신의 모든 멤버를 초기화하고 main으로 리턴하면 객체 c의 초기화가 완료된다.

생성자가 호출되는지 확인해 보려면 각 생성자에 중단점을 설정해놓고 디버거를 돌려보면 알 수 있다. 생성자에서 출력문을 통해 확인 할 수도 있다.

생성자들은 그 특성상 길이가 짧고 내부 정의하는 것이 보통이므로 대부분이 인라인이라며 함수 호출 부담이 없어 속도를 염려할 필요는 없다.

초기화 리스트를 통해 기반 클래스의 생성자를 연쇄적으로 호출하며 상속받지 않은 멤버는 자신이 직접 초기화한다. 일반적으로 기반 클래스는 파생 클래스가 동작하기 위한 전제 조건이 되기 때문에 파생 클래스의 멤버보다 상속받은 멤버가 먼저 초기화되어야 한다.

그래서 생성자 본체가 실행되기 전에 상속받은 멤버는 초기화되어야 하며 그러기 위해서는 초기화 리스트를 사용하는 방법밖에 없다.

만약 기반 클래스에 여러 개의 생성자가 오버로딩되어 있다면 초기화 리스트의 인수 목록에 따라 호출될 생성자가 결정된다.

파생 클래스의 객체가 파괴될 때는 생성자가 호출된 역순으로 파괴자가 호출된다. 먼저 자신의 파괴자가 호출되어 스스로의 멤버를 정리하여 상속 계층을 따라 부모의 파괴자가 연쇄적으로 호출되어 상속된 모든 멤버에 대한 정리 작업을 한다.

라. 멤버 함수 재정의

클래스가 파생될 때 기반 클래스로부터 대부분의 멤버를 상속받지만 일부 상속에서 제외되는 것들도 있다.

  • 생성자와 소멸자
  • 대입 연산자
  • 정적 멤버 변수와 정적 멤버 함수
  • 프렌드 관계 지정

이 멤버들이 상속에서 제외되는 이유는 기반 클래스만의 고유한 처리를 담당하기 때문이다. 생성자와 소멸자, 대입 연산자는 특정 클래스에 완전히 종속적이며 해당 클래스의 멤버에 대해서만 동작하기 때문에 파생 클래스는 이 함수들을 직접 사용할 필요가 없다.

이런 특수한 몇 가지 멤버를 제외하고는 기반 클래스의 모든 멤버가 파생 클래스로 무조건 상속된다. 파생 클래스는 기반 클래스의 모든 멤버 변수와 멤버 함수를 상속받으므로 기반 클래스의 속성과 동작을 그대로 물려받는다. 그런데 만약 상속 받은 멤버와 똑같은 이름으로 똑같은 멤버를 다시 선언하면 어떻게 될까??

  • 예제 MemoberOverride

#include <iostream>

using namespace std;

   

class B

{

public :

int m;

B(int am) { m = am; }

void f() { puts("base function"); }

};

   

class D : public B

{

public:

int m;

D(int dm, int am) :B(am) { m = dm; }

void f() { puts("Derrived function"); }

};

   

void main()

{

D d(1,2);

printf("d.m = %d\n", d.m);

d.f();

}

d 객체에는 이름이 같은 m과 f가 각각 두 개씩 존재하는 셈인데 이 상태에서 m과 f를 참조하면 이는 객체 자신의 멤버를 의미한다. 이 상황은 전역변수와 지역변수의 이름이 중복되었을 때와 유사하며 규칙에 따라 지역변수가 우선권을 가지듯이 객체에서는 상속받은 멤버보다 자신의 멤버가 우선권을 가진다. 그래서 이름이 중복된 상속받은 멤버는 자식이 새로 정의한 멤버에 의해 가려진다.

b.B::m이라는 표현은 d 객체의 멤버 중 B로부터 상속받은 멤버 m을 의미한다.

그러나 멤버 함수의 경우는 부모의 멤버 함수가 제공하는 동작이 파생 클래스와 맞지 않을 때 재정의할 필요가 있으며 이런 경우는 빈번하다.

부모로부터 상속받은 멤버 함수를 다시 작성하는 재정의라고 하는데 원어로는 오버라이딩(Overriding)이라고 한다.

  • 예제 InheritStudent

#include <iostream>

using namespace std;

   

class human

{

protected:

char name[16];

public:

human(char *aName) { strcpy(name, aName); }

void intro() { printf("이름 : %s", name); }

void think() { puts("오늘 뭐 먹지?"); }

};

   

class student : public human

{

private :

int StNum;

public:

student(char *aName, int aStNum) : human(aName) { StNum = aStNum; }

void intro() { human::intro(); printf("학번 : %d\n", StNum); }

void think() { puts("시험 잘봐야됨."); }

void study() { puts("abc..."); }

};

   

void main()

{

student s("sss", 2005);

s.intro();

s.think();

s.study();

}

29-3 다중 상속

가. 두 개의 기반 클래스

다중 상속(Multiple Inheritance)이란 두 개 이상의 기반 클래스로부터 새로운 클래스를 상속하는 것이다. 복잡도에 비해 실용성이 떨어지므로 처음부터 너무 깊이 공부할 필요는 없다. 실제 사물의 예를 들자면 다음과 같은 것들이 다중 상속된 좋은 예이다.

  • 핸드폰, 카메라 : 카메라 폰
  • 프린터, 스캐너, 팩스 : 복합기

이미 만들어진 복수 개의 클래스들을 다중 상속하여 두 클래스의 기능을 모두 가지는 새로운 클래스를 쉽게 만들 수 있으며 더 필요한 기능을 추가하는 것도 가능하다.

  • 예제 MultiInherit

#include <iostream>

using namespace std;

   

class Date

{

protected:

int year, month, day;

public :

Date(int y, int m, int d) { year=y; month=m; day=d; }

void outDate() { printf("%d/%d/%d", year, month, day); }

};

   

class Time

{

protected:

int hour, min, sec;

public :

Time(int h, int m, int s) { hour = h; min = m; sec = s; }

void outTime() { printf("%d:%d:%d", hour, min, sec); }

};

   

class Now : public Date, public Time

{

private :

bool bEngMessage;

int milisec;

public :

Now(int y, int m, int d, int h, int min, int s, int ms, bool b = false )

: Date(y,m,d), Time(h,min,s) { milisec=ms; bEngMessage=b; }

void outNow()

{

printf( bEngMessage ? "now is" : "지금은 " );

outDate();

printf(" ");

outTime();

printf(":%d", milisec);

puts(bEngMessage ? "." : " 입니다.");

}

};

   

void main()

{

Now n(2015, 2, 28, 5, 23, 15, 99);

n.outNow();

}

다중 상속을 받을 때는 클래스 선언문의 : 다음에 기반 클래스의 목록을 콤마로 구분하여 적는다. 이 때 각 기반 클래스의 상속 액세스 지정은 서로 다를 수 있으므로 개별적으로 지정해야 하며 한쪽을 생략하여 적으면 private이 적용된다.

Now클래스의 경우 Date, Time순으로 다중 상속되었으므로 Date의 생성자가 먼저 호출되고 다음으로 Time의 생성자가 호출될 것이다.

나. 다중 상속의 문제점

한 클래스로부터 두 번 상속을 받는 것은 금지 되어 있는데 이렇게 파생된 클래스에는 똑같은 이름의 멤버들이 두 개씩 존재하게 되므로 멤버 이름 간에 충돌이 생기기 때문이다.

  • 예제 VirtualBase1

#include <iostream>

using namespace std;

   

class A

{

protected:

int a;

public :

A(int aa) { a = aa; }

};

   

class B : public A

{

protected:

int b;

public:

B(int aa, int ab) : A(aa) { b = ab; }

};

   

class C : public A

{

protected:

int c;

public:

C(int aa, int ac) : A(aa) { c = ac; }

};

   

class D : public B, public C

{

protected:

int d;

public:

D(int aa, int ab, int ac, int ad) : B(aa, ab), C(aa, ac) { d = ad; }

void fD()

{

b = 1;

c = 2;

//a = 3; // 에러

}

};

   

void main()

{

D d(1,2,3,4);

}

4개의 클래스가 계층을 구성하고 있는데 클래스간의 상속 관계를 그림으로 그려 보면 다음과 같다. 여러가지 복잡한 문제를 일으키기 때문에 이런 클래스 계층돌르 공포의 다이아몬드(또는 마르므모) 계층도라고 부른다.

  • 예제 VirtualBase2

#include <iostream>

#include <math.h>

#include <windows.h>

using namespace std;

   

void gotoxy(int x, int y);

   

class coord

{

protected:

int x, y;

public :

coord(int ax, int ay) { x = ax; y = ay; }

void GetXY(int &rx, int &ry) const { rx = x; ry = y; }

void SetXY(int ax, int ay) { x = ax; y = ay; }

};

   

class point : public coord

{

protected:

char ch;

public :

point(int ax, int ay, char ach) : coord(ax, ay) { ch = ach; }

void show()

{

gotoxy(x,y);

printf("%c", ch);

}

void hide()

{

gotoxy(x,y);

printf(" ");

}

};

   

class circle : public point

{

protected:

int Rad;

public:

circle(int ax, int ay, char ach, int aRad) : point(ax, ay, ach) { Rad = aRad; }

void show()

{

for ( double a = 0 ; a < 360 ; a += 15 )

{

gotoxy(int(x+sin(a*3.14/180)*Rad), int(y-cos(a*3.14/180)*Rad));

printf("%c", ch);

}

}

void hide()

{

for ( double a = 0 ; a < 360 ; a += 15 )

{

gotoxy(int(x+sin(a*3.14/180)*Rad), int(y-cos(a*3.14/180)*Rad));

printf(" ");

}

}

};

   

class Message : public coord

{

private:

char mes[128];

public:

Message(int ax, int ay, char *m) : coord(ax, ay)

{

strcpy(mes, m);

}

   

void show()

{

gotoxy(x-strlen(mes)/2,y);

printf("%s", mes);

}

};

   

class CirMessage : public circle, public Message

{

public:

CirMessage(int ax, int ay, char ach, int aRad, int mx, int my, char *m)

: circle(ax,ay,ach,aRad), Message(mx, my, m)

{}

void show()

{

circle::show();

Message::show();

}

};

   

void main()

{

CirMessage cm(10, 10, '.', 8, 40, 15, "test");

   

cm.show();

}

void gotoxy(int x, int y)

{

COORD Pos={x,y};

SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),Pos);

}

한 클래스를 간접적으로 두 번 상속받을 경우 이 클래스의 멤버가 중복되어 메모리가 낭비되고 어떤 멤버를 칭하는지 알 수 없는 모호함이 발생한다.

다. 가상 기반 클래스

한 클래스를 두 번 상속받더라도 이 클래스의 멤버들은 한 번만 상속하도록 하는 것이 가상 기반 클래스(Virtual Base Class)라고 한다. 이렇게 지정된 클래스는 간접적으로 두 번 상속되더라도 결과 클래스에는 자신의 멤버를 한 번만 상속시킨다.

class B : virtual public A

...

class C : virtual public A

...

class D : public B, public C

D(int aa, int ab, int ac, int ad) : A(aa), B(aa,ab), C(aa, ac) { d = ad; }

   

가상 상속을 받으면 중복된 멤버가 한 번밖에 나타나지 않으므로 객체의 크기는 줄어들어야 한다. 하지만 실제로는 중복된 멤버의 관리를 위해 숨겨진 포인터가 추가되기 때문에 반드시 그렇다고 할 수는 없다. 중복 멤버의 관리 방법은 컴파일러마다 다른데 비주얼 C++의 경우 가상 기반 클래스 하나에 대해 4바이트씩 더 추가되며 중복된 멤버의 크기가 4바이트 이상일 때만 객체 크기가 줄어든다.

라. 다중 상속의 효용성

다중 상속의 경우는 부작용이 상당히 많다. 한 클래스를 간접적으로 두 번 상속할 수 있어 멤버가 중복될 수도 있으며 이런 문제를 해결하기 위해 별도의 예외 문법을 만들 필요도 있다. 또한 가상 기반 클래스가 아닌 부모로부터 다중 상속할 경우 부모 클래스 타입의 포인터가 자식 객체를 가리킬 수 없어 다형성에 방해가 되는 문제도 있다.

다중 상속이 아니더라도 둘 이상의 클래스를 재활용할 수 있는 다른 문법들이 존재하며 다중 상속이 아니면 문제를 풀 수 없는 경우는 거의 없다.

29-4 클래스 재활용

가. 포함

상속만이 클래스를 재활용하는 유일한 기법은 아니다.

상속 외에도 전통적인 포함 방법을 사용할 수 있다.

포함(Containment)이란 재활용하고 싶은 클래스의 객체를 멤버 변수로 선언하는 방법이다. 클래스에 포함되는 멤버의 타입에는 제한이 없으므로 다른 클래스의 객체도 당연히 멤버가 될 수 있다.

  • 예제 MemObject

#include <iostream>

using namespace std;

   

class Date

{

protected:

int year, month, day;

public:

Date(int y, int m, int d) { year = y; month = m; day = d; }

void outDate() { printf("%d/%d/%d", year, month, day); }

};

   

class Product

{

private:

char name[64];

char company[32];

Date vaildTo;

int price;

public:

Product(char *aN, char *aC, int y, int m, int d, int aP) : vaildTo(y,m,d)

{

strcpy(name, aN);

strcpy(company, aC);

price = aP;

}

void outProduct()

{

printf("이름 : %s\n", name);

printf("제조사 : %s\n", company);

printf("유효기간 :");

vaildTo.outDate();

puts("");

printf("가격 : %d\n", price);

}

};

   

void main()

{

Product s("aaa", "n", 2009, 8, 15, 900);

s.outProduct();

}

객체는 생성자 본체가 실행되기 전에 상속받은 모든 멤버와 포함된 객체를 완전히 초기화해야 한다. 그래서 포함된 객체는 반드시 초기화 리스트에서 초기화한다. 이 때 초기화 리스트에는 클래스 이름이 아닌 초기화하고자 하는 객체 멤버 이름을 사용한다.

Product가 Date를 포함하고 있는 이런 관계를 HAS A 관계라고 하는데 일종의 소유 관계이며 상속 관계를 표현하는 IS A와는 의미가 다르다. 두 클래스의 관계가 IS A관계일 때는 주로 public 상속을 하고 HAS A 관계일 때는 포함 기법이 적합하다. 그러나 모든 클래스의 관계가 이처럼 명확하게 구분된느 것은 아니므로 절대적인 재활용 법칙이라고 하기는 어렵다.

나. private 상속

파생 클래스는 자신이 직접 정의한 것이든 상속받은 것이든 결과적으로 자신의 소유가 된 멤버에 대해 원하는 대로 정보 은폐를 할 수 있어야 하며 C++은 이런 방법을 제공하는데 이것이 바로 상속 액세스 지정자이다.

private 상속은 부모의 public, protected멤버를 private으로 바꾼다. 그래서 파생 클래스에서는 이 멤버를 액세스 할 수 있지만 외부에서는 상속받은 멤버를 참조할 수 없다. private 상속은 포함과 유사한 효과가 있으며 HAS A 관계를 구현하는 또 다른 방법이다.

  • 예제 PrivateInherit

#include <iostream>

using namespace std;

   

class Date

{

protected:

int year, month, day;

public:

Date(int y, int m, int d) { year = y; month = m; day = d; }

void outDate() { printf("%d/%d/%d", year, month, day); }

};

   

class Product : private Date

{

private:

char name[64];

char company[32];

int price;

public:

Product(char *aN, char *aC, int y, int m, int d, int aP) : Date(y,m,d)

{

strcpy(name, aN);

strcpy(company, aC);

price = aP;

}

void outProduct()

{

printf("이름 : %s\n", name);

printf("제조사 : %s\n", company);

printf("유효기간 :");

outDate();

puts("");

printf("가격 : %d\n", price);

}

};

   

void main()

{

Product s("aaa", "n", 2009, 8, 15, 900);

s.outProduct();

}

포함은 클래스 타입의 객체를 멤버로 선언하는데 비해 private 상속은 기반 클래스로부터 필요한 멤버를 상속받는 기법이다.

   

:: 포함과 private 상속

포함과 private 상속은 둘 다 기존의 클래스를 재활용하는 기법이라는 면에서는 공통적이고 HAS A 관계를 표현하는 목적도 동일하지만 차이점이 있다. 가장 큰 차이점은 한 클래스에서 같은 타입의 객체 복수 갤르 동시에 재활용 할 수 있는가 하는 점이다.

Date ManuFact; // 제조 일자

Date VaildTo; // 유효기간

포함은 멤버의 개수에 제한이 없으므로 얼마든지 많은 Date 객체를 포함할 수 있다.

class Product : private Date, private Date // 에러

   

포함과 private 상속의 또 다른 차이점은 proected 멤버에 대한 액세스 허가 여부이다. 포함의 경우 포함된 객체의 public 액세스 속성을 가지는 것만 직접 참조할 수 있다. 반면 private 상속의 경우 protected 멤버를 파생 클래스가 액세스할 수 있으므로 포함보다는 좀 더 긴밀한 관계라고 할 수 있다.

:: 인터페이스 상속과 구현 상속

이번에는 포함 또는 private 상속과 public 상속은 어떤 점이 다른지 연구해 본다. 포함이나 private 상속은 둘 다 객체의 구현만 재사용할 뿐이지 인터페이스는 상속받지 않는데 비해 public 상속은 구현뿐만 아니라 인터페이스까지도 같이 상속한다는 점이 다르다. 구현 상속이란 객체의 구체적인 동작만 재사용할 수 있고 인터페이스를 물려받지 않는 상속이며 멤버 함수를 호출 할 수는 있지만 스스로 멤버 함술르 가지지는 않는 상속이다.

private 상속이 포함과 유사하다고 하는 가장 큰 이유는 구현만 상속할 뿐 인터페이스를 상속하지 않기 때문이다. private 상속은 기반 클래스의 모든 멤버를 상속과 동시에 private 속성으로 바꾸어 버린다. 그래서 Product의 내부에서는 상속받은 멤버 함수 outDate를 호출할 수는 있지만 외부에 대해서는 이 멤버 함수가 숨겨지므로 Product는 이 인터페이스를 가지지 않는 것과 같아진다.

Date 객체를 포함하는 Product도 outDate라는 인터페이스가 존재하지 않는다.

반면 public 상속의 경우는 기반 클래스의 멤버를 물려받아 완전한 자기 것으로 만드는 것이므로 기반 클래스에 정의되어 있는 멤버 함수를 직접 호출할 수 있다. 파생 클래스는 기반 클래스의 인터페이스를 그대로 물려받아 외부로 공개하며 후손 클래스에게도 물려줄 수 있다. public 상속은 포함과 달리 구현과 인터페이스를 동시에 물려받는 것이다.

이것이 HAS A 와 IS A를 구분하는 중요한 기준다. 그래서 클래스를 재활용해야 할 때 두 클래스의 관계를 잘 판단해 보고 IS A 관계에 가까우면 public 상속을 하는 것이 좋고 HAS A 관계이면 포함시키거나 private 상속하는 것이 더 좋다.

정리하자면 private 상속과 public 상속은 상속받은 인터페이스가 외부로 공개되는가 아닌가의 차이점이 있다.

세 경우 모두 결과적으로 캡슐화되는 정보의 목록은 동일하지만 이 멤버들이 더시 왔는지와 외부에 대한 인터페이스 공개 여부가 다르다. 클래스의 단순한 재사용만을 목적으로 한다면 포함이나 private 상속 중 하나를 쓸 수 있되 일반적으로 포함이 훨씬 더 쉽고 직관적인 방법이기도 하다.

인터페이스 상속은 클래스간의 계층 관계를 이룸으로써 다형성을 구현할 수 있다는 점에서 단순한 재활용 이상의 의미를 가진다. 일단 상속받은 후 일부 함수의 동작을 재정의할 수도 있고 객체 타입에 따라 다른 동작을 하도록 만들 수도 있다. 객체 지향의 진정한 매력은 바로 다형성인데 이를 위한 전제 조건이 바로 public 상속이다.

또한 인터페이스, 즉 멤버 함수의 목록만 상속받고 구현은 전혀 상속받지 않는 순수 가상 함수라는 방법도 있다.

:: protected 상속

파생클래스를 다시 파생시킬 때 독특한 특징이 있다. 2차 파생된 클래스가 애초의 기반 클래스에 접근할 수 있다는 점에서 private 상속관느 다르며 기반 클래스의 멤버들을 외부에서 접근할 수 없다는 점에서 public 상속과도 다르다.

다. 중첩 클래스

중첩 클래스란 클래스 선언문 안에 다른 클래스가 선언되는 형태이다.

  • 예제 NestClass

#include <iostream>

using namespace std;

   

class outer

{

private:

class inner

{

private:

int memA;

public:

inner(int a) : memA(a){}

int GetA() { return memA; }

} obj;

public:

outer(int a) : obj(a){}

void outOuter() { cout << "멤버값 = " << obj.GetA() << endl; }

};

   

   

void main()

{

outer o(345);

// inner i(345); // 에러

o.outOuter();

}

이중 연결 리스트

class LinkedList

{

private:

struct node

{

int data;

node *prev, *next;

};

node *head, *tail;

public :

LinkedList();

~LinkedList();

Insert(node *p, int a);

Delete(node *p);

int GetData(node *p);

};

라. 상속의 방향성

클래스 간의 상속 방향은 당연히 부모로부터 자식으로 내려가는 것이다. 부모가 자식에게 멤버를 상속 시켜주는 것이므로 당연하다고 생각되겠지만 실제로 프로그램을 작성할 때는 부모보다 자식이 먼저 만들어지는 경우가 더 많다. 처음부터 부모의 멤버 목록을 완벽하게 작성해 놓고 자식 클래스를 파생시켜가면서 클래스 계층을 만드는 경우보다 자식들을 만들다보니 공통의 부모가 필요해진다는 것이다. 즉 상향식(Bottom Up)인데 사람의 사고는 특수한 것을 만들고 이 특수한 것으로부터 일반성을 추출해 내는 쪽에 더 익숙해 있다.

  • 문방구 관리 프로그램

제품마다 중복되는 멤버들이 많다는 것을 알 수 있다.

또한 공통 속성을 가지는 제품도 나타나게 된다.

처음 그림보다 좀 더 복잡해 보이기는 하지만 이렇게 상속 계층을 만들어 두면 코드를 관리하기가 훨씬 더 쉬워진다.

상속이란 기반 클래스로부터 파생 클래스를 정의하는 기술이지만 현실의 개발 절차는 거꾸로인 경우가 많고 그것이 사람의 생리에 훨씬 더 가깝다. 개별 클래스를 만들다 보면 공통 속성이 발견되고 이 속성들을 가지고 별도의 클래스를 만든 후 파생시키는 것이다. 시행 착오 없이 처음부터 한 번에 완벽한 클래스 계층도를 디자인 할 수 있다면 좋을 것이도 실제로 이런 설계 작업을 도와주는 툴들도 있지만 이것은 무척 어려운 일이다. 필요한 클래스를 만들어 가면서 통폐합을 반복하다 보면 점점 좋은 모양이 나오게 된다. 업무 분석이 잘되어 있고 경험이 풍부하면 디자인 작업이 빠르고 정확해지며 디자인이 깔끔하면 개발도 효율적으로 진행된다.

반응형

'책정리 > 혼자 연구하는 C,C++ 2' 카테고리의 다른 글

31장 템플릿  (0) 2015.03.05
30장 다형성  (0) 2015.03.03
28장 연산자 오버로딩  (0) 2015.02.27
27장 캡슐화  (0) 2015.02.20
26장 생성자  (0) 2015.02.20