🤯 언리얼을 하기 위해 C++ 기억 되살리기 프로젝트
개체지향 프로그래밍 (OOP) 3
1. 상속
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Animal.h
class Animal
{
public:
Animal(int age);
private:
int mAge;
};
// Cat.h
class Cat : public Animal
{
public:
Cat(int age, const char* name);
private:
char* mName;
};
// Cat.cpp
Cat::Cat(int age, const char* name)
: Animal(age) // 부모로부터 상속을 받았으므로, 부모의 생성자 호출
{ // 초기화 리스트 중인데 가장 먼저 할 일은 부모의 생성자를 호출하는 것
// mAge는 부모에게 있으므로 부모한테 생성자 호출하라고 던지는 거임
size_t size = strlen(name) + 1;
mName = new char[size];
strcpy(mName, name);
}
|
1-1. 상속이란?
- 다른 클래스의 특성을 내려 받음
- 베이스 클래스(부모 클래스), 파생 클래스(자식 클래스)
- 파생 클래스의 개체는 다음의 것들을 가짐
- 베이스 클래스의 멤버 변수
- 베이스 클래스의 멤버 메서드
- 자신의 생성자와 소멸자
- 파생 클래스는 멤버 변수 및 메서드 추가 가능
1-2. 메모리를 보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Animal.cpp
Animal::Animal(int age)
: mAge(age)
{
}
// Cat.cpp
Cat::Cat(int age, const string& name)
: Animal(age)
{
// 부모 생성자를 암시적으로 먼저 호출 후 자식 생성자 호출하여 초기화 됨
size_t size = strlen(name) + 1;
mName = new char[size];
strcpy(mName, name);
}
// main.cpp
Cat* myCat = new Cat(2, "Mew");
// myCat 변수는 스택에 4바이트를 먹음
|
자식 클래스로 만든 오브젝트에 대한 포인터를 부모 포인터로 할 수 있음.
Animal* a = new Cat(...);
1-3. 생성자 호출 순서
- 베이스 클래스의 생성자가 먼저 호출 됨
- 그 다음으로 파생 클래스의 생성자가 호출 됨
- 부모 클래스의 특정 생성자를 호출 할 땐 초기화 리스트를 사용해야 함
- 안그러면 호출할 데가 없음. 자식 생성자 끝나고 나면 그다음부턴 대입임.
1
2
3
4
5
6
| Cat::Cat(int legs, int age, const string& callingName)
: Animal(legs, age) // 명시적 호출
, mCallingName(callingName)
{
}
|
매개변수 없는 생성자가 ‘없는’ 부모 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Animal.cpp
Animal::Animal(int age)
: mAge(age)
{
// 매개변수 있는 생성자만 있음
}
// Cat.cpp
Cat::Cat(int age, const string& name)
{
// <!--------- 컴파일 에러--------->
// 초기화 리스트에서 부모 생성자를 호출하지 않으므로
// 암시적으로 부모의 생성자 Animal()을 호출하게 됨
// 그러나 부모엔 Animal()이 없고 Animal(int age)만 있음
// 그래서 호출할 수 있는 생성자가 없어 컴파일 에러
size_t size = strlen(name) + 1;
mName = new char[size];
strcpy(mName, name);
}
// main.cpp
Cat* myCat = new Cat(2, "Mew");
|
1-4. 자식 개체 지우기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Animal.cpp
Animal::~Animal()
{
}
// Cat.cpp
Cat::~Cat()
{
// 자식부터 지워짐
delete mName;
// 이후 ~Animal()을 호출
}
// main.cpp
delete myNeighboursCat;
|
- 생성자 호출 순서와 정반대 (자식->부모)
- 자식 클래스 소멸자의 마지막에서 부모 클래스의 소멸자가 자동으로 호출됨
2. 다형성 (feat. 상속)
2-1. 다형성에 들어가기 전에, 멤버 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // Animal.h
class Animal
{
public:
int GetAge();
private:
int mAge;
};
// Cat.h
class Cat : public Animal
{
public:
const char* GetName();
private:
char* mName;
};
// main.cpp
// 각 값은 스택 또는 힙에 위치해 있음 (myCat은 스택에, 값은 힙에...)
Cat* myCat = new Cat(5, "Coco");
Cat* yourCat = new Cat(2, "Mocha");
myCat->GetName(); // 그렇다면 멤버 함수는 어디에 있을까?
yourCat->GetName();
|
- 멤버 함수도 메모리 어딘가에 위치해 있음
- 멤버 함수는 컴파일 시에 한 번만 메모리에 할당됨
2-2. 함수 오버라이딩, 바인딩, 가상함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| class Animal
{
public:
void Speak(); // " An animal";
}
class Cat : public Animal
{
public:
void Speak(); // "Meow";
}
Cat* myCat = new Cat();
myCat->Speak(); // Meow
Animal* yourCat = new Cat();
yourCat->Speak(); // An animal
|
정적 바인딩
c++에선 정적 바인딩을 함. 무늬 따라 간다는 것.
Cat* myCat = new Cat(5, "Mocha");
Cat* 으로 타입을 정의했으므로 무조건 Cat에 있는 함수를 호출해줄 거라는 것.
Animal* yourCat = new Cat(5, "Mocha");
마찬가지로 Animal…
동적 바인딩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // Animal.h
class Animal
{
public:
virtual void Move(); // 가상 함수!
virtual void Speak(); // 실행하는 것의 실체를 찾아서 호출해줘~
};
// Animal.cpp
void Animal::Move()
{
}
void Animal::Speak()
{
std::cout << "An Animal" << std::endl;
}
// Cat.h
class Cat : public Animal
{
public:
void Speak();
};
// Cat.cpp
void Cat::Speak()
{
std::cout << "Meow" << std::endl;
}
// main.cpp
Cat* myCat = new Cat(2, "Coco");
myCat->Speak(); // Meow??
Animal* yourCat = new Cat(5, "Mocha");
yourCat->Speak(); // Meow??
|
둘 다 실체는 Cat이지만 타입이 다름. 무늬가 Animal이다.
하지만 virtual이 붙었으므로 실체를 찾는다 -> 둘 다 Meow가 되는것 맞음.
가상 함수
- 자식 클래스의 멤버 함수가 언제나 호출 됨
- 부모의 포인터 또는 참조를 사용 중이더라도 (무늬가 뭐든 간에)
- 동적 바인딩 / 늦은 바인딩 (dynamic / late)
- 실행 중에 어떤 함수를 호출할지 결정한다
- 당연히 정적 바인딩보다 느림
- 이를 위해 가상 테이블이 생성됨
- 모든 가상 멤버함수의 주소를 포함함
- 이 테이블을 이용해 어떤 함수를 호출해야 하는지 찾음
- 클래스 마다 하나 vs 개체마다 하나?
- 한 클래스에 속한 모든 개체에 대해서 함수는 딱 하나만 있음. (위에 한 번만 할당된다고 했었음!)
- 그렇기 때문에 가상 테이블도 각 개체마다 있는게 아니라, 클래스마다 하나씩 있는게 맞음.
- 개체를 생성할 때 해당 클래스의 가상 테이블 주소가 함께 저장됨.
- eg. Speak()이 어딨지? => 가상 테이블이 0x009ED8에 있고 Speak()는 두 번째 함수니까
0x009EDC에 저장된 주소에 있을 것임. (두번째니까 4바이트 x 2 해서 8바이트 더한 곳에 위치)
가상 소멸자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // Animal.h
class Animal
{
public:
~Animal();
private:
int mAge;
};
// Cat.h
class Cat : public Animal
{
public:
~Cat();
private:
char* mName;
}
// main.cpp
Cat* myCat = new Cat(2, "Coco");
delete myCat;
// 이렇게 소멸자가 비 가상 소멸자인 경우
// ~Cat() 먼저 호출되고, 마지막에 부모 ~Animal() 호출하여 잘 지워짐.
Animal* yourCat = new Cat(5, "Mocha");
delete yourCat;
// 하지만 이 경우 무늬가 Animal이므로, 정적 바인딩으로 되어 ~Animal() 호출만 됨.
// ~Cat(); 호출 안 됨. 메모리 누수! char* mName;
|
💡 그래서 가상 소멸자를 만들어줘야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // Animal.h
class Animal
{
public:
virtual ~Animal();
private:
int mAge;
};
// Cat.h
class Cat : public Animal
{
public:
virtual ~Cat(); // 부모가 가상이면 자식 소멸자도 자동으로 가상으로 됨
// 그치만 가독성을 위해 붙여주자
private:
char* mName;
}
// Cat.cpp
Cat::~Cat() // 요기엔 virtual 안붙임
{
delete mName;
}
// main.cpp
Cat* myCat = new Cat(2, "Coco");
delete myCat;
// 이렇게 소멸자가 비 가상 소멸자인 경우
// ~Cat() 먼저 호출되고, 마지막에 부모 ~Animal() 호출하여 잘 지워짐.
Animal* yourCat = new Cat(5, "Mocha");
delete yourCat;
// 하지만 이 경우 무늬가 Animal이므로, 정적 바인딩으로 되어 ~Animal() 호출만 됨.
// ~Cat(); 호출 안 됨. 메모리 누수! char* mName;
|
- 멤버함수의 가상성은 상속됨
- 부모 클래스에서 가상으로 선언된 멤버 함수가 있다면 전부 가상함수
- virtual 키워드가 없어도!
- 자식 클래스에서 동일한 시그니처를 가진 함수 역시 가상함수!
2-3. 다중 상속
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Faculty.h
class Faculty
{
};
// Student.h
class Student
{
};
// TA.h
class TA : public Student, public Faculty
{
// Student(), Faculty() 순으로 호출
};
class TA : public Faculty, public Student
{
// Faculty(), Student() 순으로 호출
};
class TA : public Student, public Faculty
: Faculty()
, Student()
{
// Student(), Faculty() 순으로 호출
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // Faculty.h
class Faculty
{
public:
void DisplayData();
};
// Student.h
class Student
{
public:
void DisplayData();
};
// TA.h
class TA : public Faculty, public Student
{
};
// main.cpp
TA* myTA = new TA();
myTA->DisplayData(); // <-- 컴파일 에러! 뭘 호출해줘야 할지 모르니까.
// 해결 방법
myTA->Student::DisplayData(); // 이렇게 직접 부모클래스를 넣어줘야 함.
|
가상 부모 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Animal.h
class Animal
{
};
// Tiger.h
class Tiger : virtual public Animal
{
};
// Lion.h
class Lion : virtual public Animal
{
};
// Liger.h
class Liger : public Tiger, public Lion
{
// Tiger, Lion에 붙은 virtual public Animal 의 virtual 키워드는
// Animal이 하나만 있게 보장을 해준다
// 그치만 가급적 이렇게 쓰지 말고 인터페이스(interface)를 써라
};
|
2-4. 추상 클래스 (abstract)
구체적인 함수의 구현이 안되어 있는 클래스가 추상 클래스 (함수 하나라도)
-> 즉 순수 가상함수를 가지고 있는 부모 클래스를 말한다!
- 추상 클래스에서 개체를 만들 수 없음
- 구현된 함수가 없는데. 개체를 만들면. 함수 호출하면 에러나니까.
- 추상 클래스를 포인터나 참조형으로는 사용 가능 (동적 바인딩! 자식에서 구현된게 호출되니까)
Animal myAnimal;
: 컴파일 에러. 추상 클래스는 개체 생성 안됨.
Animal* myAnimal = new Animal();
: 컴파일 에러. 개체 생성 안된다구!
Animal* myCat = new Cat();
: 개체 생성 잘 됨.
Animal& myCatRef = *myCat;
: myCatRef는 myCat의 주소를 가리킨다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Animal.h
class Animal
{
public:
virtual ~Animal();
virtual void Speak() = 0; // <- 이거! "구현하지 않을게" 순수 가상 함수
// 이 함수가 있음으로써 Animal class는 추상 클래스가 된다.
private:
int mAge;
};
// Cat.h
class Cat : public Animal
{
public:
~Cat();
void Speak();
private:
char* mName;
}
|
- 순수 가상 함수
- 구현체가 없는 멤버 함수
- 자식 클래스가 구현해야 함
- 구현 안하면 컴파일 에러!
2-5. 인터페이스 (Interface)
추상 클래스랑 거의 비슷한데, 인터페이스는 함수
만 있음.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // IFlyable.h
class IFlyable
{
public:
virtual void Fly() = 0;
};
// IWalkable.h
class IWalkable
{
public:
virtual void Walk() = 0;
};
class Bat : public IFlyable, public IWalkable
{
public:
void Fly();
void Walk();
};
class Cat : public IWalkable
{
public:
void Walk();
};
|
- C++은 자체적으로 인터페이스를 지원 안 함
- 그래서 순수 추상 클래스를 사용하여 흉내내는 거임
- 순수 가상 함수만 가짐
- 멤버 변수는 없음
1
2
3
4
5
| class IFlyable
{
public:
virtual void Fly() = 0;
};
|
댓글남기기