9장. 가상 함수와 추상 클래스
파생클래스에서 함수를 재정의하는 사례
•
함수 중복
상속을 사용하는데 있어서, Base Class 의 Destructor는 virtual public 이거나 protected 여야 한다.
→ protected로 선언이 되면, Base Class를 object로 만들지 않겠다는 뜻
→ 거의 모든 경우에는 virtual public으로 선언이 된다
가상 함수와 오버라이딩
가상 함수 (Virtual Function)
•
virtual 키워드의 의미
◦
동적 바인딩 지시어
◦
컴파일러에게 함수에 대한 호출 바인딩을 실행 시간까지 미루도록 지시
•
함수 오버라이딩 (Function Overriding) → 함수 중복(파생클래스 중복함수, 기본클래스 함수 겹침)
◦
파생 클래스에서 기본 클래스의 가상 함수와 동일한 이름의 함수 선언
▪
기본 클래스의 가상 함수의 존재감 상실시킴
▪
파생 클래스에서 Overriding한 함수가 호출되도록 동적 바인딩
▪
함수 재정의
▪
다형성의 한 종류
함수 재정의와 오버라이딩 사례 비교
함수 재정의 (redefine)
class Base {
public:
void f() {
cout << "Base::f() called" << endl;
}
};
class Derived : public Base {
public:
void f() {
cout << "Derived::f() called" << endl;
}
};
C++
복사
→ 동등한 호출 기회를 가진 함수 f()가 두 개 존재
오버라이딩 (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;
}
};
C++
복사
→ 두 개의 함수 f()가 존재하지만, Base의 f()는 존재감을 잃고, 항상 Derived의 f()가 호출된다.
가상 함수를 재정의 하는 것 → 오버라이딩
가상 함수를 재정의 하지 않는 것 → 함수 재정의
Java의 경우, 멤버 함수가 가상이냐, 아니냐 구분되지 않으며, 함수 재정의는 곧 오버라이딩이며, 무조건 동적 바인딩이다 일어난다.
예제 9-2) 오버라이딩과 가상함수 호출
#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; }
};
int main() {
Derived d, *pDer;
pDer = &d;
pDer->f();
// Derived::f() 호출
Base *pBase;
pBase = pDer; // 업 캐스팅
pBase->f();
// 동적 바인딩 발생!! Drived::f() 실행
}
C++
복사
오버라이딩의 목적
•
파생 클래스에서 구현할 함수 인터페이스 제공 (파생 클래스의 다형성)
다형성의 실현 예)
•
draw() 가상함수를 가진 기본 클래스 Shape
•
오버라이딩을 통해 Circle, Rect, Line 클래스에서 자신만의 draw() 구현
class Shape {
protected:
virtual void draw() { }
// 가상함수 선언,
// 파생클래스에서 재정의할 함수에 대한 인터페이스 역할
};
class Circle : public Shape {
protected:
virtual void draw() {
// Circle을 그린다
// 오버라이딩, 다형성 실현
}
};
class Rect : public Shape {
protected:
virtual void draw() {
// Rect를 그린다
}
};
class Line : public Shape {
protected:
virtual void draw() {
// Line을 그린다
}
};
void paint(Shape* p) {
p->draw();
// p가 가리키는 객체에 오버라이딩된 draw() 호출
}
paint(new Circle()); // Circle을 그린다
paint(new Rect()); // Rect을 그린다
paint(new Line()); // Line을 그린다
C++
복사
동적 바인딩
동적 바인딩
•
파생클래스에 대해
•
기본 클래스에 대한 포인터로, 가상함수를 호출하는 경우
•
객체 내에 오버라이딩한 파생클래스의 함수를 찾아 실행
◦
실행 중에 이루어짐
▪
실행시간 바인딩, 런타임 바인딩, 늦은 바인딩 으로 불림
C++ 오버라이딩 특징
•
오버라이딩 성공 조건
◦
가상함수 이름
◦
매개변수 타입과 개수
◦
리턴 타입이 모두 일치
•
오버라이딩 시 virtual 지시어 생략 가능
◦
가상함수의 virtual 지시어는 상속됨,
◦
파생클래스에서 virtual 생략 가능
•
가상함수의 접근 지정
◦
private, protected, public 중 자유롭게 지정
예제 9-3) 상속이 반복되는 경우, 가상함수 호출
#include <iostream>
using namespace std;
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; // 파생클래스 GreandDerived 객체 g
Base *bp; // 기본클래스 Base의 포인터 bp
Derived *dp; // 파생클래스 Derived의 포인터 dp
GrandDerived *gp; // 파생클래스 Derived의 파생클래스 GrandDerived의 포인터 gp
bp = dp = gp = &g;
bp->f(); // bp->f() => gp->f()
dp->f(); // dp->f() => gp->f()
gp->f(); // gp->f()
// 동적 바인딩에 의해 모두 GrandDerived의 함수 f() 호출
// 모두 GrandDerived 파생클래스의 f() 함수가 출력된다.
}
C++
복사
오버라이딩과 범위 지정 연산자(::)
범위 지정 연산자 (::)
•
정적 바인딩 지시
•
기본클래스::가상함수()
◦
기본클래스의 가상함수를 정적 바인딩으로 호출
예제 9-4) 범위 지정 연산자 (::)를 이용한 기본 클래스의 가상함수 호출
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() {
cout << "-- Shape --";
}
};
class Circle : public Shape {
public:
virtual void draw() {
Shape::draw(); // 기본클래스 Shape의 가상함수 draw()를 정적 바인딩으로 호출
cout << "Circle" << endl;
}
};
int main() {
Circle circle; // 파생클래스 Circle의 객체 circle
Shape *pShape = &circle;
// pShape 이라는 기본클래스 Shape의 포인터 변수를 선언하고, 객체 circle의 주소값으로 초기화
pShape->draw(); // 동적 바인딩을 포함하는 호출
// 출력 : -- Shape --Circle
pShape->Shape::draw(); // 정적 바인딩 호출
// 출력 : -- Shape --
}
C++
복사
가상 소멸자
가상 소멸자
•
소멸자를 virtual 키워드로 선언
•
소멸자 호출 시, 동적 바인딩 발생
◦
파생클래스의 소멸자가 자신의 코드 실행 후,
◦
기본클래스의 소멸자를 호출하도록 컴파일 됨
class Base {
public:
virtual ~Base();
};
class Derived : public Base {
public:
virtual ~Derived();
}
int main() {
Base *p = new Derived();
delete p;
}
// 1) ~Base() 소멸자 호출
// 2) ~Derived() 실행
// 3) ~Base() 실행
C++
복사
예제 9-6) 소멸자를 가상함수로 선언
#include <iostream>
using namespace std;
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()
// ~Derived()
// ~Base()
}
C++
복사
오버로딩
정의
•
매개변수 타입이나 개수가 다르지만, 이름이 같은 함수들이 중복 작성되는 것
존재
•
클래스의 멤버들 사이
•
외부 함수들 사이
•
기본클래스와 파생클래스 사이에 존재 가능
목적
•
이름이 같은 여려 개의 함수를 중복 작성하여, 사용의 편의성 향상
바인딩
•
정적 바인딩
•
컴파일 시에 중복된 함수들의 호출을 구분
객체 지향 특성
•
컴파일 시간 다형성
함수 재정의 (가상함수가 아닌 멤버에 대해서)
정의
•
기본클래스의 멤버함수를, 파생클래스에서 이름, 매개변수 타입과 개수, 리턴 타입까지 완벽히 같은 원형으로 재작성하는 것
존재
•
상속 관계
목적
•
기본클래스의 멤버함수와 별도로, 파생클래스에서 필요하여 재작성
바인딩
•
정적 바인딩
•
컴파일 시에 함수의 호출을 구분
객체 지향 특성
•
컴파일 시간 다형성
오버라이딩
정의
•
기본클래스의 가상함수를, 파생클래스에서 이름, 매개변수 타입과 개수, 리턴 타입까지 완벽히 같은 원형으로 재작성하는 것
존재
•
상속 관계
목적
•
기본클래스에 구현된 가상함수를 무시하고, 파생클래스에서 새로운 기능으로 재작성하고자 함
바인딩
•
동적 바인딩
•
실행 시간에 오버라이딩된 함수를 찾아 실행
객체 지향 특성
•
실행 시간 다형성
가상함수와 오버라이딩 활용 사례
가상함수를 가진 기본클래스의 목적
class Shape {
Shape* next;
protected:
virtual void draw();
public:
Shape() { next = NULL; }
virtual ~Shape() {}
void paint();
Shape* add(Shape* p);
Shape* getNext() { return next; }
};
C++
복사
Shape 클래스는 상속을 위한 기본클래스로의 역할
•
가상함수 draw()로 파생클래스의 인터페이스를 보여준다
•
Shape 객체를 생성할 목적 아님
•
파생클래스에서 draw() 재정의
•
자신의 도형을 그리도록 유도
가상함수 오버라이딩
•
파생클래스마다 다르게 구현하는 다형성
void Circle::draw() {
cout << "Circle" << endl;
}
void Rect::draw() {
cout << "Rect" << endl;
}
void Line::draw() {
cout << "Line" << endl;
}
C++
복사
•
파생클래스에서 가상함수 draw()의 재정의
◦
어떤 경우에도 자신이 만든 draw()가 호출됨을 보장 받음
▪
동적 바인딩에 의해
#include <iostream>
using namespace std;
class Shape {
Shape* next;
protected:
virtual void draw();
public:
Shape() { next = NULL; }
virtual ~Shape() {}
void paint();
Shape* add(Shape* p);
Shape* getNext() { return next; }
};
class Circle : public Shape {
protected:
virtual void draw();
};
class Rect : public Shape {
protected:
virtual void draw();
};
class Line : public Shape {
protected:
virtual void draw();
};
void Circle::draw() {
cout << "Circle" << endl;
}
void Rect::draw() {
cout << "Rect" << endl;
}
void Line::draw() {
cout << "Line" << endl;
}
void Shape::paint() {
draw();
}
void Shape::draw() {
cout << "-- Shape --" << endl;
}
Shape* Shape::add(Shape *p) {
this->next = p;
return p;
}
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) {
p->paint();
p = p->getNext();
}
// 현재 연결된 모든 도형을 삭제한다
p = pStart;
while(p != NULL) {
Shape* q = p->getNext(); // 다음 도형 주소 기억
delete p; // 기본클래스의 가상 소멸자 호출
p = q; // 다음 도형 주소를 p에 저장
}
}
C++
복사
기본 클래스의 포인터 활용
기본클래스의 포인터로 파생클래스 접근
•
pStart, pLast, p의 타입이 Shape*
•
연결리스트를 따라 Shape을 상속받은 파생 객체들 접근
•
p→paint()의 간단한 호출로 파생 객체에 오버라이딩된 draw() 함수 호출
순수 가상 함수
기본 클래스의 가상 함수 목적
•
파생 클래스에서 재정의할 함수를 알려주는 역할
◦
실행할 코드를 작성할 목적이 아님
•
기본 클래스의 가상 함수를 굳이 구현할 필요가 있을까?
순수 가상 함수 (Pure Virtual Function)
•
함수의 코드가 없고, 선언만 있는 가상 멤버 함수
class Shape {
public:
virtual void draw() = 0;
};
C++
복사
추상 클래스
•
최소한 하나의 순수 가상 함수를 가진 클래스
class Shape { // Shape는 추상 클래스
Shape *next;
public:
void paint();
draw();
}
virtual void draw() = 0; // 순수 가상 함수
};
void Shape::paint() {
draw(); // 순수 가상 함수라도 호출은 할 수 있다
}
C++
복사
추상 클래스의 특징
•
온전한 클래스가 아니므로, 객체 생성 불가능
Shape shape; // 컴파일 오류
Shape *p = new Shape(); // 컴파일 오류
// 추상 클래스를 인스턴스화할 수 없다
C++
복사
•
추상 클래스의 포인터는 선언 가능
Shape *p;
C++
복사
추상 클래스의 목적
•
추상 클래스의 인스턴스를 생성할 목적 아님
•
상속에서 기본 클래스의 역할을 하기 위함
◦
순수 가상 함수를 통해
◦
파생 클래스에서 구현할 함수의 형태(원형)를 보여주는 인터페이스 역할
◦
추상 클래스의 모든 멤버 함수를
◦
순수 가상 함수로 선언할 필요 없다
추상 클래스의 상속과 구현
•
추상 클래스의 상속
◦
추상 클래스를 단순 상속하면, 자동 추상 클래스
•
추상 클래스의 구현
◦
추상 클래스를 상속 받아, 순수 가상 함수를 오버라이딩
class Shape { // 추상 클래스
public:
virtual void draw() = 0;
};
class Circle : public Shape {
public:
virtual void draw() { // 순수 가상 함수 오버라이딩
cout << "Circle";
}
string toString() { return "Circle 객체";}
};
Shape shape; // 객체 생성 오류
Circle waffle; // 정상적인 객체 생성
C++
복사
#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 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;
}
};
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() {
// 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;
// }
int main() {
Adder adder;
Subtractor subtractor;
adder.run();
subtractor.run();
}
C++
복사