[전공]

[명품 C++] 9장 가상 함수와 추상 클래스

danhan 2022. 6. 6. 16:16

출처 :

  • 명품 C++ Programming (저자 황기태)
  • 객체지향프로그래밍

파생 클래스에서 함수를 재정의

#include <iostream>
using namespace std;

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

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

void main() {
	Derived d *pDer; // Derived 포인터 - 파생 클래스 객체를 가리킴
	pDer = &d;
	pDer->f();       // Derived::f() 호출

	Base* pBase;
	pBase = pDer;    // 업캐스팅, pDer은 d를 가리킴, d의 부모 클래스 Base를 가리킴
	pBase->f();      // pBase로 Derived 고유멤버에 접근 불가능
                   // Base::f() 호출

가상 함수와 오버라이딩

가상 함수 virtual function

  • virtual 키워드로 선언된 멤버 함수
  • virtual 키워드의 의미
    • 동적 바인딩 지시어
    • 컴파일러에게 함수에 대한 호출 바인딩을 실행 시간까지 미루도록 지시
    class Base {
    public:
    	virtual void f();
    };
    

함수 오버라이딩 function overriding

  • 파생 클래스에서 기본 클래스의 가상 함수와 동일한 이름의 함수 선언
    • 기본 클래스의 가상 함수의 존재감 상실시킴
    • 파생 클래스에서 오버라이딩한 함수가 호출되도록 동적 바인딩
    • 함수 재정의라고도 부름
    • 다형성의 한 종류

오버라이딩과 가상 함수 호출

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

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

int main() {
	Derived d, *pDer;
	pDer = &d;
	pDer->f();     // Derived::f() 호출
	Base * pBase;
	pBase = pDer;  // 업 캐스팅 pDer을 Base로 본다.
	pBase->f();    // 동적 바인딩 발생!! Derived::f() 실행
}

오버라이딩의 목적

  • 파생 클래스에서 구현할 함수 인터페이스 제공
  • 파생 클래스의 다형성
  • 다형성 실현
    • draw() 가상 함수를 가진 기본 클래스 Shape
    • 오버 라이딩을 통해 Circle, Rect, Line 클래스에서 자신만의 draw() 구현
    // 포인터형은 Shape으로 되어있지만 Circle, Rect, Line 각자 다르게 그려짐
    void paint(Shape* p) {
    	// 파생클래스의 draw()가 실행됨
    	p->draw();
    }
    
    paint(new Circle()); // Circle을 그린다.
    paint(new Rect());   // Rect을 그린다.
    paint(new Line());   // Line을 그린다.
    

동적 바인딩

  • 파생 클래스에 대해 기본 클래스에 대한 포인터로 가상 함수를 호출하는 경우
  • 객체 내에 오버라이딩한 파생 클래스의 함수를 찾아 실행
    • 실행 중에 이루어짐 → 실행시간 바인딩, 런타임 바인딩, 늦은 바인딩

오버라이딩된 함수를 호출하는 동적 바인딩

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();              // 동적 바인딩되어 Circle의 paint()가 불림
	delete pShape;
}

C++ 오버라이딩의 특징

  • 오버라이딩의 성공 조건
    • 가상 함수 이름, 매개 변수 타입과 개수, 리턴 타입이 모~~두 일치
    • 오버라이딩 시 파생 클래스에서 virtual 지시어 생략 가능
    • 가상 함수의 접근 지정 private, protected, public 중 자유롭게 지정

상속이 반복되는 경우 가상 함수 호출

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

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

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

int main() {
	GrandDerived g;
	Base *bp;
	Derived *dp;
	GrandDerived *gp;

	bp = dp = gp = &g;  // 업캐스팅

	// 동적 바인디에 의해 모두 GrandDerived의 f() 호출
	bp->f();
	dp->f();
	gp->f();
}

오버라이딩과 범위 지정 연산자 ::

  • 범위 지정 연산자
    • 정적 바인딩 지시
    • 기본클래스::가상함수() 형태로 기본 클래스의 가상 함수를 정적으로 호출
class Shape {
public:
	virtual void draw() { cout << "--Shape--"; }
};

class Circle : public Shape {
public:
	virtual void draw() {
		// 기본 클래스의 draw() 호출
		Shape::draw(); 
		cout << "Circle" << endl;
	}
};

int main() {
	Circle circle;
	Shape * pShape = &circle;
	// 동적바인딩 Circle의 draw() 호출
	pShape->draw();
	// 정적바인딩 Shape의 draw() 호출
	pShape->Shape::draw();
}

가상 소멸자

  • 소멸자를 virtual 키워드로 선언
  • 소멸자 호출 시 동적 바인딩 발생
class Base {
public:
	virtual ~Base();
};

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

int main() {
	Base *p = new Derived();
	delete p;
	// ~Base() 호출
	// ~Derived() 실행 - 동적 바인딩에 의해 실행, 소멸은 파생클래스부터
	// ~Base() 실행
}

// virtual 선언이 안되어있을 경우 ~Base()만 실행되고, 파생클래스는 소멸할 수 없음

소멸자를 가상 함수로 선언

class Base {
public:
	virtual ~Base() { cout << "~Base()" << endl; }
};

class Derived: public Base {
public:
	virtual ~Derived() { cout << "~Derived()" << endl; }
};

int main() {
	Derived *dp = new Derived();
	Base *bp = new Derived();
	delete dp; // Derived의 포인터로 소멸
	delete bp; // Base의 포인터로 소멸
}

// 파생클래스를 소멸하면 파생 - 기본 순으로 소멸 ~Derived()  ~Base() 순서로 소멸
// 업캐스팅된 bp를 소멸하면 동적바인딩이 일어나 ~Derived() ~Base() 순서로 소멸

가상 함수와 오버라이딩 활용 사례

가상 함수를 가진 기본 클래스의 목적 (기출)

  • 링크드 리스트
#include <iostream>
#include "Shape.h"
#include "Circle.h"
#include "Rect.h"
#include "Line.h"
using namespace std;

int main() {
	Shape *pStart=NULL;
	Shape *pLast;
	pStart = new Circle(); // 처음에 원 도형을 생성한다.
	pLast = pStart;
	pLast = pLast->add(new Rect());
	pLast = pLast->add(new Circle()); 
	pLast = pLast->add(new Line());
	pLast = pLast->add(new Rect());

	// 현재 연결된 모든 도형을 화면에 그린다.
	Shape* p = pStart;
	while(p != NULL) {
	 // 동적 바인딩으로 Circle, Rect, Line의 draw() 호출
		p->paint();      
		p = p->getNext();
	}

	// 현재 연결된 모든 도형을 삭제한다.
	p = pStart;
	while(p != NULL) {
		// 다음 도형 주소 기억
		Shape* q = p->getNext();
		// 기본 클래스의 가상 소멸자 호출 -  현재 도형 객체 소멸
		delete p;
		// 다음 도형 주소를 p에 저장
		p = q;
	}
}

Circle
Rectangle
Circle
Line
Rectangle

순수 가상 함수

  • 기본 클래스의 가상 함수 목적
    • 파생 클래스에서 재정의할 함수를 알려주는 역할
    • 실행할 코드를 작성할 목적이 아님
    • 굳이.. 기본 클래스의 가상 함수를 구현할 필요가 있을까??
  • 순수 가상 함수
    • 함수의 코드가 없고 선언만 있는 가상 멤버 함수
class Shape {
public:
	virtural void draw = 0;
};

추상 클래스

  • 하나 이상의 순수 가상 함수를 가진 클래스
  • 온전한 클래스가 아니므로 객체 생성 불가능
    • 순수 가상 “함수”는 호출은 가능하다.
  • 추상 클래스 포인터는 선언 가능 - 동적 바인딩하기 위해

추상 클래스의 목적

  • 추상 클래스의 인스턴스를 생성할 목적 아님
  • 상속에서 기본 클래스의 역할을 하기 위함
    • 순수 가상 함수를 통해 파생 클래스의 구현할 함수의 원형을 보여주는 인터페이스 역할
    • 추상 클래스의 모든 멤버 함수를 순수 가상함수로 선언할 필요는 없음

추상 클래스의 상속과 구현

  • 추상 클래스를 단순 상속하면 자동 추상 클래스
class Shape {  // 추상 클래스
public:
	virtual void draw() = 0;
};

class Circle : public Shape {  // 단순 상속받은 Circle도 추상 클래스
public:
	string toString() { return "Circle 객체"; }
};
  • 추상 클래스를 상속받은 파생 클래스를 추상 클래스로 만들지 않으려면 순수 가상 함수를 오버라이딩해야함
class Shape {
public:
	virtual void draw() = 0;
};

class Circle : public Shape {
public:
	// Circle을 추상클래스로 만들지 않으려면
	// 순수 가상 함수 draw()를 overriding 해야함
	virtual void draw() {
	cout << "Circle";
	}
	string toString() { return "Circle 객체"; }
};

추상 클래스 구현 연습

class Calculator {
public:
	virtual int add(int a, int b) = 0; // 두 정수의 합 리턴
	virtual int subtract(int a, int b) = 0; // 두 정수의 차 리턴
	virtual double average(int a [], int size) = 0; // 배열 a의 평균 리턴. size는 배열의 크기
};

class GoodCalc : public Calculator {
public:
	int add(int a, int b) { return a + b; }
	int subtract(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->subtract(2, 3) << endl;
	cout << p->average(a, 5) << endl;
	delete p;
}
#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 {
protected:
	// 순수 가상 함수 구현
	int calc(int a, int b) { return a + b; }
};
	
class Subtractor : public Calculator {
protected:
	// 순수 가상 함수 구현
	int calc(int a, int b) { return a - b; }
};

int main() {
Adder adder;
Subtractor subtractor;
adder.run();
subtractor.run();
}