프로그래밍 언어/열혈 C++

08 상속과 다형성 by 윤성우의 열혈 C++

당장하자 2022. 3. 8. 23:28

함수 오버라이딩

  • 기초 클래스의 함수와 유도 클래스의 함수의 이름이 같고 매개변수의 자료형과 개수가 같을때 발생한다.
  • 오버라이딩 된 기초 클래스의 함수는 유도 클래스의 함수에 가려진다.
  • 오버라이딩 된 함수를 호출하고 싶다면 유도 클래스::함수 형태로 호출할 수 있다.
  • 함수의 내부가 완전히 똑같아도 클래스내의 다른 멤버를 접근하기 위해 오버라이딩 하기도 한다.

 

객체 포인터 변수

  • 객체 포인터는 객체 뿐만 아니라 객체를 상속하는 유도 클래스의 객체로도 초기화될 수 있다. (역은 안됨)
  • 하지만 유도 클래스의 객체로 초기화해도 컴파일러는 포인터의 자료형으로 포인터 연산 가능성을 판단하기 때문에 유도 클래스의 멤버함수를 호출할 수 없다.
즉, C++ 컴파일러는 포인터를 이용한 연산의 가능성 여부를 판단할 때, 포인터의 자료형을 기준으로 판단하지, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다. -열혈 C++

 

  • 함수를 오버라이딩해도 자료형에 따라 함수를 호출해주기 때문에 기초 클래스의 함수가 호출된다.

여기서 유도 클래스의 멤버함수 호출을 위해 가상함수를 배워보자

 

가상함수 (Virtual Function)

  • 기초 클래스의 멤버함수에 virtual 키워드를 선언함으로써 가상함수가 된다.
  • 오버라이딩된 유도 클래스의 함수는 키워드가 없어도 가상함수가 된다.
  • 이 때 함수호출시, 포인터의 자료형이 아닌 실제 가리키는 객체를 참조해서 그 객체의 멤버함수가 호출된다.

 

상속을 하는 이유

상속을 통해 일련의 클래스 집합의 공통적 규약을 정의할 수 있습니다.

새로운 자식 객체가 나와도 부모 객체로 바라볼 수 있고(객체 포인터)
행동의 방식 또한 해결할 수 있습니다. (오버라이딩과 가상함수)

 

순수 가상함수와 추상 클래스

  • 순수 가상함수란 함수의 몸체가 정의되지 않은 함수를 의미한다.
  • 함수뒤에 "=0" 을 추가해줌으로써 표현할 수 있다.
virtual void GetID() const = 0;
  • 순수 가상함수를 지닌 클래스를 객체로 생성하려고 하면 컴파일 에러가 발생한다.

클래스중에는 기초 클래스로서만 의미를 가지고 객체의 생성을 목적으로 정의되지 않은 클래스가 존재한다.

이를 실수로 객체의 생성에 참조한다면 프로그래머의 실수이다. 이를 막기위해 사용된다.

 

08-1 문제

#include <iostream>
#include <cstring>
using namespace std;

class Employee
{
private:
    char* name;
    int salary;
public:
    Employee(char* name, int salary)
        :salary(salary)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name, name);
    }
    virtual int GetSalary() const // 가상함수
    {
        return salary;
    }
    virtual void ShowSalaryInfo() const // 가상함수
    {
        cout << "Name: " << name << endl;
    }
};

class Salesman: public Employee
{
private:
    int salesResult;
    double bonusRatio;
public:
    Salesman(char* name, int salary, double bonusRatio)
        :Employee(name, salary), salesResult(0), bonusRatio(bonusRatio)
    { }
    void AddSalesResult(int value)
    {
        salesResult += value;
    }
    int GetSalary() const	// 가상함수
    {
        return Employee::GetSalary() + salesResult * bonusRatio;
    }
    void ShowSalaryInfo() const	// 가상함수
    {
        Employee::ShowSalaryInfo();
        cout << "Salary: " << GetSalary() << endl;
    }
};

class ForeignSalesWorker : public Salesman
{
private:
    const double riskRatio;
public:
    ForeignSalesWorker(char* name, int salary, double bonusRatio, double riskRatio)
        :Salesman(name, salary, bonusRatio), riskRatio(riskRatio)
        {}

    void ShowSalaryInfo() const	// 가상함수
    {
        Salesman::ShowSalaryInfo();
        cout << "Risk pay: " << Salesman::GetSalary() * riskRatio << endl;
        cout << "Sum: " << Salesman::GetSalary() * (1 + riskRatio) << endl; 
    }
};

class EmployeeHandler
{
private:
    Employee* empList[100];
    int empNum;
public:
    EmployeeHandler():empNum(0){}
    void AddEmployee(Employee* emp)
    {
        empList[empNum++] = emp;
    }
    void ShowAllSalaryInfo() const
    {
        for(int i=0; i<empNum; i++)
            empList[i]->ShowSalaryInfo();
    }
};

int main()
{
    EmployeeHandler manager;

    ForeignSalesWorker* kim = new ForeignSalesWorker("Kim", 3500, 0.1, 0.3);
    kim->AddSalesResult(7000);
    manager.AddEmployee(kim);

    ForeignSalesWorker* lee = new ForeignSalesWorker("Lee", 3500, 0.1, 0.2);
    lee->AddSalesResult(7000);
    manager.AddEmployee(lee);

    ForeignSalesWorker* park = new ForeignSalesWorker("Park", 2500, 0.1, 0.1);
    park->AddSalesResult(7000);
    manager.AddEmployee(park);

    manager.ShowAllSalaryInfo();

    return 0;
}

 

 

가상 소멸자

  • 소멸자에도 기초 클래스의 소멸자에만 virtual 키워드를 선언하면 유도 클래스의 소멸자 모두 가상 소멸자가 된다.
  • 기초 클래스의 포인터로 초기화된 유도 클래스에 소멸자를 호출하면 기초 클래스의 소멸자가 호출된다. 따라서 메모리의 누수가 발생하게 되기때문에 가상 소멸자를 선언해줘야 한다.

 

참조자의 참조 가능성

  • 참조자 또한 객체 포인터에 적용되는 모든 개념이 적용된다. (참조 자료형에 따른 함수호출, 가상함수, 오버라이딩)