객체의 주소값을 저장하는 포인터 변수
Person* ptr; //Person class의 객체를 가리키기 위한 ptr 포인터변수
ptr = new Person(); //포인터 변수의 객체 참조
C++에서 객체 포인터 변수는 가리키는 객체 뿐만 아니라, 그 클래스를 직접/간접 상속하는 자식 클래스의 객체도 가리킬 수 있다.
//Person을 직접 상속하는 Student
class Student : public Person {
...
};
//Person을 간접 상속하는 High_Student
class High_Student : public Student {
...
};
int main(){
Person* ptr; //Person class의 객체를 가리키는 ptr 포인터변수
ptr = new Student();
ptr = new High_Student();
}
Q.이런 특성을 갖게 되는 이유는 무엇일까?
그 해답은 상속의 의미로부터 찾을 수 있다.
상속의 관계는 IS-A(~는 ~이다.)의 관계로 표현되는데, 따라서 Person을 상속받는 Student에서 상속의 관계는 “Student는 Person이다.”로 표현되는데, 이는 클래스와 해당 클래스로 만들어진 객체간의 관계와 유사하다.
이러한 상속의 관계때문에 Person의 포인터 변수는 Student객체를 Person 객체의 일종으로 간주하게 된다.
따라서 실제로 객체 포인터가 가리키는 객체의 자료형이 Student 클래스라도, 객체 포인터의 자료형이 Person 클래스라면 자식 클래스인 Student만 가지는 멤버에는 접근할 수 없다 !
#include<iostream>
using namespace std;
class Person {
private:
char name[10];
public:
Person(char* name) {
strcpy(this->name, name);
}
void ShowInfo() const {
cout<<"name: "<<name<<endl;
}
};
//Person을 상속하는 Student
class Student : public Person {
private:
int number; //Student만이 갖는 멤버 함수
public:
Student(char* name, int num) : Person(name), number(num) {}
// Studnet만이 갖는 멤버 함수
void GetNumber() const {
cout<<"number: "<<number<<endl;
}
// 함수 오버라이딩
void ShowInfo() const {
Person::ShowInfo(); // 기존 함수
GetNumber();
}
};
int main(){
Person* ptr; //Person class의 객체를 가리키는 ptr 포인터변수
ptr = new Student("vaughan", 2022);
//ptr->GetNumber(); //컴파일 오류
ptr->ShowInfo(); //오버라이딩 이전의, Person 클래스에 정의된 기존 함수를 사용함
return 0;
}
virtual
키워드를 이용하여 가상함수로 선언된 함수는 포인터 변수를 이용하여 함수를 호출할 때 포인터 변수의 자료형을 기반으로 결정하지 않고 실제로 가리키는 객체를 참조하여 호출대상을 결정한다.
#include <iostream>
using namespace std;
class First {
public:
virtual void MyFunc() { cout<<"FirstFunc"<<endl; }
};
class Second : public First {
public:
virtual void MyFunc() { cout<<"SecondFunc"<<endl; }
};
class Third : public Second {
public:
virtual void MyFunc() { cout<<"ThirdFunc"<<endl; }
};
int main() {
Third* tptr = new Third; //선언 자료형 : Third, 실제 가리키는 객체 자료형 : Third
Second* sptr = tptr; //선언 자료형 : Second, 실제 가리키는 객체 자료형 : Third
First* fptr = sptr; //선언 자료형 : First, 실제 가리키는 객체 자료형 : Third
tptr->MyFunc();
sptr->MyFunc();
fptr->MyFunc();
delete tptr;
return 0;
}
추상클래스
실제로 객체생성은 하지 않고, 상속만을 위해 정의된 클래스
순수 가상함수
함수의 몸체가 정의되지 않은 함수
=0
으로 표현한다.// 추상 클래스
class Employee {
private:
char name[100];
public:
Employ(char* name) {
strcpy(this->name, name);
}
void ShowYourname() const {
cout<<"name: "<<name<<endl;
}
//순수가상함수
virtual int GetPay() const = 0;
virtual void ShowSalaryInfor() const = 0;
}
멤버변수와 멤버변수
가상함수의 동작원리
1개이상의 가상함수를 포함하는 클래스에 대해서는 컴파일러가 가상함수 테이블을 만든다.
가상함수 테이블은 실제 호출되야할 함수의 위치정보(=주소)를 담고 있는 테이블이다.
이때 오버라이딩 된 가상함수의 주소정보는 자식클래스의 테이블에 포함되지 않는다.
→ 따라서 오버라이딩 된 가상함수를 호출하면 자동적으로 가장 마지막에 오버라이딩 한 자식 클래스의 멤버함수가 호출되는 것이다.
가상함수 테이블의 예시
AAA를 상속받은 자식클래스 BBB의 V-table
key | value |
---|---|
void BBB::Func1() | 0x1024 |
void AAA::Func2() | 0x2048 |
void BBB::Func3() | 0x4096 |
어떤 회사의 전체 직원에게 제공하는 급여를 계산하고 관리하고자한다.
#include <iostream>
#include <cstring>
using namespace std;
class Employee {
private:
char name[100];
public:
Employee(char* name){
strcpy(this->name, name);
}
void ShowYourName() const {
cout<<"name: "<<name<<endl;
}
/*Employee형 포인터 변수를 이용해 Employee를 상속받는 자식클래스의 메소드 호출을 위한 가상함수 선언*/
virtual int GetPay() const = 0;
virtual void ShowSalaryInfo() const = 0;
};
class PermanentWorker : public Employee {
private:
int salary; // 월 급여
public:
PermanentWorker(char* name, int money) : Employee(name), salary(money) {}
int GetPay() const {
return salary;
}
void ShowSalaryInfo() const {
ShowYourName();
cout<<"salary: "<<GetPay()<<endl<<endl;
}
};
class TemporaryWorker : public Employee {
private:
int workTime; // 일한 시간
int payPerHour; // 시간당 급여
public:
TemporaryWorker(char* name, int pay) : Employee(name), workTime(0), payPerHour(pay) {}
void AddWorkTime(int time) {
workTime+=time;
}
int GetPay() const {
return workTime*payPerHour;
}
void ShowSalaryInfo() const {
ShowYourName();
cout<<"salary: "<<GetPay()<<endl<<endl;
}
};
class SalesWorker : public PermanentWorker {
private:
int salesResult; // 판매 실적
double bonusRatio; // 상여금 비율
public:
SalesWorker(char* name, int money, double ratio) : PermanentWorker(name, money), salesResult(0), bonusRatio(ratio) {}
void AddSalesResult(int value) {
salesResult+=value;
}
// 함수 오버라이딩
int GetPay() const {
/*PermanentWorker::GetPay() : 현재 자식 클래스에서 재정의된 함수가 아닌 부모 클래스의 원본 함수를 호출*/
return PermanentWorker::GetPay() + (int)(salesResult*bonusRatio); // 기본 원급 + 상여금
}
// 함수 오버라이딩
void ShowSalaryInfo() const {
ShowYourName();
cout<<"salary: "<<GetPay()<<endl<<endl;
}
};
class EmployeeHandler {
private:
Employee* empList[50];
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();
}
void ShowTotalSalary() const {
int sum=0;
for(int i=0 ; i<empNum ; i++)
sum+=empList[i]->GetPay();
cout<<"salary sum: "<<sum<<endl;
}
~EmployeeHandler(){
for(int i=0 ; i<empNum ; i++)
delete empList[i];
}
};
int main(){
//직원관리를 목적으로 설계된 control class 객체 생성
EmployeeHandler handler;
//정규직 등록
handler.AddEmployee(new PermanentWorker("KIM", 1000));
handler.AddEmployee(new PermanentWorker("LEE", 1500));
//임시직 등록 (알바)
TemporaryWorker* alba = new TemporaryWorker("Jung", 700);
alba->AddWorkTime(5);
handler.AddEmployee(alba);
//영업직 등록
SalesWorker* seller = new SalesWorker("Hong", 1000, 0.1);
seller->AddSalesResult(7000);
handler.AddEmployee(seller);
//이번 달 지불할 급여 정보
handler.ShowAllSalaryInfo();
//이번 달 지불할 급여 총합
handler.ShowTotalSalary();
return 0;
}
함수 오버라이딩 : 부모 클래스와 동일한 이름의 함수를 자식 클래스에서 재정의 하는 것
함수 오버로딩 : 부모 클래스와 동일한 이름의 함수를 자식 클래스에 재정의할 때, 매개변수의 자료형 및 개수가 다른 경우로, 이때는 함수 호출시 전달된 인자에 따라 호출되는 함수가 결정된다.
→ 이는 상속의 관계에서도 구성할 수 있다.
가상함수 말고도 virtual 키워드를 붙여줘야하는 대상, 소멸자
가상 소멸자의 필요성
virtual
키워드의 필요성가상 소멸자의 특징
가상 소멸자의 사용
class First {
. . .
public:
virtual ~First() { . . . . } // 가상 소멸자
}
//자식클래스
class Second : public First {
. . .
public:
virtual ~Second() { . . . . }
// ~Second() {} 로 정의해도(키워드없이) 자동으로 가상 소멸자로 정의된다.
}
C++에서 어떤 클래스의 참조자는 그 클래스를 직접/간접적으로 상속하는 모든 객체를 참조할 수 있다.
First 클래스 또는 이를 직/간접적으로 상속하는 클래스의 객체가 인자의 대상이 되는 함수
인자로 전달되는 객체의 실제 자료형과는 관계없이, 참조자가 First 클래스 타입이므로 함수 내에서는 First 클래스 내에서 정의된 멤버에 대해서만 접근할 수 있다.
void GoodFunction(const First &ref) {}
다중상속은 둘 이상의 클래스를 동시에 상속하는 것을 말한다.
기본적으로 상속방법은 기존과 동일하다.
,
를 이용하여 명시할 수 있다.#includ <iostream>
using namespace std;
class BaseOne {
public:
void SimpleFuncOne() { cout<<"BaseOne"<<endl; }
};
class BaseTwo {
public:
void SimpleFuncTwo() { cout<<"BaseTwo"<<endl; }
};
// 다중상속 받은 클래스
class MultiDerived : public BaseOne, protected BaseTwo {
public:
void ComplexFunc() {
SimpleFuncOne();
SimpleFuncTwo();
}
};
부모클래스이름::멤버
의 형태로 접근해야한다.#includ <iostream>
using namespace std;
class BaseOne {
public:
void SimpleFunc() { cout<<"BaseOne"<<endl; }
};
class BaseTwo {
public:
void SimpleFunc() { cout<<"BaseTwo"<<endl; } // 동일한 이름의 멤버함수
};
// 다중상속 받은 클래스
class MultiDerived : public BaseOne, protected BaseTwo {
public:
void ComplexFunc() {
//모호성 해결
BaseOne::SimpleFunc();
BaseTwo::SimpleFunc();
}
};
간접상속으로 인한 다중상속의 모호성
MiddleOne::Func()
: MiddleOne클래스가 상속한 Base클래스의 Func() 함수 호출
MiddleTwo::Func()
: MiddleTwo클래스가 상속한 Base클래스의 Func() 함수 호출
→ 그러나 이경우, Base클래스의 멤버가 1개씩만 존재하는 것이 타당하기 때문에 해결책이 필요하다.
만약 가상으로 Base 클래스를 상속하는 두 클래스를 다중상속하게 되면, Base클래스의 멤버가 1개씩만 존재하게 할 수 있다..
→ 가상 상속을 하면 Base클래스의 생성자가 1번만 호출된다.
본문은 ⟪열혈 C++ 프로그래밍, 윤성우⟫ 도서에 기반하여 정리한 내용입니다.