관리 메뉴

공부 기록장 💻

[C++] 가상 함수(Virtual Function) - 함수 재정의(function redefine)와 오버라이딩(Overriding), 동적 바인딩(Dynamic Binding), 순수 가상 함수(Pure Virtual Function), 추상 클래스(Abstract Class) 본문

# Language & Tools/C++

[C++] 가상 함수(Virtual Function) - 함수 재정의(function redefine)와 오버라이딩(Overriding), 동적 바인딩(Dynamic Binding), 순수 가상 함수(Pure Virtual Function), 추상 클래스(Abstract Class)

dream_for 2021. 5. 20. 23:57

(명품 C++ 프로그래밍 Ch 9)

 

 

함수 재정의 (function redefine)

- 함수는 파생 클래스에서 기본 클래스와 동일한 형식의 함수를 재정의하여 사용하는 것

 

다음과 같이 Base 기본 클래스를 상속한 파생 클래스 Derived는

Base의 멤버 함수인 f()를 재정의하여 구현하였다. 

 

class Base{
public:
	void f() { cout << "Base::f() called" << endl;
}

class Derived : public Base{
public:
	// Base의 멤버 f() 함수를 재정의
	void f() { cout << "Derived::f() called' << endl;
}

 

 

파생 클래스 포인터 -> 파생 클래스 객체의 함수 호출

 

파생 클래스 객체를 가리키는 파생 클래스 포인터로 

재정의된 함수를 호출하면, 파생 클래스에 작성된 멤버를 기본적으로 호출한다.

 

 

아래에서 파생 클래스의 포인터 pDer이 파생 클래스 객체 d를 가리킬 때 f()를 호출하면,

파생 클래스에서 재정의된 Derived의 멤버 f()가 호출된다.

 

재정의 되기 이전의, 기본 클래스의 멤버를 호출하려면 범위 지정 연산자(::)를 사용하면 된다.

 

Derived d, *pDer; // 파생 클래스 Derived의 객체 d 생성, 포인터 pDer 선언
pDer = &d; // 파생 클래스 포인터 -> 파생클래스 객체

pDer->f(); // Derived의 멤버 f() 호출
pDer->Base::f(); // Base의 멤버 f() 호출

 

 


업캐스팅을 통한 기본 클래스의 포인터 -> 파생 클래스 객체

 

이번에는 업캐스팅을 통해 기본 클래스의 포인터로 파생 클래스의 객체를 가리키는 경우 함수를 호출하는 경우이다.

 

 

pBase는 기본 클래스에 대한 포인터이므로, 기본 클래스의 멤버에만 접근할 수 있다.

따라서 f()를 호출하면, 기본 클래스 Base의 멤버 f()가 호출된다.

 

Base *pBase = pDer; // 업캐스팅. 기본 클래스의 포인터에 파생 클래스 객체의 주소 대입
pBase->f(); // Base의 멤버 f() 호출

 

 

 

위와 같은 재정의된 함수에 대한 호출 관계는 컴파일 시에 결정된다. (정적 바인딩)

 

 

 


가상 함수 오버라이딩 (Virtual Function Overriding)

 

함수 오버라이딩(Function Overriding)

- 파생 클래스에서 기본 클래스에 작성된 가상 함수를 재정의 하여, 기본 클래스에 작성된 가상 함수를 무력화시키는 것

- 기본 클래스의 포인터를 이용하든, 파생 클래스의 포인터를 이용하든, 가상 함수를 호출하면 파생 클래스에 오버라이딩된 함수가 항상 실행된다.

 

 

이전에 상속 선언을 할 때, virtual 키워드를 이용하여 가상 상속(virtual inheritance)를 하고, 다중 상속의 문제점을 해결하는 것에 대해 배웠다.

이번에는 가상 함수(virtual function) 에 대해 알아보자.

 


가상 함수(virtual function)

- virtual 키워드로 선언된 멤버 함수

- virtual: 컴파일러에게 자신에 대한 호출 바인딩을 실행 시간까지 미루도록 지시하는 키워드

 

 

가상 함수 선언

 

아래의 예시에서 기본 클래스 Base의 멤버 함수 f()를 가상 함수로 선언하였다.

 

class Base{ // 기본 클래스
public:
	virtual void f(); // 가상 함수 f() 선언
};

 

 

 

다음의 예제를 살펴보자. (virtual 키워드 생략)

A는 기본 클래스이고, B는 A를 상속받고, C는 B를 상속받는 계층적 구조이다.

 

 

#include <iostream>
using namespace std;

class A{
public:
	void func(){f();}
	virtual void f(){cout<<"A::f() called"<<endl;}
}; 

class B:public A{
public:
	void f(){cout<<"B::f() called"<<endl;}
};

class C:public B{
public:
	void f(){cout<<"C::f() called"<<endl;}
};

int main(){
	A *aP, a; 	aP = &a;
	B *bP, b;	bP = &b;
	C *cP, c;	cP = &c;
	
	cout<<"aP->f() : "; aP->f();
	cout<<"bP->f() : "; bP->f();
	cout<<"cP->f() : "; cP->f();
	
	aP = bP = cP; // 업캐스팅
	
	cout<<endl<<"업캐스팅 : aP=cP, bP=cP"<<endl;
	cout<<"aP->f() : "; aP->f();
	cout<<"bP->f() : "; bP->f();
	
	cout<<"aP->func() : "; aP->func();
}

 

A<-B<-C의 상속 계층. 그리고 A의 f()는 가상함수로 호출되었다.

B와 C 클래스 내의 f() 앞에는 virtual 선언이 없지만, 이는 virtual 가상 함수도 자동적으로 상속된다.

 

 

각 클래스의 포인터를 선언하고, 각 클래스의 객체의 주소를 대입하여 f()를 호출하면,

virtual 가상 함수로 작성된 A의 f()와 관계 없이 각 포인터의 클래스 타입에 따라 f()가 실행되는 것을 확인할 수 있다.

 

	A *aP, a; 	aP = &a;
	B *bP, b;	bP = &b;
	C *cP, c;	cP = &c;

 

 

다음과 같이 업캐스팅이 일어난 후에는,

virtual 키워드로 작성된 f() 가 객체의 클래스 타입에 따라 f()가 호출됨을 확인할 수 있다.

또한, aP 포인터로 A클래스 내의 func() 를 호출하게 되면,

func()내의 가상 함수 f()가 실행됨에 따라 C클래스 내의 f() 를 실행하게 된다.

 

(만약 virtual 키워드로 작성되지 않았다면, 포인터의 클래스 타입에 따라 함수가 호출될 것임)

 

 

	aP = bP = cP; // 업캐스팅
	
	cout<<endl<<"업캐스팅 : aP=cP, bP=cP"<<endl;
	cout<<"aP->f() : "; aP->f();
	cout<<"bP->f() : "; bP->f();
	
	cout<<"aP->func() : "; aP->func();

 

 

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


 

함수 오버라이딩(function overriding)

- 파생 클래스에서 기본 클래스의 가상 함수를 재정의하는 것

- 함수 재정의컴파일 시간 다형성(compie time polymorphism) 이라면, 오버라이딩행시간 다형성(run time polymorhpism)을 실현한다.

 

 

 

위에 함수 재정의(function redefine)를 설명할 때 사용한 예시를 이번에는 가상 함수로 작성한 것을 살펴보자.

기본 클래스와 파생 클래스의 멤버 f() 각각에 virtual 키워드를 사용하여 가상 함수로 선언/구현하였다.

 

이번에는 재정의(정적 바인딩-컴파일 시간에 결정)가 아닌,

오버라이딩(동적 바인딩-실행 시간에 결정)이 적용되었기 때문에, 

파생 클래스 객체가 생성될 때 기본클래스의 f(), 파생클래스의 f() 공간 모두 존재하지만,

기본 클래스의 f() 는 존재감을 잃고, 항상 Derived 파생 클래스의 f() 만이 호출된다.

 

class Base{
public:
	virtual void f() { cout << "Base::f() called" << endl; // 가상 함수
}

class Derived : public Base{
public:
	// Base의 멤버 f() 가상 함수 오버라이딩
	virtual void f() { cout << "Derived::f() called" << endl; // 오버라이딩
 }

 

기본 클래스의 포인터로 접근하든, 파생 클래스의 포인터로 접근하든,

항상 파생 클래스의 멤버 f() 가 호출된다. 

모든 호출은 실행 시간 중에 Derived 의 f() 함수로 동적 바인딩 된다.

 

 

 

 

오버라이딩의 목적

 

- 기본 클래스에 가상 함수를 만드는 목적은, 파생 클래스들이 자신의 목적에 맞게 가상 함수를 재정의 하도록 하는 것이다.

- 기본 클래스의 가상 함수는 상속 받는 파생 클래스에서 구현해야 할 일종의 함수 인터페이스를 제공한다.

- '하나의 인터페이스에 대해 서로 다른 모양의 구현' 이라는 객체 지향 언어의 다형성(polymorphsim)을 실현하는 도구이다.

 

 


동적 바인딩(Dynamic Binding)

- 실행 시간 바인딩(run-time binding), 늦은 바인딩(late binding)

- 가상 함수를 호출하는 코드를 컴파일 할 때, 컴파일러는 바인딩을 실행 시간에 결정하도록 미루어둔다. 나중에 가상 함수가 호출되면, 실행 중에 객체 내에 오버라이딩 된 가상함수를 동적으로 찾아 호출한다.

 

 

 

동적 바인딩이 발생하는 구체적 경우

 

- 파생 클래스의 객체에 대해, 기본 클래스의 포인터로 가상 함수가 호출될 때

 

  • 기본 클래스 내에 멤버 함수가 가상 함수 호출
  • 파생 클래스 내에 멤버 함수가 가상 함수 호출
  • main() 등 외부 함수에서 기본 클래스의 포인터로 가상 함수 호출
  • 다른 클래스에서 가상함수 호출

 


 

상속 관련 지시어 - override, final

 

 

1. override 지시어

컴파일러에게 오버라이딩을 확인하도록 지시한다.

- 파생 클래스에서 오버라이딩 하려는 가상 함수의 원형 바로 뒤에 작성한다.

 

 

다음과 같이 Shape을 상속하는 Rect 클래스가 있다고 할 때,

가상 함수로 선언된 draw() 를 Rect 클래스에서 오버라이딩하고자 시도했지만 타이핑 오류가 난 경우를 살펴보자.

컴파일러는 drow() 가 새로운 함수가 작성된 것으로 판단하고, 해당 함수를 정상적으로 호출하여 실해한다.

 

class Shape{
public:
	virtual void draw(); // 가상 함수
}

class Rect : public Shape{
public:
	// void draw();
	void drow(); // 오버라이딩하려고 시도했지만, 잘못 타이핑 한 경우
}

 

 

이번에는 override 지시어를 추가했다.

override 지시어를 사용하면, 컴파일러에게 오버라이딩을 확인하도록 지시하여

해당 함수가 기본 클래스에서 가상 함수로 작성되어 있지 않은 경우 컴파일 오류를 발생시킨다.

따라서 개발자는 함수 작성 실수를 금방 발견하게 된다.

 

class Rect : public Shape{
public:
	// void draw();
	void drow() override; // override 지시어 - 컴파일러에게 오버라이딩 확인하도록 지시
}

 

 


1. final 지시어

- 파생 클래스에서 오버라이딩 무력화시킴

- 클래스의 상속 자체를 금지시킴

 

 

 

1) 가상 함수 오버라이딩 무력화

- 기본 클래스에서 가상 함수 원형 뒤에 final 지시어를 작성하면, 파생 클래스에서는 이 가상함수를 오버라이딩 할 수 없게 된다.

 

class Shape{
public:
	virtual void draw() final; // draw()의 오버라이딩 금지 선언
}

class Rect : public Shape{
public:
	 void draw(); // 컴파일 오류 발생! - 금지된 오버라이딩 시도
}

 

 

 

2) 클래스 상속 금지

- 상속을 금지할 기본 클래스의 이름 바로 뒤에 final 지시어를 작성한다.

- 파생 클래스 또한 final로 선언할 수 있다.

 

class Shape final {...} // Shape의 상속 금지 선언

class Rect : public Shape {...} // 컴파일 오류! - Shape 상속 불가

 

 

 


C++ 오버라이딩 특징

 

  • 가상 함수의 이름과 매개변수 타입, 개수 뿐 아니라 리턴 타입도 일치해야 오버라이딩이 성공한다.
  • 오버라이딩 시, 파생 클래스에서의 virtual 키워드는 생략 가능하다. (virtual 속성 또한 상속됨)
  • 가상 함수의 접근 지정이 자유롭다.

 


 

범위 지정 연산자(::)를 이용해 무력화된 기본 클래스의 가상 함수 실행

 

- 오버라이딩에 의해 무시되고 존재감을 상실한 기본 클래스의 가상함수에 접근하고자 하면, 범위 지정 연산자(::)를 사용하면 된다.

- 가상 함수를 정적 바인딩으로 호출하게 된다.

 

 

다음과 같이 Shape 기본 클래스를 상속 받은 Circle 의 객체 circle이 있다.

draw() 함수는 가상 함수로 선언되었다고 할 때, 오버라이딩에 의해 기본 클래스의 포인터 또는 파생 클래스의 포인터로 draw()를 호출하면 파생 클래스의 오버라이딩 된 멤버 함수 draw() 가 호출된다.

 

하지만 범위 지정 연산자를 통해, 기본 클래스의 draw()를 호출하도록 하면,

정적 바인딩으로 무력화됐던 기본 클래스의 멤버 draw()가 실행되게 할 수 있다.

 

Circle circle; // Shape 을 상속한 Circle 파생 클래스의 객체 circle
Shape *pShape = &circle; // 기본 클래스의 포인터 -> 파생 클래스 객체

pShape->draw(); // 오버라이딩 - Circle의 draw() 호출
pShape->Shape::draw(); // Shape의 draw() 호출

 

 

 


가상 소멸자

 

- 기본 클래스의 소멸자를 만들 떄, 가상 함수로 작성할 것이 권유된다.

- 기본 클래스에 대한 포인터로 파생 클래스의 객체를 delete 하는 경우 정상적인 소멸이 되도록 하기 위해서이다.

 

 

동적 생성 - new, delete

다음과 같이 기본 클래스 Base의 포인터로 파생 클래스 Derived의 객체를 동적 생성하고,

p에 대한 메모리를 해제하도록 delete를 하면,

포인터 p가 Base 타입이므로 컴파일러는 ~Base() 소멸자만 호출하여 실행하고,

생성된 Derived의 객체에 대한 소멸자 ~Derived()는 실행되지 않는다.

 

Base *p = new Derived(); // 기본 클래스 포인터로 파생 클래스 객체 생성
delete p; // 기본 클래스의 소멸자만 실행

 

 

소멸자를 가상 함수로 선언 하면, 

동적 바인딩에 의해 ~Base() 에 대한 호출이 ~Derived() 에 대한 호출로 변경되어

 ~Derived() 코드 실행 후, ~Base() 에 대한 코드가 정상적으로 실행되어

기본/파생 클래스의 소멸자가 모두 순서대로 실행된다.

(생성자는 기본 클래스의 생성자부터, 소멸자는 파생 클래스의 소멸자부터 실행하도록 컴파일되므로)

 

class Base{
public:
	virtual ~Base(); // 가상 소멸자 -> 파생 클래스의 소멸자 호출
}

class Derived : public Base{
public:
	virtual ~Derived();
 }

 

위의 예시에서는, 소멸자가 가상함수로 선언되었기 때문에,

1) ~Base() 소멸자 호출

2) ~Derived() 소멸자 실행(동적 바인딩)

3) ~Base() 실행

순으로 정상적으로 기본/파생 클래스의 소멸자가 모두 실행된다.

 

 

 

 


상속과 오버로딩, 함수 재정의, 오버라이딩 비교

 

 

오버로딩 (Overloading) - 함수 중복

- 매개 변수의 타입이나 개수가 다른 함수들을 여러개 중복 작성

- 상속 관계에서도 기본 클래스의 이름이 같은, 매개 변수 ㅏㅌ입이나 개수가 다른 함수를 파생 클래스에서 멤버 함수로 작성 가능

 

오버 라이딩 (Overriding) - 가상 함수

- 기본 클래스의 가상 함수를 파생 클래스에서 재작성하여 동적 바인딩을 유발시키는 것 (실행 시간 바인딩)

 

함수 재정의 (function redefine)

- 가상 함수가 아닌 멤버 함수를 재작성하여 정적 바인딩으로 처리하는 것

 

 

 


순수 가상 함수 (Pure Virtual Function)

 

- 기본 클래스의 작성된 가상 함수의 목적은, 객체를 생성하고 해당 함수를 실행하는 것이 아닌, 파생 클래스에서 재정의하여 구현할 함수를 알려주는 인터페이스의 역할을 한다고 볼 수 있다.

- 따라서 실행이 목적이 아니라면, 굳이 기본 클래스의 가상 함수에 코드를 작성할 필요가 없다.

 

 

순수 가상 함수 : 코드가 없고 선언만 있는 가상 함수

- 원형 뒤에 =0; 으로 선언한다.

 

public Shape{
public:
	virtual void draw()=0; // 순수 가상 함수 선언
}

 


추상 클래스 (Abstract Class)

- 최소 하나의 순수 가상 함수를 가진 클래스 

- 추상 클래스는 실행 코드가 없는 순수 가상 함수를 가지고 있으므로 불완전한 클래스이다.

- 추상 클래스의 인스턴스(객체)를 생성할 수 없다.

 

 

추상 클래스의 목적과 용도

 

- 상속을 위한 기본 클래스로 활용하기 위함

- 파생 클래스가 구현할 함수의 원형을 보여주는 인터페이스 역할

- 추상 클래스로 기본 방향을 잡아놓고, 파생 클래스들을 목적에 따라 구현하면 작업이 용이해짐(응용 프로그램의 설계와 구현을 분리)

- 계층적 상속 관계를 가진 클래스들의 구조를 만들 때 적합

 

 

 

 

다음과 같이, 순수 가상 함수인 draw()를 가지고 있는 Shape 클래스는 추상 클래스라 할 수 있다.

추상 클래스를 상속받는 파생 클래스 또한 자동으로 추상클래스가 된다. (순수 가상 함수를 그대로 상속받음)

따라서 위의 Circle 클래스 또한 추상 클래스라 할 수 있다.

 

// 추상 클래스 Shape
class Shape{
public:
	void paint(){
    	draw(); // 순수 가상 함수 호출
    }
    virtual void draw()=0; // 순수 가상 함수 
};

// 추상 클래스 Shape을 상속 받은 Circle 또한 추상 클래스
class Circle : public Shape { ... };

 

 

 

아래처럼 포인터를 선언하는 것은 가능하지만,

추상 클래스의 객체를 생성하는 것은 불가능하다. (**추상 클래스를 인스턴스화할 수 없다는 컴파일 오류 발생)

 

Shape *p; // 추상 클래스의 포인터 선언

// Shape shape; // 컴파일 오류 ! - 추상 클래스의 객체 생성 불가능
// Shape *p = new Shape(); // 컴파일 오류

// Circle circle; // 컴파일 오류
// Circle *c = new Circle(); // 컴파일 오류

 

 

 

 


추상 클래스의 구현

 

- 추상 클래스의 순수 가상 함수의 코드를 작성하는 것을 의미한다.

- 모든 순수 가상함수를 오버라이딩 하여 구현해야만, 정상 클래스처럼 작동할 수 있게 된다.

 

추상 클래스 Shape을 상속 받은 Circle 클래스 내에서

순수 가상 함수였던 draw()를 오버라이딩 하여 구현하였다.

더이상 Circle 클래스는 추상 클래스가 아니며, 정상적으로 객체를 생성할 수 있는 클래스로써 작동한다.

// 추상 클래스 Shape
class Shape{
public:
	void paint(){
    	draw(); // 순수 가상 함수 호출
    }
    virtual void draw()=0; // 순수 가상 함수 
};

// 순수 가상 함수를 구현하여 정상 클래스가 된 Circle 클래스
class Circle : public Shape {
public:
	virtual void draw() { cout << "Circle" << endl; } // 순수 가상 함수 구현
};

 

 

728x90
반응형
Comments