C++/C++ 기초

[C++] 순수 가상 함수와 추상 클래스 ( Pure virtual function and Abstract class )

Song 컴퓨터공학 2023. 7. 17. 19:25
 

[C++] 가상 함수와 오버라이딩 ( Virtual function and overriding )

오버라이딩이란, 부모 클래스로부터 상속받은 메소드의 내용을 재정의(변경) 해야 하는 것을 오버라이딩이라고 합니다. 다른 말로 파생 클래스에서 기본 클래스에 작성된 가상 함수를 재작성하

songsite123.tistory.com

 

위 포스팅에서 가상함수와 오버라이딩의 개념에 대해 알아보았습니다. 이번 포스팅에서는 순수 가상 함수를 사용한 추상 클래스에 대해 알아보도록 하겠습니다.

 

 

 순수 가상 함수 (Pure virtual function)

먼저 순수 가상 함수(Pure virtual function) 이란 함수의 코드가 없고 선언만 있는 가상 함수 를 말합니다. 순수 가상 함수는 멤버 함수의 원형 뒤에 = 0; 으로 선언하며 아래는 예시입니다.

class Base{
public:
	virtual void f() = 0;
}

위처럼 아무 코드 없이 선언만 하고 = 0; 을 붙여 선언한 함수가 순수 가상 함수입니다. 순수 가상 함수는 코드가 없으므로 따로 구현해서는 안됩니다. 굳이 이를 이용하는 이유는 위 링크의 포스팅을 읽고 오셨으면 유추할 수 있습니다. 바로 파생 클래스에서 이를 오버라이딩 해서 사용하기 위함이죠.

 

 

 추상 클래스 (Abstract class)

 

순수 가상 함수가 하나라도 포함되어 있는 클래스를 추상 클래스(Abstract class) 라고 합니다. 추상 클래스는 실행 코드가 없는 순수 가상 함수를 가지고 있기 때문에 불완전한 클래스 입니다. 그렇기 때문에 추상 클래스의 인스턴스(객체)를 생성하려는 코드를 실행시키면 컴파일 오류가 발생합니다.

class Base{
public:
	virtual void f() = 0;
}

int main(){
	Base B;			// 컴파일 오류. Base 는 추상 클래스
	Base* bp = new B;	// 컴파일 오류. Base 는 추상 클래스
	Base* Bp;		// 추상 클래스의 포인터만 선언하는 것은 가능
}

 

객체도 생성할 수 없는 이 추상 클래스는 어디에 써야할까요? 추상 클래스는 상속을 위한 기본 클래스로 사용하는 것이 목적입니다. 추상 클래스의 모든 멤버를 순수 가상 함수로 선언할 필요는 없으나, 순수 가상 함수를 통해 파생 클래스가 구현할 함수의 원형을 보여주는 일종의 인터페이스 역할을 하게 됩니다. 또한 추상 클래스를 상속 받은 클래스 또한 순수 가상 함수를 포함하기 때문에 추상 클래스입니다.

 

이러한 추상 클래스를 구현하여 사용하려면, 상속 받은 추상 클래스의 모든 순수 가상 함수를 오버라이딩하여 구현해야 합니다. 순수 가상 함수를 모두 구현하면 그 클래스는 더 이상 추상 클래스가 아니고 따라서 객체도 생성하고 사용할 수 있는 온전한 클래스가 됩니다.

#include <iostream>
using namespace std;

class Calculator {
public:
	virtual int add(int a, int b) = 0;
	virtual int substract(int a, int b) = 0;
	virtual double average(int a[], int size) = 0;
};

class GoodCalc : public Calculator {
public:
	int add(int a, int b) { return a + b; }
	int substract(int a, int b) { return a - b; }
	double average(int a[], int size) {
		double sum = 0;
		for (int i = 0; i < size; i++)
			sum += a[i];
		return sum / size;
	}
};

int main() {
	int a[] = { 1, 2, 3, 4, 5 };
	Calculator* p = new GoodCalc();
	cout << p->add(2, 3) << endl;
	cout << p->substract(2, 3) << endl;
	cout << p->average(a, 5) << endl;
	delete p;
}

순수 가상 함수로 이루어진 Calculator 클래스는 GoodCalc 클래스에서 모두 오버라이딩 되기 때문에 GoodCalc 클래스는 추상 클래스가 아닙니다. GoodClac 의 내부 동작을 몰라도 Calculator 클래스만 안다면 사용하는 것에는 지장이 없겠죠? 이것이 지난 포스팅에서 말했던 내용들입니다.

#include <iostream>
using namespace std;

class Calculator {
	void input() {
		cout << "정수 2개를 입력하세요>> ";
		cin >> a >> b;
	}
protected:
	int a, b;
	virtual int calc(int a, int b) = 0;
public:
	void run() {
		input();
		cout << "계산된 값은 " << calc(a, b) << endl;
	}
};

class Adder : public Calculator {
public:
	int calc(int a, int b) { return a + b; }
};

class Substractor : public Calculator {
public:
	int calc(int a, int b) { return a - b; }
};

int main() {
	Adder adder;
	Substractor substractor;
	adder.run();
	substractor.run();
}

혹은 이렇게 공통된 기능들은 전부 기본 클래스에 구현되어 있고, 달라지는 연산 부분만 파생 클래스로 구현하여 상황에 맞는 객체에 대해 연산을 진행할 수도 있습니다. 위 같은 예시들 처럼 프로그램을 작성할 때 추상 클래스로 기본 방향을 잡아놓고, 파생 클래스를 목적에 따라 구현하면 작업이 쉬워집니다.