728x90

황기태 저자의 명품 C++ Programming 개정판을 읽고 학습한 내용을 정리한 포스트입니다!

https://search.shopping.naver.com/book/catalog/32436115747

 

C++ Programming : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

 

상속 관계에서의 함수 재정의

  • 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