C++은 할건데, C랑 다른 것만 합니다. 9편 클래스, virtual, override, final 키워드

0

C++

목록 보기
9/10

클래스와 다운 캐스팅, virtual, override, final 키워드

https://modoocode.com/210

본 포스팅은 위 포스트를 정리한 내용입니다.

1. is-a와 has-a

상속은 관계의 표현이다. 부모의 관계를 표현하는데 있어서 자식은 부모의 모든 특성을 물려받는다. 이것을 상속 받는다고 한다.

그렇다면 자식은 부모와 같다. 따라서 자식과 부모는 is-a관계가 성립된다.

가령, 군인병장이라면, 병장군인이므로, 병장 is a 군인이 성립한다. 따라서, 이는 상속관계이므로 병장군인의 특성을 상속받는다.

단, 이를 바꾸어 말하면 상속관계는 성립하지 않는다. 즉, 자식은 부모의 모든 특성을 물려받지만, 부모는 자식의 특성을 갖지 못한다.

위의 예제만해도, 군인 is a 병장은 말이 안된다. 군인은 병장뿐만 아니라, 여러 계급이 있기 때문이다.

따라서, 다음과 같이 그림을 그릴 수 있다.

[사진 - 자식 클래스가 부모 클래스로 화살표를 쏘는 사진]

참고로, 화살표 모형은 상속을 일컫으며 상속받는 입장에서 쏘는 것이다.

그런데, 다음과 같은 상황이 있을 때는 어떻게 표현해야 할까??
가령, 군인이 있고 수통과 총, 군장들은 군인과 상속관계가 아니다.

즉, 군인 is a 군장, 수통, 총이 가능하지 않다. 따라서, 이를 has a관계로 표현한다.

군인 has a 군장, 수통, 총은 가능하다. 이는 상속관계가 아닌 composition관계, 즉 포함관계라고 한다.

이때에는 상속이 아니라, 맴버 변수로 표현한다.

class Soldier {
    int gunNumber;
    int waterBottle;
    ...
}

composition 관계는 다음처럼 표현한다.

[사진 - composition 관계 표현]

2. 오버라이딩( overriding )

위의 군인과 병장의 관계를 클래스로 구현해보자

#include <iostream>

using namespace std;

class Soldier {
    string name;

    public:
        Soldier() : name("solider") {
            cout << "Soldier class initialization" << endl; 
        }

        void training(){
            cout << name << " " << "헛둘헛둘" << endl;
        }
};

class Sergeant : public Soldier {
    string name;
    public:
        Sergeant() : name("sergeant") , Soldier() {
            cout << "Sergeant class initialization" << endl;
        }

        void training(){
            cout << name << " " << "훈련을 하나마나" << endl;
        }
};

int main(){
    Soldier soldier;
    soldier.training();

    Sergeant sergeant;
    sergeant.training();
}

병장 is a 군인 이다. 때문에 병장은 군인을 상속받을 수 있다. 병장은 군인의 훈련(trainning 메서드)를 오버라이드하여 구현할 수 있다.

결과

Soldier class initialization
solider 헛둘헛둘
Soldier class initialization
Sergeant class initialization
sergeant 훈련을 하나마나

우리가 원하는대로 실행이 된 것을 확인할 수 있다.

그렇다면, 병장 is a 군인이므로, 군인변수에 병장을 넣을 수 있지 않을까??

int main(){
    Soldier *soldier = new Sergeant();
    soldier->training();
}

다음과 같이 solider 포인터 변수 내부에 Sergeant 객체를 넣어줄 수 있다. 이렇게 되면 내부가 Sergeant이므로 Sergeant의 함수를 호출할 수 있어보이지만

Soldier class initialization
Sergeant class initialization
solider 헛둘헛둘

결과는 soldier 클래스의 trainning메서드가 나온다. 이렇게 결과가 나오는 이유는 다음과 같다.

[사진3 리모컨]

이렇게 파생클래스(병장)에서 기반클래스(군인)로 캐스팅하는 것을 업캐스팅이라고 한다.

다음과 같이 부모 클래스인 Soldier에게는 Sergeant 클래스의 정보에 접근할 수 있는 방법이 없다. 접근할 수 있는 버튼이 없는데 어떻게 Sergeant 객체의 메서드와 맴버 변수들을 호출할 수 있는가. 불가능하다.

참고로 자바와는 다르게 동작한다. 자바의 경우에서는 기반 클래스 변수안에 파생 클래스가 들어가서 오버라이드된 메서드를 호출한다면 파생 클래스의 메서드를 호출한다.

또한, 반대로 다운 캐스팅은 불가능하다.

int main(){
    Sergeant *sergeant = new Soldier();
}

다음의 코드는 다운 캐스팅인데, 기반 클래스 객체를 파생 클래스변수에서 받을 수가 없다. 이것이 불가능한 이유는 기반 클래스에는 없는 함수, 변수들이 파생 클래스에 있고, 파생 클래스의 변수(리모컨)에는 이를 호출할 수 있는 버튼(메서드, 맴버변수)들이 있다. 그러나 버튼을 눌러도 나올 수 있는 것들이 객체(기반 클래스)에는 없으므로 에러가 발생하는 것이다.

[사진4 파생 클래스와 기반 클래스의 리모컨]

그러나 다음의 관계도 성립이 가능하다. 병장은 군인이다. ,군인은 병장은 아니지만, 병장이 될 수 있다.

될 수 있다가 핵심이다. 즉, SoldierSergeant를 변수에 넣어 저장할 수 있지만, 호출하지 못했다. 이는 SoldierSergeant가 아니기 때문에 누를 수 있는 버튼이 없기 때문이다.

그러나, 군인은 병장이 될 수 있다 따라서 Soldier 클래스를 Sergeant로 다운 타입 캐스팅해주면 된다.

여기서 말하는 다운 타입 캐스팅은 위에서 불가능했던 예제와는 다르다. 위의 예제에서는 내부 객체가 기반 클래스(군인)이고, 받는 변수가 파생 클래스(병장)이었기 때문에 안되었던 것이고, 지금은 받는 변수가 기반 클래스이고, 내부 객체가 파생 클래스인 것이다.

즉, 내부 객체(파생 클래스)의 맴버 변수, 맴버 함수를 가지고 있는데 변수(리모컨, 기반 클래스)는 그걸 구동할 버튼이 없어 다운캐스팅으로 버튼을 만들어주는 것이다.

2.1 다운 캐스팅 방법 1, 파생 클래스의 변수에 넣기

첫번째 방법은 파생 클래스의 변수에 넣어주는 방법이다.

int main(){
    Soldier *soldier;
    Sergeant sergeant;
    soldier = &sergeant;

    Sergeant *tempSergeant = soldier;
}

하지만 이렇게 해주면 에러가 발생할 것이다. 이는 위에서 명시적으로 다운캐스팅한 것과 마찬가지인 방법이기 때문에 컴파일러가 위험하다고 판단하여 막은 것이다.

2.2 다운 캐스팅 방법 2, static_cast<Type>()

정적 캐스팅 방법으로 static_cast<Type>()으로 구현하는 방법이 있다.

int main(){
    Soldier *soldier;
    Sergeant sergeant;
    soldier = &sergeant;

    Sergeant *tempSergeant = static_cast<Sergeant*>(soldier);
    tempSergeant->training();
}
Soldier class initialization
Sergeant class initialization
sergeant 훈련을 하나마나

사용방법은 아주 단순하게 static_cast<Type>(Base) 이다.

그런데 해당 방법은 어떤 프로그램에서는 런타임 에러를 내기도 하고, 런타임 에러가 안난다해도 디버깅이나 코딩에 있어 어려움을 주는 요소이기도 하다. 그래서 static_case<Type>(Base)는 좋지 않은 방법이다.

2.3 다운 캐스팅 방법 3, Dynamic_case(Type)()

Sergeant *tempSergeant = dynamic_cast<Sergeant*>(soldier);

static_case와 같은 방법으로 dynamic_cast로 사용이 가능하지만 캐스팅 컴파일 오류가 발생할 것이다.

위의 방법들은 모두 좋은 해결책들이 아니다. 그래서 나온 것이 virtual 키워드이다.

3. virtual 키워드

다운 캐스팅을 직접적으로 하지 않아도, 이 문제를 해결할 수 있는 방법이 있다. 그것이 바로 virtual 키워드이다.

virtual키워드는 기반 클래스에서 오버라이드를 할 함수 앞부분에 적어주면 된다.

#include <iostream>

using namespace std;

class Soldier {
    string name;

    public:
        Soldier() : name("solider") {
            cout << "Soldier class initialization" << endl; 
        }

        virtual void training(){
            cout << name << " " << "헛둘헛둘" << endl;
        }
};

class Sergeant : public Soldier {
    string name;
    public:
        Sergeant() : name("sergeant") , Soldier() {
            cout << "Sergeant class initialization" << endl;
        }

        void training(){
            cout << name << " " << "훈련을 하나마나" << endl;
        }
};

int main(){
    Soldier *soldier;
    Sergeant sergeant;
    soldier = &sergeant;
    soldier->training();
}
Soldier class initialization
Sergeant class initialization
sergeant 훈련을 하나마나

아주 재밌게도 virtual 클래스로 선언해놓으면 다음과 같이 기반 클래스의 함수를 파생 클래스에서 오버라이드해놓았을 때 파생 클래스의 함수를 호출해준다.

virtual의 이러한 일을 동적 바인딩(dynamic binding)이라고 한다. 동적 바인딩은 컴파일 시에 어떤 함수가 실행될 지를 정하지 않고 런타임 시에 정해지는 일을 가리킨다.

즉 virtual 키워드가 붙는다면 기반 클래스의 변수에서 함수를 실행할 때, 자신 내부의 객체가 어떤 클래스의 객체인지를 본다. 만약 기반 클래스라면 기반 클래스의 함수를, 만약 파생 클래스라면 파생 클래스의 함수를 실행시켜준다. 그래서 컴파일 단계가 아닌 런타임 단계에서 동적으로 어떤 함수를 호출할 지 바인딩이 되는 것이다.

그러나 virtual keyword가 없다면, 정적 바인딩이기 때문에 기반 클래스이구나! 싶으면 기반 클래스의 함수를 실행시킨다. 즉, 정적 바인딩은 컴파일 타임에 어떤 함수가 호출되는 지 알고 있어 바인딩을 시켜주는 것을 말한다.

단, 참고로 virtual 키워드가 붙은 함수를 virtual 함수( 가상 함수 )라고한다. 파생 클래스의 함수가 기반 클래스의 함수를 오버라이드하기 위해서는 두 함수의 꼴이 정확해야 한다.

4. override 키워드

c++11에서는 파생 클래스에서 기반 클래스의 가상 함수를 오버라이드 하는 경우, override 키워드를 통해서 명시적으로 나타낼 수 있다.

override 키워드를 사용하는 이유는 실수로 오버라이드를 잘못해줄 때 컴파일러가 오버라이드 되는게 없다고 에러를 알려줄 수 있다.

#include <iostream>

using namespace std;

class Soldier {
    string name;

    public:
        Soldier() : name("solider") {
            cout << "Soldier class initialization" << endl; 
        }

        virtual void training(int i){
            cout << name << " " << "헛둘헛둘" << i << endl;
        }
        virtual void training(char a){
            cout << name << " " << "헛둘헛둘" << a << endl;
        }
};

class Sergeant : public Soldier {
    string name;
    public:
        Sergeant() : name("sergeant") , Soldier() {
            cout << "Sergeant class initialization" << endl;
        }

        void training(int i) override {
            cout << name << " " << "훈련을 하나마나" << i * 10 << endl;
        }
};

int main(){
    Soldier *soldier;
    Sergeant sergeant;
    soldier = &sergeant;
    soldier->training(3);
}
Soldier class initialization
Sergeant class initialization
sergeant 훈련을 하나마나30

다음의 예제를 보도록 하자

virtual void training(int i){
    cout << name << " " << "헛둘헛둘" << i << endl;
}

Base 클래스인 Solider 클래스의 virtual 함수로 이전과는 달리 int 타입을 받는 인자가 하나 있다.

void training(int i) override {
    cout << name << " " << "훈련을 하나마나" << i * 10 << endl;
}

다음과 같이 override로 virtual 키워드인 함수를 오버라이딩하면 된다.

그럼 override키워드를 왜 사용하는 것일까?? 이는 나는 override 함수에요~! 를 컴파일러에게 알려주기 위한 것이다. 사실 override라고 안쓰고 virtual로 써도 문제는 없다.

class Soldier {
    string name;

    public:
        Soldier() : name("solider") {
            cout << "Soldier class initialization" << endl; 
        }

        virtual void training(int i){
            cout << name << " " << "헛둘헛둘" << i << endl;
        }
};

class Sergeant : public Soldier {
    string name;
    public:
        Sergeant() : name("sergeant") , Soldier() {
            cout << "Sergeant class initialization" << endl;
        }

        virtual void training(int i) {
            cout << name << " " << "훈련을 하나마나" << i * 10 << endl;
        }
};

다음과 같이 병장 클래스의 함수가 override하고 있는데 virtual 키워드로 써도되는 이유는 어찌됐거나 기반 클래스에서 해당 virtual 키워드를 보고나서 동적으로 자신이 무슨 객체인지를 찾고, 파생 클래스의 함수를 호출한다. 그런데 오버라이드된 파생 클래스도 virtual이지만 자신이 어떤 객체로 이루어져 있는 지 확인해보니 자기자신(군인) 객체로 이루어져 있기 때문에 자신의 함수를 호출한다.

그래서 virtual로 쓰나 override로 쓰나 문제없이 구동되는 것이다. 그런데 왜 override로 써줄까??

override 키워드가 붙으면 자신이 override하고 있는 함수가 없을 경우 컴파일 에러를 발생시킨다.

#include <iostream>

using namespace std;

class Soldier {
    string name;

    public:
        Soldier() : name("solider") {
            cout << "Soldier class initialization" << endl; 
        }

        virtual void training(int i){
            cout << name << " " << "헛둘헛둘" << i << endl;
        }
};

class Sergeant : public Soldier {
    string name;
    public:
        Sergeant() : name("sergeant") , Soldier() {
            cout << "Sergeant class initialization" << endl;
        }

        virtual void training(char i) {
            cout << name << " " << "훈련을 하나마나" << i * 10 << endl;
        }
};

int main(){
    Soldier *soldier;
    Sergeant sergeant;
    soldier = &sergeant;
    soldier->training(3);
}

만약 override 키워드를 사용하지 않고 virtual 키워드를 사용했다고 보자, 다음의 상황에서 training 함수를 보면

  • 기반 클래스(군인)의 training 함수
virtual void training(int i){
    cout << name << " " << "헛둘헛둘" << i << endl;
}
  • 파생 클래스(군인)의 training 함수
virtual void training(char i) {
    cout << name << " " << "훈련을 하나마나" << i * 10 << endl;
}

둘을 잘보면 비슷해보이는데 다르다. 함수의 인자 타입이 다르기 때문이다. 때문에 이들은 전혀 다른 함수가 된다.

따라서, 결과는 다음과 같다.

Soldier class initialization
Sergeant class initialization
solider 헛둘헛둘3

기반 클래스인 군인 클래스의 training 함수가 호출된 것을 확인할 수 있다.

기반 클래스의 training함수를 호출하는데 이는 virtual키워드로 자신 내부가 어떤 객체로 이루어져 있는지 보게된다. 자신이 병장 클래스 객체로 되어있어도 ,해당 함수를 가지고 있는건 군인 클래스 밖에 없기 때문에 군인 클래스의 함수를 호출하게 된다.

즉, 해당 함수가 override가 안되었기 때문에 virtual로 두어도 virtual이 없는 함수와 똑같은 결과를 반환하는 것이다.

그래서 override 키워드를 사용하는 것이다.

class Sergeant : public Soldier {
    string name;
    public:
        Sergeant() : name("sergeant") , Soldier() {
            cout << "Sergeant class initialization" << endl;
        }

        void training(char i) override {
            cout << name << " " << "훈련을 하나마나" << i * 10 << endl;
        }
};

만약 다음과 같이 병장 클래스의 training 함수에 override 키워드를 붙이면, 컴파일러는 애는 오버라이드하고 있는 부모 함수가 없는데??라고 하면서 컴파일 오류를 반환한다.

이렇게 override 키워드를 적어주어 이 함수가 override로 쓰인다라는 것을 명시적으로 해주는 효과와 자신이 진짜 override를 해주었는지 안해주었는지를 check해주는 기능을 해주는 것이다.

5. final 키워드

C++11에 추가된 final 키워드는 자신은 기반 클래스의 함수를 오버라이딩 하지만 이후부터는 더 이상 오버라이딩을 하지못하게하겠다 라는 것이다. 여기서 상속은 끝이다. 라는 기능을 한다.

class Soldier {
    string name;

    public:
        Soldier() : name("solider") {
            cout << "Soldier class initialization" << endl; 
        }

        virtual void training(int i){
            cout << name << " " << "헛둘헛둘" << i << endl;
        }
};

class Sergeant : public Soldier {
    string name;
    public:
        Sergeant() : name("sergeant") , Soldier() {
            cout << "Sergeant class initialization" << endl;
        }

        void training(int i) final {
            cout << name << " " << "훈련을 하나마나" << i * 10 << endl;
        }
};

class Corporal : public Sergeant {
    string name;
    public:
        Corporal() : name("sergeant") , Sergeant() {
            cout << "Corporal class initialization" << endl;
        }

        void training(int i) override {
            cout << name << " " << "훈련을 열심히 한다!" << i * 10 << endl;
        }
};

다음의 예제를 보자, 이번에는 Corporal(이등병) 함수를 추가하였다. 이등병 is a 병장은 마음에는 안들지만 그냥 그렇다고 치자 중요한건 final 키워드의 동작을 보는 것이기 때문이다.

이등병의 trainingSergeant(병장)을 상속하여 training 함수를 override하고 있다. 그러나, 이는 컴파일 단계에서 에러가 발생한다.

왜냐하면 Sergeant 클래스는 training 함수를 오버라이딩하지만, 이후부터는 오버라이딩 하지 말라고 final 키워드를 썼다.

중요한 것은 Sergeant 클래스가 final키워드를 썼다고해서 override를 안하는 것은 아니다. override는 하지만, '이후부터 자신을 상속하는 친구들은 이 함수를 override하지 못하게 할 것이다'. 라는 것이다.

0개의 댓글