티스토리 뷰
상속
// 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 cha* name)
: Animal(age)
{
size_t size = strlen(name) + 1;
mName = new char[size];
strcpy(mName, name);
}
- 대부분 public 으로 상속받는다
- 초기화 리스트 중에 부모의 생성자를 호출한다
- 그리고, Cat에서 추가한 부분을 신경쓴다
상속이란
- 다른 클래스의 특성들을 내려받음
- 베이스(base) 클래스: 부모 클래스
- 파생(derived) 클래스: 자식 클래스
- 파생 클래스의 개체는 다음의 것들을 가짐
- 베이스 클래스의 멤버 변수
- 베이스 클래스의 멤버 메서드
- 자신의 생성자와 소멸자
- 파생클래스는 멤버 변수 및 메서드 추가 가능
베이스 클래스와 파생클래스는 “is-a 관계”임
- Cat is a Animal
- Dog is a Animal
java 와 다르게 super가 없음
- 다중 상속 문제
파생클래스의 접근제어자
class Cat : public Animal {};
class Honda : private Car {};
class AndroidPhone : protected Phone {};
- 상속시 베이스 클래스 멤버의 접근 수준을 결정할 수 있다.
- 케이스로 풀어써보면,
- 베이스 클래스를 protected로 상속받았다면,
- public인 멤버가 protected로 강등되는 것이다
- 베이스 클래스를 protected로 상속받았다면,
- private로 상속받는 예시 코드
// Animal.h
class Animal
{
public:
Animal(int age);
void Move();
private:
int mAge;
};
// Cat.h
class Cat : private Animal
{
public:
Cat(int age, const char* name);
private:
char* mName;
};
Cat myCat;
myCat.Move(); // 컴파일 에러 🚨
- 대부분은 public 으로 상속받는다
🔥 메모리로 상속을 보자
스택에 Cat 포인터를 잡고
초기화 리스트
부모 초기화(부모의 생성자 호출)
자식 초기화(자신의 생성자 로직 수행)
🔥 메모리 레이아웃
- Animal 개체로 타입을 두었을때
- age 접근
- Animal이 시작하는 메모리에서부터 특정 부분에 age 가 있다
- 즉, Animal 메모리 레이아웃을 알고 있음
- age 접근
- Cat 개체로 타입을 두었을때
- name 접근
- Cat 타입을 컴파일하는 순간, Animal의 메모리 레이아웃을 알고있음
- 상속했기때문에
- 따라서, Animal 바이트를 뛰어넘은 다음, 접근함
- Cat 타입을 컴파일하는 순간, Animal의 메모리 레이아웃을 알고있음
- name 접근
- 정리해보면,
- 부모-자식 간에 메모리 레이아웃은 연속된다
- 그리고, 언제나, 부모 메모리부터 들어간다
- 이렇게 포인터로, 부모 포인터로 자식 개체 쓰는게 이해될 것이다
- 생성자를 왜 부모, 자식 순으로 호출이 되는지도 이해될 것이다
생성자 호출 순서
- 베이스 클래스의 생성자가 먼저 호출된다
- 명시적 또는 암시적으로
- 즉, 암시적호출을했는데, 없다면 컴파일 에러 위험이 있다 🚨
- 그 다음으로 파생 클래스의 생성자가 호출된다
- 부모 클래스의 특정 생성자를 호출할 때는 초기화 리스트를 사용해야한다
암시적으로 부모 클래스의 생성자 호출하기
case1. 매개변수 없는 생성자가 있는 베이스 클래스
// Animal.h
class Animal
{
public:
Animal();
private:
int mAge;
};
- new 로 생성했다면 스택에 포인터를 잡고
- 생성자가 호출될때, 초기화 리스트 타이밍에 암시적으로 부모의 생성자가 호출된다.
- 부모의 생성자를 특정하지 않았다면, 기본생성자가 호출된다
- 힙에 할당된 메모리에,
- 부모의 생성자가 호출되면서 초기화되고
- 자식의 초기화가 이뤄진다
- 슬라이드로 보면 아래와 같다
case2. 매개변수 없는 생성자가 없는 베이스 클래스
// Animal.h
class Animal
{
public:
Animal(int age);
private:
int mAge;
};
// Animal.cpp
Animal::Animal(int age)
: mAge(age)
{
}
// Cat.cpp
Cat::Cat(int age, const string& name)
{
size_t size = strlen(name) + 1;
mName = new char[size];
strcpy(mName, name);
}
// main.cpp
Cat* myCat = new Cat(2, "Meow");
- 자식 개체의 생성자에서 암시적으로 Animal() 을 호출하지만,
- 부모 클래스에서 기본생성자가 자동으로 생성되지 않으므로
- 컴파일 에러가 발생한다
소멸자 호출 순서
자식 개체 지우기
// Animal.cpp
Animal::~Animal()
{
}
// Cat.cpp
Cat::~Cat()
{
delete mName;
// ~Animal()을 호출
}
// main.cpp
delete myNeighboursCat;
// ...
- delete
- 자식부터 지운다
- 그리고, 자동적으로 부모의 소멸자가 호출된다
- 슬라이드로 보면 아래와 같다
- 스택의 순서처럼 이뤄진다
소멸자 호출순서
- 👁️ 생성자 호출 순서와 정반대
- 파생 클래스 소멸자의 마지막에서, 베이스 클래스의 소멸자가 🔥 자동적으로 호출된다
왜 자동적으로 호출될까?
- 소멸자는 하나만 존재한다
- 각 클래스에는 하나의 소멸자만 존재하기 때문에, 이를 명시적으로 호출할 필요는 없다
- 구현 관점에서 생각해봐도, 스택처럼 제거하는게 합당하다
🔥 다형성 (Polymorphism)
- Poly : 여러개, 다각형, 폴리곤
- morph : 모습이 변한다
다형성을 배우기 전에… 멤버 함수 분석
class Animal
{
public:
// ...
int GetAge();
private:
int mAge;
};
class Cat : public Animal
{
public:
// ...
const char* GetName();
private:
char* mName;
};
Cat* myCat = new Cat(5, "Coco");
Cat* yourCat = new Cat(2, "Mocha");
멤버 함수는 메모리 어디에 존재하게되는가?
myCat->GetName();
yourCat->GetName();
- 멤버 함수도 메모리 어딘가에 위치해 있다
- 당연하게도, 모든 것은 메모리 어딘가에 위치해 있어야함
- 그런데 각 개체마다 멤버 함수의 메모리가 잡혀있을까?
- 아래 둘의 코드는 동작이 완전히 일치한다
myCat->GetName();
yourCat->GetName();
- 그 대신 각 멤버 함수는 컴파일 시에 딱 한번만 메모리에 “할당”된다
- 저수준에서, 멤버 함수는 전역함수와 그다지 다르지 않다
📝 클래스와 함수는 모두 코드영역에 먼저 로딩이 되고, 전역적으로 존재한다
- MSVC에서는 컴파일을 하고, 어셈블리를 보면,
- ecx에 개체의 시작주소를 넣는다.
- 이것이 this call 이다
- Sample Code
#include <iostream>
class A
{
public:
A()
{
int a = 0;
++a;
}
~A()
{
int a = 0;
--a;
}
int GetA()
{
return mA;
}
private:
int mA;
};
int main()
{
A a;
int ret = a.GetA();
return 0;
}
- ret = a.GetA()를 기준으로, 디버그모드에서 어셈블리를 보시거나 Release에서 최적화 옵션을 끄면 아래와 같다
00411020 mov ecx,dword ptr [a]
00411023 mov dword ptr [ret],ecx
저수준 뷰
- 슬라이드로 확인해보자
함수 오버라이딩(overriding)
- 같은 시그니처, 다른 행동 정의
- Animal
- speak
void Animal::Speak()
{
std::cout << "Animal speaking" << std::endl;
}
- Cat
- speak : Meow
void Cat::Speak()
{
std::cout << "Meow" << std::endl;
}
- Dog
- speak : Woof
void Dog::Speak()
{
std::cout << "Woof" << std::endl;
}
뭐가 출력될까 (정적 바인딩 ✅, 동적 바인딩❌)
// Animal.h
class Animal
{
public:
void Speak();
}
// Animal.cpp
void Animal::Speak()
{
std::cout << "Animal speaking" << 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();
myCat->Speak();
Animal* yourCat = new Cat();
yourCat->Speak();
- 실제 구현(new Cat())에 따라서, 둘다 Meow 출력 ❌ (동적 바인딩)
- 타입(Cat*, Animal* ; aka. 무늬)에 따라서, 각각 Meow, Animal speaking 출력 ✅ (정적 바인딩)
// Main.cpp
Cat* myCat = new Cat();
myCat->Speak(); // Meow
Animal* yourCat = new Cat();
yourCat->Speak(); // An animal is Speaking
- C++ 는 무늬따라서 간다! (기본이 정적 바인딩)
정적 바인딩 (aka. 무늬따라 간다)
정적 바인딩 - 멤버 변수
저수준 뷰
Cat* yourCat = new Cat(5, "Mocha");
Animal* yourCat = new Cat(5, "Mocha");
정적 바인딩 - 멤버 함수
저수준 뷰
동적 바인딩 (virtual 키워드, aka. 실체 따라간다)
🔥 가상(virtual) 함수 (다형성의 핵심!)
// Animal.h
class Animal
{
public:
virtual void Move();
virtual void Speak();
};
// Animal.cpp
void Animal::Move()
{
}
void Animal::Speak()
{
std::cout << "Animal speaking" << std::endl;
}
// Cat.h
class Cat : public Animal
{
public:
void Speak();
};
// Cat.cpp
void Cat::Speak()
{
std::cout << "Meow" << std::endl;
}
뭐가 출력될까 - virtual (정적 바인딩❌, 동적 바인딩✅)
// Main.cpp
Cat* myCat = new Cat(2, "Coco");
myCat->Speak();
Animal* youtCat = new Cat(5, "Mocha");
youtCat->Speak();
// Main.cpp
Cat* myCat = new Cat();
myCat->Speak(); // Meow
Animal* yourCat = new Cat();
yourCat->Speak(); // Meow
📝 가상함수 정리
- 자식 클래스의 멤버함수가 언제나 호출됨
- 부모의 포인터 또는 참조를 사용중이더라도!
- 동적(dynamic) 바인딩 / 늦은(late) 바인딩
- 실행 중에 어떤 함수를 호출할지 결정한다
- 🔥 당연히 정적 바인딩보다 느리다
- virtual 키워드를 생략하면 개판날수 있다 (가상 소멸자)
- 🔥 이를 위해 가상 테이블(가상 함수 테이블)이 생성된다
- 모든 가상 멤머 함수의 주소를 포함
- Q. 클래스 마다 하나? vs 개체마다 하나?
Q. 가상 함수 테이블은, 클래스 마다 하나? vs 개체마다 하나?
- 클래스에 하나
- 함수는 클래스에 하나가 있는것임 (개체마다 있는 것이 아님)
- 같은 맥락에서, 클래스에 하나
- 개체를 생성할때, 해당 클래스의 가상 테이블 주소가 함께 저장됨
- 테이블을 두고, 점프해간다. 즉, 정적 바인딩보다 느리다
동적 바인딩 - 가상 멤버함수
저수준 뷰
가상 소멸자
🚨 문제가 있는 코드
// Animal.h
class Animal
{
public:
~Animal();
private:
int Age;
};
- 소멸자가 가상함수가 아님 : 비 가상 소멸자
// Cat.h
class Cat : public Animal
{
public:
~Cat();
private:
char* mName;
};
// Cat.cpp
Cat::~Cat()
{
delete mName;
}
// Main.cpp
Cat* myCat = new Cat(2, "Coco");
delete myCat; // 🟢
Animal* yourCat = new Cat(5, "Mocha");
delete youtCat; // 🔴
delete myCat; // 🟢
- 무늬대로, Cat 소멸자 호출 후
- 부모(Animal) 소멸자 암시적으로 호출
delete youtCat; // 🔴
- 무늬대로, Animal 소멸자 호출
- Cat 소멸자는 호출하지 않았음 - 메모리 누수! 🚨
→ virtual 키워드를 생략하면 개판날수 있다
가상 소멸자 적용
// Animal.h
class Animal
{
public:
virtual ~Animal();
private:
int Age;
};
// Cat.h
class Cat : public Animal
{
public:
virtual ~Cat(); // 여기는 virtual 키워드 생략 가능하다. 넣어주는게 좋은 습관
private:
char* mName;
};
// Cat.cpp
Cat::~Cat()
{
delete mName;
}
저수준 뷰 - Cat
저수준 뷰 - Animal
습관 - 모든 소멸자에는 virtual 키워드를 넣자
- 가상함수는 느리다
- 가상소멸자가 있는 클래스를 상속받지 않아도 그럴이유가 있을까?
- 실수 방지용
- 철저하게 계산해서, virtual 키워드를 적용하지 않을 순 있다
- 하지만, 놓치고 상속해버리면 개판된다
- 즉, 상속받겠다는걸 내가 막을수 없으니깐 하는 것
- C++14/17 에는 상속을 막을 해결책이 있다
다중(multiple) 상속
- 잊혀져가는 흑마법
- 약간 오묘해지는 그런 기능
- Java, C# 등의 언어에서는 다중상속을 지원조차 안함
- 다른 패턴으로 쓰는게 나은 선택일 수도? ㅋ
- C++에서 super를 못쓰는 이유이다
- 어떤 부모?
- 부모클래스의 생성자를 직접 호출해야함
// Faculty.h
class Faculty
{
};
// Student.h
class Student
{
};
// TA.h
class TA : public Student, public Faculty
{
};
// Main.cpp
TA* myTA = new TA();
어느 부모의 생성자가 먼저 호출될까?
- 파생 클래스에서 등장한 부모 클래스 순서대로 호출된다
- 초기화 리스트의 순서는 상관 없다
class TA : public Student, public Faculty
{
};
// Student(), Faculty() 순
class TA : public Student, public Faculty
: Faculty()
, Student()
{
};
// Student(), Faculty() 순
문제점 1 - 어떤 함수가 호출되나?
// Faculty.h
class Faculty
{
public:
void DisplayData();
};
// Student.h
class Student
{
public:
void DisplayData();
};
// TA.h
class TA : public Student, public Faculty
{
};
// Main.cpp
TA* myTA = new TA();
myTA->DisplayData(); // ❓ , 어떤 부모의 DisplayData() ?
- 해결책 - 직접 부모클래스를 특정해준다
// Main.cpp
TA* myTA = new TA();
myTA->Student::DisplayData();
문제점 2 - 다이아몬드 문제
- Animal 이 2개가 존재하게되는 문제
- 해결책? - 가상 베이스 클래스
// 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
{
};
🔥 다중 상속을 최대한 쓰지말자. 대신 인터페이스를 사용하자
- 🤔 먼 훗날에 발생할 일까지 대비해서 virtual 로 상속받는게 맞을까?
- 물론, 한번에 설계하는 경우에는 괜찮은 선택지이겠으나, 굳이? ㅋ
- 다중 상속을 최대한 쓰지말자. 대신 인터페이스를 사용하자
- 인터페이스는, 다중상속보다는 약간의 제약이 있긴하다
- 추상클래스부터 보자
추상(abstract) 클래스
- 구체적인 클래스가 아닌 클래스
- 구체 클래스 : 구현체가 있고, 데이터가 있고
- 추상 클래스
- 함수 중에 하나라도 구현이 되어있지 않다면 추상클래스라 부름
- 함수가 구현되지 않았다는게 뭘까?
- 구현이 안됐는데 함수를 왜 만들까?
// Animal.h
class Animal
{
public:
virtual ~Animal();
virtual void Speak() = 0; // 동물이 말은 해야겠는데, 어떻게 말해야할지 모르겟어
private:
int mAge;
};
- virtual void Speak() = 0;
- 동물이 말은 해야겠는데, 어떻게 말해야할지 모르겟어
- 0을 대입해서 구현하지 않았음을 뜻함
- 구현하지 않았으므로, Animal 클래스는 추상클래스
// Cat.h
class Cat : public Animal
{
public:
~Cat();
void Speak();
private:
char* mName;
};
순수(pure) 가상함수
- 구현체가 없는 멤버함수
- 파생 클래스가 구현해야함
파생클래스가 구현하지 않는다면?
class Animal
{
public:
virtual void Speak() = 0;
private:
int mAge;
};
class Cat : public Animal
{
public:
// void Speak();
private:
char* mName;
};
🚨 컴파일 에러 - 파생클래스에 Speak() 이 구현되어있지 않다
순수 가상함수 선언하는 법
virtual void Speak() = 0;
virtual float GetArea() = 0;
추상 클래스
- 순수 가상함수를 가지고 있는 베이스 클래스를 추상클래스라고 함
- 👁️ 추상 클래스로 개체를 만들 수 없다
- 추상 클래스를 포인터나 참조형으로는 사용 가능하다
class Animal
{
public:
virtual void Speak() = 0;
private:
int mAge;
};
Animal myAnimal; // ??
Animal* myAnimal = new Animal(); // ??
Animal* myAnimal = new Cat(); // ??
Animal& myAnimal = *myCat; // ??
Animal myAnimal; // 불가능
Animal* myAnimal = new Animal(); // 불가능
Animal* myAnimal = new Cat(); // OK
Animal& myAnimal = *myCat; // OK
“인터페이스”
- 언어차원에서 지원하는 기능은 아님
- 데이터가 없이, 순수 가상함수들로만 이루어진 그룹
// 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++에서 인터페이스는 순수 가상 함수를 제공하여 특정 동작을 여러 클래스에서 공통적으로 구현할 수 있다. 상속이라고 표현하기보다는 동일한 인터페이스를 구현하는 것
- 다형성과 인터페이스: 인터페이스를 통해 구현된 객체들은 다형성을 사용할 수 있다.
- 예를 들어,
- IFlyable이라는 인터페이스를 구현한 여러 클래스들이 있을 때,
- 이들을 모아서 배열이나 컨테이너에 저장해 순회하면서 Fly() 메서드를 호출할 수 있다.
- 예를 들어,
- 다중 상속의 문제 회피: C++의 다중 상속은 복잡한 문제를 야기할 수 있지만, 인터페이스는 이런 문제를 피하면서도 동일한 동작을 여러 클래스에서 구현할 수 있는 깔끔한 해결책을 제공한다. 이를 통해 다형성을 살리면서도 다중 상속의 단점을 극복한다
“인터페이스”
- C++은 자체적으로 인터페이스를 지원하지 않는다
- 순수 추상클래스를 사용해서 Java의 인터페이스를 흉내낸 것이다
- 순수 가상함수만 갖는다
- 멤버 변수는 없다
class IFlyable
{
public:
virtual void Fly() = 0;
};
- 근데, 정말 필요에 의해서, 간단한 데이터를 넣는 경우도 있긴함
'C・C++ > OOP' 카테고리의 다른 글
[C++] low level 개체지향 프로그래밍 2 (1) | 2024.10.06 |
---|---|
[C++] low level 개체지향 프로그래밍 1 (2) | 2024.10.06 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 이진탐색
- tree
- 객체 변조 방어
- Memory
- 논문추천
- C
- OOP
- sleep lock
- condition variable
- 백준
- Spring MVC
- 연관관계 편의 메서드
- JPA
- Dispatcher Servlet
- S4
- 톰캣11
- generic swap
- Java
- generic sort
- CPU
- pocu
- tomcat11
- 엔티티 설계 주의점
- thread
- S1
- servlet
- 개발 공부 자료
- PS
- reader-writer lock
- core c++
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함