3.1 상속
3.1-1 상속이란,
Def) 상속(Inheritance)
다른 클래스의 멤버와 메서드를 물려받는 것을 의미함.
물려주는 다른 클래스를 부모 클래스 혹은 베이스 클래스라 하고
물려받는 본인 클래스를 자식 클래스, 파생 클래스라 함.
Note) 상속 관계를 다른 말로 is-a 관계라고도 함.
ex. Cat is a Animal. / Dog is a Animal.
다만 Animal is a Cat은 아닐 수 있음.
3.1-2 자식 클래스
Note) 자식 클래스 정의 방법
class 자식클래스명 : 접근제어자 부모클래스명 {};
class Cat : public Animal { ... };
class KiaMorning : public Car { ... };
class AndroidPhone : public Phone { ... };
Note) 접근 제어자의 기능
ex. Animal 클래스에 public 접근 제어자로 Move() 구현하고,
Cat 클래스가 Animal 클래스를 private 상속 접근제어를 하면
Move() 메서드 역시 private 접근 제어자를 따름. 즉 컴파일 에러.
클래스 멤버 접근 종류 | ||||
public | protected | private | ||
상속 접근 종류 | public | public | protected | private |
protected | protected | protected | private | |
private | private | private | private |
Ex03010201)
<hide/>
// Animal.h
#pragma once
class Animal
{
public:
Animal(const int& age);
private:
int mAge;
};
<hide/>
// Animal.cpp
#include "Animal.h"
Animal::Animal(const int& age)
: mAge(age)
{
}
<hide/>
// Cat.h
#pragma once
#include "Animal.h"
class Cat : public Animal
{
public:
Cat(const int& age, const char* name);
private:
char* mName;
};
<hide/>
// Cat.cpp
#include <cstring>
#include "Cat.h"
Cat::Cat(const int& age, const char* name)
: Animal(age) // 부모 클래스 생성자의 명시적 호출
, mName(nullptr)
{
size_t length = strlen(name) + 1;
mName = new char[length];
memcpy(mName, name, length);
}
<hide/>
// main.cpp
#include <iostream>
#include "Animal.h"
#include "Cat.h"
using namespace std;
int main()
{
Animal unknown = Animal(4);
Cat choo = Cat(7, "Choo");
return 0;
}
3.1-3 생성자 호출 순서
Note) 만약 아래와 같은 코드가 실행되었다고 해보자.
1. 스택 메모리에 myCat의 주소를 저장할 4byte 크기 공간 생성.
2. 해당 메모리 주소는 힙메모리를 가르키게 되고,
힙 메모리에는 Cat의 크기(int + char[]) 만큼 동적할당됨.
3. Animal의 생성자가 호출되고, age가 2살로 초기화됨.
4. Animal의 힙 메모리 공간 뒤에부터 Cat 생성자가 초기화 시작.
Cat* myCat = new Cat(2, "Kim");
Note) 생성자 호출 순서
1. 부모 클래스의 생성자(명시적 혹은 암시적으로)
2. 자식 클래스의 생성자
Note) 부모 클래스 생성자의 명시적 호출
부모 클래스의 특정 생성자를 호출할 때는
반드시 초기화 리스트를 사용해야함.
사용하지 않고 호출한다는 건, 자식 개체의
대입 과정 중에 부모 클래스 개체를 초기화하는 어불성설.
Note) 부모 클래스 생성자의 암시적 호출
1. Animal 클래스에 기본 생성자가 있는 경우
부모 클래스 생성자를 암시적 호출할 때는
당연하게도 기본 생성자가 호출되게됨.
기본 생성자가 있다면 컴파일 에러가 안남.
2. Animal 클래스에 기본 생성자가 없는 경우
암시적으로 부모 클래스 생성자를 호출하려다가
기본 생성자가 없어서 컴파일 에러가 나게됨.
3.1-4 소멸자 호출 순서
Note) 만약 아래와 같은 코드가 실행되었다고 해보자.
호출 순서: ~Cat() -> delete mName -> ~Animal()
delete myCat;
Note) 소멸자 호출 순서
생성자 호출 순서와 정반대.
자식 클래스 소멸자의 마지막에서
부모 클래스의 소멸자가 자동적으로 호출됨.
Note) 어떻게 자동으로 부모 클래스의 소멸자가 호출되는 걸까?
부모 메모리가 있고, 자식 메모리가 있는 상황.
자식 메모리가 먼저 정리되고, 부모 소멸자를 부를 때
부모 소멸자가 하나 밖에 없으므로 자동으로 호출될 수 있음.
Ex03010401)
<hide/>
// Animal.h
#pragma once
class Animal
{
public:
Animal();
Animal(const int& age);
~Animal();
int GetAge() const;
private:
int mAge;
};
<hide/>
// Animal.cpp
#include <iostream>
#include "Animal.h"
using namespace std;
Animal::Animal()
: mAge(0)
{
cout << "Animal::Animal() has been called." << endl;
}
Animal::Animal(const int& age)
: mAge(age)
{
cout << "Animal::Animal(const int& age) has been called." << endl;
}
Animal::~Animal()
{
cout << "Animal::~Animal() has been called." << endl;
}
int Animal::GetAge() const
{
return mAge;
}
<hide/>
// Cat.h
#pragma once
#include "Animal.h"
class Cat : public Animal
{
public:
Cat();
Cat(const int& age, const char* name);
~Cat();
private:
char* mName;
};
<hide/>
// Cat.cpp
#include <iostream>
#include <cstring>
#include "Cat.h"
using namespace std;
Cat::Cat() // 고의로 부모 클래스 생성자 암시적 호출.
{
cout << "Cat::Cat() has been called." << endl;
}
Cat::Cat(const int& age, const char* name)
: Animal(age) // 부모의 생성자 호출
, mName(nullptr)
{
cout << "Cat::Cat(const int& age, const char* name) has been called." << endl;
size_t length = strlen(name) + 1;
mName = new char[length + 1];
memcpy(mName, name, length + 1);
}
Cat::~Cat()
{
cout << "Cat::~Cat() has been called." << endl;
}
<hide/>
// main.cpp
#include <iostream>
#include "Animal.h"
#include "Cat.h"
using namespace std;
int main()
{
Animal unknown = Animal(4);
Cat choo = Cat(7, "Choo");
return 0;
}
3.2 다형성
3.2-1 멤버 함수의 메모리 위치
Note) 모든 것은 메모리 어딘가에 위치해 있어야만함.
멤버 함수도 메모리 어딘가에 위치해 있음.
Note) 멤버는 스택 메모리와 힙 메모리에 있다는 걸 배움.
멤버 함수는 어디에 어떻게 위치해 있을까.
Note) 일단 어떻게 위치해 있을지 고민해 보자.
각 개체마다 멤버 함수가 따로 위치해 있을까?
그러기엔 동작이 완전히 일치해서 그럴 필요가 없을듯.
인자로 받는 데이터만 달라짐.
Note) 각 멤버 함수는 컴파일 시에 딱 한번만 메모리에 할당됨.
저수준에서는 멤버 함수와 전역 함수가 그다지 다르지 않음.
그저 전역함수에 this를 인자로 자동 전달하는 함수일 뿐.
Note) 위 이야기를 저수준 뷰 해보면 아래와 같음.
ecx에 this를 저장하고 호출하는 것.
myCat->GetName();
// mov ecx, dword ptr[myCat]
// call Cat::GetName(0A16C7h)
yourCat->GetName();
// mov ecx, dword ptr[yourCat]
// call Cat::GetName(0A16C7h)
Note) 즉, 멤버 함수는 하난데 this가 달라지면서
다형성이 생겨남. 무늬는 하난데 실행해보니 다른 행동을 함.
앞으로 배울 내용들이 다형성의 깊은 이해를 도움.
3.2-2 멤버 함수 오버라이딩(Overriding)
Def) 오버라이딩(Overriding)
부모 클래스의 메서드를 자식 클래스에서
중복 작성하는 것을 의미함.
Ex03020201)
<hide/>
// Animal.h
#pragma once
class Animal
{
public:
Animal();
Animal(const int& age);
~Animal();
int GetAge() const;
void Speak();
private:
int mAge;
};
<hide/>
// Animal.cpp
#include <iostream>
#include "Animal.h"
using namespace std;
Animal::Animal()
: mAge(0)
{
}
Animal::Animal(const int& age)
: mAge(age)
{
}
Animal::~Animal()
{
}
int Animal::GetAge() const
{
return mAge;
}
void Animal::Speak()
{
cout << "Animal speaking." << endl;
}
<hide/>
// Cat.h
#pragma once
#include "Animal.h"
class Cat : public Animal
{
public:
Cat();
Cat(const int& age, const char* name);
~Cat();
void Speak();
private:
char* mName;
};
<hide/>
// Cat.cpp
#include <iostream>
#include <cstring>
#include "Cat.h"
using namespace std;
Cat::Cat()
: Animal(0)
, mName(nullptr)
{
}
Cat::Cat(const int& age, const char* name)
: Animal(age)
, mName(nullptr)
{
size_t length = strlen(name) + 1;
mName = new char[length + 1];
memcpy(mName, name, length + 1);
}
Cat::~Cat()
{
delete[] mName;
mName = nullptr;
}
void Cat::Speak()
{
cout << "Meow~" << endl;
}
<hide/>
// Dog.h
#pragma once
#include "Animal.h"
class Dog : public Animal
{
public:
Dog();
Dog(const int& age, const char* name);
~Dog();
void Speak();
private:
char* mName;
};
<hide/>
// Dog.cpp
#include <iostream>
#include <cstring>
#include "Dog.h"
using namespace std;
Dog::Dog()
: Animal(0)
, mName(nullptr)
{
}
Dog::Dog(const int& age, const char* name)
: Animal(age)
, mName(nullptr)
{
size_t length = strlen(name) + 1;
mName = new char[length];
memcpy(mName, name, length);
}
Dog::~Dog()
{
delete[] mName;
mName = nullptr;
}
void Dog::Speak()
{
cout << "Woof!" << endl;
}
<hide/>
// main.cpp
#include <iostream>
#include "Animal.h"
#include "Cat.h"
#include "Dog.h"
using namespace std;
int main()
{
Animal unknown = Animal(4);
Cat choo = Cat(7, "Choo");
Dog choco = Dog(7, "Choco");
unknown.Speak();
choo.Speak();
choco.Speak();
return 0;
}
3.2-3 바인딩과 가상 함수
Note) Ex03020201에서 만약 아래와 같이 작성한다면?
Cat* myCat = new Cat();
myCat->Speak();
Animal* yourCat = new Cat();
yourCat->Speak(); // 어떻게 출력될까?
Note) C++은 기본적으로 정적 바인딩
그래서 yourCat->Speack()은 Animal speaking을 출력함.
Java는 동적 바인딩이라, Meow~를 출력함.
그래서 C++은 무늬(개체의 자료형) 따라간다고 말하고,
Java는 실체(생성자의 자료형)를 따라간다고도 함.
Note) 정적 바인딩 - 멤버 변수
이전에 살펴보았듯 부모 자식 간의 메모리가 잘 나뉘어져 있음.
그래서 쉽게 두 메모리를 분간해서 읽을 수 있음.
ex. Animal* yourCat = new Cat();
Note) 정적 바인딩 - 메서드
ex. yourCat->Speak();
Note) 그렇다면 어떻게해야 실체를 따라가게끔 할까?
즉, 자식 클래스의 메서드가 호출되게끔 하고자 한다면?
Def) 가상 함수(Virtual Function)
Virtual 키워드가 붙은 메서드.
언제나 자식 클래스의 메서드가 호출됨.
즉, 부모 포인터 또는 부모 참조를 사용 중이더라도
자식 클래스의 메서드가 호출됨.
즉, 동적 바인딩을 보장하는 키워드
Def) 동적 바인딩(Dynamic Binding)
혹은 늦은 바인딩(Late Binding)이라고도 함.
런타임 중에 어떤 함수를 호출할지 결정함.
당연하게도 정적 바인딩보다 비교적 느림.
Note) Java는 모든 것이 기본적으로 가상 함수
그래서 반대로 final 이라는 키워드를 통해서
상속을 막고, 이를 통해 정적 바인딩만 하게끔 강제함
그럼 어떤 함수가 바인딩 될지 컴파일 중에 결정할 수 있음.
이걸 까먹으면 비 가상함수보다 언제나 느림.
Note) 동적 바인딩은 어떻게 구현하는 걸까?
가상(함수) 테이블이 생성됨. 모든 가상 멤버함수의 주소를 포함함.
그리고 _vfptr(virtual function pointer)이라는 멤버가 자동으로 추가됨.
Note) 그렇다면 가상 테이블은 클래스마다 하나씩 있을까?
아니면 개체마다 하나씩 있을까? 그 이유는 뭘까?
Note) 동적 바인딩은 왜 느릴까?
jump 명령어가 많이 사용되기 때문.
ex. 개체 자신의 Speak() 메서드를 찾기 위해서
가상 테이블 위치로 감. 거기서 n번째에 있을
자신의 Speak() 멤버 함수를 호출하기 위해서
(가상 테이블 주소 + 4 * n) 주소에 적힌 주소(함수 포인터)로 감.
Note) 가상 테이블을 점프와 관련지어서 점프 테이블 또는
룩업 테이블(Look-up table)이라고도 부름.
Note) 그렇다면 Ex03020201에는 또 하나의 문제가 있음.
무엇이 문제일까? 소멸자가 비 가상함수임.
Cat* myCat = new Cat(2, "coco");
delete myCat;
Animal* yourCat = new Cat(2, "coco");
delete yourCat;
Note) 소멸자는 항상 가상 소멸자로 작성하자.
혹자는 상속하지 않으니까 괜찮다고 할 수 있지만,
OOP에서는 모든 가능성을 열어두는 것이 좋다.
언젠가는 상속할 수도 있기 때문에, 가상 소멸자로 작성하자.
Note) 근데 가상 함수는 느리다고 했음.
진짜로 이건 절대 상속 하지 않을 클래스인데도
가상 함수를 써야할까? C++14/17에 해결책이 있음.
어찌되었든 모든 소멸자에는 언제나 virtual 키워드 붙히자.
Note) 가상 함수에 대한 한 가지 유의점.
멤버 함수의 가상성은 상속됨.
따라서 자식 클래스에서 virtual 키워드를 안붙히더라도
해당 멤버 함수는 가상 함수가 됨.
Note) 다형성 한 마디 요약
같은 부모 다른 행동.
Note) 다형성의 활용
만약 슬라임, 주황버섯, 달팽이라는 몬스터들이 있다고 해보자.
플레이어가 각각 3, 2, 10마리를 공격한 상황임.
그럼 다형성이 없다면 각 마리수만큼 배열을 만들고
3가지 배열을 돌면서 attack() 메서드를 호출하게됨.
그러나 다형성이 있다면 크기가 15인 몬스터 배열을 만들고
거기에 모든 몬스터를 저장할 수 있음. 그럼 이제
1개 배열을 돌면서 attack() 메서드를 호출하면
attack() 함수가 가상 함수라는 가정하에 알아서 호출되게 됨.
Ex03020202)
<hide/>
// Animal.h
#pragma once
class Animal
{
public:
Animal();
Animal(const int& age);
virtual ~Animal();
int GetAge() const;
void Speak();
virtual void Attack();
private:
int mAge;
};
<hide/>
// Animal.cpp
#include <iostream>
#include "Animal.h"
using namespace std;
Animal::Animal()
: mAge(0)
{
}
Animal::Animal(const int& age)
: mAge(age)
{
}
Animal::~Animal()
{
}
int Animal::GetAge() const
{
return mAge;
}
void Animal::Speak()
{
cout << "Animal speaking." << endl;
}
void Animal::Attack()
{
cout << "Animal attack." << endl;
}
<hide/>
// Cat.h
#pragma once
#include "Animal.h"
class Cat : public Animal
{
public:
Cat();
Cat(const int& age, const char* name);
virtual ~Cat();
void Speak();
void Attack(); // virtual 키워드를 안달아도 상속됨.
private:
char* mName;
};
<hide/>
// Cat.cpp
#include <iostream>
#include <cstring>
#include "Cat.h"
using namespace std;
Cat::Cat()
: Animal(0)
, mName(nullptr)
{
}
Cat::Cat(const int& age, const char* name)
: Animal(age)
, mName(nullptr)
{
size_t length = strlen(name) + 1;
mName = new char[length + 1];
memcpy(mName, name, length + 1);
}
Cat::~Cat()
{
delete[] mName;
mName = nullptr;
}
void Cat::Speak()
{
cout << "Meow~" << endl;
}
void Cat::Attack()
{
cout << "Crawl" << endl;
}
<hide/>
// Dog.h
#pragma once
#include "Animal.h"
class Dog : public Animal
{
public:
Dog();
Dog(const int& age, const char* name);
virtual ~Dog();
void Speak();
virtual void Attack();
private:
char* mName;
};
<hide/>
// Dog.cpp
#include <iostream>
#include <cstring>
#include "Dog.h"
using namespace std;
Dog::Dog()
: Animal(0)
, mName(nullptr)
{
}
Dog::Dog(const int& age, const char* name)
: Animal(age)
, mName(nullptr)
{
size_t length = strlen(name) + 1;
mName = new char[length];
memcpy(mName, name, length);
}
Dog::~Dog()
{
delete[] mName;
mName = nullptr;
}
void Dog::Speak()
{
cout << "Woof!" << endl;
}
void Dog::Attack()
{
cout << "Bite" << endl;
}
<hide/>
// main.cpp
#include <iostream>
#include "Animal.h"
#include "Cat.h"
#include "Dog.h"
using namespace std;
int main()
{
/*
Animal unknown = Animal(4);
Animal choo = Cat(7, "Choo");
Animal choco = Dog(7, "Choco");
위와같이 지역변수로 스택 메모리에 위치하게끔 하면
무조건 정적 바인딩이 발생함. 컴파일 타임에 결정되는 것이 강제되기 때문인듯.
*/
Animal* unknown = new Animal(4);
Animal* choo = new Cat(7, "Choo");
Animal* choco = new Dog(7, "Choco");
unknown->Speak();
choo->Speak();
choco->Speak();
unknown->Attack();
choo->Attack();
choco->Attack();
return 0;
}
3.2-4 다중 상속
Note) 다중 상속 하는 방법
class Faculty { ... };
class Student { ... };
class TA : public Faculty, public Student
{
...
};
Note) 다중 상속은 문제가 많음.
Java는 아에 지원하질 않음.
그래서 Java에는 super() 메서드를 쓸 수 있음.
자신의 부모는 딱 한명 뿐이기 때문.
그렇다면 다중 상속의 문제가 뭘까?
Note) 문제1: 어느 부모 클래스의 생성자가 먼저 호출되어야 할까?
자식 클래스에서 등장한 부모 클래스명 순서대로 호출됨.
이때, 초기화 리스트에 명시적으로 생성자를 호출한다 해도 상관없음.
// 이렇게 작성하면 Faculty() -> Student()
class TA : public Faculty, public Student
{
...
};
Note) 문제2: 두 부모 클래스 모두에 구현된 같은 메서드를 호출한다면?
ex. Faculty 클래스와 Student 클래스 모두에 Info() 메서드가 구현됨.
TA* ta1 = new TA();
ta1->Info(); // 두 클래스의 Info() 메서드 중 어느게 호출될까?
Note) 문제2의 해결책: 범위 지정자
ex. ta1->Student::Info();
Note) 문제3: 다이아몬드 문제
Animal 클래스를 상속 받는 Tiger 클래스와 Lion 클래스가 있다.
이때 Tiget 클래스와 Lion 클래스를 다중 상속 받는
Liger 클래스가 있다고 해보자. 그럼 Liger 클래스의 가상 테이블에
Animal() 생성자가 몇 개 있을까? 2개(메모리 낭비)
Note) 문제3의 해결책: 가상 부모 클래스
// Animal.h
class Animal
{
...
};
// Tiget.h
class Tiger : virtual public Animal
{
...
}
// Lion.h
class Lion : virtual public Animal
{
...
}
// Liger.h
class Liger : public Tiger, public Lion
{
...
}
Note) 가상 부모 클래스의 치명적 결함
후에 Liger라는 품종이 생길 것을 예측 할 수 있을까?
예측해서 가상 부모 클래스로 지정해야 한다는걸 인지할 수 있나?
상식적이진 않음.
Note) 결론은 다중 상속을 최대한 쓰지 말자.
대신 인터페이스를 사용하자. 뒤에 배움.
Java나 C#만큼 다중 상속을 지양하는 것이 좋음.
두 언어는 다중 상속을 안쓰고도 잘 버티고 있음.
3.3 추상 클래스
3.3-1 순수 가상함수
Def) 순수 가상함수(Pure Virtual Function)
구현체가 없는 멤버 함수. 자식 클래스에게 구현을 강제함.
만약 자식 클래스에서 구현되어 있지 않으면 컴파일 에러.
class Bird
{
public:
virtual void Flying() = 0;
// 일단 조류니까 날긴 날아야해. 그래서 이 메서드는 공통으로 만들게.
};
Note) 순수 가상함수 선언 방법
virtual 반환자료형 함수명(매개변수목록) = 0;
0으로 pure하게 대입해주니까 순수라고 부르는듯. 합리적 의심..
3.3-2 추상 클래스
Note) 지금까지의 클래스들은 모두 구체적인 클래스(Concrete Class)
그 반대 개념이 추상 클래스. 인터페이스를 설명하기 위해 필요한 개념.
Def) 추상 클래스(Abstract Class)
순수 가상함수가 하나 이상 선언된 클래스.
Note) 추상 클래스의 특징
1. 추상 클래스는 개체를 만들 수 없음.
2. pointer to abstract 혹은 referece of abstract는 가능.
// Animal.h
class Animal
{
public:
virtual void Speak() = 0;
...
}
// Main.c
Animal myAnimal; // 스택 메모리. 컴파일 에러.
Animal* myAnimal = new Animal(); // 힙 메모리. 컴파일 에러.
Animal* myCat = new Cat(); // 힙 메모리. 컴파일.
Animal& myCatRef = *myCat; // 스택 메모리. 컴파일.
// myCatRef는 무늬만 다를뿐 내부적으로 포인터와 거의 똑같음.
// myCat이 가상적으로 호출될 것이기 때문에 4번째도 컴파일 허용.
// 이전에 new해서 스택 메모리에 있는 개체에 대입하기 어렵다고 했음.
// 근데 myCatRef가 반드시 스택 메모리의 개념은 아님.
// 사실 포인터의 개념이기 때문에 어렵지 않음.
// 후에 쓸 때 myCatRef하고 점 찍을것이긴 함. 화살표가 아니긴 함.
3.3-3 "인터페이스"
Note) C++ 자체적으로 인터페이스를 지원하지는 않음.
추상 클래스를 사용해서 Java의 인터페이스를 흉내 냄.
즉, 멤버 없이 순수 가상함수만을 가짐.
근데 또 꼭 멤버가 없어야만 하는건 아님.
멤버가 있다면, 인터페이스를 상속 받는 클래스의 크기가 달라질 뿐.
class IFlyable
{
public:
virtual void Fly() = 0;
}
Ex03030301)
<hide/>
// IFlyable.h
#pragma once
class IFlyable
{
public:
virtual void Fly() const = 0;
};
<hide/>
// IWalkable.h
#pragma once
class IWalkable
{
public:
virtual void Walk() const = 0;
};
<hide/>
// Bat.h
#pragma once
#include "IFlyable.h"
class Bat : public IFlyable
{
public:
void Fly() const;
};
<hide/>
// Bat.cpp
#include <iostream>
#include "Bat.h"
using namespace std;
void Bat::Fly() const
{
cout << "A bat is now flying." << endl;
}
<hide/>
// Pigeon.h
#pragma once
#include "IWalkable.h"
#include "IFlyable.h"
class Pigeon : public IWalkable, public IFlyable
{
public:
void Walk() const;
void Fly() const;
};
<hide/>
// Pigeon.cpp
#include <iostream>
#include "Pigeon.h"
using namespace std;
void Pigeon::Walk() const
{
cout << "A pigeon is now walking." << endl;
}
void Pigeon::Fly() const
{
cout << "A pigeon is now flying." << endl;
}
<hide/>
// main.cpp
#include "Bat.h"
#include "Pigeon.h"
int main()
{
Bat* bat = new Bat();
bat->Fly();
Pigeon* pigeon = new Pigeon();
pigeon->Fly();
pigeon->Walk();
return 0;
}
'C++ > 문법 정리' 카테고리의 다른 글
Chapter 06. C++11/14/17 (0) | 2022.05.08 |
---|---|
Chapter 05. 템플릿과 파일 시스템 (0) | 2022.05.07 |
Chapter 04. 캐스팅과 인라인 (0) | 2022.05.06 |
Chapter 02. 클래스 (0) | 2022.05.01 |
Chapter 01. 입출력 기초 (0) | 2022.04.28 |
댓글