7-1 상속으로 들어가기에 앞서서
상속이 필요한 상황을 잘 알고 적용해야 한다.
상속을 정복하기 위한 접근 방식
1. 문제제기 - 문법과 범위를 잘 이해해야!
2. 기본 개념 소개 - 8장까지 이어짐
3. 문제해결
* Freelec 급여 담당자의 요구사항
- 급여 관리를 위해 직원 정보 저장 (고용직 : 연봉제)
- 매달 지불되어야 할 급여 정보 확인
#include <iostream>
using namespace std;
//Entity : 실체, 객체라는 의미로 실무적으론는 앤터티라고 부른다.
//즉 업무에 필요하고 유용한 정보를 저장하고 관리하기 위한 집합적인 것으로 설명할 수 있다.
//엔터티는 사람, 장소, 물건, 사건, 개념등의 명사에 해당한다.
//엔터티는 저장이 되기 위한 어떤 것(thing)
class Permanent //entity 엔터티 클래스 (데이터적 성격)
{
private:
char name[20];
int salary;
public:
Permanent(char* _name, int sal);
const char* GetName();
int GetPay();
};
Permanent::Permanent(char* _name, int sal){
strcpy(name, _name);
salary=sal;
}
const char* Permanent::GetName()
{
return name;
}
int Permanent::GetPay()
{
return salary;
}
// 컨트롤클래스 (사용자에게 제공되는 기능적 층면), 매니저클래스라고도 한다
// 플로우차트의 컨트롤, 흐름을 제어하는 것
// has-a 관계로 상속이 아니다.
class Department
{
private:
Permanent* empList[10];
int index;
public:
Department(): index(0){ };
// 생성자 초기화 리스트로 Department() { index = 0; }과 같다
// 초기화리스트는 필드에 const로 선언된 변수를 초기화하는 경우,
// 필드에 선언된 레퍼런스 변수를 초기화하는 경우 사용되어진다.
// 즉 필드에 선언과 동시에 초기화가 이루어져야 하는 경우.
// https://velog.io/@sjongyuuu/C-%EC%83%9D%EC%84%B1%EC%9E%90-%EC%B4%88%EA%B8%B0%ED%99%94%EB%A6%AC%EC%8A%A4%ED%8A%B8
void AddEmployee(Permanent* emp); //직원정보 저장
void ShowList(); //급여리스트 출력
};
void Department::AddEmployee(Permanent* emp)
{
empList[index++]=emp;
}
void Department::ShowList() //급여리스트 출력
{
for(int i=0; i<index; i++)
{
cout<<"name: "<<empList[i]->GetName()<<endl;
cout<<"salary: "<<empList[i]->GetPay()<<endl;
cout<<endl;
}
}
int main()
{
Department department;
department.AddEmployee(new Permanent("김종호", 100));
department.AddEmployee(new Permanent("홍길동", 200));
department.ShowList();
return 0;
}
클래스를 배웠음에도 프로그램을 짜는데 어려움을 느낀다면
컨트롤클래스에 대한 개념이 약해서 그렇다.
요구사항의 변경
- 급여 형태 다양화 (판매직은 연봉제 + 인센티브, 임시직은 시간당 급여 등)
요구사항의 변경을 위해 할 것
- Department 클래스의 변경 (프로그램의 전체적인 변경)
- 상속을 사용해서 앞으로 직원이나 급여의 형태가 다양해져도 Department 클래스는 변동이 없도록 수정한다.
7-2 상속의 기본 개념
Student가 Person클래스를 상속할 경우 Student는 Person 클래스의 멤버변수와 멤버함수를 물려받게 된다.
상속은 UML (클래스들의 관계)로 표시한다. UML은 그림그리기 위한 표준화 된 도구이다.
상속하는 클래스의 객체가 생성되는 과정
객체 할당 순서는 메모리에서 base 클래스를 먼저 할당한 후 derived 클래스를 할당한다.
객체를 닫을 땐 derived 클래스를 소멸하고 base클래스를 소멸한다.
1. 메모리 공간 할당
----생성자 호출-----
2. Base 클래스의 생성자 실행 (호출이 아니다)
3. Derived 클래스의 생성자 실행
호출은 Derived 클래스가 먼저 호출 (main...BBB b;)
class AAA //Base class
{
int a;
public:
AAA(){
cout<<"AAA()call"<<endl;
a=10;
}
AAA(int i){
cout<<"AAA(int i)call"<endl;
}
};
class BBB:public AAA //Derived class
{
int b;
public:
BBB(){
cout<<"BBB()call"<<endl;
b=20;
}
BBB(int i){
cout<<"BBB(int i)call"<endl;
}
};
BBB클래스 내에는 멤버로서 int a가 없지만 AAA클래스를 상속함으로서
BBB클래스의 객체 안에는 AAA클래스의 멤버가 존재하며
AAA클래스의 생성자도 객체생성과정에서 호출되어지고 있다.
AAA클래스의 생성자를 호출하게 하는 이유
AAA클래스의 생성자를 통해 AAA클래스의 멤버를 초기화 하는게
BBB클래스 내에서 AAA클래스의 멤버를 초기화하는 것보다 이상적이라서.
예제 :: 나이나 이름은 객체생성과 동시에 원하는 값으로 초기화시키지 못하는 문제
객체생성 -> Person 생성 -> Student 생성 -> Student 소멸 -> Person 소멸 순서 (스택의 FILO 구조)
* FILO 구조 : First In, Last Out 구조. LIFO라고도 함.
스택은 처음 들어간 것이 가장 마지막에 나오게 되어 있는 구조이다.
자동메모리는 스택 기반으로 동작하고, 대부분 네트워크 프로토콜도 스택을 기반으로 구성되어 있다.
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
int age;
char name[20];
public:
int GetAge() const {
return age;
}
const char* GetName() const {
return name;
}
Person(int _age=1, const char* _name="noname")
//디폴트 매개변수 설정으로 void 매개변수 호출도 사용 가능
{
age = _age;
strcpy(name, _name);
}
};
// Person클래스를 상속하지만 public 선언된 것만 상속된다.
class Student : public Person
{
char major[20]; //전공
public:
Student(const char* _major) {
strcpy(major, _major);
}
const char* GetMajor() const
{
return major;
}
void ShowData() const
{
cout << "이름:" << GetName() <<endl;
cout << "나이:" << GetAge() <<endl;
cout << "전공:" << GetMajor() << endl;
}
};
int main()
{
Student Kim("coumputer");
Kim.ShowData();
return 0;
}
7-3 객체의 생성 및 소멸 과정
위의 전공 예제에서 Person 멤버변수들을 public로 바꿔도 되지만 정보은닉을 무너뜨리고
private으로 유지하면 상속이 되더라도 private 멤버는 선언이 된 클래스 내에서만 가능해서 안된다
멤버 이니셜라이저
그렇다면 6장에서 나온 멤버이니셜라이져를 사용하면 된다. const멤버변수를 초기화시킬 방법으로 나왔던 개념이다.
멤버이니셜라이져를 사용하면,
베이스 클래스의 생성자를 명시적으로 호출하여 베이스 클래스의 멤버 변수를 사용자 임의 값으로 초기화 할 수 있다.
// Student(int a, char* _name, char* _major): Person(a, _name) { ...
Base 클래스의 생성자 명시적 호출
BBB(int j):AAA(j)
{
cout<<"BBB(int j) Call"<<endl;
}
멤버이니셜라이져는 const 선언된 멤버변수를 사용자 임의의 값으로 초기화 할 때도 사용된다.
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
int age;
char name[20];
public:
int GetAge() const {
return age;
}
const char* GetName() const {
return name;
}
Person(int _age=1, const char* _name="noname")
{
age = _age;
strcpy(name, _name);
}
~Person() //소멸자
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
int num;
char major[20]; //전공
public:
Student(int a, const char* _name, const char* _major) : Person(a, _name) {
//Student(int a, char* _name, const char* _major) : num(a) { //const 멤버변수를 초기화
//멤버 이니셜라이져 - 클래스 이름일 경우 a라는 이름의 인자값을 받을 수 있는 생성자를 호출
//age=a;
//strcpy(name, _name);
strcpy(major, _major);
}
const char* GetMajor() const
{
return major;
}
void ShowData() const
{
cout << "이름:" << GetName() <<endl;
cout << "나이:" << GetAge() <<endl;
cout << "전공:" << GetMajor() << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Student Kim(20, "kim", "computer");
Kim.ShowData();
return 0;
}
소멸자의 호출 순서
1. Derived 객체 소멸자 호출
2. Base 객체 소멸자 호출
3. 메모리 반환
스택의 FILO 구조로 소멸된다. 함수 호출 및 소멸과 동일하다. (출처)
protected 멤버
상속 관계에 놓여있을 경우 하위클래스에서 접근 허용
그 외에는 private 멤버와 동일한 특성을 보인다.
class AAA
{
private: int a;
protected: int b;
};
class BBB: public AAA
{
public:
void SetData()
{
a=10; //private member. error
b=20; //protected member
}
};
int main(void)
{
AAA aaa; //private이라서 컴파일에러
aaa.a=10;
aaa.b=20; //protected라서 컴파일에러
return 0;
}
예제
protected 멤버변수를 사용하면 자식 클래스에서도 사용 가능하지만,
protected보다 멤버이니셜라이저를 사용하는 게 좋은 구조이다.
Person 클래스의 name을 char Person_name으로 변경한다면 Student 클래스 내의 name까지 바꿔줘야 하는,
한 클래스의 변경이 다른 클래스까지의 변경을 유도하기 때문이다.
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
protected:
int age;
char name[20];
public:
int GetAge() const {
return age;
}
const char* GetName() const {
return name;
}
Person(int _age=1, const char* _name="noname")
{
age = _age;
strcpy(name, _name);
}
~Person() //소멸자
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
char major[20]; //전공
public:
Student(int a, const char* _name, const char* _major) { : Person(a, _name) {
// 직접 멤버 변수를 변경하면 Person 멤버변수가 변경되면 Student 클래스에서도 변경이 불가피하다.
//Student(int a, char* _name, const char* _major) : num(a) { //const 멤버변수를 초기화
// 멤버 이니셜라이져 - 클래스 이름일 경우 a라는 이름의 인자값을 받을 수 있는 생성자를 호출
// 생성자이니셜라이저를 이용해서 초기화하면
// Person클래스의 멤버 변수를 변경하더라도 해당 클래스 내에서만 멤버변수를 변경하면 된다.
// 객체 내의 변수를 외부에서 직접 변경하지 않도록 설계하면 유지보수에 유리하고 오류도 적을 것.
age=a;
strcpy(name, _name);
strcpy(major, _major);
}
const char* GetMajor() const
{
return major;
}
void ShowData() const
{
cout << "이름:" << GetName() <<endl;
cout << "나이:" << GetAge() <<endl;
cout << "전공:" << GetMajor() << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Student Kim(20, "kim", "computer");
Kim.ShowData();
return 0;
}
7-5 세 가지 형태의 상속
- public 상속이 대부분이다.
접근 권한 변경
- Base 클래스의 멤버는 상속되는 과정에서 접근 권한 변경
protected 상속일 때, 자기(protected)보다 넓은 접근 권한이 있다면 좁힌다. (public멤버는 protected로...)
자기(protected)보다 좁은 접근 권한이 있다면 ... private 멤버의 경우 접근 불가가 된다. 표를 참고하면 된다.
7-6 상속을 하는 이유
문제의 도입
- 운송 수단에 관련된 클래스 정의
- 자동차, 열차, 선박, 비행기 등등... 중복되는 부분이 생긴다.
중복되는 것들은 상위 객체에 넣어 상위 객체를 가져와서 코드를 간소화 할 수 있다.
문제의 해결
- 클래스를 상속하여 구성하면 코드가 간결해진다.
- 클래스를 확장하기 좋다.
// 탈 것
class Vehicle {
int pass;
int bag;
public:
Vehicle(int person = 0, int weight = 0);
void Ride(int person);
void Load(int weight);
};
// 비행기
class Airplane: public Vehicle {
int crew; //승무원 인원
public:
Airplane(int p = 0, int b = 0, int c = 0):Vehicle(p,w);
void TakeCrew(int crew);
};
// 기차
class Train:public Vehicle {
int length; //열차 칸 수
public:
Train(int p = 0, int b = 0, int l = 0):Vehicle(p,w);
void SetLength(int len);
};
'C, C++ > 열혈 C++ 프로그래밍' 카테고리의 다른 글
[윤성우 열혈 C++] 08.상속과 다형성 영상 (0) | 2021.10.12 |
---|---|
08.상속과 다형성 - 1. 객체 포인터의 참조관계 (0) | 2021.10.08 |
윤성우 열혈 C++ 07.상속의 이해 연습문제 (0) | 2021.10.07 |
07.상속의 이해 책 (0) | 2021.10.06 |
06. friend와 static 그리고 const (책) (0) | 2021.09.30 |