Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-19 10:17
관리 메뉴

nomad-programmer

[Programming/C++] 다형성(Polymorphism) <가상함수(Virtual Function), 순수 가상함수(Pure Virtual Function), 추상 클래스(Abstract Class)> 본문

Programming/C++

[Programming/C++] 다형성(Polymorphism) <가상함수(Virtual Function), 순수 가상함수(Pure Virtual Function), 추상 클래스(Abstract Class)>

scii 2021. 2. 1. 21:56

함수를 override 했다는 것은, 해당 객체에서 호출되어야 하는 함수를 바꾼다는 의미이다. 하지만 포인터 변수의 자료형에 따라 호출되는 함수 종류가 달라지는 것은 문제가 있다. 다음의 예제를 보자.

#include <iostream>

#pragma warning(disable: 4996)

using std::cout;
using std::cin;
using std::endl;


class First {
public:
    void MyFunc() {
        cout << "FirstFunc" << endl;
    }
};

class Second : public First {
public:
    void MyFunc() {
        cout << "SecondFunc" << endl;
    }
};

class Third : public Second {
public:
    void MyFunc() {
        cout << "ThirdFunc" << endl;
    }
};


int main() {
    Third* tptr = new Third();
    Second* sptr = tptr;
    First* fptr = sptr;

    fptr->MyFunc();
    sptr->MyFunc();
    tptr->MyFunc();

    delete tptr;

    return 0;
}

/* 결과

FirstFunc
SecondFunc
ThirdFunc

*/

위의 결과를 보면 First형 포인터 변수는 First클래스에 정의된 MyFunc() 메소드가 호출되고, Second형 포인터 변수는 Second클래스에 정의된 MyFunc() 메소드를 호출하고, Third형 포인터 변수는 Third클래스에 정의된 MyFunc() 메소드를 호출하는 것을 알 수 있다. 
그런데 포인터의 형에 따라 호출되는 메소드가 달라지는 것은 문제가 있다.

그래서 C++은 이러한 상황이 발생하지 않도록 '가상함수'라는 것을 제공하고 있다. 그런데 이 가상함수라는 것은 C++의 개념이 아닌 객체지향의 개념이다. 따라서 C++뿐만이 아니라 JAVA, C#과 같은 객체지향 언어에서도 이와 동일한 개념의 문법이 제공되고 있다. 물론 적용 방법에는 아주 약간의 차이를 보인다.

class First {
  public:
    virtual void MyFunc() { cout << "First!" << endl; }
};

위에서 보다시피 가상함수의 선언은 virtual 키워드의 선언을 통해 이뤄진다. 그리고 이렇게 가상함수가 선언되고 나면, 이 함수를 오버라이딩 하는 함수도 가상함수가 된다. 다음의 예제를 보자.

#include <iostream>

#pragma warning(disable: 4996)

using std::cout;
using std::cin;
using std::endl;


class First {
public:
    virtual void MyFunc() {
        cout << "FirstFunc" << endl;
    }
};

class Second : public First {
public:
    virtual void MyFunc() {
        cout << "SecondFunc" << endl;
    }
};

class Third : public Second {
public:
    virtual void MyFunc() {
        cout << "ThirdFunc" << endl;
    }
};


int main() {
    Third* tptr = new Third();
    Second* sptr = tptr;
    First* fptr = sptr;

    fptr->MyFunc();
    sptr->MyFunc();
    tptr->MyFunc();

    delete tptr;

    return 0;
}

/* 결과

ThirdFunc
ThirdFunc
ThirdFunc

*/

First클래스에서 virtual 선언을 하면 다른 클래스에서는 하지 않아도 된다. 허나 이렇게 virtual 선언을 넣어서 메소드가 가상메소드라는 것을 알리는게 좋다.

위의 결과에서 보이듯, 함수가 가상함수로 선언되면 해당 함수 호출 시, 포인터의 자료형을 기반으로 호출대상을 결정하지 않고, 포인터 변수가 실제로 가리키는 객체를 참조하여 호출의 대상을 결정한다.

상속을 하는 이유?

상속을 통해 연관된 일련의 클래스에 대해 공통적인 규약을 정의할 수 있다. (C#의 interface)

순수 가상함수(Pure Virtual Function)와 추상 클래스(Abstract Class)

class Entity {
  private:
    char name[100];
    
  public:
    explicit Entity() {}
    virtual ShowName() const {}
    virtual int GetData() const {
      return 0;
    }
};

이 클래스는 베이스 클래스로서만 의미를 가질 뿐, 객체의 생성을 목적으로 정의된 클래스는 아니다. 이렇듯 클래스 중에서는 객체생성을 목적으로 정의되지 않는 클래스도 존재한다.
따라서 다음과 같은 문장이 만들어진다면, 이는 프로그래머의 실수가 틀림이 없다.

Entity* entity = new Entity("Hello World!");

하지만, 이는 문법적으로 아무런 문제가 없는 문장이기 때문에 이러한 실수는 컴파일러에 의해서 발견되지 않는다. 따라서 이러한 경우에는 다음 코드에서 보이듯이, 가상함수를 '순수 가상함수'로 선언하여 객체의 생성을 문법적으로 막는 것이 좋다.

class Entity {
  private:
    char name[100];
    
  public:
    explicit Entity() {}
    virtual ShowName() const = 0;    // 순수 가상함수
    virtual int GetData() const = 0; // 순수 가상함수
};

 

순수 가상함수란?

함수의 몸체가 정의되지 않은 함수를 의미한다. 그리고 이를 표현하기 위해 '0의 대입'을 표시한다. 

그런데 이것은 0의 대입을 의미하는 것이 아니고, '명시적으로 몸체를 정의하지 않았음'을 컴파일러에게 알리는 것이다. 따라서 컴파일러는 이 부분에서 함수의 몸체가 정의되지 않았다고 컴파일 오류를 일으키지 않는다.

그러나 Entity클래스는 순수 가상함수를 지닌, 완전하지 않은 클래스가 되기 때문에 다음과 같이 객체를 생성하려 들면 컴파일 에러가 발생한다.

Entity* entity = new Entity("Hello World!");  // 에러 발생

여기서 두 가지 이점을 얻을 수 있다. 

첫째, 잘못된 객체 생성을 막을 수 있게 되었다.

둘째, ShowName(), GetData()함수는 파생 클래스에 정의된 함수가 호출되게끔 돕는데 의미가 있었을 뿐, 실제로 실행이 되는 함수는 아니었는데, 이를 보다 명확히 명시하는 효과도 얻게 되었다.

그리고 이렇듯 하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스를 가리켜 '추상 클래스(abstract class)'라 한다. 이는 완전하지 않은, 그래서 객체생성이 불가능한 클래스라는 의미를 지닌다.


다형성(Polymorphism)

가상함수의 호출 관계에서 보인 특성을 가리켜 '다형성'이라 한다. 그리고 이는 객체지향을 설명하는데 있어서 매우 중요한 요소이다. '다형성(Polymorphism)'이란 '동질이상'을 의미한다. 즉, 다음의 의미를 담고 있다.

모습은 같은데 형태는 다르다.

이를 C++에 적용하면, 다음과 같이 이야기할 수 있다.

문장은 같은데 결과는 다르다.

다음의 클래스를 살펴보자.

class First {
public:
	virtual void SimpleFunc() { cout << "First" << endl; }
};

class Second : public First {
public:
	virtual void SimpleFunc() { cout << "Second" << endl; }
};


int main(const int argc, const char* const argv[]) {
	First* ptr = new First();
	ptr->SimpleFunc();
	delete ptr;

	ptr = new Second();
	ptr->SimpleFunc();
	delete ptr;

	return 0;
}

/* 결과

First
Second

*/

위의 main 함수에는 다음의 문장이 두 번 등장한다.

ptr->SimpleFunc();

그런데 ptr은 동일한 포인터 변수이다. 그럼에도 불구하고 실행결과는 다르다. 포인터 변수 ptr이 참조하는 객체의 자료형이 다르기 때문이다. 이것이 바로 C++에서의 '다형성'의 예이다.

Comments