※ 상속 (inheritance)

- 자기자신이 갖는 멤버는 물론 기본클래스의 멤버까지 받아오는 방법이다.

cf. 정보은닉은 하나의 객체 내에서도 진행되기에 private에 직접 접근이 안될 뿐! 간접접근은 가능하다.

 

★★<파생클래스의 생성자는 기본클래스의 멤버까지 초기화 해줘야 한다!>★★

- 유도클래스의 생성자는 기본클래스의 생성자를 호출하여 멤버초기화 하는 것이 좋다.

- 유도클래스는 initializer를 이용해 상속하는 클래스의 생성자를 호출할 수 있다.

 

 

 

※ 유도클래스의 객체생성 과정

1. 유도클래스의 객체생성과정에서 기초클래스의 생성자는 무.조.건! 호출

2. 유도클래스의 생성자에서 기초클래스의 생성자 호출을 명시하지 않으면 => 기초클래스의 void 생성자가 호출!

 

 

※ IS-A 

is-a 즉, "일종의 ~이다" 로 해석되며 상속관계가 성립하기 위한 기본 조건이다.

 

 

 

 

 

 

 

※ 바인딩 (binding)

함수나 변수의 주소가 결정되는 것으로 다음 2가지 종류가 있다.

- early binding (정적 바인딩): 컴파일시간에 바인딩 되는 것 (초기화 리스트, 일반적 변수)

- late binding (동적 바인딩): 실행시간(run time)에 바인딩 되는 것 =>  virtual 선언된 모든 것!

 

간단히 말하자면 변수의 데이터 형이 결정되는 시점에 따라 나뉜다.

 

조금 더 자세히 말하자면 다음과 같다.

정적 바인딩: 컴파일시 관련 라이브러리, 객체와 링크해 실행 모듈을 만드는 것

동적 바인딩: 컴파일시 정보를 갖고 있지만 실제 실행시간에 해당 객체와 링크하여 실행 모듈을 만드는 것. 

 

정적 바인딩을 사용하면 컴파일 시 데이터 형이 정해지므로 실행에 효율적이고,

동적 바인딩을 사용하면 실행 시 데이터 형이 변경되므로 적응력이 뛰어난 프로그램을 제작할 수 있다.

 

 

 

※ 동적 바인딩의 발생

동적 바인딩의 발생은 다음과 같다

- 기본/파생클래스 내 멤버함수가 가상함수 호출

- 외부함수에서 기본클래스의 포인터로 가상함수 호출

- 다른 클래스에서 가상함수 호출

 

이렇게 가상함수가 호출되면 실행 중 객체 내에 오버라이딩(overriding)된 가상함수를 동적으로 찾아 호출한다.

 

§ 오버라이딩한 함수를 직접 호출하는 동적바인딩 예시코드 (객체지향 프로그래밍을 위한 C++의 이해)

 

 

컴퓨터 입장에서 p  c 모두 Parent 를 가리키는 포인터들이므로, 당연히 아래처럼 호출했을 때

p->paint();
c->paint();
모두 Parent 의 print() 가 호출되어야 하겠지만, 실제로는 print() 가 가상함수므로,
"실제로 p 와 c 가 가리키는 객체의 print()",
즉 p->f() 는 Parent 의 print()를, c->f() 는 Child 의 print()가 호출된다.
이와 같은 일이 가능한 이유는 print()를 가상함수로 만들었기 때문입니다.

 

 

 

 

 

※ virtual function (가상함수)

- 가상함수: 기본 클래스 내의 함수를 파생클래스에서 재정의 (같은 이름의 함수를 overriding)하고자 할 때 사용.

- 가상함수가 포인터에 의해 호출된 경우, 포인터가 가리키는 객체의 클래스에 따라 컴파일러가 결정해 호출한다.

- 따라서 기본클래스 내의 멤버함수 앞에 virtual을 통해 overriding이 가능하다.

 

이런 가상함수가 나오게 된 계기는 다음과 같은 의문 때문이다.

 

Q. 포인터변수 자료형에 따라 호출되는 함수가 달라지는 것에 문제가 있지 않을까?

A. virtual을 이용하는 가상함수는 overriding하는 함수도 가상함수가 되어버린다.

즉, 가상함수로 선언되면 해당 함수호출시 포인터의 자료형 기반 호출이 아닌 포인터변수가 실제 실제로 가리키는 객체를 참조하여 호출의 대상을 결정한다.

 

 

 

※ 순수가상함수 

- 함수의 몸체가 정의되지 않은, 구현이 없는 가상 함수로 구현 대신, 가상함수에 0(NULL)값을 대입하면 된다.

- 가상함수: 상속관계(기본/파생클래스)에서 오버라이딩을 할 수 있는 함수

- 순수가상함수: 파생클래스에서 구현되어야하는 함수, 인터페이스로 이 클래스를 사용하겠다는 의미

virtual int GetPay() const = 0;
virtual void ShowSalaryInfo() const = 0;

 

※ 추상클래스와 override

- 추상클래스: 순수가상함수를 포함하는 클래스

- 추상클래스에 의한 포인터객체, 참조객체등의 정의는 가능하다.

- 추상클래스의 사용불가능한 용도: 변수, 멤버데이터  /  인수형식  /  함수 반환형식  /  명시적 변환형식

class Animal {
public:
    virtual void virtualFunc() = 0;
};

- 순수가상함수는 기본클래스내에서 특별한 연산, 동작도 정의하지 않고, 단지 선언만 하는 가상함수다.

- 따라서 순수가상함수는 본체를 갖지 않는 함수다.

- 클래스 디자인에서 반드시 필요하지만 사용자에게 그 부분을 구현할 것을 알려야 하는데, 이때 추상클래스를 사용!

 

예제 1.

#include <iostream>
using namespace std;

class Graphic{  // 추상 클래스 생성
public:
    virtual void Draw() = 0;  // 순수 가상함수
};

class Line : public Graphic{ // 추상 클래스 상속
public:
    virtual void Draw() {
        cout << "선을 그린다." << endl;
    }
};

class Circle : public Graphic {  // 추상클래스 상속
public:
    virtual void Draw() {
        cout << "원을 그린다." << endl;
    }
};

class Rect : public Graphic {
public:
    virtual void Draw() {
        cout << "사각형을 그린다." << endl;
    }
};

int main() {
    Graphic *ptG[3];     // 객체 배열 선언
    // 객체 정의
    ptG[0] = new Line;   
    ptG[1] = new Circle;   
    ptG[2] = new Rect;

    for (int i = 0; i < 3; i++) {
        ptG[i]->Draw();
    }

    for (int i = 0; i < 3; i++) {
        delete ptG[i];
    }
}

 

예제 2. Override 키워드

virtual 키워드: 가상함수의 시작부분에 붙임

override 키워드: 파생클래스의 가상함수에 붙임 (이 가상함수가 상속받아서 오버라이딩한 함수다라는 것을 표현)

class Parent{
public:
    virtual void func() = 0;
};

class Child : public Parent {
public:
    void func() override {
        cout << "func()함수 선언" << endl;
    }
};

int main() {
    Parent *p = new Child();
    Child *c = new Child();

    c->func();

    delete c;
}

 

예제 3. 순수 가상 소멸자

- 순수가상함수가 하나도 없는 클래스를 추상클래스로 만들고 싶을 때, 순수 가상 소멸자로 만들 수 있다.

class Animal{
public:
    Animal() { };
    virtual ~Animal() = 0;
};
.
.
.
Animal::~Animal() { } // 가상 소멸자 호출

 

 

 

 

 

※ 가상소멸자

virtual 키워드가 필요한 대상은 2가지이다.

가상함수
가상소멸자

- 가상 생성자는 객체가 생성되기 전에 이루어지기에 정의가 불가능하다.

- 가상 소멸자는 몸체의 내용이 없어도 가능하기에 가상생성자를 없애기 위해 사용할 수 있다.

 

- 순수 가상함수가 하나도 없는 클래스를 추상클래스로 만들고 싶을 때, 소멸자를 순수 가상함수로 만든다.

- 파생 클래스의 소멸자가 호출된 후 기본클래스 소멸자도 호출되므로 순수 가상 소멸자의 정의의 필요성을 알 수 있다.

class Parent{
public:
    virtual ~Parent(){  // 가상 소멸자 선언
        cout << "기본클래스 소멸자" << endl;
    }
};

class Child : public Parent {
public:
    ~Child(){
        cout << "파생클래스 소멸자" << endl;
    }
};

int main() {
    Parent *p = new Child;  // 파생클래스로 객체 선언
    delete p;
    // 객체 제거 => 파생클래스 가상 소멸자 => 기본클래스 가상 소멸자 실행
}

 

 

 

if, 가상함수처럼 기초클래스의 소멸자만 virtual 선언을 하면?

=> 마찬가지로 유도클래스의 소멸자들도 모두 가상소멸자로 선언된다!

 

※ 가상함수의 동작원리와 가상함수테이블 참조

 

 

 

 

 

 

 

 

※ 다중상속 (multiple inheritance)

둘 이상의 클래스를 동시에 상속하는 것으로 아래와 같은 견해들이 있다.

- "보통 다중상속은 다양한 문제가 있기에 가급적 사용하지 않는 것을 추천"

- "다만 예외적으로 매우 제한적인 사용까지는 부정할 필요는 없다."

다중상속은 어느 클래스에 선언된 멤버에 접근할 것인가에 대한 모호성이 발생한다.

따라서 같은 이름의 함수의 경우, 아래와 같이 해결할 수 있다.

Base1::Func();
Base2::Func();

또 하나의 모호성은 가상상속 부분이다.

 

virtual선언이 되지 않은 상태에서 객체가 생성되면 하나의 객체 안에 2개의 클래스 멤버가 존재하여 함수 내에서의 이름만으로 다른 함수를 호출할 수 없다.

 

따라서 이 경우 어느 클래스를 통해 간접 상속한 클래스의 멤버함수를 호출할 건지 다음과 같이 정의 해야 한다.

MiddleDerived1::Func();  //MiddleDerived1클래스가 상속한 Base클래스의 Func함수 호출
MiddleDerived2::Func();  //MiddleDerived2클래스가 상속한 Base클래스의 Func함수 호출

 

혹은 virtual 키워드로 아래와 같이 해결할 수 있다.

class MiddleDerived1 : virtual public Base { . . . };
class MiddleDerived2 : virtual public Base { . . . };
class LastDerived : public MiddleDerived1, public MiddleDerived2 { . . . };

 

 

 

 

 

 

※ 다형성 (polymorphism)

- 객체지향의 매우 중요한 요소로 동질이상(同質異像)을 의미. (모습은 같은데 형태가 다름)

즉, 문장은 같은데 결과는 다르다는 뜻이다.

ex) virtual로 선언된 같은 이름, 다른 클래스의 함수를 동일한 포인터 변수로 받아도 다른 결과값이 나올 수 있다.

+ Recent posts