01 상속(inheritance)에 들어가기에 앞서
상속 : 클래스 재활용과 더불어 어떤 점들이 더 있다.-는 아래에서 설명
컨트롤 클래스(핸들러 클래스) : 기능의 처리를 실제로 담당하는 클래스
#include <iostream>
#include <cstring>
using namespace std;
// 직원 데이터
class PermanentWorker
{
private:
char name[100];
int salary;
public:
PermanentWorker(char* name, int money)
: salary(money)
{
strcpy(this->name, name);
}
int GetPay() const
{
return salary;
}
void ShowSalaryInfo() const
{
cout<<"name: "<<name<<endl;
cout<<"salary: "<<GetPay()<<endl<<endl;
}
};
// 컨트롤 클래스 (실 기능 처리)
class EmployeeHandler
{
private:
// 정직원 객체들을 저장한 배열을 멤버로 지닌다
PermanentWorker* empList[50];
int empNum;
public:
// 생성
EmployeeHandler(): empNum(0)
{ }
// 새로운 직원 등록
void AddEmployee(PermanentWorker* 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()
{
// 컨트롤 클래스 객체생성
EmployeeHandler handler;
// 직원 등록
handler.AddEmployee(new PermanentWorker("KIM", 1000));
handler.AddEmployee(new PermanentWorker("LEE", 1500));
handler.AddEmployee(new PermanentWorker("JUN", 2000));
// 이달 지불할 급여 정보
handler.ShowAllSalaryInfo();
// 이달 지불할 총급여
handler.ShowTotalSalary();
return 0;
}
소프트웨어 설계에서 변경에 대응하는 프로그램의 유연성, 기능의 추가에 따른 확장성이 중요
새로 프로그램을 만드는 형태로 하면 안된다. 위 예제는 그런 점이 부족하다. 상속을 적용해면 해결할 수 있다.
02 상속의 문법적인 이해
예 : 자신의 고유한 특성 + 부모에게 여러 특징들을 물려 받는 것.
A클래스가 B클래스를 상속하게 되면 A클래스는 B클래스가 지닌 모든 멤버들을 물려받는다
A 객체에는 A클래스에 선언된 멤버와 B클래스에 선언된 멤버 모두 존재한다.
1. 메모리공간 할당
2. Base 클래스 생성자 실행
3. Derived 클래스의 생성자 실행
예제
#include <iostream>
#include <cstring>
using namespace std;
class Person
{
private:
int age; // 나이
char name[50]; // 이름
public:
Person(int myage, char* myname)
: age(myage)
{
strcpy(name, myname);
}
void WhatYourName() const
{
cout<<"My name is "<<name<<endl;
}
void HowOldAreYou() const
{
cout<<"I'm "<<age<<" years old"<<endl;
}
};
class UnivStudent: public Person // Person 클래스의 상속
{
private:
char major[50]; // 전공
public:
UnivStudent(char* myname, int myage, char* mymajor)
: Person(myage, myname) // 상속받은 클래스의 생성자
{
// UnivStudent 클래스의 생성자는 Person 클래스의 멤버까지 초기화해야 한다.
strcpy(major, mymajor);
}
void WhoAreYou() const{
WhatYourName(); // 상속받은 클래스의 멤버함수라서 호출이 가능하다
HowOldAreYou();
cout<<"My major is "<<major<<endl<<endl;
}
};
int main()
{
UnivStudent ustd1("Lee", 22, "Computer eng.");
ustd1.WhoAreYou();
UnivStudent ustd2("Yoon", 21, "Elec eng.");
ustd2.WhoAreYou();
return 0;
}
UnivStudent 클래스의 생성자는 Person 클래스의 멤버까지 초기화해야 한다. 그래서 UnivStudent 클래스의 생성자는 Person 클래스의 생성자를 호출하는 형태로 Person클래스의 멤버를 초기화하는게 좋다.
상속받는 클래스는 이니셜라이저를 이용해서 상속하는 클래스의 생성자 호출을 명시할 수 있다.
* 멤버는 클래스가 정의될 때 멤버의 초기화를 목적으로 정의된 생성자를 통해서 초기화하는게 가장 안정적이다.
상속받은 멤버변수의 접근 제한의 기준은 클래스이다.
클래스 외부에서는 private 멤버에 접근 불가하고, 상속받아도 멤버변수에 직접접근이 불가하다.
클래스 내 정의된 public 함수를 통해 간접 접근해야 한다.
Person ↔ UnivStudent
상위클래스 ↔ 하위 클래스
기초 클래스(base) ↔ 유도(derived) 클래스 *derive : 유래, 파생
슈퍼 클래스 ↔ 서브 클래스
부모 클래스 ↔ 자식 클래스
유도 클래스의 객체 생성과정
#include <iostream>
#include <cstring>
using namespace std;
class SoBase
{
private:
int baseNum;
public:
SoBase(): baseNum(20)
{
cout<<"SoBase()"<<endl;
}
SoBase(int n): baseNum(n)
{
cout<<"SoBase(int n)"<<endl;
}
void ShowBaseData()
{
cout<<baseNum<<endl;
}
};
class SoDerived: public SoBase
{
private:
int deriveNum;
public:
SoDerived(): deriveNum(30)
{
cout<<"SoDerived()"<<endl;
}
SoDerived(int n): deriveNum(n)
{
cout<<"SoDerived(int n)"<<endl;
}
SoDerived(int n1, int n2): SoBase(n1), deriveNum(n2)
{
cout<<"SoDerived(int n1, int n2)"<<endl;
}
void ShowDeriveData()
{
ShowBaseData();
cout<<deriveNum<<endl;
}
};
int main()
{
cout<<"case1........"<<endl;
SoDerived dr1;
dr1.ShowDeriveData();
cout<<"-------------"<<endl;
cout<<"case2........"<<endl;
SoDerived dr2(12);
dr2.ShowDeriveData();
cout<<"-------------"<<endl;
cout<<"case3........"<<endl;
SoDerived dr3(23, 24);
dr3.ShowDeriveData();
return 0;
}
유도 클래스의 객체생성 과정에서 기초 클래스의 생성자는 100% 호출된다.
유도 클래스의 생성자에서 기초 클래스의 생성자 호출을 명시하지 않으면 기초 클래스의 void 생성자가 호출된다.
유도 클래스의 객체생성 과정에서 생성자가 두 번 호출된다.
1) 기초클래스의 생성자 2)유도 클래스의 생성자
SoDerived dr3(23, 24); ---- 메모리 공간 할당
SoDerived(int n1, int n2): SoBase(n1), deriveNum(n2) { ... } ---- 객체생성문에 의해 23, 24가 전달되면서 유도클래스 생성자 호출
SoBase(n1) ---- 기초클래스 생성자 호출 : 상속 관계에 의해 기초 클래스의 생성자 호출을 위해 이니셜라이저 확인, 매개변수 n1으로 전달된 값을 인자로 받을 수 있는 SoBase클래스의 생성자 호출
SoBase(int n): baseNum(n) { }
---- 기초클래스 멤버변수 초기화
---- 유도 클래스의 생성자 실행 완료
---- 유도클래스의 멤버변수 초기화 완료
SoDerived dr1; ---- 메모리 공간 할당
---- 유도 클래스 생성자 호출
SoDerived(): deriveNum(30) { ... } ---- 생성자 관련 초기화 내용이 없으면 기초 클래스의 void 생성자를 대신 호출
deriveNum = 30; ---- 기초클래스의 멤버변수 초기화 완료
---- 유도클래스 생성자의 나머지 실행
---- 초기화된 SoDerived 객체 생성
유도 클래스의 객체 생성과정에서도 적용되는 원칙 : 클래스의 멤버는 해당 클래스의 생성자를 통해서 초기화해야 한다.
유도 클래스 객체의 소멸과정
유도 클래스의 객체 생성 과정에서는 생성자가 2번 호출,
유도 클래스의 객체 소멸 과정에서는 소멸자가 2번 호출된다.
#include <iostream>
using namespace std;
class SoBase
{
private:
int baseNum;
public:
SoBase(int n): baseNum(n)
{
cout<<"SoBase(): "<<baseNum<<endl;
}
~SoBase()
{
cout<<"~SoBase(): "<<baseNum<<endl;
}
};
class SoDerived: public SoBase
{
private:
int derivNum;
public:
SoDerived(int n): SoBase(n), derivNum(n)
{
cout<<"SoDerived(): "<<derivNum<<endl;
}
~SoDerived()
{
cout<<"~SoDerived(): "<<derivNum<<endl;
}
};
int main()
{
SoDerived drv1(15);
SoDerived drv2(27);
return 0;
};
유도 클래스의 객체가 소멸될 때
유도 클래스의 소멸자가 실행된 뒤 기초 클래스의 소멸자가 실행된다.
스택에 생성된 객체의 소멸순서는 생성순서와 반대.
생성자에서 동적 할당한 메모리 공간은 소멸자에서 해제한다.
#include <iostream>
#include <cstring>
using namespace std;
class Person
{
private:
char* name;
public:
Person(char* myname)
{
name=new char[strlen(myname)+1];
strcpy(name, myname);
}
~Person()
{
delete []name;
}
void WhatYourName() const
{
cout<<"My name is "<<name<<endl;
}
};
class UnivStudent: public Person
{
private:
char* major;
public:
UnivStudent(char* myname, char* mymajor)
: Person(myname)
{
major=new char[strlen(mymajor)+1];
strcpy(major, mymajor);
}
~UnivStudent()
{
delete []major;
}
void WhoAreYou() const
{
WhatYourName();
cout<<"My major is "<<major<<endl<<endl;
}
};
int main()
{
UnivStudent st1("Kim", "Math");
st1.WhoAreYou();
UnivStudent st2("Hong", "Physics");
st2.WhoAreYou();
return 0;
}
03 protected 선언과 세 가지 형태의 상속
멤버변수의 범위
접근제어 지시자의 접근 범위는 private < protected < public 과 같다.
private, protected 는 외부에서 접근 불가, 클래스 내부에서는 접근 가능하나 상속이 되면 조금 달라진다.
protected, public 선언된 멤버변수는 상속하는 유도클래스에서 접근할 수 있고 private 멤버는 컴파일 에러가 발생한다.
protected는 유도클래스에게만 제한적으로 접근을 허용한다. (private과 유일한 차이점)
세 가지 형태의 상속
class Derived: public Base { ... }
class Derived: protected Base { ... }
class Derived: private Base { ... }
protected 상속
protected보다 접근의 범위가 넓은 멤버는 protected로 변경시켜서 상속하겠다는 뜻
#include <iostream>
using namespace std;
class Base
{
private:
int num1;
protected:
int num2;
public:
int num3;
Base(): num1(1), num2(2), num3(3)
{ }
};
class Derived: protected Base{ };
int main()
{
Derived drv;
cout<<drv.num3<<endl; //error
}
private 상속
private보다 접근의 범위가 넓은 멤버는 protected로 변경시켜서 상속
#include <iostream>
using namespace std;
class Base
{
private:
int num1;
protected:
int num2;
public:
int num3;
Base(): num1(1), num2(2), num3(3)
{ }
};
class Derived: private Base{ };
int main()
{
Derived drv;
cout<<drv.num3<<endl; //error
}
위 코드는 아래의 형태가 되고, num2, num3는 Derived 클래스 내에서만 접근이 가능한 멤버가 된다. 다른 클래스가 Derived클래스를 다시 상속하면 Derived 클래스 모든 멤버가 private이거나 접근불가여서 public으로 받아도 모두 접근불가가 되어 의미 없는 상속이 된다.
class Base :
{
접근불가:
int num1;
private:
int num2;
private:
int num3;
};
private 상속이 이뤄진 클래스를 재상속할 경우, 멤버함수를 포함하여 모든 멤버가 접근불가가 되어서 의미없는 상속
public 상속
private을 제외한 나머지는 그냥 그대로 상속한다.
상속의 대부분은 public 상속이며 다중상속과 같은 특별한 때가 아니면 나머지는 잘 안 사용한다.
04 상속을 위한 조건
상속으로 클래스의 관계를 구성하려면 조건이 필요하다. 상속을 위한 최소한의 조건을 정리해본다.
상속을 위한 기본 조건인 IS-A 관계의 성립
유도클래스 = 기초클래스가 지니는 모든 것 + 유도클래스만의 추가적 특징
기초 클래스 - 유도 클래스
------------------------------
전화기 - 무선전화기
컴퓨터 - 노트북컴퓨터
통화, 계산 + 이동성
------------------------------
무선전화기 is a 전화기
놋북 is a 컴퓨터
(~이다)
상속관계가 성립하려면 기초클래스와 유도클래스 간에 IS-A 관계가 성립해야 한다.
성립하지 않는다면 적절한 상속관계가 아닐 확률이 높다.
#include <iostream>
#include <cstring>
using namespace std;
class Computer //소유자 정보 저장, 계산 함수
{
private:
char owner[50];
public:
Computer(char* name){
strcpy(owner, name);
}
void Calculate()
{
cout<<"요청 내용을 계산합니다."<<endl;
}
};
class NotebookComp: public Computer // 배터리 관련 변수, 함수 추가
{
private:
int Battery;
public:
NotebookComp(char* name, int initChag)
:Computer(name), Battery(initChag)
{ }
void Charging() { Battery+=5; }
void UseBattery() { Battery-=1; }
void MovingCal()
{
if(GetBatteryInfo()<1){
cout<<"충전이 필요합니다"<<endl;
return;
}
cout<<"이동하면서 ";
Calculate();
UseBattery();
}
int GetBatteryInfo() { return Battery; }
};
class TabletNoteBook: public NotebookComp // 펜을 등록, 등록이 된 펜을 사용해야 필기가 가능한 상황 표현
{
private:
char regstPenModel[50];
public:
TabletNoteBook(char* name, int initChag, char* pen)
: NotebookComp(name, initChag)
{
strcpy(regstPenModel, pen);
}
void Write(char* penInfo)
{
if(GetBatteryInfo()<1)
{
cout<<"충전이 필요합니다"<<endl;
return;
}
if(strcmp(regstPenModel, penInfo)!=0)
{
cout<<"등록된 펜이 아닙니다";
return;
}
cout<<"필기 내용을 처리합니다"<<endl;
UseBattery();
}
};
int main(){
NotebookComp nc("이수종", 5);
TabletNoteBook tn("정수영", 5, "ISE-241-242");
nc.MovingCal();
tn.Write("ISE-241-242");
return 0;
}
TabletNotebook 클래스 객체 생성과정에서
TabletNotebook 클래스가 상속하는 NotebookComp 클래스의 생성자와 NotebookComp 클래스가 상속하는 Computer 클래스의 생성자가 모두 호출된다
HAS-A 관계(소유의 관계)도 상속의 조건은 되지만 복합 관계로 이를 대신하는 게 일반적이다.
유도클래스는 기초클래스가 지닌 모든걸 소유하기에 소유의 관계도 상속으로 표현할 수 있다.
#include <iostream>
#include <cstring>
using namespace std;
class Gun
{
private:
int bullet; //장전된 총알 갯수
public:
Gun(int bnum):bullet(bnum)
{ }
void Shot()
{
cout<<"BBANG!"<<endl;
bullet--;
}
};
class Police: public Gun
{
private:
int handcuffs; //소유한 수갑 갯수
public:
Police(int bnum, int bcuff)
: Gun(bnum), handcuffs(bcuff)
{ }
void PutHandCuff()
{
cout<<"SNAP!"<<endl;
handcuffs--;
}
};
int main(void)
{
Police pman(5, 3); //총알5, 수갑 3
pman.Shot();
pman.PutHandCuff();
return 0;
}
상속이 아니고서도 소유관계를 표현할 수 있다. 위의 코드보다 확장성이 훨씬 좋다.
#include <iostream>
#include <cstring>
using namespace std;
class Gun
{
private:
int bullet; //장전된 총알 갯수
public:
Gun(int bnum):bullet(bnum)
{ }
void Shot()
{
cout<<"BBANG!"<<endl;
bullet--;
}
};
class Police
{
private:
int handcuffs; //소유한 수갑 갯수
Gun* pistol; // 소유한 권총
public:
Police(int bnum, int bcuff)
: handcuffs(bcuff)
{
if(bnum>0)
pistol=new Gun(bnum);
// 생성자에서 Gun객체를 생성해서 참조
else
pistol=NULL;
}
void PutHandCuff()
{
cout<<"SNAP!"<<endl;
handcuffs--;
}
void Shot()
{
// Gun 객체를 멤버변수를 통해 참조하는 구조여서 별도의 함수를 정의해야 한다.
if(pistol==NULL)
cout<<"Hut BBANG!"<<endl;
else
pistol->Shot();
}
~Police()
{
if(pistol==NULL)
delete pistol;
}
};
int main(void)
{
Police pman1(5, 3);
pman1.Shot();
pman1.PutHandCuff();
// 총 없는 경찰 객체
Police pman2(0, 3);
pman2.Shot();
pman2.PutHandCuff();
return 0;
}
상속으로 묶인 두 클래스는 강환 연관성을 띈다. HAS-A 관계의 표현에도 사용될 수 있으나 프로그램의 변경에 많은 제약을 가져다 줄 수 있다.
'C, C++ > 열혈 C++ 프로그래밍' 카테고리의 다른 글
[윤성우 열혈 c++] 07.상속의 이해 영상 (0) | 2021.10.07 |
---|---|
윤성우 열혈 C++ 07.상속의 이해 연습문제 (0) | 2021.10.07 |
06. friend와 static 그리고 const (책) (0) | 2021.09.30 |
05.복사생성자(copy constructor) (책) (0) | 2021.09.29 |
05.복사생성자 (0) | 2021.09.28 |