Search
Duplicate

9장. 가상 함수와 추상 클래스

생성일
2022/05/09 06:46
태그
C++

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++
복사