티스토리 뷰

상속

// 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로 강등되는 것이다
  • 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 메모리 레이아웃을 알고 있음
  • Cat 개체로 타입을 두었을때
    • name 접근
      • Cat 타입을 컴파일하는 순간, Animal의 메모리 레이아웃을 알고있음
        • 상속했기때문에
      • 따라서, Animal 바이트를 뛰어넘은 다음, 접근함
  • 정리해보면,
    • 부모-자식 간에 메모리 레이아웃은 연속된다
    • 그리고, 언제나, 부모 메모리부터 들어간다
      • 이렇게 포인터로, 부모 포인터로 자식 개체 쓰는게 이해될 것이다
      • 생성자를 왜 부모, 자식 순으로 호출이 되는지도 이해될 것이다

생성자 호출 순서

  • 베이스 클래스의 생성자가 먼저 호출된다
    • 명시적 또는 암시적으로
    • 즉, 암시적호출을했는데, 없다면 컴파일 에러 위험이 있다 🚨
  • 그 다음으로 파생 클래스의 생성자가 호출된다
  • 부모 클래스의 특정 생성자를 호출할 때는 초기화 리스트를 사용해야한다

암시적으로 부모 클래스의 생성자 호출하기

case1. 매개변수 없는 생성자가 있는 베이스 클래스

// Animal.h
class Animal
{
public:
	Animal();
private:
	int mAge;
};
  1. new 로 생성했다면 스택에 포인터를 잡고
  2. 생성자가 호출될때, 초기화 리스트 타이밍에 암시적으로 부모의 생성자가 호출된다.
    1. 부모의 생성자를 특정하지 않았다면, 기본생성자가 호출된다
  3. 힙에 할당된 메모리에,
    1. 부모의 생성자가 호출되면서 초기화되고
    2. 자식의 초기화가 이뤄진다
  • 슬라이드로 보면 아래와 같다

 

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();
  1. 실제 구현(new Cat())에 따라서, 둘다 Meow 출력 ❌ (동적 바인딩)
  2. 타입(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 개체마다 하나?

  1. 클래스에 하나
    • 함수는 클래스에 하나가 있는것임 (개체마다 있는 것이 아님)
    • 같은 맥락에서, 클래스에 하나
  2. 개체를 생성할때, 해당 클래스의 가상 테이블 주소가 함께 저장됨
  • 테이블을 두고, 점프해간다. 즉, 정적 바인딩보다 느리다

동적 바인딩 - 가상 멤버함수

저수준 뷰

가상 소멸자

🚨 문제가 있는 코드

// 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; // 🟢

  1. 무늬대로, Cat 소멸자 호출 후
  2. 부모(Animal) 소멸자 암시적으로 호출

delete youtCat; // 🔴

  1. 무늬대로, 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
링크
«   2024/12   »
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
글 보관함