728x90
황기태 저자의 명품 C++ Programming 개정판을 읽고 학습한 내용을 정리한 포스트입니다!
https://search.shopping.naver.com/book/catalog/32436115747
상속 관계에서의 함수 재정의
- c++에서는 파생 클래스에 기본 클래스의 멤버 함수와 동일한 이름과 원형으로 함수를 재정의(redefine)하여 사용할 수 있다.
- 아래와 같이 함수 재정의를 해준 뒤 호출해보자.
#include <iostream>
using namespace std;
class Base {
public:
void f() { cout << "Base::f() called" << endl; }
};
class Derived : public Base {
public:
void f() { cout << "Derived::f() called" << endl; }
};
void main() {
Derived d, *pDer;
pDer = &d; // 객체 d를 가리킨다.
pDer->f(); // Derived의 멤버 f() 호출
Base* pBase;
pBase = pDer; // 업캐스팅. 객체 d를 가리킨다.
pBase->f(); // Base의 멤버 f() 호출
}
/*
Derived::f() called
Base::f() called
*/
업캐스팅을 통해 pBase가 객체 d를 가리키지만, pBase는 Base 클래스에 대한 포인터이므로 컴파일러는 Base의 멤버함수 f()를 호출한다.
- 위의 예제에서 Base의 멤버 함수 f()와 완전히 동일한 원형으로 멤버 함수 f()를 Derived에 재정의하였다.
- 상속에 있어 기본 클래스의 멤버 함수로 원하는 작업을 할 수 없는 경우, 파생 클래스에서 동일한 원형으로 함수를 재정의하여 해결할 수 있다.
- 즉, 파생 클래스에서 기본 클래스와 동일한 형식의 함수를 재정의하는 경우, 기본 클래스에 대한 포인터로는 기본 클래스의 함수를 호출하고, 파생 클래스의 포인터로는 파생 클래스에 작성된 함수를 호출한다.
- 이런 호출 관계는 컴파일 시에 결정된다 (정적 바인딩).
Tip!! 범위 지정 연산자로 Base 멤버 접근 가능
- 범위 지정 연산자(::)를 사용하면 기본 클래스의 멤버 함수와 파생 클래스에 재정의된 함수를 구분하여 호출 할 수 있다.
pDer->f(); // Derived의 멤버 f() 호출
pDer->Base::f(); // Base의 멤버 f() 호출
가상 함수와 오버라이딩
- 가상 함수(virtual function)과 오버라이딩(overriding)은 상속에 기반을 둔 기술로서 객체 지향 언어의 꽃으로서 상속을 활용한 소프트웨어 재사용이 가능하게 한다.
오버라이딩 개념
- 아래는 오버라이딩의 예시이다.
- 기태네 집에 '새로운 기태'가 들어와서 '원래 기태'를 무력화시키면, '새로운 기태'가 기태네 집의 주인이 되며, 다른 사람이 바깥에서 기태를 부르면 항상 '새로운 기태'가 대답한다.
- 즉, 오버라이딩은 파생 클래스에서 기본 클래스에 작성된 가상 함수를 재작성하여, 기본 클래스에 작성된 가상 함수를 무력화시키고, 객체의 주인 노릇을 하는 것
- 따라서, 기본 클래스의 포인터를 이용하든 파생클래스의 포인터를 이용하든 가상 함수를 호출하면, 파생 클래스에 오버라이딩된 함수가 항상 실행
가상 함수 선언과 오버라이딩
- 가상 함수(virtual function)란 virtual 키워드로 선언된 멤버 함수
- virtual은 컴파일러에게 자신에 대한 호출 바인딩을 실행 시간까지 미루도록 지시하는 키워드
- 가상 함수는 기본 클래스나 파생 클래스 어디에서나 선언될 수 있다.
class Base {
public:
virtual void f(); // f()는 가상 함수
};
- 파생 클래스에서 기본 클래스의 가상 함수를 재정의하는 것을 '함수 오버라이딩(function overriding)' 또는 '오버라이딩'이라고 한다.
- 또한, 멤버 함수에만 적용되므로 변수 오버라이딩이라는 용어는 없다.
- 앞서 살펴본 함수 재정의가 컴파일 시간 다형성(compile time polymorphism)이라고 한다면, 오버라이딩은 실행 시간 다형성(run time polymorphism)을 실현한다.
- 위의 그림과 같이 가상 함수를 재정의 하는 경우와 아닌 경우에 따라 프로그램의 실행이 완전히 달라진다.
- 가상 함수를 재정의하는 오버라이딩의 경우 함수가 호출되는 실행 시간에 동적 바인딩이 바생
- 반대의 경우 컴파일 시간에 결정된 함수가 단순히 호출(정적 바인딩)
- JAVA의 경우 함수 재정의는 곧 오버라이딩이며, 무조건 동적 바인딩이 일어나 이런 혼란이 발생하지 않는다.
오버라이딩과 가상 함수 호출
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f() called" << endl; }
};
class Derived : public Base {
public:
virtual void f() { cout << "Derived::f() called" << endl; }
};
void main() {
Derived d, *pDer;
pDer = &d; // 객체 d를 가리킨다.
pDer->f(); // Derived::f() 호출
Base* pBase;
pBase = pDer; // 업캐스팅. 객체 d를 가리킨다.
pBase->f(); // 동적 바인딩 발생!! Derived::f() 실행
}
/*
Derived::f() called
Derived::f() called
*/
- 함수 재정의 예제에서 virtual 키워드로 선언해준 것만 다르다.
- 따라서 결과가 이전의 함수 재정의와는 다르게 Derived가 2번 호출된다.
- 이유는 객체 d에는 2개의 f() 함수가 있으나, Derived의 f()가 Base f()를 무시하도록 오버라이딩되었기 때문이다.
오버라이딩 목적
- 기본 클래스에 가상 함수를 만드는 목적은 파생 클래스들이 자신의 목적에 맞게 가상 함수를 재정의 하도록 하는 것
- 기본 클래스의 가상 함수는 상속받는 파생 클래스에서 구현해야 할 일종의 함수 인터페이스를 제공
- 다시 말해, 가상 함수는 '하나의 인터페이스에 대해 서로 다른 모양의 구현'이라는 OOP 언어의 다형성(polymorphism)을 실현하는 도구
- Shape은 기본 클래스로서, 여러 종류의 파생 클래스에게 상속된다.
- draw() 함수를 가상 함수로 선언하였기에, 파생 클래스들은 draw() 함수를 오버라이딩하여 자신만의 모양으로 나타낸다.
- 이것이 오버라이딩을 통한 다형성의 실현이다.
동적 바인딩
동적 바인딩: 오버라이딩된 함수가 무조건 호출
- 가상 함수 호출하는 코드 컴파일 시, 컴파일러는 바인딩을 실행 시간에 결정하도록 미룬다.
- 나중에 가상 함수가 호출되면, 실행 중에 객체 내에 오버라이딩된 가상 함수를 동적으로 찾아 호출
- 이 과정을 동적 바인딩(dynamic binding) = 실행 시간 바인딩(run time binding) = 늦은 바인딩(late binding)이라고 부른다.
동적 바인딩이 발생하는 구체적 경우
- 기본 클래스의 객체에 대해서는 가상 함수가 호출되더라도 동적 바인딩은 일어나지 않는다. => 객체 내에 오버라이딩된 가상 함수가 없기 때문
- 동적 바인딩은 파생 클래스의 객체에 대해, 기본 클래스의 포인터로 가상 함수가 호출될 때 일어난다.
- 기본 클래스 내의 멤버 함수가 가상 함수 호출
- 파생 클래스 내의 멤버 함수가 가상 함수 호출
- main()과 같은 외부 함수에서 기본 클래스의 포인터로 가상 함수 호출
- 다른 클래스에서 가상 함수 호출
- 위의 예시들이 동적 바인딩이 발생하는 구체적인 예시이며, 가상 함수를 호출하면 무조건 동적 바인딩을 통해 파생 클래스에 오버라이딩된 가상 함수가 실행된다.
동적 바인딩 사례
- 다음 2가지 사례를 보자.
- 첫 번째 코드의 경우 자연스럽게 Shape의 draw()가 실행된다.
#include <iostream>
using namespace std;
class Shape {
public:
void paint() {
draw();
}
virtual void draw() {
cout << "Shape::draw() called" << endl;
}
};
int main() {
Shape* pShape = new Shape();
pShape->paint();
delete pShape;
}
// Shape::draw() called
- 아래 코드의 경우 Shape를 상속받고 draw()를 오버라이딩한 Circle 클래스가 있다.
- 실행 결과 paint() 함수는 pShape이 가리키는 객체 속에 오버라이딩한 draw()가 있는 것을 발견하고, 동적 바인딩을 통해 Circle의 draw()를 호출한다.
- 이처럼 기본 클래스에서 자신의 멤버를 호출하더라도 그것이 가상 함수이면 역시 동적 바인딩이 발생한다.
- 또한, 한 가지 재밌는 점은, Shape의 paint() 개발자는 파생 클래스의 개발자가 작성할 draw()를 미리 호출하고 있다는 점이며, 이런 부분은 추상 클래스를 사용할 때 더욱 두각을 나타낸다.
#include <iostream>
using namespace std;
class Shape {
public:
void paint() {
draw();
}
virtual void draw() {
cout << "Shape::draw() called" << endl;
}
};
class Circle : public Shape {
public:
virtual void draw() {
cout << "Circle::draw() called" << endl;
}
};
int main() {
Shape *pShape = new Circle();
pShape->paint();
delete pShape;
}
// Circle::draw() called
Tip!! override와 final 지시어
override 지시어
- override 지시어를 사용하면 개발자의 타이핑 실수 등을 컴파일 할 때 부터 발견 할 수 있다.
- override는 컴파일러에게 오버라이딩을 확인하도록 지시하는 것으로, 파생 클래스의 오버라이딩하려는 가상 함수의 원형 바로 뒤에 작성
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw(){
cout << "shape";
}
};
class Rect : public Shape {
public:
void draw() override { // override는 draw()가 Shape의 가상 함수를 오버라이딩하고 있는지 확인하라는 지시
cout << "rect";
}
// 아래 코드의 경우 drow()가 작성된 것으로 판단하고 오류를 발생시키지 않는다.
// void drow();
// 아래 코드와 같이 하면, 컴파일러가 Shape 클래스에 가상함수로 선언된
// drow() 함수를 찾을 수 없어 컴파일 오류를 발생시킨다.
/* void drow() override {
cout << "rect";
} */
};
int main() {
Rect r;
r.draw();
}
final 지시어
- final 지시어를 사용하면 파생 클래스에서 오버라이딩을 할 수 없게 하거나, 클래스의 상속 자체를 금지할 수 있다.
- 첫째, final 지시어를 가상 함수의 원형 바로 뒤에 작성하면, 파생 클래스는 이 가상 함수를 오버라이딩 할 수 없다.
// 1. 오버라이딩을 금지하는 final 사용 사례
class Shape1 {
public:
virtual void draw() final { // draw()의 오버라이딩 금지 선언
cout << "shape";
}
};
class Rect1 : public Shape1 {
public:
void draw() { // Shape의 draw()를 오버라이딩할 수 없어 컴파일 오류 발생
cout << "rect";
}
};
- 둘째, 클래스 이름 바로 뒤에 final을 작성하면 다른 클래스는 이 클래스를 상속받을 수 없다. 즉, Shape 클래스는 어떤 클래스에도 상속되지 않는다.
// 2. 클래스의 상속을 금지하는 final 사용 사례
class Shape2 final { // Shape2의 상속 금지 선언
};
class Rect2 : public Shape2 { // 상속받을 수 없는 Shape2를 상속받으려고 하여 컴파일 오류
};
- 파생 클래스 Rect도 final로 아래와 같이 사용할 수 있다.
// 3. 클래스의 상속을 금지하는 final 사용 사례
class Shape3 {
};
class Rect2 final : public Shape3 { // Rect2의 상속 금지 선언
};
class RoundRect : public Rect2 { // Rect2를 상속받으려고 하여 컴파일 오류
};
int main() {
}
728x90
'Programming Language > C++' 카테고리의 다른 글
[C++] 가상 함수와 오버라이딩의 활용 사례 (1) | 2023.12.18 |
---|---|
[C++] 오버라이딩 (0) | 2023.12.18 |
[C++] 다중 상속과 가상 상속 (0) | 2023.12.15 |
[C++] public, protected, private 상속 (0) | 2023.12.14 |
[C++] 상속과 생성자, 소멸자 (1) | 2023.12.14 |