virtual

728x90
반응형

Class의 상속과정에서 일어날 수 있는 문제 중의 하나인 Object Slicing에 대해서 알아본다. 그와 더불어 Operator Overloading에서 일어날 수 있는 문제에 대해서도 배워보자. 

 

 

상속으로 인한 Onject Slicing (오브젝트 슬라이싱) 문제점

 

Object Slicing이란 말 그래도 오브젝트 즉 객체가 Slicing 되는 것이다. 어떻게 해야지 Slicing이 되는지 Slicing이 되면 무슨 문제가 생기는지 아래 코드를 통해서 설명한다. 

#include <iostream>

class Animal
{
public:
	Animal() = default;
	virtual void speak()
	{
		std::cout << "Animal" << std::endl;
	}
	virtual ~Animal() = default;
private:
	double animalData = 0.0f;
};

class Dog : public Animal
{
public:
	Dog(double data) : dogData{ data } {};
	void speak() override
	{
		std::cout << "woof~" << std::endl;
	}
private:
	double dogData;
};

int main()
{
	Dog  choco{ 9.0 };

	Animal cloneChoco = choco;

	return 0;
}

Animal 클래스에는 멤버 변수 animalData, Dog 클래스에는 멤버 변수 dogData가 존재하고 main함수에서 Dog 객체인 choco를 선언하고 dogData를 9.0으로 초기화해주었다. 그다음 Animal 객체 cloneChoco를 Copy Constructor를 이용하여 Choco를 복사하여 객체를 생성하였다. 이 과정에서 생기는 문제가 바로 Object Slicing이다. 

 

아래 그림은 Object Slicing이 일어나는 이유에 대해서 그린 그림이다. 

Animal 객체인 cloneChoco에 choco오브젝트를 Copy Constructor로 복사할 경우 내부 Memory Layout에서 각 변수들이 어떻게 복사되는지 나타낸 그림이다. 위 그림에서 보면 Animal객체인 cloneChoco는 dogData를 가지고 있지 않기 때문에 dogData는 잘리게 된다. 또한 Vitual Table을 가리키는 특수한 변수 *vT는 복사가 이루어지지 않는다. 특히 아래 같이 함수를 정의한 경우 Object Slicing이 많이 일어난다. 

void print_object(Animal other) const
{
	std::cout << other.print() << std::endl;
}

위와 같이 함수 파라미터를 Animal형으로 정의를 할 경우 결국엔 Copy Assignment가 일어나기 때문에 Animal객체에 Dog객체를 Copy 한 것과 동일하게 Object Slicing을 초래하게 된다. 

 

위 두 경우의 문제를 해결하는 방법은 포인터 혹은 레퍼런스를 이용하여 해당 객체를 가리키는 방법이다. 

 

더 나아가서 Copy Constructor와 Copy Assignment를 delete를 통하여 아예 방지하는 방법도 있다. 아래 코드는 delete를 통한 Conpy Constructor와 Assignment를 사용 못하게 하는 방법이다.

#include <iostream>

class Animal
{
public:
    Animal() = default;
    Animal& operator=(const Animal& ohter) = delete;
    Animal(const Animal& other) = delete;
    virtual void speak()
    {
        std::cout << "Animal" << std::endl;
    }
    virtual ~Animal() = default;
private:
    double animalData = 0.0f;
};

class Dog : public Animal
{
public:
    Dog(double data) : dogData{ data } {};
    void speak() override
    {
        std::cout << "woof~" << std::endl;
    }
private:
    double dogData;
};

int main()
{
    Dog choco{ 9.0 };
    
    Animal cloneChoco = choco; //ERROR
    
    Dog cream{ choco }; //ERROR
    
	return 0;
}

위와 같은 해결방법은 Base 클래스에서 직접적으로 Copy Constructor와 Copy Assignment를 delete 해줌으로 Animal 객체를 Dog객체로 생성하는 것을 방지해줬다. 하지만 또 다른 에러가 발생을 하게 되는데 그 에러는 바로 Dog객체 간의 Copy Constructor가 호출이 불가능 해진다는 점이다. 이유는 Base클래스에서 Copy Construcotr를 delete를 해주면 해당 속성이 Derived클래스에도 적용이 되기 때문이다. 그렇다면 다시 아래와 같이 수정할 수 있다. 

#include <iostream>

class Animal
{
public:
    Animal() = default;
    Animal& operator=(const Animal& ohter) = delete;
    virtual void speak()
    {
        std::cout << "Animal" << std::endl;
    }
    virtual ~Animal() = default;
protected:
    Animal(const Animal& other) = default;
private:
    double animalData = 0.0f;
};

class Dog : public Animal
{
public:
    Dog(double data) : dogData{ data } {};
    void speak() override
    {
        std::cout << "woof~" << std::endl;
    }
private:
    double dogData;
};

int main()
{
    Dog choco{ 9.0 };
    
    Animal cloneChoco = choco; //ERROR
    
    Dog cream{ choco }; //OK
    
	return 0;
}

Copy Constructor를 Dog 클래스에서 접근이 가능하고 외부에서는 접근이 불가능하게 protected키워드를 이용하여 선언을 해줌으로 문제가 해결이 가능하다. 

 

 

상속으로 인해 Operator Overloading (연산자 오버 로딩)에서 생길 수 있는 문제점

 

Animal 객체에 대해서 ==연산자를 오버 로딩을 하면 아래 함수와 같다. 

bool operator==(const Animal& lhs, const Animal& rhs)
{
    return lhs.animalData == rhs.animalData;
}

위와 같이 함수 오버 로딩을 하였을 경우 생기는 문제는 Animal레퍼런스로 Dog을 가리킬 수 있기 때문에 Dog객체 간 ==연산자를 사용할 경우 implicit 하게 위 함수를 호출하게 된다. Dog객체 간 ==연산자의 사용하는 이유의 핵심은 "dogData가 같나"를 비교하는 것이지만 본의 아니게 animalData가 같은지만 확인하는 결과가 나온다. 위를 방지하기 위해 Dog객체에 대한 ==연산자 오버 로딩을 해주어야 한다

bool operator==(const Animal& lhs, const Animal& rhs)
{
    return lhs.animalData == rhs.animalData;
}

bool operator==(const Dog& lhs, const Dog& rhs)
{
    return lhs.animalData == rhs.animalData && lhs.dogData == rhs.dogData;
}

*위 함수 모두 편의상 animalData와 dogData를 public키워드에 선언을 했다고 가정하고 짠 함수이다.

 

이렇게 상속으로 인해서 발생할 수 있는 문제 두 가지를 다루어봤다. 

 

Ref.

https://www.youtube.com/channel/UCHcG02L6TSS-StkSbqVy6Fg

반응형
728x90
반응형

Multiple Inheritance (다중 상속)으로 인해 Dianmond Problem (다중 상속 문제)를 야기할 수 있는데 Dianmond Problem과 해당 문제를 해결하는 방법에 대해서 배워보자. 

 

 

Diamond Problem (다중 상속 문제)

 

Diamond Problem은 다중으로 상속을 했을 때 나타나는 문제이다. 

하이브리드 동물 Liger를 클래스로 나타낸다면 아래 그림과 같을 것이다. 

위 그림처럼 Tiger와 Lion은 둘 다 동물이기 때문에 Animal클래스를 상속을 받고 Tiger와 Lion의 교배로 낳은 Liger는 두 동물의 상속으로 만들어진다. 위 그림을 코드화 시키면 아래와 같다. 

#include <iostream>

class Animal
{
public:
	Animal()
	{
		std::cout << "Animal Constructor\n";
	}
	virtual ~Animal() = default;
private:
	double animalData;
};

class Lion : public Animal
{
public:
	Lion()
	{
		std::cout << "Lion Constructor\n";
	}
	virtual ~Lion() = default;
private:
	double lionData;
};

class Tiger : public Animal
{
public:
	Tiger()
	{
		std::cout << "Tiger Constructor\n";
	}
	virtual ~Tiger() = default;
private:
	double tigerData;
};

class Liger : public Lion, public Tiger
{
public:
	Liger()
	{
		std::cout << "Liger Constructor\n";
	}
	virtual ~Liger() = default;
private:
	double ligerData;
};

int main()
{
	Liger miniLiger;
}

위 코드를 보면 딱히 문제점이 없어 보인다. 하지만 Liger객체를 생성하여 생성자가 호출이 되는 순서를 보게 되면 이상한 현상이 일어나게 된다. 

코드 출력

출력 화면을 보게 되면 Animal의 생성자가 2번 호출이 되는 것을 확인할 수 있다. 사실 쉽게 생각하면 Tiger와 Lion은 각각 Animal을 상속을 받았기 때문에 Animal object를 가지게 되고 또 Liger는 Tiger와 Lion을 다중 상속을 받았기 때문에 Animal 객체가 두 번 생성이 되는 것이다.  또 다른 문제는 만약 Liger 객체를 위와 같이 생성을 했다고 가정을 하면 Animal에 animalData를 접근하는 멤버 함수를 만들고 animalData를 접근하려고 한다면 생성된 두 개의 Animal객체 중 어떤 Animal객체의 멤버 함수를 호출하는가라는 문제가 또 생긴다. 

 

이러한 현상을 방지하기 위해 우리는 Virtual Inheritance라는 것을 사용하면 위 문제를 해결할 수 있다. 

 

 

Virtual Inheritance (가상 상속)

 

가상 상속이란 클래스 상속에서 "Virtual"키워드를 통하여 가상으로 상속을 받게 하는 방법이다. Virtual Function에서 Derived클래스에서 해당 Virtual Function에 대한 재정의를 기대한다고 말했는데 Virtual Inheritance에서도 동일하게 다른 클래스에서 해당 Base클래스를 상속받을 것을 기대한다고 보면 된다. 만약 다른 클래스에서 해당 Base클래스를 상속받지 않았을 경우는 자기가 받으면 문제는 해결이 된다. 

 

위에 코드를 가상 상속으로 수정한 코드는 아래와 같다. 

#include <iostream>

class Animal
{
public:
	Animal()
	{
		std::cout << "Animal Constructor\n";
	}
	virtual ~Animal() = default;
private:
	double animalData;
};

class Lion : virtual public Animal
{
public:
	Lion()
	{
		std::cout << "Lion Constructor\n";
	}
	virtual ~Lion() = default;
private:
	double lionData;
};

class Tiger : virtual public Animal
{
public:
	Tiger()
	{
		std::cout << "Tiger Constructor\n";
	}
	virtual ~Tiger() = default;
private:
	double tigerData;
};

class Liger : public Lion, public Tiger
{
public:
	Liger()
	{
		std::cout << "Liger Constructor\n";
	}
	virtual ~Liger() = default;
private:
	double ligerData;
};

int main()
{
	Liger miniLiger;
}

위와 같이 virtual 키워드를 통해서 Diamond Problem을 해결할 수 있다. 

 

 

Virtual Inheritance를 통해서 상속을 받게 되면 기존 상속과 Memory Layout이 다르다. thunk함수와 offset을 이용하여 맞는 함수를 가리키게 되는데 이 부분은 복잡하고 또한 Virtual Inhertance자체가 잘 사용이 되지 않는 기능이기 때문에 해당 Memory Layout은 영상을 참고하여 한번 보는 것으로 끝낸다. 궁금하다면 하단 레퍼런스에서 링크를 통해 보길 바란다. 

 

 

Ref.

https://www.youtube.com/watch?v=0izSeUXpwDw&list=PLDV-cCQnUlIar6Wx3rkXHs7wbfmlz34Fz&index=7&t=300s 

반응형
728x90
반응형

클래스의 크기(Memory Size)를 통해서 Virtual Table 혹은 vTable에 대해서 알아보자. 

먼저 아래 코드를 보면 

#include <iostream>

class Animal
{
public:
    void speak()
    {
        std::cout << "Animal\n"
    }
private:
    double height;
};

int main()
{
    std::cout << sizeof(Animal) << '\n';
    return 0;
}

*double의 크기는 8바이트로 간주

main함수에서 sizeof() 연산자를 이용하여 Animal클래스의 크기를 출력을 해보면 8바이트가 출력이 된다. 이는 Animal클래스가 private 하게 가지고 있는 double형 멤버 변수 height에 의한 것이다.

 

어쩌면 의문이 드는 것이 spaek() 함수에 대한 메모리는 저장이 되지 않는 것인가?이다. 멤버 함수는 일반 함수와 동일하게 "code"영역에 저장이 되게 된다. 객체가 생성될 때마다 매번 멤버 함수가 할당이 되면 비효율적이기 때문에 "code"영역에 저장하고 공유를 한다. 

 
저번 포스팅에서 공부한 Virtual Function을 이용하여 상속이 될 경우 클래스의 크기는 어떻게 변할까?

class Animal
{
public:
	virtual void speak()
	{
		std::cout << "Animal" << std::endl;
	}
private:
	double hetght;
};

class Dog : public Animal
{
public:
	virtual void speak() override
	{
		std::cout << "Woof" << std::endl;
	}
private:
	double weight;
};

int main()
{
	std::cout << "Animal size : " << sizeof(Animal) << std::endl;
	std::cout << "Dog size : " << sizeof(Dog) << std::endl;
	return 0;
}

위 코드를 실행시켜볼 경우 Animal 클래스의 사이즈는 16바이트, Dog 클래스의 사이즈는 26바이트로 출력이 된다. 이는 Virtual Function을 사용하여 Virtual Table이 생성이 되고 이를 가리키는 pointer가 객체의 메모리 영역에 생기기 때문이다. 

 

이해하기 쉽게 아래 그림으로 클래스에서 어떻게 Virtual Function이 호출이 되는지 그림으로 나타냈다. 

 

 

Dynamic Polymorphism (동적 다형성)

프로그래밍에서 말하는 다형성은 사실 생물학에서 가져온 용어이다. 동일한 종(species) 중에서도 다양한 변이를 가지는 현상을 만한다. C++에서는 Polymorphism은 동일한 객체 혹은 함수가 특정 상황에서는 다른 게 작동하는 것을 의미한다. 우리는 이미 이런 특성을 공부하지 않았는가? 결국엔 객체들이 Polymorphism을 가지게 하기 위해서는 Virtual Function을 Override 함으로써 다형성을 얻을 수 있다. 

 

그럼 여기서 Dynamic은 무엇을 뜻하느냐? Polymorphism을 Compile-time이 아닌 Run-time에 얻는 것을 의미한다. C++ 같은 객체지향 언어는 상속이 이루어진 클래스의 Base클래스 포인터 타입에 Derived 클래스의 주소를 가리키는 것이 가능하기 때문에 동적으로 다형성을 취득할 수 있다. 

 

아래 코드는 Virtual Function으로 Polymorphism을 가지게 하고 Animal 포인터 타입 변수로 사용자의 입력에 따라 동적으로 객체를 생성하게 하여 Dynamic Polymorphism을 얻게 한 코드이다. 


#include <iostream>

class Animal
{
public:
	virtual void speak() const
	{
		std::cout << "Animal" << std::endl;
	}
private:
	double hetght;
};

class Dog : public Animal
{
public:
	virtual void speak() const override
	{
		std::cout << "Woof" << std::endl;
	}
private:
	double weight;
};

int main()
{
	Animal* animalPtr = nullptr; 	//Animal *형 변수를 만들어 null pointer로 초기화
	int i;
	std::cout << "which one do you want to create?\n1. Animal\n2. Dog\n";
	std::cin >> i;

	if (i == 1)
	{
		animalPtr = new Animal();
	}
	else if (i == 2)
	{
		animalPtr = new Dog();
	}
	//사용자가 1을 넣으면 Animal객체가 생성이 되고 2를 입력하면 Dog객체가 생성이 된다
   	//동적으로 만들어진 객체는 Heap영역에 생성이 되고 stack영역에 있는 animalPtr이 객체를 가리키게 된다. 
	animalPtr->speak();

	delete animalPtr;	//동적할당 메모리 해제
	return 0;
}

 

 

Ref.

https://nocodeprogram.com/lecture/1/78474/

https://micropilot.tistory.com/3072

반응형
728x90
반응형

이번 포스팅에서는 Virtual Funcction (가상 함수) 사용법을 포스팅한다. 

 

Virtual Function (가상 함수)

가상 함수란 상속되는 자식 클래스에서 재정의 될 것으로 예상을 하는 멤버 함수이다. 이런 가상 함수는 Dynamic Polymorphism에 의한 동적 타입에 의해서 결정이 된다. 

 

다음 코드는 일반 멤버 함수를 virtual function으로 정의한 예시를 보여주는 코드이다. 

#include <iostream>
class Animal
{
public:
    void print()
    {
        std::cout << "I am Animal" << std::endl;
    }
    virtual void speak()
    {
        std::cout << "Animal" << std::endl;
    }
};

class Dog : public Animal
{
public:
    void print()
    {
        std::cout << "I am Dog" << std::endl;
    }
    void speak() override
    {
        std::cout << "woof" << std::endl;
    }
};

int main()
{
    Animal* Ptr1;
    Animal* Ptr2;
    
    Ptr1 = new Dog();
    Ptr2 = new Animal();
    
    Ptr1->print();
    Ptr1->speak();
    
    Ptr2->print();
    Ptr2->speak();
}

 

 

출력

위 코드에서 먼저 Animal *형 변수  Ptr1과 Ptr2를 생성하였다. Ptr1은 Dog객체를, Ptr2는 Animal객체를 가리킨다. 클래스에서 각 클래스에서 print()는 non-virtual 멤버 함수이며 speak()는 virtual 멤버 함수이다. 각 포인터 변수의 타입은 Animal* 타입이기 때문에 어떤 클래스 객체를 가리키고 있던 print() 멤버 함수는 non-virtual 하기 때문에 Animal클래스의 print() 멤버 함수를 호출하게 된다. 하지만 virtual 함수인 speak() 멤버 한수는 가리키고 있는 객체에 speak() 멤버 함수가 재정의 되어있다면 재정의된 함수를 호출하게 된다. 

반응형

+ Recent posts