OOP

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
반응형

OOP Intro이기 때문에 각 특징들에 대해서 간단하게 설명한다. 

 

C++는 객체지향 OOP (Object Oriented Programming) 언어이고 다중 패러다임 (Multi Paradigm) 프로그래밍 언어이다. 

 

먼저 C++는 procedural, functional, OOP, generic 네 가지의 패러다임을 이용해서 프로그래밍을 하게 된다. 그중에서 OOP 특성을 중심으로 구현이 된다.

 

OOP란 클래스를 중심으로 프로그래밍을 하는 것을 의미한다. 클래스란 definition이라고 생각하고 그 definition으로 객체가 생성이 된다. 객체는 실제 메모리 공간을 차지하는 클래스에 대한 변수라고 생각하면 된다. 

 

class Dog
{
public:
	Dog(int age) : mAge{age} {};
private:
	int mAge;
};

int main()
{
	Dog choco {9};	//객체 생성
    return 0;
}

위 코드를 보면 "Dog"라는 클래스가 정의되었다. 클래스는 mAge라는 변수를 통해 객체의 "나이"를 저장한다. main함수에서 Dog의 Constructor를 이용하여 choco객체의 "나이"를 9로 초기화를 시켜준다. 

 

OOP는 4개의 특성이 있다. 

  • Abstraction
  • Encapsulation
  • Inheritance
  • Polymorphism

Abstraction

추상화라는 특징이다. 현실세계의 어떠한 물체 혹은 문제를 추상화를 하는 것이라고 생각하면 쉽다. 위 코드에서 현실세계에 존재하는 강아지라는 생물은 여러 특성(사족보행, 평균수명, 귀여움 등)이 있지만 현재 프로그램에서 나이만 필요할 경우 나이에 대한 멤버 변수만 생성을 해주어 강아지라는 동물을 추상화를 하여 구현한다는 특징이다. 

 

Encapsulation

캡슐화라는 특징이다. 클래스 내부의 중요한 멤버 변수들을 외부에서 수정이 불가하도록 하는 특징이다. 예를 들면 강아지 클래스에 강아지의 평균수명 값을 저장하고 있는 멤버 변수가 있다고 가정을 해보자. 그러면 외부에서 강아지의 평균수명을 임의대로 수정을 하게 되면 문제가 생기게 된다. 캡슐화는 private, protected 등 키워드를 통해 구현이 된다. 위 코드에서 mAge 멤버 변수는 private키워드에 포함이 되기 때문에 외부에서 해당 멤버 변수를 직접 접근, 수정하는 것이 불가능하다. 

 

Inheritance

상속성이라는 특징이다. 상속성의 가장 중요한 점은 코드의 재사용이라는 점이다. 예를 들면 현실 세계에서 강아지라는 생물은 동물에 속한다. 결국 강아지도 동물의 특성을 가지고 있다. 동물의 특성으로는 사람의 언어를 못한다는 특성이 있다고 가정하면 동물 클래스에 "사람과 대화 불가"라는 코드를 만들고 강아지 클래스에 동물 클래스를 상속을 받으면 동물 클래스의 특징을 이어받는다. 상속성을 이용해 중복되는 코드를 또 생성하지 않고 재사용할 수 있다는 장점이 있다.

 

Polymorphism 

다형성이라는 특징이다. 다형성에는 대표적으로 function overloading과 function overriding이 있다. 다형성이라는 특징은 형태가 여러 가지 존재한다는 특징이다. 겉은 같아 보이지만 특정 파라미터 혹은 특정 상황에 맞추어 호출하는 함수가 달라지는 특징이다. 해당 특징에 대해서는 function overloading과 function overriding 포스트에서 따로 자세하게 다룰 예정이다. 

 

 

우리가 c++ 프로그래밍을 하면서 유의할 점은 OOP가 목적이 되어서는 안된다는 점이다. 우리의 목적은 읽기 편하고, 이해하기 쉽고, 유지보수가 용이한 프로그램을 짜는 것이 목적이다. OOP를 정확하게 따르는 프로그램을 만드는 것이 목적이 아니다. 읽기 편하고, 이해하기 쉽고, 유지보수가 용이한 프로그램을 짜되 파포먼스까지 좋은 프로그램을 짜기 위해서 C++을 이용하는 것이다. 우리가 가장 포커스를 맞춰야 하는 점은 프로그래밍을 통해서 가치를 창출해내는 것이 우리의 목표임을 잊지 말자.

 

 

Ref.

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

 

 

본 포스팅은 유튜버 코드없는프로그래밍 채널을 공부하면서 정리한 내용입니다. 

반응형

+ Recent posts