CFile 클래스를 사용하면 바이너리 또는 텍스트모드로 파일을 열어서 사용할수가 있습니다. 하지만 CFile 클래스는 파일을 입출력하는 함수가 텍스트기반이 아닌 바이너리 기반의 함수형태만 제공하기 때문에 텍스트 파일을 입출려할때 다소 불편함이 있습니다. 예를들어, 텍스트 파일을 열고 한줄씩 텍스트 데이터를 읽는다고 한다면 한자씩 읽어서 아스키코드를 비교해야하는 번거로운 작업을 프로그래머가 직접 구성해야 합니다.
이런불편함을 줄여주고자 예전에 런타임함수에서 사용하던 fgets, fputs 같은 함수를 제공하는 CStdioFile 클래스를 추가적으로 제공하는 것입니다.
The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conformant name: new-name.
To turn off deprecation warnings for these functions, define the preprocessor macro_CRT_NONSTDC_NO_WARNINGS. You can define this macro at the command line by including the option/D_CRT_NONSTDC_NO_WARNINGS.
int main(void)
{
Simple * sim1 = new ....;
Simple * sim2 = new ....;
}
두 포인터 변수는 new 연산자에 의해 생성된 객체가 가리킨다.
Q. 두 포인터 변수가 가리키는 객체의 자료형은 무엇일까 생각해보자.
sim1, sim2가 가리키는 객체는 Simple 클래스이거나 Simple 클래스를 상속하는 클래스의 객체일 것이다.
2. 전장 내용확인
class Base
{
public:
void BaseFunc() { cout<<"Base"<<endl; }
};
class Derived : public Base
{
public:
void DerivedFunc() { cout<<"Derived"<<endl; }
};
int main(void)
{
Base * bptr = new Derived(); //컴파일 ㅇㅋ
bptr->DerivedFunc(); //에러
....
}
컴파일 에러 이유 : bptr이 Base형 포인터이기 때문이다.
가리키는 대상은 Derived 객체지만 컴파일되지 않는다. C++ 컴파일러는 포인터연산의 가능성 여부를 판단할 때, 포인터의 자료형을 기준으로 판단하지 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다.
int main(void)
{
Base * bptr = new Derived(); //컴파일 ㅇㅋ
Derived * dptr = bptr; //에러 - 포인터 bptr의 포인터 형만 갖고 대입가능성을 판단한다.
....
}
역시 포인터 형만 갖고 대입가능성을 판단하여 역시 컴파일 에러가 난다. Base * bptr = new Derived();가 컴파일이 되는 이유 : Derived 클래스는 Base 클래스의 유도클래스니까 Base 클래스의 포인터 변수로 Derived 객체의 참조가 가능하니 컴파일에 문제가 없다.
int main(void)
{
Derived * dptr = new Derived(); //컴파일 ㅇㅋ
Base * bptr = dptr; //컴파일 ㅇㅋ
....
}
다른 예
#include <iostream>
using namespace std;
class First
{
public:
void FirstFunc() { cout << "firstFunc" << endl; }
};
class Second: public First
{
public:
void SecondFunc() { cout << "SecondFunc" << endl; }
};
class Third: public Second
{
public:
void ThirdFunc() { cout << "ThirdFunc" << endl; }
};
int main()
{
Third* tptr = new Third();
Second* sptr = tptr;
First* fptr = sptr;
}
Third형 포인터 변수 tptr이 가리키는 객체는 Second형 포인터 변수 sptr도 가리킬 수 있으므로 컴파일 오류가 없다. 그러나 객체를 참조하는 포인터의 형에 따라서 호출할 수 있는 함수의 종류에는 제한이 따른다.
int main()
{
Third* tptr = new Third();
Second* sptr = tptr;
First* fptr = sptr;
tptr->FirstFunc(); // (o)
tptr->SecondFunc(); // (o)
tptr->ThirdFunc(); // (o)
sptr->FirstFunc(); // (o)
sptr->SecondFunc(); // (o)
//sptr->ThirdFunc(); // (x) class Second에 ThirdFunc 멤버가 없습니다
fptr->FirstFunc(); // (o)
//fptr->SecondFunc(); // (x)
//fptr->ThirdFunc(); // (x) class First에 ThirdFunc 멤버가 없습니다
}
결론 : 포인터 형에 해당하는 클래스에 정의된 멤버에만 접근이 가능하다.
함수의 오버라이딩과 포인터 형
#include <iostream>
using namespace std;
class First
{
public:
void MyFunc() { cout << "firstFunc" << endl; }
};
class Second: public First
{
public:
void MyFunc() { cout << "SecondFunc" << endl; } //오버라이딩
};
class Third: public Second
{
public:
void MyFunc() { cout << "ThirdFunc" << endl; }
};
int main()
{
Third* tptr = new Third();
Second* sptr = tptr;
First* fptr = sptr;
fptr->MyFunc();
sptr->MyFunc();
tptr->MyFunc();
delete tptr;
return 0;
}
third형 포인터 변수 tptr 이 참조하는 객체에는 총 3개의 MyFunc()이 있고, 오버라이딩 관계라서 가장 마지막에 오버라이딩한 Third 클래스의 MyFunc()이 호출된다.
가상함수
포인터 변수의 자료형에 따라 호출되는 함수의 종류가 달라지지 않게 하기 위한 함수 virtual 키워드의 선언을 통해 이뤄진다.
올라갈수록 일반화, 아래로 내려갈수록 구체화된다. (특수화된다) 즉 범위는 좁아지고 특성(멤버함수, 멤버변수)과 기능은 많아진다. → C++ 상속적 특성
잘못된 상속의 예
사람은 학생이다 (X)
HAS-A (소유) 관계에 의한 상속
- 경찰은 몽둥이를 소유한다
예제
// hasa1.cpp
#include <iostream>
using namespace std;
//몽둥이
class Cudgel {
public:
void Swing()
{
cout << "몽둥이를 휘둘러라!" << endl;
}
};
//몽둥이를 가진 경찰
class Police: public Cudgel
{
public:
void UseWeapon() {
Swing();
}
};
int main()
{
Police pol;
pol.UseWeapon();
return 0;
}
HAS-A 관계에 의한 상속 그리고 대안 (대안방법이 낫다)
- 포함 관계를 통해 소유 관계를 표현 - 객체 멤버에 의한 포함 관계의 형성 - 객체 포인터 멤버에 의한 포함 관계의 형성
예제 :: 객체 멤버에 의한 포함 관계
/* hasa2.cpp */
class Cudgel //몽둥이
{
public:
void Swing(){ cout<<"Swing a cudgel!"<<endl; }
};
class Police //몽둥이를 소유하는 경찰
{
Cudgel cud; //멤버변수로 객체가 옴
// 구조체 때 배웠던 것처럼
// 클래스 멤버로 클래스 객체가 올 수 있다
public:
void UseWeapon(){ cud.Swing(); }
};
int main()
{
Police pol;
pol.UseWeapon();
return 0;
}
클래스 객체가 멤버로 포함되기 위해서는 반드시 인자값을 받지 않는 void 생성자를 지녀야 한다. // Cudgel cud(10); // (x) 구조체처럼 C++ 클래스 객체 멤버는 생성과 동시에 초기화 불가 (자바나 C#은 가능)
클래스의 멤버는 Cudgel클래스에 void생성자가 반드시 있어야 한다.
예제 :: 객체 포인터 멤버에 의한 포함 관계
// hasa3.cpp
#include <iostream>
using namespace std;
//몽둥이
class Cudgel {
public:
void Swing()
{
cout << "몽둥이를 휘둘러라!" << endl;
}
};
//몽둥이를 가진 경찰
class Police
{
Cudgel* cud; //객체의 포인터라 변경 가능하다.
public:
Police()
{
cud = new Cudgel; //객체 동적 생성, 힙에 존재.. void 생성자 호출
}
~Police()
{
delete cud;
}
void UseWeapon()
{
cud->Swing(); //스택에 존재
}
};
int main()
{
Police pol;
pol.UseWeapon();
return 0;
}
pol 이름으로 메모리공간 할당 멤버로 cud 포인터 메모리공간 4바이트 할당 Police 생성자 호출 cud 객체 동적 생성 (Cudgel 객체를 동적할당) 명시적인 게 없으므로 void 생성자(디폴트 생성자) 호출하면서 Cudgel 객체를 생성 Police 객체는 Cudgel객체를 가리킨다
예제 2, 3 결과는 같으나 3이 낫다.
=> 논리적인 포함관계는 상속보다는 대안 방법 (포함, 3번 예제의 사용) 이 낫다. 느슨하면서 확실한 관계가 좋고, 커플링(결합도)이 낮아지는 게 좋다
8-2 상속된 객체와 포인터 관계
객체포인터, 객체포인터 권한에 대해 알아본다.
객체 포인터
- 객체의 주소 값을 저장할 수 있는 포인터 (포인터인데 객체를 가리킬 수 있는 포인터) - AAA(Person)클래스의 포인터는 AAA객체 뿐만 아니라, AAA클래스를 상속하는 Derived 클래스(Student) 객체의 주소 값도 저장 가능
Student는 Person이다. Person 클래스 포인터 p로 Student 객체를 가리킬 수 있다. Student 객체로는 Person을 가리킬 수 없다.
예제
// inherit8_2.cpp
#include <iostream>
using namespace std;
class Person
{
public:
void Sleep() {
cout << "Sleep" << endl;
}
};
class Student: public Person
{
public:
void Study() {
cout << "Study" << endl;
}
};
class PartTimeStd : public Student
{
public:
void Work() {
cout << "Work" << endl;
}
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
Person* p3 = new PartTimeStd;
p1->Sleep();
p2->Sleep();
p3->Sleep();
return 0;
}
객체 포인터의 권한
- 포인터를 통해서 접근할 수 있는 객체 멤버의 영역 - Base 클래스의 객체 포인터는 가리키는 대상에 상관없이 Base 클래스 내에 선언된 멤버에만 접근
B클래스 포인터로 접근하면 B클래스 내로 접근이 가능
#include <iostream>
using namespace std;
class A
{
public:
void a(){
cout<<"a()"<<endl;
}
};
class B : public A
{
public:
void b(){
cout<<"b()"<<endl;
}
};
class C : public B
{
public:
void c(){
cout<<"c()"<<endl;
}
};
int main()
{
C* c = new C(); //void 생성자 호출
c->a();
c->b();
c->c();
return 0;
}
#include <iostream>
using namespace std;
class A
{
public:
void a(){
cout<<"a()"<<endl;
}
};
class B : public A
{
public:
void b(){
cout<<"b()"<<endl;
}
};
class C : public B
{
public:
void c(){
cout<<"c()"<<endl;
}
};
int main()
{
C* c = new C(); //void 생성자 호출
B* b = c; // b클래스의 포인터, c클래스의 포인터로 포인터의 타입이 달라도 대입 가능하다
// C클래스의 포인터는 C클래스의 포인터이자 B, A클래스의 포인터도 되는 것이기 떄문이다
// 그래서 타입이 좌 우가 일치한다.
//A* a = c; //그래서 이것도 가능하다.
A* a = b; // 이것도 가능하다.
//cout<<c<<endl;
//cout<<b<<endl;
//cout<<a<<endl;
// 출력을 해보면 같은 위치를 가리키고 있다
b->a(); // b라는 포인터가 가리키는 객체 안에는 a, b, c 함수가 있는데
b->b(); // 호출할 수 있는 함수는 a, b로 제한되어진다
// b클래스 내에 선언되어있는, 비클래스가 상속하고 있는 클래스의 멤버에만 접근이 가능하다.
//b->c(); // 컴파일타임에 에러가 발생
a->a();
//a->b();
//a->c();
return 0;
}
예제
// CPointer2.cpp
#include <iostream>
using namespace std;
class Person
{
public:
void Sleep() {
cout << "Sleep" << endl;
}
};
class Student : public Person
{
public:
void Study() {
cout << "Study" << endl;
}
};
class PartTimeStd : public Student
{
public:
void Work() {
cout << "Work" << endl;
}
};
int main()
{
Person* p3 = new PartTimeStd;
p3->Sleep();
//p3->Study(); //error
//p3->Work(); //error
// 컴파일과정에서는 판단이 안되고 런타임에 p3이 PartTimeStd객체라는게 판단이 되기 때문에 허용이 안된다
return 0;
}
salary가 빠진 이유는 고용직이 매달 가져가는 급여(봉급) 과 임시직 일당은 달리 계산해야 되어서
#include <iostream>
#include <cstring>
using namespace std;
/**** Employee *****/
class Employee
{
protected:
char name[20]; //이름
public:
Employee(char* _name);
const char* GetName(); //이름참조
};
Employee::Employee(char* _name) //생성자
{
strcpy(name, _name);
}
const char* Employee::GetName()
{
return name;
}
/**** Permanent *****/
class Permanent : public Employee
{
private:
int salary; //기본급여
public:
Permanent(char* _name, int sal);
int GetPay();
};
Permanent::Permanent(char* _name, int sal)
:Employee(_name) { //생성자
//Permanent 생성자는 생성자를 통해서 필요한 이름정보와 급여정보를 받아와야 한다
//상속받고 있는 employee 생성자를 호출하는데
//멤버이니셜라이저로 이름정보는 employee 클래스에 넘겨주고
//Permanent 기본급여를 관리해야해서 salary에 대입
salary=sal; //기본급여
//생성자 내에서 Employee 생성자를 상속하는데
}
int Permanent::GetPay()
{
return salary;
}
/**** Temporary ****/
class Temporary : public Employee
{
private:
int time; //일한시간 //Employee의 멤버로 올리지 않았음..올려도 되고..상황에 따라 판단.
int pay; //시간당급여
public:
Temporary(char* _name, int _time, int _pay);
int GetPay();
};
Temporary::Temporary(char* _name, int _time, int _pay)
: Employee(_name)
{
// 생성자
// 멤버이니셜라이저와 함께 초기화
time=_time;
pay=_pay;
}
int Temporary::GetPay()
{
return time*pay; //시간당 시급
}
/**** Department ****/
// 콘트롤 클래스
class Department
{
private:
//Permanent * empList[10];
Employee* empList[10];
int index;
public:
Department(): index(0) { };
void AddEmployee(Employee* emp); //Employee 객체를 상속하는 객체는 전달 가능할 것
void ShowList(); //급여 리스트 출력
};
void Department::AddEmployee(Employee* 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;
// empList[i]는 employee클래스의 포인터이고
// 가리키는 대상은 Permanent가 될 수 있지만
// 포인터 객체의 권한 특성으로
// 컴파일 타임에 에러가 발생한다...
cout<<endl;
}
}
/**** main function ****/
int main()
{
// 직원을 관리하는 control 클래스
Department department;
// 직원 등록
// Permanent객체 생성이 되고 그 자리로
// Permanent객체가 저장된 위치의 포인터가 리턴되어 AddEmployee 인자로 전달이 된다
department.AddEmployee(new Permanent("KIM", 1000));
department.AddEmployee(new Permanent("LEE", 1500));
department.AddEmployee(new Temporary("HAN", 10, 200));
department.AddEmployee(new Temporary("JANG", 12, 300));
// 이번 달에 지불해야할 급여
department.ShowList();
return 0;
}
이 부분의 주석을 풀면
8-3 상속된 객체와 참조 관계
객체 레퍼런스
-객체를 참조할 수 있는 레퍼런스 -클래스 포인터의 특성과 일치 (AAA를 상속하고 있는 클래스의 객체도 참조할 수 있다)
#include <iostream>
using namespace std;
class Person
{
public:
void Sleep(){
cout<<"Sleep"<<endl;
}
};
class Student : public Person
{
public:
void Study(){
cout<<"Study"<<endl;
}
};
class PartTimeStd : public Student
{
public:
void Work(){
cout<<"Work"<<endl;
}
};
int main(void)
{
PartTimeStd p;
Student& ref1=p; //Student 참조를 가지고 PartTimeStd 객체를 참조하고 있다
Person& ref2=p;
p.Sleep();
ref1.Sleep();
ref2.Sleep();
return 0;
}
참조를 쓰게 되면 Call by reference 인지 value인지 의미가 불분명하기 때문에쓰기 꺼려하기도 한다. 따라서 둘 중 하나를 고르자면 포인터에 더 익숙해지는 게 낫다.
객체 포인터의 권한
- 포인터를 통해서 접근할 수 있는 객체 멤버의 영역 - AAA객체와, AAA를 상속하고 있는 클래스의 객체도 가리킬 수 있다 (IS-A 관계) (Person 클래스의 p포인터로..) - 그러나 AAA 클래스의 객체 포인터는 가리키는 대상에 상관없이 AAA클래스 내에 선언된 멤버에만 접근
객체 레퍼런스의 권한
-객체를 참조하는 레퍼런스의 권한 -클래스 포인터의 권한과 일치
#include <iostream>
using namespace std;
class Person
{
public:
void Sleep(){
cout<<"Sleep"<<endl;
}
};
class Student : public Person
{
public:
void Study(){
cout<<"Study"<<endl;
}
};
class PartTimeStd : public Student
{
public:
void Work(){
cout<<"Work"<<endl;
}
};
int main(void)
{
PartTimeStd p;
p.Sleep();
p.Study();
p.Work();
Person& ref=p;
ref.Sleep();
// ref.Study(); // Error의 원인
// ref.Work(); // Error의 원인
// 호출 불가능하다. Person 클래스의 참조이기 때문에.
return 0;
}
8-4 Static Binding & Dynamic Binding
*오버로딩과 비교해서 복습
오버라이딩(Overriding)
- Base 클래스에 선언된 멤버와 같은 형태의 멤버를 Derived 클래스에서 선언 - Base 클래스의 멤버를 가리는 효과 - 보는 시야(Pointer)에 따라서 달라지는 효과 - 오버라이딩 자체가 재정의는 아니다. 재정의를 포함할 뿐.
fct 함수는 B클래스의 fct에 의해 오버라이딩 되었다 = 가려졌다 / 은닉되었다
보는 시야를 달리하면 볼 수도 있다는 이야기.
#include <iostream>
using namespace std;
class AAA
{
public:
void fct(){
cout<<"AAA"<<endl;
}
};
class BBB : public AAA
{
public:
void fct(){ //AAA 클래스의 fct() 함수를 오버라이딩.
cout<<"BBB"<<endl;
}
};
int main(void)
{
BBB b;
b.fct(); // BBB가 출력!!!!
return 0;
}
두 번째 예제
#include <iostream>
using namespace std;
class AAA
{
public:
void fct(){
cout<<"AAA"<<endl;
}
};
class BBB : public AAA
{
public:
void fct(){ //AAA 클래스의 fct() 함수를 오버라이딩.
cout<<"BBB"<<endl;
}
};
int main(void)
{
// BBB b;
// b.fct(); // BBB가 출력
BBB* b=new BBB;
b->fct(); // BBB가 출력 !!!
AAA* a=b;
a->fct(); // AAA가 출력 !!!
delete b;
return 0;
}
멤버 함수를 가상으로 선언하기
- 오버라이딩 되는 경우의 특징은? : 과장해서 말하자면 가상...즉 없는 것으로 간주한다. - 가상이니 대신 BBB의 fct()가 호출된다 - virtual의 특성도 상속된다
오버라이딩이 재정의가 아니고 virtual 키워드가 없으면 은닉, virtual 키워드를 넣어주면 재정의 특성을 갖게 된다
재정의 특성에 대한 단점도 있다. 9장에서 다룬다.
#include <iostream>
using namespace std;
class AAA
{
public:
virtual void fct(){
cout<<"AAA"<<endl;
}
};
class BBB : public AAA
{
public:
void fct(){ //AAA 클래스의 fct() 함수를 오버라이딩.
cout<<"BBB"<<endl;
}
};
int main(void)
{
// BBB b;
// b.fct(); // BBB가 출력
BBB* b=new BBB;
b->fct(); // BBB가 출력 !!!
AAA* a=b;
a->fct(); // BBB가 출력 !!!
delete b;
return 0;
}
virtual의 특성도 상속된다.
#include <iostream>
using namespace std;
class AAA
{
public:
virtual void fct(){
cout<<"AAA"<<endl;
} //이 클래스만 있으면 의미가 없다
// 상속될 때만 virtual의 의미가 있음.
// A* p = new A
// p-> fct();
// A a;
// a.fct()
};
class BBB : public AAA
{
public:
void fct(){ //AAA 클래스의 fct() 함수를 오버라이딩.
// virtual void fct()
// 자동으로 virtual 키워드가 들어가나
// 코드가독성을 위해 명시적으로 써주는게 좋다
cout<<"BBB"<<endl;
}
};
class CCC : public BBB
{
public:
//상속하면서 오버라이딩
void fct(){ // virtual
cout<<"CCC"<<endl;
// 얘는 또 의미가 없다
// 얘를 상속받는 다른 클래스가 없어서
}
};
int main(void)
{
BBB* b=new CCC;
b->fct(); //CCC출력
AAA* a=b;
a->fct(); //CCC출력
delete b;
return 0;
}
Static Binding vs. Dynamic Binding
스태틱 바인딩 : 호출될 함수가 정해진 것
A a;
a.f();
다이나믹 바인딩: 포인터가 아니고 포인터가 가리키는 객체에 따라 호출되는 함수가 동적으로 달라진다.
A* a = new ???
A클래스가 f함수는 virtual B클래스에 f함수가 있고 상속, C클래스(f함수 있음) 에 의해 상속되어진다
오버라이딩 된 함수의 호출
오버라이딩 된 함수의 호출이 필요한 이유 (연습문제 8-3 관련)
#include <iostream>
using namespace std;
class AAA
{
public:
virtual void fct(){
cout<<"AAA"<<endl;
}
};
class BBB : public AAA
{
public:
void fct(){
AAA::fct(); //방법 1. : 오버라이딩 되어있어도 AAA 호출 가능
// fct(); //재귀로 무한루프..사용ㄴㄴ
cout<<"BBB"<<endl;
}
};
int main(void)
{
AAA* a= new BBB;
cout<< "첫 번째 시도" <<endl;
a->fct();
cout<< "2 번째 시도" <<endl;
a->AAA::fct(); // 방법 2.
return 0;
}
8-5 Employee Problem 완전한 해결
활용해야 할 문법적 요소
- 함수 오버라이딩 - virtual 함수의 오버라이딩 - 상속되어지는 virtual 특성 - 오버라이딩 된 함수의 호출 방법
(이전 단계에서 GetPay()함수가 호출이 안되는 문제가 있었다)
Employee클래스의 포인터배열 이기 때문에 상속하는 모든 클래스의 객체는 참조가 가능하다 그러나 그대로 Employee클래스에 GetPay()를 선언하면 Employee클래스의 GetPay가 호출되니 virtual를 추가해줬다.
상속이 가져다주는 이점과 함께 생각해서 정리해본다.
#include <iostream>
#include <cstring>
using namespace std;
/**** Employee *****/
class Employee
{
protected:
char name[20]; //이름
public:
Employee(char* _name);
const char* GetName(); //이름참조
virtual int GetPay() //add
{
return 0;
}
};
Employee::Employee(char* _name) //생성자
{
strcpy(name, _name);
}
const char* Employee::GetName()
{
return name;
}
/**** Permanent *****/
class Permanent : public Employee
{
private:
int salary; //기본급여
public:
Permanent(char* _name, int sal);
int GetPay();
};
Permanent::Permanent(char* _name, int sal)
:Employee(_name) { //생성자
//Permanent 생성자는 생성자를 통해서 필요한 이름정보와 급여정보를 받아와야 한다
//상속받고 있는 employee 생성자를 호출하는데
//멤버이니셜라이저로 이름정보는 employee 클래스에 넘겨주고
//Permanent 기본급여를 관리해야해서 salary에 대입
salary=sal; //기본급여
//생성자 내에서 Employee 생성자를 상속하는데
}
int Permanent::GetPay()
{
return salary;
}
/**** Temporary ****/
class Temporary : public Employee
{
private:
int time; //일한시간 //Employee의 멤버로 올리지 않았음..올려도 되고..상황에 따라 판단.
int pay; //시간당급여
public:
Temporary(char* _name, int _time, int _pay);
int GetPay();
};
Temporary::Temporary(char* _name, int _time, int _pay)
: Employee(_name)
{
// 생성자
// 멤버이니셜라이저와 함께 초기화
time=_time;
pay=_pay;
}
int Temporary::GetPay()
{
return time*pay; //시간당 시급
}
/**** Department ****/
// 콘트롤 클래스
class Department
{
private:
//Permanent * empList[10];
Employee* empList[10];
int index;
public:
Department(): index(0) { };
void AddEmployee(Employee* emp); //Employee 객체를 상속하는 객체는 전달 가능할 것
void ShowList(); //급여 리스트 출력
};
void Department::AddEmployee(Employee* 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;
}
}
/**** main function ****/
int main()
{
// 직원을 관리하는 control 클래스
Department department;
// 직원 등록
// Permanent객체 생성이 되고 그 자리로
// Permanent객체가 저장된 위치의 포인터가 리턴되어 AddEmployee 인자로 전달이 된다
department.AddEmployee(new Permanent("KIM", 1000));
department.AddEmployee(new Permanent("LEE", 1500));
department.AddEmployee(new Temporary("HAN", 10, 200));
department.AddEmployee(new Temporary("JANG", 12, 300));
// 이번 달에 지불해야할 급여
department.ShowList();
return 0;
}
순수 가상함수와 추상 클래스
위의 예제에서 Employee에서 GetPay() 함수는 호출되지 않았다. 호출되기 위해서 존재가 아니라 대신 호출되기 위해서 교량의 역할로 존재한다.
virtual int GetPay();
호출은 되지만 실행되지 않으니 몸체를 생략해버리면 컴파일이 안 된다.
선언만 존재하는 순수 가상함수
virtual int GetPay() = 0; //선언만 하고 정의를 일부로 안한 것이라고 컴파일러에 알려주는 법
- 추상클래스 : 하나 이상 순수가상함수를 가지는 클래스 - 추상클래스는 객체화될 수 없다. 완전한 클래스가 아니다. (Employee클래스는 불완전 클래스) 그래서 객체화 될 수 없다. 포인터 선언은 가능하다.
Person형 포인터는 Person 객체, Person을 상속하는 유도 클래스의 객체도 가리킬 수 있다.
class Student: public Person
{ .... } ;
Person* ptr = new Student();
Student 클래스를 상속하는 유도 클래스 PartTimeStudent가 다음의 형태로 정의되었다고 가정하면 Person형 포인터변수는 PartTimeStudent 객체도 가리킬 수 있다.
class PartTimeStudent: public Student
{ ..... };
Person* ptr = new PartTimeStudent();
즉, C++에서 AAA형 포인터 변수는 AAA 객체 또는 AAA를 직접/간접적으로 상속하는 모든 객체를 가리킬 수 있다. (객체의 주소값을 저장할 수 있다.)
예
// ObjectPointer.cpp
#include <iostream>
using namespace std;
class Person
{
public:
void Sleep() {
cout<<"Sleep"<<endl;
}
};
class Student: public Person
{
public:
void Study() {
cout<<"Study"<<endl;
}
};
class PartTimeStudent: public Student
{
public:
void Work() {
cout<<"Work"<<endl;
}
};
int main()
{
Person* ptr1 = new Student();
Person* ptr2 = new PartTimeStudent();
Student* ptr3 = new PartTimeStudent();
ptr1->Sleep();
ptr2->Sleep();
ptr3->Study();
delete ptr1; delete ptr2; delete ptr3;
return 0;
}
Student *s가 Person 클래스의 객체를 가리키는 건 문제가 된다. 역방향은 성립하지 않기 때문이다 (IS-A가 아님)
유도클래스의 객체까지 가리킬 수 있다
C++에서 AAA형 포인터 변수는 AAA 객체 또는 AAA를 직접/간접적으로 상속하는 모든 객체를 가리킬 수 있다. (객체의 주소값을 저장할 수 있다.) Student, PartTimeStudent 객체는 Person객체의 일종이기 때문이다.
전 단원의 급여관리 예제 확장 + 함수오버라이딩
정규직, 영업직, 임시직 모두 고용인이며 영업직은 정규직의 일종이다. EmployeeHandler 클래스가 저장,관리 하는 대상이 Employee 객체가 되면 이후 Employee 클래스를 직간접적으로 상속하는 클래스가 추가되어도 EmployeeHandler 클래스는 변화가 없다.
// EmployeeManager2.cpp
#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;
}
};
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 EmployeeHandler
{
private:
Employee* empList[50]; //Employee 객체의 주소값 저장
// Employee 클래스를 상속하는 클래스의 객체도 이 배열에 함께 저장 가능
int empNum;
public:
EmployeeHandler()
: empNum(0)
{ }
void AddEmployee(Employee* emp) //Employee 객체의 주소값을 전달
// Employee 클래스를 상속하는 클래스 PermenentWorker 객체의 주소값도 전달 가능
{
empList[empNum++]=emp;
}
void ShowAllSalaryInfo() const{
}
void ShowTotalSalary() const{
int sum=0;
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;
}
EmployeeHandler 객체가 여전히 PermanentWorker 객체를 저장/관리하고 있다.
그래서
영업직 급여 = 월 기본급여 + 인센티브 임시직 급여 = 시간당급여 * 일한시간
형태로 정의를 추가한다.
임시직 클래스
// 임시직. 실제 일을 한 시간으로 급여 계산
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;
}
};
영업직 클래스
// 영업직 (정규직 일종). Employee가 아닌 PermanentWorker 상속
// 기본급여과 관련된 부분을 멤버로 포함, 상여금 부분만 멤버로 추가
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
{
return PermanentWorker::GetPay() //PermanentWorker의 GetPay() 호출
+(int)(salesResult*bonusRatio);
}
void ShowSalaryInfo() const
{
ShowYourName();
cout<<"salary: "<<GetPay()<<endl<<endl; //SalesWorker의 GetPay() 호출
}
};
정규직과 영업직에는 동일 함수 GetPay()와 ShowSalaryInfo()가 있다. (기본클래스 유도클래스에 같은 함수가 있다)
* 함수 오버라이딩 : 함수오버로딩과 혼동 x 기초 클래스와 동일한 이름의 함수를 유도클래스에서 정의되어도 매개변수의 자료형 및 개수가 다르면 함수오버로딩이 되어 전달되는 인자에 따라 호출함수가 결정된다. 함수오버로딩은 상속관계에서도 구성 가능.
오버라이딩 된 기초클래스 함수는 오버라이딩 한 유도클래스 함수에 가려진다. SalesWorker 클래스 내에서 GetPay를 호출하면 SalesWorker 클래스에 정의된 GetPay함수가 호출된다.
단 SalesWorker 클래스 내에서도 PermanentWorker::GetPay() 이렇게 호출하면 PermanentWorker 클래스 내부의 GetPay가 호출된다.
아래처럼도 호출 가능하다. seller 객체의 PermanentWorker 클래스에 정의된 ShowSallryInfo()를 호출..이런 뜻
// EmployeeManager3.cpp
#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;
}
};
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 EmployeeHandler
{
private:
Employee* empList[50]; //Employee 객체의 주소값 저장
// Employee 클래스를 상속하는 클래스의 객체도 이 배열에 함께 저장 가능
int empNum;
public:
EmployeeHandler()
: empNum(0)
{ }
void AddEmployee(Employee* emp) //Employee 객체의 주소값을 전달
// Employee 클래스를 상속하는 클래스 PermenentWorker 객체의 주소값도 전달 가능
{
empList[empNum++]=emp;
}
void ShowAllSalaryInfo() const{
}
void ShowTotalSalary() const{
int sum=0;
cout<<"salary sum: "<<sum<<endl;
}
~EmployeeHandler()
{
for(int i=0; i<empNum; i++)
delete empList[i];
}
};
// 임시직. 실제 일을 한 시간으로 급여 계산
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;
}
};
// 영업직 (정규직 일종). Employee가 아닌 PermanentWorker 상속
// 기본급여과 관련된 부분을 멤버로 포함, 상여금 부분만 멤버로 추가
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
{
return PermanentWorker::GetPay() //PermanentWorker의 GetPay() 호출
+(int)(salesResult*bonusRatio);
}
void ShowSalaryInfo() const
{
ShowYourName();
cout<<"salary: "<<GetPay()<<endl<<endl; //SalesWorker의 GetPay() 호출
// PermanentWorker 클래스의 ShowSalaryInfo()랑 같은 내용의 함수인데도
// 오버라이딩 한 이유는 상속이 되었어도 어느 GetPay를 부를 지 컨트롤 할 수 없어서
}
};
int main(void)
{
// 직원관리를 목적으로 설계된 컨트롤 클래스의 객체 생성
EmployeeHandler handler;
// 정규직 등록
handler.AddEmployee(new PermanentWorker("KIM", 1000));
handler.AddEmployee(new PermanentWorker("LEE", 1500));
// 임시직 등록
TemporaryWorker* alba=new TemporaryWorker("Jung", 700);
alba->AddWorkTime(5); //5시간 일한결과 등록
handler.AddEmployee(alba);
// 영업직 등록
SalesWorker* seller=new SalesWorker("Hong", 1000, 0.1) ;
seller->AddSalesResult(7000); //영업실적 7000
handler.AddEmployee(seller);
// 이달 지불할 급여 정보
handler.ShowAllSalaryInfo();
// 이달 지불할 급여 총합
handler.ShowTotalSalary();
return 0;
}
메인 함수 내 handler.AddEmployee(new PermanentWorker("KIM", 1000)); 와 같은 코드 4줄에서 발생. 에러 내용 : 인수 1을(를) const char[4]에서 char* (으)로 변환할 수 없습니다. 이유 : "aa" 문자열은 상수인데 변수에 값을 넣으려 해서. C에서는 문자열 리터럴이 char의 배열이지만, C++에서는 const char의 배열이다.
에러 수정 : 1) 포인터가 아닌 배열로 선언 2) (char*) 형식으로 형변환 3) 자료형을 const char* 형태로 바꾸기 4) const_cast<char*> 사용하기
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) { ...
상속 관계에 놓여있을 경우 하위클래스에서 접근 허용 그 외에는 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);
};
#include <iostream>
using namespace std;
class Car
{ //기본 연료 자동차
private:
int gasolineGauge;
public:
int GetGasGauge()
{
return gasolineGauge;
}
};
class HybridCar: public Car
{ //하이브리드 자동차
private:
int electricGauge;
public:
int GetElecGuage()
{
return electricGauge;
}
};
class HybridWaterCar: public HybridCar
{ //하이브리드 수중차
private:
int waterGuage;
public:
void ShowCurrentGuage()
{
cout<<"잔여 가솔린: "<<GetGasGauge()<<endl;
cout<<"잔여 전기량: "<<GetElecGuage()<<endl;
cout<<"잔여 워터량: "<<waterGuage<<endl;
}
};
푼 것
#include <iostream>
using namespace std;
class Car
{ //기본 연료 자동차
private:
int gasolineGauge;
public:
Car(int mygasolineGauge) : gasolineGauge(mygasolineGauge)
{ }
int GetGasGauge()
{
return gasolineGauge;
}
};
class HybridCar: public Car
{ //하이브리드 자동차
private:
int electricGauge;
public:
HybridCar(int mygasolineGauge, int myelectricGauge)
: Car(mygasolineGauge), electricGauge(myelectricGauge)
{ }
int GetElecGuage()
{
return electricGauge;
}
};
class HybridWaterCar: public HybridCar
{ //하이브리드 수중차
private:
int waterGuage;
public:
HybridWaterCar(int mygasolineGauge, int myelectricGauge, int mywaterGuage)
: HybridCar(mygasolineGauge, myelectricGauge), waterGuage(mywaterGuage)
{ }
void ShowCurrentGuage()
{
cout<<"잔여 가솔린: "<<GetGasGauge()<<endl;
cout<<"잔여 전기량: "<<GetElecGuage()<<endl;
cout<<"잔여 워터량: "<<waterGuage<<endl;
}
};
int main()
{
HybridWaterCar car1(1,2,3);
car1.ShowCurrentGuage();
return 0;
}
2. 다음 두 클래스에 적절한 생성자와 소멸자를 정의해보자. 그리고 확인을 위한 main 함수를 정의해보자.
#include <iostream>
using namespace std;
class MyFriendInfo
{
private:
char* name;
int age;
public:
void ShowMyFreindInfo()
{
cout<<"이름: "<<name<<endl;
cout<<"나이: "<<age<<endl;
}
};
class MyFriendDetailInfo: public MyFriendInfo
{
private:
char* addr;
char* phone;
public:
void ShowMyFreindDetailInfo()
{
ShowMyFreindInfo();
cout<<"주소: "<<addr<<endl;
cout<<"전화: "<<phone<<endl<<endl;
}
};
#include <iostream>
#include <cstring>
using namespace std;
// 사각형
class Rectangle
{
private:
int x;
int y;
int rectangleArea;
public:
Rectangle(int width, int length)
: x(width), y(length)
{
rectangleArea = x*y;
}
void ShowAreaInfo()
{
cout<<"면적 : "<<rectangleArea<<endl;
}
};
// 정사각형
class Square: public Rectangle
{
private:
int l;
public:
Square(int line)
: Rectangle(line, line)
{ }
};
int main(void)
{
Rectangle rec(4,3);
rec.ShowAreaInfo();
Square sqr(7);
sqr.ShowAreaInfo();
return 0;
}
2. 책을 의미하는 Book 클래스와 전자 책을 의미하는 Ebook 클래스를 정의하고자 한다. 전자 책도 책의 일종이므로 다음의 형태로 클래스 상속관계를 구성하고자 한다. (클래스에 선언되어야 할 멤버변수만 제시)
class Book
{
private:
char* title; // 책의 제목
char* isbn; // 국제표준도서번호
int price; // 책의 정가
....
};
class EBook: public Book
{
private:
char* DRMKey; // 보안관련 키
....
};
main 함수와 함께 실행이 가능하도록 위의 클래스를 완성해보자
int main(void)
{
Book book("좋은 C++", "555-12345-890-0", 20000);
book.ShowBookInfo();
cout<<endl;
EBook ebook("좋은 C++ ebook", "555-12345-890-1", 10000, "fdx9w0i8kw");
ebook.ShowEBookInfo();
return 0;
}
실행 예
제목: 좋은 C++ ISBN: 555-12345-890-0 가격: 20000
제목: 좋은 C++ ebook ISBN: 555-12345-890-1 가격: 10000 인증키: fdx9w0i8kw
푼 것
#include <iostream>
#include <cstring>
using namespace std;
class Book
{
private:
char* title; // 책의 제목
char* isbn; // 국제표준도서번호
int price; // 책의 정가
public:
Book(const char* btitle, const char* bisbn, int bprice)
: price(bprice)
{
title=new char[strlen(btitle)+1];
isbn=new char[strlen(bisbn)+1];
strcpy(title, btitle);
strcpy(isbn, bisbn);
}
void ShowBookInfo()
{
cout<<"제목: "<<title<<endl;
cout<<"ISBN: "<<isbn<<endl;
cout<<"가격: "<<price<<endl;
}
~Book(){
delete []title;
delete []isbn;
}
};
class EBook: public Book
{
private:
char* DRMKey; // 보안관련 키
public:
EBook(const char* btitle, const char* bisbn, int bprice, const char* bDRMKey)
: Book(btitle, bisbn, bprice)
{
DRMKey=new char[strlen(bDRMKey)+1];
strcpy(DRMKey, bDRMKey);
}
void ShowEBookInfo()
{
ShowBookInfo();
cout<<"인증키: "<<DRMKey<<endl;
}
~EBook(){
delete []DRMKey;
}
};
int main(void)
{
Book book("좋은 C++", "555-12345-890-0", 20000);
book.ShowBookInfo();
cout<<endl;
EBook ebook("좋은 C++ ebook", "555-12345-890-1", 10000, "fdx9w0i8kw");
ebook.ShowEBookInfo();
return 0;
}
#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 : 유래, 파생 슈퍼 클래스 ↔ 서브 클래스 부모 클래스 ↔ 자식 클래스
유도 클래스의 객체생성 과정에서 기초 클래스의 생성자는 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 관계의 표현에도 사용될 수 있으나 프로그램의 변경에 많은 제약을 가져다 줄 수 있다.