짧은 설명
- 생성자는 한 객체에서 하나 이상 한번에 호출할 수 있다.
- 부모자식간에 생성자를 명시적으로 호출할 수 있다.
- 한 객체 선언 시 여러 생성자를 한번에 호출할 수 있다.
- 부모의 오버로딩 생성자에서 기본 생성자를 호출한다면?
- 자식 멤버에서 부모의 객체가 멤버변수로 있다면?
- 호출 순서
- 부모 클래스 생성자 - 따로 지정 안하면 기본 생성자 호출
- 부모 멤버 초기화
상속
- 부모클래스의 멤버를 물려받는 것이다.
- 자식 클래스는 부모 클래스의 private에 접근할 수 없다.
- 간접 참조 : 포인터, 레퍼런스
- 업캐스팅 / 다운캐스팅 : 변수들의 형변환이다.
- 절차 지향 : 순서대로 작업 처리
- ~한다. → 함수의 목적
- 함수들의 집합 처리 프로그래밍을 의미한다.
- 객체 지향 : 주체 우선 처리
- ~한다가 아니라 ‘누가’ 뭘 한다 처럼 주체가 중요한 프로그래밍이다.
- 클래스들의 상호작용이다.
상속 기본
- 상속은 기존 클래스의 특성을 물려받아 새로운 클래스를 만드는 객체지향 프로그래밍의 핵심 개념이다.
- 코드 재사용성 향상: 공통 기능을 기본 클래스에 정의
- 계층적 관계 표현: 현실 세계의 계층 구조를 프로그램으로 모델링
- 계층 관계 : 파일시스템처럼 부모안에 자식안의 자식같이 계층이 나눠진것이다.
- 유지보수 용이: 공통 기능 수정 시 한 곳만 수정
파생 클래스
- 파생 클래스(Derived Class): 상속을 받는 클래스, 자식 클래스라고도 함
- 기본 클래스(Base Class): 상속을 제공하는 클래스, 부모 클래스라고도 함
class 파생클래스명 : 접근지정자 상속받을 기본클래스명 {
// 파생 클래스의 멤버들
};
class Animal {
public:
void Breathe() { cout << "숨을 쉰다." << endl; }
int age;
};
class Dog : public Animal {
public:
void Walk() { cout << "걷는다." << endl; }
};
class Sparrow : public Animal {
public:
void Fly() { cout << "난다." << endl; }
};
int main() {
Dog d;
d.age = 7;
d.Breathe(); // 상속받은 멤버 함수
d.Walk(); // 자신의 멤버 함수
cout << d.age << endl;
Sparrow s;
s.age = 2;
s.Breathe(); // 상속받은 멤버 함수
s.Fly(); // 자신의 멤버 함수
cout << s.age << endl;
}
- Animal 클래스는 모든 동물의 공통 특성인 Breathe() 메서드와 age 속성을 가짐
- Dog 클래스는 Animal을 상속받아 Walk() 메서드를 추가
- Sparrow 클래스는 Animal을 상속받아 Fly() 메서드를 추가
- 파생 클래스 객체는 기본 클래스의 모든 public 멤버에 접근 가능
생성자 호출 순서
- 상속 관계에서 객체 생성 시 생성자 호출 순서:
- 기본 클래스 생성자가 먼저 호출
- 파생 클래스 생성자가 나중에 호출
class Base {
public:
Base() {
cout << "Base()" << endl; // ------- 1
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived()" << endl; // -------- 2
}
};
int main() {
Derived d;
}
객체 생성 / 소멸 순서
class Ice {
public:
Ice() { cout << "Ice()" << endl; }
~Ice() { cout << "~Ice()" << endl; }
};
class Pat {
public:
Pat() { cout << "Pat()" << endl; }
~Pat() { cout << "~Pat()" << endl; }
};
class Bingsoo {
public:
Bingsoo() { cout << "Bingsoo()" << endl; }
~Bingsoo() { cout << "~Bingsoo()" << endl; }
private:
Ice ice;
};
class PatBingsoo : public Bingsoo {
public:
PatBingsoo() { cout << "PatBingsoo()" << endl; }
~PatBingsoo() { cout << "~PatBingsoo()" << endl; }
private:
Pat pat;
};
int main() {
cout << "===== 1 =====" << endl;
PatBingsoo* p = new PatBingsoo;
cout << "===== 2 =====" << endl;
delete p;
cout << "===== 3 =====" << endl;
}
===== 1 =====
Ice()
Bingsoo()
Pat()
PatBingsoo()
===== 2 =====
~PatBingsoo()
~Pat()
~Bingsoo()
~Ice()
===== 3 =====
- 생성 순서: 기본 클래스 멤버 → 기본 클래스 → 파생 클래스 멤버 → 파생 클래스
- 소멸 순서: 파생 클래스 → 파생 클래스 멤버 → 기본 클래스 → 기본 클래스 멤버
상속이 필요한 경우
코드 중복 제거
- 개선 전
- class Image { public: operator string() //string 형변환 오버로드 { return "사진"; } }; class TextMessage { public: TextMessage(int sendTime, string sendName, string text) { this->sendTime = sendTime; this->sendName = sendName; this->text = text; } int GetSendTime() const { return sendTime; } string GetSendName() const { return sendName; } string GetText() const { return text; } private: int sendTime; // 중복 string sendName; // 중복 string text; }; class ImageMessage { public: ImageMessage(int sendTime, string sendName, Image* image) { this->sendTime = sendTime; // 중복 this->sendName = sendName; // 중복 this->image = image; } int GetSendTime() const { return sendTime; } // 중복 string GetSendName() const { return sendName; } // 중복 Image* GetImage() const { return image; } private: int sendTime; // 중복 string sendName; // 중복 Image* image; }; int main() { Image* dogImage = new Image; TextMessage* hello = new TextMessage(10, "두들", "안녕"); ImageMessage* dog = new ImageMessage(20, "두들", dogImage); cout << "보낸 시간 : " << hello->GetSendTime() << endl; cout << "보낸 사람 : " << hello->GetSendName() << endl; cout << " 내 용 : " << hello->GetText() << endl; cout << endl; cout << "보낸 시간 : " << dog->GetSendTime() << endl; cout << "보낸 사람 : " << dog->GetSendName() << endl; cout << " 내 용 : " << (string)*dog->GetImage() << endl; cout << endl; delete dogImage; delete hello; delete dog; }
- 상속을 사용하지 않으면 여러 클래스에서 동일한 코드를 반복해야 함
- 개선 후
- class Image { public: operator string() { return "사진"; } }; class Message { public: Message(int sendTime, string sendName) { this->sendTime = sendTime; this->sendName = sendName; } int GetSendTime() const { return sendTime; } string GetSendName() const { return sendName; } private: // 자식 클래스에서도 이 멤버들에 직접적인 접근 불가 int sendTime; string sendName; }; class TextMessage : public Message { public: TextMessage(int sendTime, string sendName, string text) : Message(sendTime, sendName) { // 부모 클래스 생성자 호출, 자기 바로 위에만 가능. this->text = text; } string GetText() const { return text; } private: string text; }; class ImageMessage : public Message { public: ImageMessage(int sendTime, string sendName, Image* image) : Message(sendTime, sendName) { // 부모 클래스 생성자 호출 this->image = image; } Image* GetImage() const { return image; } private: Image* image; }; int main() { Image* dogImage = new Image; TextMessage* hello = new TextMessage(10, "두들", "안녕"); ImageMessage* dog = new ImageMessage(20, "두들", dogImage); cout << "보낸 시간 : " << hello->GetSendTime() << endl; cout << "보낸 사람 : " << hello->GetSendName() << endl; cout << " 내 용 : " << hello->GetText() << endl; cout << endl; cout << "보낸 시간 : " << dog->GetSendTime() << endl; cout << "보낸 사람 : " << dog->GetSendName() << endl; cout << " 내 용 : " << (string)*dog->GetImage() << endl; cout << endl; delete dogImage; delete hello; delete dog; }
- 공통 멤버 변수와 메서드를 Message 클래스에 통합
- 코드 중복 제거
- 유지보수가 용이해짐
오버라이드
- 상속 관계 : 부모자식간에 모든 객체를 참조할 수 있다.
- A / B : A / C : B
- A* or A& → A객체에 참조 가능 , B객체에 참조 가능 , C객체에 참조 가능
- 간접 참조하고 있는 주소를 통해 해당 클래스의 함수를 호출할 수 있다.
- 파생 클래스에서 기본 클래스와 동일한 이름의 멤버를 정의하면 기본 클래스의 멤버가 가려짐
- 부모 자식에 같은 이름의 멤버 함수가 있다면, 멤버 하이딩 또는 오버라이딩이 발생한다.
- 상속관계에서 동일한 이름의 함수가 있을때 참조하고 있는 주소의 함수가 호출된다.
- 멤버 하이딩 : 부모와 같은 이름의 함수이지만, 부모가 가상 함수가 아닌 함수의 호출이다.
- 정적 바인딩이다.
- 포인터나 참조 타입이 아닌 객체의 데이터형 기준으로 호출된다.
- 오버 라이딩 : 부모가 가상 함수를 붙였을 때 같은 이름의 함수를 호출하는 것이다.
- 저장하는 객체의 데이터형과 무관하게 실행 당시에 참조하고 있는 주소의 데이터형의 함수가 호출된다.
class Base {
public:
int a = 10;
};
class Derived : public Base {
public:
int a = 20; // Base의 a를 가림 / 멤버 은닉 / 멤버 하이딩
};
int main() {
Base b;
Derived d;
cout << b.a << endl; // 10 출력
cout << d.a << endl; // 20 출력 (Derived의 a)
}
- 객체.클래스명::멤버: 특정 클래스의 멤버를 명시적으로 호출
- 가려진 기본 클래스의 멤버에 접근할 때 유용
- class Base { public: void Print() { cout << "From Base!" << endl; } }; class Derived : public Base { public: void Print() override { // Base의 Print()를 오버라이드 cout << "From Derived!" << endl; } }; int main() { Derived d; d.Print(); // Derived의 Print() 호출 d.Base::Print(); // 명시적으로 Base의 Print() 호출 d.Derived::Print(); // 명시적으로 Derived의 Print() 호출 }
- 객체.클래스명::멤버: 특정 클래스의 멤버를 명시적으로 호출
- 가려진 기본 클래스의 멤버에 접근할 때 유용
가상 함수 / 오버라이드 / 동적 바인딩
- 가상 함수 : 멤버함수 앞에 virtual을 선언한다.
- 동작에는 차이가 없다.
- 차이 : 자식 함수에 같은 이름이 있다면 ‘오버라이드’ 되는 것을 의미한다.
- 필요에 의해서 재정의를 안해도 된다.
- 오버 라이딩 : 동적 바인딩에 의해 호출되는것이다. ( = 재정의)
- 오버 로딩 : 정적 바인딩에 의해 호출되는 것이다.
- 정적 바인딩 : 컴파일 전에 참조를 결정하는 것이다.
- 바인딩 : 함수를 호출할때 같은 이름의 함수가 여러개일때 어떤 함수를 호출할지 참조를 확인하는 기능이다.
- 동적 바인딩 : 실행 중에 호출할 떄 참조를 확인하여 어떤 함수를 호출할 지 정하는 것이다.
- 부모 클래스의 간접 참조로 자기 자식 객체도 참조할 수 있다.
class Weapon {
public:
Weapon(int power) : power(power) {
cout << "Weapon(int)" << endl;
}
virtual void Use() { // 가상 함수!!
cout << "Weapon::Use()" << endl;
}
protected:
int power;
};
class Sword : public Weapon {
public:
Sword(int power) : Weapon(power) {
cout << "Sword(int)" << endl;
}
void Use() overrride { // 오버라이드
cout << "Sword::Use()" << endl;
swing();
}
private:
void swing() {
cout << "Swing sword." << endl;
}
};
class Magic : public Weapon {
public:
Magic(int power, int manaCost) : Weapon(power), manaCost(manaCost) {
cout << "Magic(int, int)" << endl;
}
void Use() overrride { // 오버라이드
cout << "Magic::Use()" << endl;
cast();
}
private:
void cast() {
cout << "Cast magic." << endl;
}
int manaCost;
};
int main() {
Sword mySword(10);
Magic myMagic(15, 7);
Weapon* currentWeapon; //현재 Weapon, Sword, Magic을 참조할 수 있다.
currentWeapon = &mySword; //Sword 자식 간접참조 / 동적 바인딩을 위한 참조 할당이다.
currentWeapon->Use(); // 동적 바인딩에 의해 Sword::Use() 호출
}
- virtual 키워드로 선언
- 기본 클래스 포인터로 파생 클래스 객체를 가리킬 때, 실제 객체의 타입에 따라 호출될 함수가 결정됨
- 동적 바인딩(Dynamic Binding): 실행 시간에 호출할 함수가 결정됨
가상 소멸자
- 소멸자 앞에 virtual을 붙인것이다.
- 앞으로 소멸자는 전부 가상 소멸자로 선언해라
- 부모 소멸자가 호출되면 자식의 소멸자가 호출이 안되는 경우가 있기 때문이다.
- 객체의 소멸자가 호출될때 부모의 소멸자까지 역순으로 호출된다.
- 그럼 왜 자식 소멸자는 호출이 안된다는 걸까?
- 이유 : 참조를 하면 부모클래스* 변수 = &자식클래스 변수; 이렇게 포인터 변수로 자식클래스 참조를 받았을때 같은 이름이기에 소멸자는 해당 데이터형의 소멸자만 호출하게 된다.
- virtual을 붙였을 떄는? : 소멸자의 호출 순서는 자식 → 부모이기에 자식 소멸자 호출 → 부모 소멸자 호출 순서로 진행하게 된다.
class Image {
public:
operator string() {
return "사진";
}
};
class Message {
public:
Message(int sendTime, string sendName) {
this->sendTime = sendTime;
this->sendName = sendName;
}
virtual ~Message() {} // 가상 소멸자
int GetSendTime() const { return sendTime; }
string GetSendName() const { return sendName; }
virtual string GetContent() const { return ""; }
private:
int sendTime;
string sendName;
};
class TextMessage : public Message {
public:
TextMessage(int sendTime, string sendName, string text)
: Message(sendTime, sendName) {
this->text = text;
}
string GetContent() const { return text; } // 오버라이드
private:
string text;
};
class ImageMessage : public Message {
public:
ImageMessage(int sendTime, string sendName, Image* image)
: Message(sendTime, sendName) {
this->image = image;
}
string GetContent() const { return (string)*image; } // 오버라이드
private:
Image* image;
};
void printMessage(Message* m) {
cout << "보낸 시간 : " << m->GetSendTime() << endl;
cout << "보낸 사람 : " << m->GetSendName() << endl;
cout << " 내 용 : " << m->GetContent() << endl; // 동적 바인딩
cout << endl;
}
int main() {
Image* dogImage = new Image;
TextMessage* hello = new TextMessage(10, "두들", "안녕");
ImageMessage* dog = new ImageMessage(20, "두들", dogImage);
printMessage(hello);
printMessage(dog);
delete dogImage;
delete hello;
delete dog;
}
- 기본 클래스 포인터로 파생 클래스 객체를 삭제할 때 올바른 소멸자가 호출되도록 보장
- 실행 시 참조되어있는 주소(클래스)의 소멸자가 호출된다.
- 가상 함수를 가진 클래스는 항상 가상 소멸자를 가져야 함
참조를 사용한 다형성
class Image {
public:
operator string() {
return "사진";
}
};
class Message {
public:
Message(int sendTime, string sendName) {
this->sendTime = sendTime;
this->sendName = sendName;
}
virtual ~Message() {} // 가상 소멸자
int GetSendTime() const { return sendTime; }
string GetSendName() const { return sendName; }
virtual string GetContent() const { return ""; }
private:
int sendTime;
string sendName;
};
class TextMessage : public Message {
public:
TextMessage(int sendTime, string sendName, string text)
: Message(sendTime, sendName) {
this->text = text;
}
string GetContent() const { return text; } // 오버라이드
private:
string text;
};
class ImageMessage : public Message {
public:
ImageMessage(int sendTime, string sendName, Image* image)
: Message(sendTime, sendName) {
this->image = image;
}
string GetContent() const { return (string)*image; } // 오버라이드
private:
Image* image;
};
void printMessage(const Message& m) { // 참조로 받기
cout << "보낸 시간 : " << m.GetSendTime() << endl;
cout << "보낸 사람 : " << m.GetSendName() << endl;
cout << " 내 용 : " << m.GetContent() << endl;
cout << endl;
}
int main() {
Image* dogImage = new Image;
Message* messages[] = {
new TextMessage(10, "두들", "안녕"),
new ImageMessage(20, "두들", dogImage),
new TextMessage(30, "두들", "잘가")
};
// 범위 기반 for문
for (Message* m : messages) {
//실제로 참조하고 있는 클래스에 따라 다른 함수가 호출된다.
printMessage(*m);
}
// 배열의 각 칸마다 동적 할당된 객체를 가리키는 포인터가 들어있으므로 각각 삭제
for (Message* m : messages) {
delete m;
}
delete dogImage;
}
순수 가상 함수 / 추상 클래스
- 순수 가상 함수 : 구현은 없고 선언만 있는 가상 함수다.
- 추상 클래스 : 순수 가상 함수를 하나 이상 선언되어있는 클래스다.
const double PI = 3.141592653589793;
class Shape {
public:
virtual ~Shape() {}
virtual double GetArea() const = 0; // 순수 가상 함수
virtual void Resize(double factor) = 0; // 순수 가상 함수
};
**class** Circle **: public Shap**e {
public:
Circle(double r) : r(r) {}
double GetArea() const {
return r * r * PI;
}
void Resize(double factor) {
r *= factor;
}
private:
double r;
};
class Rectangle : public Shape {
public:
Rectangle(double a, double b) : a(a), b(b) {}
double GetArea() const {
return a * b;
}
void Resize(double factor) {
a *= factor;
b *= factor;
}
private:
double a, b;
};
int main() {
// Shape는 추상 클래스이므로 객체를 만들 수 없음
// Shape s; // 오류!
// Shape를 가리키는 포인터 배열로 여러 도형을 한꺼번에 관리
Shape* shapes[] = { new Circle(1), new Rectangle(1, 2) };
for (Shape* s : shapes) {
s->Resize(2); // 각 도형을 2배씩 확대
}
for (Shape* s : shapes) {
cout << s->GetArea() << endl; // 각 도형의 넓이를 출력
}
for (Shape* s : shapes) {
delete s;
}
}
- 순수 가상 함수: = 0으로 선언된 가상 함수
- 반드시 상속받은 자식 클래스에서 재정의를 해야한다.
- 일반 가상함수는 필요에 의해서 재정의를 해도 되고, 안해도 된다.
- 추상 클래스: 하나 이상의 순수 가상 함수를 가진 클래스
- 추상 클래스는 인스턴스(객체)를 생성할 수 없음
- 상속 받아서 부모클래스 로만 사용하라는 의도다.
- 파생 클래스는 모든 순수 가상 함수를 구현해야 함
배열과 상속 - 잘못된 사용
struct Animal {
float xpos = 1;
float ypos = 2;
};
struct FlyingAnimal : public Animal {
float zpos = 3;
};
void printAnimals(Animal* a, int n) { // Animal *a는 Animal a[]와 동일
for (int i = 0; i < n; i++) {
cout << "(" << a[i].xpos << ", " << a[i].ypos << ")" << endl;
//a[i] == *(a + i) -> 8바이트씩 커지는 것이다.
}
}
int main() {
FlyingAnimal* arr = new FlyingAnimal[5];
printAnimals(arr, 5); // 문제 발생!
delete[] arr;
}
- Animal과 FlyingAnimal의 크기가 다름
- 배열 인덱싱 시 잘못된 메모리 접근 발생
해결법
struct Animal {
float xpos = 1;
float ypos = 2;
virtual ~Animal() {}
};
struct FlyingAnimal : public Animal {
float zpos = 3;
};
void printAnimals(Animal** a, int n) { // 포인터의 포인터 사용
for (int i = 0; i < n; i++) {
cout << "(" << a[i]->xpos << ", " << a[i]->ypos << ")" << endl;
}
}
int main() {
Animal** a = new Animal*[5];
for (int i = 0; i < 5; i++) {
a[i] = new FlyingAnimal;//자식으로 캐스팅히는 다운 캐스팅이다.
}
printAnimals(a, 5);
for (int i = 0; i < 5; i++) {
delete a[i]; // 각 객체 삭제
}
delete[] a; // 포인터 배열 삭제
}
- 객체 배열 대신 포인터 배열 사용
- 각 포인터가 개별 객체를 가리키도록 함
상속 관계 형변환
업 캐스팅
- 자식에서 부모로 올라가는 캐스팅
- 파생 클래스 포인터를 기본 클래스 포인터로 변환
- 암시적 변환: 컴파일러가 자동으로 처리
- 항상 안전: 파생 클래스는 기본 클래스의 모든 멤버를 포함
다운 캐스팅
- 부모에서 자식으로 내려가는 캐스팅
- 기본 클래스 포인터를 파생 클래스 포인터로 변환
struct Base {
int a = 1;
virtual ~Base() {}
};
struct Drv1 : Base {
float x = 3.14;
};
struct Drv2 : Base {
int y = 3;
};
int main() {
Base* b = new Drv1; //자식 -> 부모 : 업 캐스팅 (이런 경우는 부작용 없다.)
//Drv1* d1 = static_cast<Drv1*>(b); // 올바른 다운캐스팅
Drv1* d1 = (Drv1*)b; // 올바른 다운캐스팅
cout << d1->x << endl;
delete b;
}
- static_cast는 컴파일 타임에 타입 검사만 수행
- 실제 객체 타입과 캐스팅 타입이 다르면 런타임 오류 발생 가능
객체지향 프로그래밍 4대 특성
캡슐화
- 데이터와 함수를 하나의 단위로 묶음
- 정보 은닉을 통한 내부 구현 보호
상속성
- 기존 클래스의 특성을 물려받아 새로운 클래스 생성
- 코드 재사용성 향상
다형성
- 하나의 인터페이스로 여러 구현체 사용
- 가상 함수와 오버라이딩으로 구현
추상화
- 복잡한 시스템을 단순한 개념으로 표현
- 추상 클래스와 인터페이스로 구현