본문 바로가기
Programming/C++

상속(Inheritance)의 이해

by OKOK 2017. 7. 31.

객체지향이 지니는 이점은 모델링에 근거를 둡니다. 기존에 정의해 놓은 클래스의 재활용을 목적으로 만들어진 문법적 요소가 상속이다. 


#pragma warning(disable:4996)

#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;

}

};

 


이름과 그여정보를 저장할 수 있도록 클래스를 정의하였습니다. 위에서 정의한 클래스의 객체를 저장 및 관리하기 위한 클래스입니다. PermanentWorker 객체의 저장을 목적으로 배열을 멤버로 지니고 있으며, 저장된 객체의 급여 정보를 출력하기 위한 함수를 멤버로 지니고 있습니다.


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(void)

{

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;


요구 사항의 변경에 대응하는 프로그램의 유연성, 기능의 추가에 따른 프로그램의 확장성이 소프트웨어의 설계에 있어서 중요합니다. 프로그램 사용자의 업무형태가 바뀌어서 프로그램 변경을 요구할 수도 있는 일이고, 회사의 업무가 확장되어 프로그램의 기능추가를 요구할 수도 있는 일입니다. 


상속의 문법적인 이해


#pragma warning(disable:4996)

#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

{

private:

char major[50];

public:

UnivStudent(char * myname, int myage, char *mymajor)

:Person(myage, myname)

{

strcpy(major, mymajor);

}

void WhoAreYou() const

{

WhatYourName();

HowOldAreYou();

cout << "My major is " << major << endl << endl;

}

};


int main(void)

{

UnivStudent ustd1("Lee", 22, "Computer eng.");

ustd1.WhoAreYou();


UnivStudent ustd2("Yoon", 21, "Electronic eng.");

ustd2.WhoAreYou();

return 0;


접근제한의 기준은 클래스입니다. 클래스 외부에서 private 멤버에 접근이 불가능하다. 정보의 은닉은 하나의 객체 내에서도 진행이 됩니다. 상위 클래스, 하위 클래스, 기초 클래스, 유도 클래스 슈퍼 클래스, 서브 클래스, 부모 클래스, 자식 클래스 기초 클래스와 유도클래스라는 일반적으로 사용되는 표현을 주로 사용하겠습니다.


유도 클래스의 객체 생성과정

기초 클래스의 생성자 호출이 중요합니다.

#include <iostream>

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 derivNum;

public:

SoDerived() : derivNum(30)

{

cout << "SoDerived()" << endl;

}

SoDerived(int n) : derivNum(n)

{

cout << "SoDerived(int n)" << endl;

}

SoDerived(int n1, int n2) : SoBase(n1), derivNum(n2)

{

cout << "SoDerived(int n1, int n2)" << endl;

}

void ShowDerivData()

{

ShowBaseData();

cout << derivNum << endl;

}

};


int main(void)

{

cout << "case1" << endl;

SoDerived dr1;

dr1.ShowDerivData();

cout << "---" << endl;

cout << "case2" << endl;

SoDerived dr2(12);

dr2.ShowDerivData();

cout << "---" << endl;

cout << "case3" << endl;

SoDerived dr3(23, 24);

dr3.ShowDerivData();

return 0;


유도 클래스의 객체생성 과정에서 기초 클래스의 생성자는 100% 호출됩니다. 유도 클래스의 생성자에서 기초 클래스의 생성자 호출을 명시하지 않으면, 기초클래스의 void 생성자가 호출됩니다. 기초 클래스의 생성자 호출이 완료도기ㅗ,


기초 클래스의 생성자 호출이 완료되고, 이로 인해서 기초 클래스의 멤버변수가 먼저 초기화 됩니다. 호출은 되었지만 아직 완전히 실행이 끝나지 않은 유도 클래스의 생성자 실행이 완료되면서, 다음 그림과 같이 유도 클래스의 멤버변수도 초기화 완료됩니다. 실제로는 기초 클래스의 생성자 호출이 추가된 것이 전부입니다. 클래스의 멤버는 해당 클래스의 생성자를 통해서 초기화해야 한다는 원칙이 유도 클래스의 객체 생성과정에서도 지켜져야 합니다. 


유도 클래스 객체의 소멸 과정

#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(void)

{

SoDerived drv1(15);

SoDerived drv2(27);

return 0;


유도 클래스의 객체가 소멸될 때에는, 유도 클래스의 소멸자가 실행되고 난 다음에 기초 클래스의 소멸자가 실행됩니다. 스택에 생성된 객체의 소멸순서는 생성순서와 반대입니다. 기초 클래스의 소멸자도, 유도 클래스의 소멸자도 호출된다는 사실입니다. 이러한 객체소멸의 특성 때문에 상속과 연관된 클래스의 소멸자는 다음의 원칙을 지켜서 정의해야 합니다. 생성자에서 동적 할당한 메모리 공간은 소멸자에서 해제합니다. 


#pragma warning(disable:4996)


#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(void)

{

UnivStudent st1("Kim", "Mathe");

st1.WhoAreYou();

UnivStudent st2("Hong", "Physics");

st2.WhoAreYou();

return 0;


UnivStudent의 기초 클래스인 Person 클래스 소멸자는 생성장에서 할당한 메모리 공간을 해제하도록 정의하였습니다. UnivStudent 의 소멸자에서도 자신의 생성자에서 할당한 메모리 공간에 대한 해제만을 책임지고 있습니다. 어차피 기초 클래스의 소멸자가 호출이 되면서 기초 클래스의 생성자에서 할당한 메모리 공간을 해제하기 때문입니다.



protected 선언과 세 가지 형태의 상속

앞서, 멤버들에 대한 public과 private 선언에 대해서는 설명을 했지만, protected 선언은 상속과 관련이 있기 때문에 설명을 미루었습니다. 즉, public 이 허용하는 접근의 범위가 가장 넓고, private이 허용하는 접근의 범위가 가장 좁다. 반면, protected 는 그 중간 범위의 접근을 허용합니다. protected로 선언된 멤버변수는 이를 상속하는 유도 클래스에서 접근이 가능합니다. 이것이 private과 protected의 유일한 차이점입니다. 기초 클래스와 이를 상속하는 유도 클래스 사이에서도 정보은닉은 지켜지는 게 좋습니다. 


protected 상속이 의미하는 바는 다음과 같습니다. protected 보다 접근의 범위가 넓은 멤버는 protected로 변경시켜서 상속하겠습니다. public 상속이 의미하는 바는 public 보다 접근의 범위가 넓은 멤버는 public 으로 변경시켜서 상속하겠다 입니다. private 을 제외한 나머지는 그냥 그대로 상속한다는 뜻이니다. 


상속을 위한 조건

상속을 위한 기본 조건인 is-a 관계의 성립. 상속의 기본 문법에서 보이듯이, 유도 클래스는 기초 클래스가 지니는 모든 것을 지니고, 거기에다가 유도 클래스만의 추가적인 특성이 더해집니다. 


#pragma warning(disable:4996)

#include <iostream>

#include <cstring>

using namespace std;


class Computer

{

private:

char owner[50];

public:

Computer(char * name)

{

strcpy(owner, name);

}

void Calculate()

{

cout << "calculator" << 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 << "need" << endl;

return;

}

cout << "moving";

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 << "need." << endl;

return ;

}

if (strcmp(regstPenModel, penInfo) != 0)

{

cout << "not pen";

return;

}

cout << "processing" << endl;

UseBattery();

}

};


int main(void)

{

NotebookComp nc("su jjon", 5);

TabletNotebook tn("suyoung", 5, "ISE");

nc.MovingCal();

tn.Write("ISE");

return 0;


모든 컴퓨터의 공통적인 특성을 Computer 클래스 하나에 표현하였습니다. 모든 컴퓨터는 소유자가 있으니 소유자 정보를 저장할 수 있도록 정의하였고, 또 계산의 기능도 있으니 계산과 관련된 함수를 하나 정의하였습니다. NotbookComp는 노트북 컴퓨터를 표현할 클래스입니다. 노트북 컴퓨터는 배터리가 있어서 이동이 가능하므로 이와 관련된 변수 및 함수를 추가하였고, 컴퓨터를 사용할 때마다 배터리가 소모되는 상황을 표현하였습니다. TabletNotebook은 펜이 있어서 필기가 가능한 노트북 컴퓨터를 표현한 클래스입니다. 화살표는 상속을 의미합니다. 화살표의 머리는 기초 클래스를 향하도록 표시해야 합니다. 


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);

pman.Shot();

pman.PutHandcuff();

return 0;


Gun 은 총을 표현한 클래스입니다. 표현의 간결함을 위해서 총알을 추가로 장전하는 등의 기능은 생략하였습니다. Police는 경찰을 표현한 클래스입니다. 경찰은 기본적으로 수값을 지닌다고 가정하였습니다. 


#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);

else

pistol = NULL;

}

void PutHandcuff()

{

cout << "SNAP" << endl;

handcuffs--;

}


void Shot()

{

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;


Gun 클래스를 상속하는 것이 아니라, 생성자에서 Gun 객체를 생성해서 이를 참조하고 있습니다. Gun 클래스를 상속한다면 별도의 Shot 함수를 정의할 필요가 없습니다. 그러나 Gun 객체를 멤버변수를 통해 참조하는 구조이기 때문에 이렇게 별도의 함수를 정의해야 합니다. 총을 소유하지 않은 경찰을 객체로 생성하고 있습니다. 생성자의 첫 번째 인자로 0인 전달되면, 23행의 멤버변수는 NULL로 초기화되어, 총의 사용이 불가능해진다.


권총을 소유하지 않은 경찰을 표현하기 어렵습니다. 경찰이 권총과 수갑뿐만 아니라 전기봉 소유의 표현이 어렵습니다. 상속으로 묶이 두 개의 클래스는 강한 연관성을 가집니다.