C++ 상속, 다형성, virtual

LeemHyungJun·2023년 2월 15일
0

C++ Study

목록 보기
11/12
post-thumbnail

C++에서 헷갈리거나 중요한 것을 정리한 것입니다.

상속

  • 생성자 호출 순서는 부모 클래스의 생성자 먼저 호출
  • 소멸자 호출 순서는 자식 소멸자 먼저 호출

다형성

  • 멤버 변수

    • 생성 시 스택 메모리에 할당 / 동적 할당 시 힙 메모리
    • 스택 메모리 할당 모습 (멤버 변수)
    • 이름과 나이를 가지는 동물 클래스
      • 코드
        Cat* myCat = new Cat(5, "Nabi");
        Cat* yourCat = new Cat(2, "Jerry");
      • 동작 모습
  • 멤버 함수

    • 멤버 함수는 메모리 어딘가에 위치해 있다.
    • 멤버 함수는 컴파일 시에 딱 한번만 메모리에 할당
      -> 멤버 함수는 같은 공간을 공유하게 된다.
  • 멤버함수 동작 예시

    • 위에서 생성한 것 처럼 만든 두개의 개체를 아래의 두 코드 실행시킨 다면 어떤 동작을 할 것인가
      • myCat->GetName();
      • yourCat->GetName();
    • myCat과 yourCat 모두 Cat 개체를 생성했기 때문에 아래 어셈블리 코드에서 둘 다 GetName() 함수를 호출하게 된다.
    • 어셈블리 코드 구현 모습
    ...
    int GetAge(Animal* ptr) const
    {
     return ptr->mAge;
    }
    ...
    const char* GetName(Cat* ptr) const
    {
     return ptr->mName;
    }
    ...
  • 정적 바인딩

    • C++의 기본 동작은 정적 바인딩이다.
    • 무늬따라 간다. (Cat 포인터는 Cat을 Animal 포인터는 Animal을 나타낸다)
    • 정적 바인딩이 더 빠르기 때문이다.
    • 그러나 다형성을 위해서는 virtual 키워드가 필요하다.
  • 정적 바인딩 예시 코드

//Animal.h
class Animal
{
	public:
    	int GetAge();
        void speak() const; //"animal" 출력
    private:
    	int mAge;
};

//Cat.h
class Cat: public Animal
{
public:
	const char* GetName();
    void speak() const; //"cat" 출력
private:
	char* mName;
}

//main.cpp
int main()
{
	Cat* cat = new Cat();
    Animal* animalCat = new Cat();
    
    //멤버 함수는 컴파일 시에 딱 한번만 메모리에 할당 (같은 공간을 차지)
    cat->speak(); //"cat" 출력
    animalCat->speak(); //"animal" 출력
}
  • 정적 바인딩 동작 예시

    • 아래의 코드를 실행했을 때
      Cat* myCat = new Cat();
      myCat->Speak(); //아래 어셈블리 코드에서 아래 Speak() 호출
      Animal* yourCat = new Cat();
      yourCat->Speak(); //아래 어셈블리 코드에서 위 Speak() 호출

    • 어셈블리 코드
      void Speak(Animal* ptr) const { //생략 }
      void Speak(Cat* ptr) const { //생략 }

함수 오버라이딩

  • 덮어쓰기
  • 하는 행동의 정의는 같지만, 실제 하는 행동은 다르다.
  • 함수 오버로딩과 헷갈리지 말 것
    • 함수 오버로딩: 같은 동작을 하는데, 파라메터가 다른 것

가상 함수

  • 위에 언급한 코드에서 speak() 함수에 virtual을 추가하면 가상 함수가 된다.
  • 이렇게 하면 동적 바인딩을 하여 자식 클래스의 멤버함수가 언제나 호출되게 된다.
    • 동적 바인딩이 된 함수
      • 가상 멤버 함수
      • late 바인딩 -> 실행 중에 어떤 함수를 호출할 지 결정
  • 추가적으로 가상 테이블이 생성되는데, 이 가상 테이블은 클래스 마다 하나를 가진다.
  • 동적 바인딩 예시 코드
class Animal
{
public:
	//생성자 생략
	virtual void Speak() const //virtual 키워드!!
	{
		cout << "Animal" << endl;
	}
};

class Cat :public Animal
{
public:
	//생성자 생략
	void Speak() const
	{
		cout << "Meow" << endl;
	}
};

int main()
{
	Cat* myCat = new Cat(2, "Jerry");
	Animal* yourCat = new Cat(5, "Nabi");

	myCat->Speak(); // Meow
	youtCat->Speak(); //Meow
}

가상 테이블

  • 모든 가상 멤버함수의 주소 포함
  • 클래스마다 하나? vs 개체마다 하나?
    -> 클래스 마다 하나를 가진다.
  • 개체를 생성할 때 해당 클래스의 가상 테이블 주소가 함께 저장된다.
    -> 각 오브젝트 마다 자신의 가상 테이블 주소를 가진다.
  • 느리다
    • speak()함수를 호출할 때
      -> 가상 함수테이블 주소로 jump
      -> 가상 함수테이블에서 speak() 함수를 찾아서 jump
  • 가상 멤버함수 테이블 구조 (동적 바인딩) - 위에 작성한 코드 기반 동작 모습
    • 힙에 할당한 개체는 Cat 클래스로 만들어진 개체이므로, 해당 개체들이 가지는 가상 테이블은 둘 다 Cat 가상 테이블을 가리킨다. -> 둘 다 Meow 출력되는 이유

가상 소멸자

  • 가상 소멸자는 모든 클래스에서 할 것!
  • 소멸자도 함수와 마찬가지로 virtual 키워드를 사용하지 않으면, 정적 바인딩을 하여 무늬따라 간다.
  • 아래의 예시에서 Animal* myAnimal = new Cat(5, "Jerry"); 처럼 개체를 생성하고 delete myAnimal 을 할 경우 ~Animal() 만 호출되기 때문에 mName 이라는 힙에 할당한 변수를 메모리에서 해제하지 못해서 메모리 누수가 발생한다.
class Animal
{
public:
	virtual ~Animal(); 
	//~Animal(); 소멸자의 정적 바인딩 -> 메모리 누수 발생
private:
	int mAge;
};

class Cat :public Animal
{
public:
	virtual ~Cat(); //virtual 안써도 되지만 쓰는 습관
private:
	char* mName;
};

Cat::~Cat()
{
	delete mName;
}

int main()
{
	Cat* myCat = new Cat(2, "Nabi");
	delete myCat;	//->~Cat() 호출
					//->~Animal() 호출					

	Animal* myAnimal = new Cat(5, "Jerry");
	delete myAnimal; //소멸자에 virtual 키워드 없을 때 
    				 //~Animal()만 호출된다. 
					 
}

다중 상속

  • 부모가 두개인 클래스
  • 여러 문제가 발생할 수 있고, 좋은 구조는 아니기 때문에 다중 상속의 구조를 사용해야 할 때에는 인터페이스를 사용

추상 클래스

  • 구체적인 클래스
    • 멤버 함수의 구현이 되어 있으며, 멤버 함수가 존재하는 클래스
  • 추상 클래스
    • 구체적인 함수의 구현이 되어있지 않은 클래스
    • 순수 가상함수를 하나 이상 가지고 있는 베이스 클래스
    • 추상 클래스의 개체를 만들 수 없다. (Animal 클래스가 추상 클래스 Cat 클래스는 Animal 클래스의 파생 클래스라고 가정)
    • 포인터나 참조형으로는 사용 가능
      • Animal myAnimal1; -> 불가능
      • Animal* myAnimal2 = new Animal(); -> 불가능
      • Animal* myCat = new Cat(); -> 가능
      • Animal& myCatRef = *myCat; -> 가능
  • 순수 가상 함수 형식
    virtual <반환타입> <함수명> = 0;
  • 추상 클래스의 순수 가상함수는 파생 클래스에서 구현해야한다
    -> 구현하지 않으면 컴파일 오류 발생

인터페이스

  • 순수 추상 클래스를 사용하여 자바의 인터페이스를 흉내낸 방식
  • 순수 가상함수만 가지고 멤버 변수가 없는 클래스
  • 파생 클래스에서 순수 가상함수를 구현해야 한다.
  • 인터페이스 예시 코드
class IFlyable //인터페이스 클래스
{
public:
	virtual void Fly() = 0;
};

class IWalkable
{
public:
	virtual void Walk() = 0;
};


class Bat :public IFlyable, public IWalkable
{
public:
	void Fly()
	{
		//구현
	}
	void Walk()
	{
		//구현
	}
};

0개의 댓글