CLASS
-
[C++] Object Slicing (오브젝트 슬라이싱)2021.08.22
-
[C++] Virtual Function (가상 함수)2021.08.20
-
[C++] Class Access Specifier (클래스 접근 제한자)2021.08.20
-
[C++] Member Initializer List (멤버 초기화 리스트)2021.08.18
[C++] Shared Pointer (쉐어드 포인터) : Circular Reference (순환 참조)
Unique Pointer에 이어서 Smart Pointer의 한 종류인 Shared Pointer에 대해서 정리한다.
Shared Pointer (쉐어드 포인터)
이름에서도 알 수 있듯이 해당 포인터는 Exclusive Ownership을 가지는 Unique pointer와 다르게 Shared Ownership을 가지게 된다. 그 말은 하나의 오브젝트를 여러 개의 포인터가 가질 수 있다. 하지만 모든 스마트 포인터는 RAII콘셉트를 제공을 해야 한다. Shared Pointer는 여러 개의 포인터가 가리킴에도 불구하고 어떻게 오브젝트의 메모리 할당을 해제시키는지 알아보자.
아래 코드는 쉐어드 포인터를 선언하는 코드이다.
class Dog
{
public:
Dog() : mAge { 0 }
{
std::cout << "dog Constructor" << std::endl;
}
~Dog()
{
std::cout << "Dog destructor" << std::endl;
}
private:
int mAge;
}
int main()
{
std::shared_ptr<Dog> dogPtr = std::make_shared<Dog>();
}
위 코드에서 dogPtr이라는 쉐어드 포인터를 생성하고 Dog객체를 할당하였다. 위 과정에서 해당 포인터의 scope단위는 main함수이고 객체가 생성이 되면서 Constructor를 부르게 되고 main함수가 종료되면서 자동적으로 Destrucotr를 호출하게 된다.
하지만 쉐어드 포인터의 특성으로 다른 쉐어드 포인터가 하나의 오브젝트를 가리킬 수 있다. 아래의 코드는 여러 개의 쉐어드 포인터가 하나의 오브젝트를 가리키는 예이다.
int main()
{
std::shared_ptr<Dog> dogPtr = std::make_shared<Dog>();
std::shared_ptr<Dog> dogPtr1 = dogPtr;
}
위와 같은 경우 Unique Pointer와 다르게 하나의 오브젝트를 두 개의 포인터가 가리키게 된다. 만약에 여기서 끝이라면 일반 포인터와 다를 바가 없다. 일반 포인터와 다른 점은 가리키는 메모리 공간에 몇 개의 포인터가 오브젝트를 가리키는지 Count를 한다. Count 중에서도 Strong Count와 Weak Count가 있는데 쉐어드 포인터가 가리키게 되면 Strong Count가 올라가고 오브젝트는 Strong Count가 0일 때 메모리를 해제를 결정하게 된다. Weak Pointer가 가리키게 되는 경우는 Weak Count가 올라가지만 메모리를 해제하기 위해서 참고되지는 않는다.
Shared Pointer은 개발자가 Rsource의 Life Cycle을 고려하지 않고(메모리 해제를 고려하지 않고) 한 오브젝트의 Ownership을 여러 Scope에서 공유가 가능하게 만들어준다.
하지만 이러한 특성 때문에 의도치 않은 Memory Leak이 일어날 수 있다.
아래 코드는 Shared Pointer 사용함에 있어서 Memory Leak이 일어나는 가장 쉬운 예이다.
#include <iostream>
#include <memory>
class Cat
{
public:
Cat(std::string name) : mName{ name }
{
std::cout << mName << " cat constructor" << std::endl;
}
~Cat()
{
std::cout << mName << " cat destructor" << std::endl;
}
std::shared_ptr<Cat> mVar;
private:
std::string mName;
};
int main()
{
std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
kitty->mVar = kitty;
std::cout << kitty.use_count() << std::endl;
}
먼저 클래스 내에 Shared Pointer mVar를 추가로 생성하였다. main함수에서 Shared Pointer kitty를 선언하고 오브젝트를 생성하고 오브젝트 내의 Shared Pointer mVar를 오브젝트 자신인 kitty를 가리키게 만들었다. 그다음 kitty 오브젝트의 Strong Count를 확인하기 위해 use_count()를 이용해 출력을 한 결과 2가 출력이 된다. 이 경우 Memory Leak이 발생하게 되는데 아래 그림을 통해 알아보자.
Shared Pointer인 kitty는 main함수 scope가 종료됨에 따라 메모리 해제가 된다. kitty가 사라지고 오브젝트의 Count가 2에서 1로 줄게 된다. Shared Pointer가 해제되고 오브젝트 또한 자동으로 해제되는 것을 기대했지만 오브젝트 내부의 Shared Pointer가 오브젝트를 아직 가리키고 있기 때문에 main함수가 종료됐음에도 불구하고 메모리 해제가 안되어 Memory Leak이 일어난 경우이다.
위 예는 Memory Leak이 일어날 수 있다는 것을 보여주기 위한 예로 실제 많이 발생되지 않는다. 실제로 자주 발생되는 Memory Leak에 대한 코드는 아래와 같다.
Circular Reference (순환 참조)
#include <iostream>
#include <memory>
class Cat
{
public:
Cat(std::string name) : mName{ name }
{
std::cout << mName << " cat constructor" << std::endl;
}
~Cat()
{
std::cout << mName << " cat destructor" << std::endl;
}
std::shared_ptr<Cat> mVar;
private:
std::string mName;
};
int main()
{
std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
std::shared_ptr<Cat> nabi = std::make_shared<Cat>("nabi");
kitty->mVar = nabi;
nabi->mVar = kitty;
std::cout << "kitty count : " << kitty.use_count() << std::endl;
std::cout << "nabi count : " << nabi.use_count() << std::endl;
}
순환 참조란 위 코드와 그림처럼 클래스 내부에서 Shared Pointer로 다른 클래스가 서로 가리키는 것을 의미한다. kitty 오브젝트와 nabi 오브젝트가 Shared Pointer kitty와 nabi에 의해서 가리키게 되고 내부의 mVar Shared Pointer가 서로의 오브젝트를 가리키게 되면 Shared Pointer가 main함수에서 메모리 해제가 되어도 각 오브젝트의 Count는 1로 메모리 해제가 안 되는 것을 말한다.
위와 같은 Memory Leak을 해결하기 위해 Weak_ptr을 이용하여 Circular Reference를 구현할 수 있다. 다음 포스팅은 Weak_Ptr에 대해서 공부할 예정이다.
Ref.
https://www.youtube.com/watch?v=tg34hwP0P0M&list=PLDV-cCQnUlIbOBiPvBaRPezOLArubgZbQ&index=4
'Modern C++' 카테고리의 다른 글
[C++] Template Intro. (템플릿) : Function Template (함수 템플릿) (0) | 2021.08.26 |
---|---|
[C++] Weak Pointer (위크 포인터) : Circular Reference (순환 참조) 해결 (0) | 2021.08.25 |
[C++] Smart Pointer (스마트 포인터): Unique Pointer (유니크 포인터) (0) | 2021.08.23 |
[C++] Smart Pointer Intro (스마트 포인터) (0) | 2021.08.22 |
[C++] Object Slicing (오브젝트 슬라이싱) (0) | 2021.08.22 |
[C++] Smart Pointer (스마트 포인터): Unique Pointer (유니크 포인터)
Smart Pointer에서 간단하게 Smart Pointer에 대해서 알아보았다. 이번에는 스마트 포인터의 한 종류인 Unique Pointer (유니크 포인터)에 대해서 공부한다.
Unique Pointer (유니크 포인터)
유니크 포인터에 대해서 공부하기 전에 유니크 포인터의 Exclusive Ownership이 뭔지 얘기하고 시작한다.
Exclusive Ownership (소유권 독점)
소유권 독점이란 하나의 Object에 단 하나의 Pointer만 가리킬 수 있다는 것이다. 말 그래로 소유권을 독점한다라는 뜻이다. 일반적인 포인터를 사용하게 되면 한 Object당 여러 개의 포인터가 가리킬 수 있는데 Exclusive Ownership은 아니다.
아래
코드는 하나의 객체에 다수의 포인터가 가리킬 경우 생길 수 있는 문제 중 하나이다.
#include <iostream>
class Cat
{
public:
Cat() : mAge{ 0 }
{
std::cout << "cat constructor" << std::endl;
}
~Cat()
{
std::cout << "cat destructor" << std::endl;
}
private:
int mAge;
};
void memoryDealloc(Cat* dealloc)
{
delete dealloc;
}
int main()
{
Cat* choco = new Cat();
memoryDealloc(choco);
delete choco;
}
choco객체를 동적으로 선언을 해주고 메모리를 해제해주는 memoeyDealloc함수를 만들어 delete 해준다. 이 과정에서 main함수에서 실수로 또 delete를 해주게 된다면 한 메모리 공간에 대해서 delete를 두 번 해주는 상황이 발생한다.
이러한 문제를 방지하기 위해서 스마트 포인터를 사용하면 된다.
int main()
{
std::unique_ptr<Cat> catPtr = std::make_unique<Cat>();
std::unique_ptr<Cat> catPtr1 = catPtr; //ERROR
}
main함수를 스마트 포인터를 이용하여 다시 고쳐 쓰면 위와 같은데 위 코드에서 스마트 포인터 catPtr과 catPtr1이 선언이 되고 catPtr1는 catPtr이 가리키고 있는 객체의 주소 값을 복사해온다. 하지만 Exclusive Ownership특성 때문에 하나의 객체를 여러 개의 포인터가 가리킬 수 없기 때문에 에러가 난다. 하지만 catPtr1에 해당 객체의 주소 값을 저장하고 싶다면 객체의 소유권을 뺏어오면 된다.
int main()
{
std::unique_ptr<Cat> catPtr = std::make_unique<Cat>();
std::unique_ptr<Cat> catPtr1 = std::move(catPtr);
}
위와 같이 dogPtr을 R-Value로 바꾸고 dogPtr1에 소유권을 이전하면 문제가 생기지 않는다.
스마트 포인터 사용 예
Animal Class와 Zoo Class를 추가로 만든다. 그다음 막 개장한 동물원이라서 동물원 안에 동물이 하나밖에 없는 상황이라고 가정을 하면 Zoo Class에 멤버 변수로 Animal 객체 하나만 가지게 된다. 원래 스마트 포인터를 사용하지 않았다면 Rule og Three에 의해서 Destructor, Copy / Move Constructor와 Copy / Move Assignment를 만들어줘야 하지만 스마트 포인터를 이용하면 알아서 scope 단위에서 메모리를 해제해주고 Copy Constructor와 Assignment는 애초에 스마트 포인터가 copy가 안되기 때문에 만들어 주지 않아도 되고 Mov Constructor와 Assignment는 컴파일러가 만들어 주는 함수로 커버가 가능하기 때문에 스마트 포인터를 사용하게 되면 일반 멤버 변수처럼 클래스를 구성해도 무방하다.
'Modern C++' 카테고리의 다른 글
[C++] Weak Pointer (위크 포인터) : Circular Reference (순환 참조) 해결 (0) | 2021.08.25 |
---|---|
[C++] Shared Pointer (쉐어드 포인터) : Circular Reference (순환 참조) (0) | 2021.08.25 |
[C++] Smart Pointer Intro (스마트 포인터) (0) | 2021.08.22 |
[C++] Object Slicing (오브젝트 슬라이싱) (0) | 2021.08.22 |
[C++] Virtual Inheritance (가상 상속) : Diamond Problem (다중 상속 문제) 해결 (0) | 2021.08.21 |
[C++] Object Slicing (오브젝트 슬라이싱)
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.
'Modern C++' 카테고리의 다른 글
[C++] Smart Pointer (스마트 포인터): Unique Pointer (유니크 포인터) (0) | 2021.08.23 |
---|---|
[C++] Smart Pointer Intro (스마트 포인터) (0) | 2021.08.22 |
[C++] Virtual Inheritance (가상 상속) : Diamond Problem (다중 상속 문제) 해결 (0) | 2021.08.21 |
[C++] Multiple Inheritance (다중 상속) (0) | 2021.08.21 |
[C++] Pure Virtual Function (순수 가상 함수), Abstract Class (추상 클래스) (0) | 2021.08.20 |
[C++] Virtual Inheritance (가상 상속) : Diamond Problem (다중 상속 문제) 해결
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
'Modern C++' 카테고리의 다른 글
[C++] Smart Pointer Intro (스마트 포인터) (0) | 2021.08.22 |
---|---|
[C++] Object Slicing (오브젝트 슬라이싱) (0) | 2021.08.22 |
[C++] Multiple Inheritance (다중 상속) (0) | 2021.08.21 |
[C++] Pure Virtual Function (순수 가상 함수), Abstract Class (추상 클래스) (0) | 2021.08.20 |
[C++] Virtual Table (가상 테이블)과 Dynamic Polymorphism (동적 다형성) (0) | 2021.08.20 |
[C++] Pure Virtual Function (순수 가상 함수), Abstract Class (추상 클래스)
앞서 Virtual Function과 Virtual Table 그리고 Polymophism에 대해서 공부를 하였다. 이번에는 Pure Virtual Function (순수 가상 함수)에 대해서 공부한다.
Pure Virtual Function (순수 가상 함수)
순수 가상 함수는 엄청 쉽다. 기존 Virtual Function에서 definition을 해주지 않고 값을 0으로 해주면 그것에 Pure Virtual Function이 된다. 또한 순수 가상 함수가 존재하는 클래스는 Abstract Class라고 부른다.
아래 코드는 간단한 Pure Virtual Function을 만든 예이다.
class Human
{
public:
virtual void walk() const = 0;
private:
int age;
};
사람을 클래스로 만든다고 가정을 하면 사람의 공통적인 특징인 걷기가 있을 것이다. walk() 멤버 함수를 virtual로 선언한 뒤 0으로 초기화를 해주어 Pure Virtual Function을 만들었다.
walk() 멤버 함수를 0으로 초기화를 하면 해당 Human클래스의 객체 생성이 불가능하다. 쉽게 생각하면 Human의 객체는 walk() 멤버 함수를 호출할 방법이 없기 때문이다. 결론적으로 Class 내부에 Pure Virtual Function이 1개라도 존재한다면 해당 클래스의 객체 생성이 불가능하다. 이는 클래스가 Abstract Class가 된다는 의미이다.
Pure Virtual Function의 상속 관계
Pure Virtual Function도 Virtual Function이기 때문에 상속받은 클래스에서의 재정의를 기대한다. 결국 Pure Virtual Function이 존재하는 Abstract Class를 상속받은 Class에서 해당 Pure Virtual Function을 재정의 하지 않으면 상속받은 Derived Class도 객체 생성이 불가능해진다. 결국 Derived Class를 객체 생성을 하기 위해서는 Pure Virtual Function에 대한 재정의가 반드시 이루어져야 한다.
#include <iostream>
class Human
{
public:
virtual void walk() const = 0;
private:
int age;
};
class Asian
{
public:
virtual void walk() const
{
std::cout << "Walking..." << std::endl;
}
private:
int intelligence;
}
int main()
{
Human Human1; //Abstract Class객체 생성 불가.
Asian gildong; //Pure Virtual Function가 재정의 되어 객체 생성 가능
return 0;
}
Asian 클래스에서 Pure Virtual Function에 대한 재정의가 이루어졌기 때문에 해당 객체를 선언이 가능하다.
Pure Virtual Function을 사용하는 이유?
Pure Virtual Function을 사용하는 이유는 Interface라는 개념을 사용하기 위함이다.
Interface
Interface란 Abstract Class를 의미하지만 specific 하게 멤버 변수와 Pure Virtual Function만 존재하는 Abstract Class를 의미한다.
Interface를 사용하는 이유는 쉽게 생각하면 "틀 제공"과 같다. 위에서 구성한 클래스를 보게 되면 Human이라는 interface를 생성하였다. walk() 멤버 함수만 구현하였지만 Human 즉 사람이 가지고 있는 공통적인 특성들이 있을 것이다. 예를 들면 잠을 자는 것, 감정이 있는 것 등등. 결국 Interface가 궁극적으로 해주는 역할은 어떤 대상의 전체적인 틀을 명시해주고 Derivde클래스에서 해당 특징들에 대한 자세한 내용을 기술하여 사용하기 위함이다.
개인적으로 게임의 몬스터를 만든다고 생각을 하면 이해하기 쉬웠다. 핵심은 전체적인 틀을 제공하여 세부사항은 상속받은 클래스에서 재정의하기를 기대하는 것이다.
'Modern C++' 카테고리의 다른 글
[C++] Virtual Inheritance (가상 상속) : Diamond Problem (다중 상속 문제) 해결 (0) | 2021.08.21 |
---|---|
[C++] Multiple Inheritance (다중 상속) (0) | 2021.08.21 |
[C++] Virtual Table (가상 테이블)과 Dynamic Polymorphism (동적 다형성) (0) | 2021.08.20 |
[C++] Virtual Function (가상 함수) (0) | 2021.08.20 |
[C++] Class Access Specifier (클래스 접근 제한자) (0) | 2021.08.20 |
[C++] Virtual Function (가상 함수)
이번 포스팅에서는 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() 멤버 함수가 재정의 되어있다면 재정의된 함수를 호출하게 된다.
'Modern C++' 카테고리의 다른 글
[C++] Pure Virtual Function (순수 가상 함수), Abstract Class (추상 클래스) (0) | 2021.08.20 |
---|---|
[C++] Virtual Table (가상 테이블)과 Dynamic Polymorphism (동적 다형성) (0) | 2021.08.20 |
[C++] Class Access Specifier (클래스 접근 제한자) (0) | 2021.08.20 |
[C++] Class Keywords (클래스 키워드) : const, explicit (0) | 2021.08.19 |
[C C++] Overloading (오버 로딩) (0) | 2021.08.19 |
[C++] Class Access Specifier (클래스 접근 제한자)
Class에는 Access Specifier (접근 제한자)가 존재한다. 상속에 관련된 내용을 공부하기 전에 접근 제한자와 상속관계의 접근 제한자에 대해서 포스팅한다.
먼저 기본적인 클래스의 접근 제한자에 대해서 알아보면 접근 제한자는 총 3가지가 존재한다. public, protected, private이 있다.
Public
public 접근 제한자에 선언된 멤버 변수 혹은 멤버 함수는 어디에서든지 접근이 가능하다.
Protected
protected에 선언된 멤버 함수 혹은 멤버 변수는 클래스 내부와 상속된 클래스에서 섭근이 가능하다.
Private
private에 선언된 멤버 변수 혹은 멤버 함수는 오직 클래스 내부에서 접근이 가능하다.
*접근 권한자를 명시해주지 않을 경우 private으로 간주한다.
#include <iostream>
class Dog
{
public:
Dog() = default;
Dog(int age) : mAge{ age } {};
void public_print() const
{
protected_print();
}
protected:
void protected_print() const
{
std::cout << "The dog is " << mAge << " years old\n";
}
private:
int mAge;
};
int main()
{
Dog choco{ 9 };
choco.public_print();
}
위 코드에서 클래스를 먼저 보게 되면 public, protected와 private 접근 제한자가 모두 사용된 것을 볼 수 있다. 먼저 private을 보면 integer형 멤버 변수 mAge가 선언되었다. 해당 멤버 변수는 클래스 내부에서만 접근이 가능하다. 다음으로 protected를 보게 되면 protercted_print() 멤버 함수가 존재한다. 이 멤버 함수에서는 private에 mAge를 불러와 출력을 하는 함수이다. protected 또한 현재 상속된 클래스가 존재하지 않기 때문에 클래스 내부에서만 접근이 가능하다. 마지막으로 public을 보면 기본 생성자와 integer 변수를 파라미터로 가지는 생성자가 있다. 그다음으로 public_print() 멤버 함수가 존재한다. 해당 멤버 함수에서 protected에 존재하는 protected_print() 멤버 함수를 호출하게 되는 멤버 함수이다.
결론적으로 main함수에서 choco 객체를 생성하고 public에 있는 public_print()를 호출하고 그다음 protected에 protected_print() 함수를 호출하여 private에 있는 mAge를 불러와 출력을 하게 된다.
Inheritance (상속)
상속에 가장 기초인 상속 접근 제한자에 대해서 간략하게 말하자면 아래 코드와 같다.
class A
{
public:
int x;
protected:
int y;
private:
int z;
};
class B : public A
{
// x is public
// y is protected
// z is not accessible from B
};
class C : protected A
{
// x is protected
// y is protected
// z is not accessible from C
};
class D : private A // 'private' is default for classes
{
// x is private
// y is private
// z is not accessible from D
};
위 코드는 Stack overflow의 "Difference between private, public and protected inheritance" 질문에서 발췌한 코드이다.
상속을 받기 위해서 어떻게 상속을 받을 것이냐를 정해주는 것이 접근 제한자이다. 각 접근 제한자에 대해서 상속받은 클래스 B, C, D에 대해서 A클래스의 멤버들의 접근 권한을 코드의 주석으로 나타냈다.
Ref.
'Modern C++' 카테고리의 다른 글
[C++] Virtual Table (가상 테이블)과 Dynamic Polymorphism (동적 다형성) (0) | 2021.08.20 |
---|---|
[C++] Virtual Function (가상 함수) (0) | 2021.08.20 |
[C++] Class Keywords (클래스 키워드) : const, explicit (0) | 2021.08.19 |
[C C++] Overloading (오버 로딩) (0) | 2021.08.19 |
[C++] Copy / Move Constructor & Assignment (복사, 이동 생성자와 대입 연산자) (0) | 2021.08.19 |
[C++] Copy / Move Constructor & Assignment (복사, 이동 생성자와 대입 연산자)
많은 내용을 다루다 보니 제목이 다소 이상하게 지어졌다. 본 포스트에서 다룰 내용은 아래와 같다.
- Constructor (생성자)
- Destructor (소멸자)
- Copy / Move Constructor (복사 / 이동 생성자)
- Copy / Move Assignment (복사 / 이동 대입 연산자)
통상적으로 클래스를 만들 때 생성자를 만들어 주는 경우가 많다 나머지 4개의 메소드는 컴파일러가 자동으로 만들어 주기 때문에 생략하고 클래스를 구성해도 무방하다. 하지만, 리소스를 포인터로 관리하는 경우(Raw pointer)에는 컴파일러가 자동으로 만들어주는 메소드로는 사용이 안될 경우에 위 5개의 메소드를 다 직접 정의해주어야 한다.
컴파일러가 만들어주는 메소드를 사용하면 안 되는 이유는 간단하다. 기본 Destructor (소멸자)를 사용하게 될 경우에는 포인터가 가리키고 있는 동적 할당된 메모리가 해제가 안되기 때문이다. 또한 Copy Construtor와 Copy Assignment가 동적 할당된 메모리 자체를 복사하는 것 (Deep copy)이 아닌 "Shallow copy"만 일어나기 때문이다.
클래스를 구성할 때 아래 세 가지 룰을 따른다.
Rule of Zero
클래스의 Instance로 Raw pointer가 존재하지 않게 클래스를 구성하는 방법이다. Raw pointer가 존재하지 않다면 따로 수동적으로 메모리 할당을 해제해줄 필요가 없기 때문에 5개의 멤버 함수를 다 정의해줄 필요가 없다.
class rule_of_zero
{
std::string cppstring;
public:
rule_of_zero(const std::string& arg) : cppstring(arg) {}
};
Rule of Three
클래스의 Instance로 Raw pointer가 존재할 경우 위에서 말한 "Shallow copy"문제와 메로리 할당을 컴파일러가 자동으로 정의해주는 소멸자에서 해제가 안되기 때문에 Destructor, Copy Constructor 그리고 Copy Assignment를 정의해주어야 한다.
#include <cstddef>
#include <cstring>
class rule_of_three
{
char* cstring; // raw pointer used as a handle to a dynamically-allocated memory block
rule_of_three(const char* s, std::size_t n) // to avoid counting twice
: cstring(new char[n]) // allocate
{
std::memcpy(cstring, s, n); // populate
}
public:
rule_of_three(const char* s = "")
: rule_of_three(s, std::strlen(s) + 1)
{}
~rule_of_three() // I. destructor
{
delete[] cstring; // deallocate
}
rule_of_three(const rule_of_three& other) // II. copy constructor
: rule_of_three(other.cstring)
{}
rule_of_three& operator=(const rule_of_three& other) // III. copy assignment
{
if (this == &other) return *this;
std::size_t n{std::strlen(other.cstring) + 1};
char* new_cstring = new char[n]; // allocate
std::memcpy(new_cstring, other.cstring, n); // populate
delete[] cstring; // deallocate
cstring = new_cstring;
return *this;
}
public:
operator const char *() const { return cstring; } // accessor
};
Rule of Five
Rule of Three에서 모든 Copy가 Deep Copy가 될 필요가 없는 경우가 생길 필요가 없기 때문에 데이터의 소유권을 가져올 수 있는 Move Constructor와 Move Assignment까지 정의를 해야 한다.
class rule_of_five
{
char* cstring; // raw pointer used as a handle to a dynamically-allocated memory block
public:
rule_of_five(const char* s = "")
: cstring(nullptr)
{
if (s) {
std::size_t n = std::strlen(s) + 1;
cstring = new char[n]; // allocate
std::memcpy(cstring, s, n); // populate
}
}
~rule_of_five()
{
delete[] cstring; // deallocate
}
rule_of_five(const rule_of_five& other) // copy constructor
: rule_of_five(other.cstring)
{}
rule_of_five(rule_of_five&& other) noexcept // move constructor
: cstring(std::exchange(other.cstring, nullptr))
{}
rule_of_five& operator=(const rule_of_five& other) // copy assignment
{
return *this = rule_of_five(other);
}
rule_of_five& operator=(rule_of_five&& other) noexcept // move assignment
{
std::swap(cstring, other.cstring);
return *this;
}
// alternatively, replace both assignment operators with
// rule_of_five& operator=(rule_of_five other) noexcept
// {
// std::swap(cstring, other.cstring);
// return *this;
// }
};
위 코드들은 cpprefernce에서 가져온 코드이다. 더 자세한 내용은 아래 링크를 참조하길 바란다.
https://en.cppreference.com/w/cpp/language/rule_of_three
The rule of three/five/zero - cppreference.com
[edit] Rule of three If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three. Because C++ copies and copy-assigns objects of user-defined types in va
en.cppreference.com
아래는 필자가 Raw pointer 없이 Rule of Five를 따르는 클래스이다.
/*
한개의 메소드만 가지고 있지만 컴파일러가 알아서 메소드들을 만들어 준다 CGF - Compiler Generaterd Functions
1. constructor
2. destructor
3. copy / move constructor
4. copy / move assignment
일반적으로 생성자는 우리가 만들어 주는 경우가 많지만
아래 5개의 메소드는 일반적으로 우리가 만들지 않는다.
*/
#include <iostream>
#include <string>
class Dog
{
public:
Dog() = default; //default constructor 다른 constructor를 만들고 기본 생성자를 통해 생성할 경우 오류가 남. 컴파일러가 만든것을 사용한다.
Dog(std::string name, int age) : mName{ name }, mAge{ age } {}; //constructor
~Dog() {}; //destructor
Dog(const Dog& other) : mName{ other.mName }, mAge{ other.mAge } //copy constructor
{
//std::memcpy(); 포인터가 존재할 경우 메모리 복사
}
Dog(Dog&& other) noexcept : mName{ std::move(other.mName) }, mAge{ other.mAge } //move constructor
{
//mptr = other.mPtr other이 가르키고있는 주수를 복사하고 ohter의 포인터를 nullptr로 초기화 시켜준다 (소유권 이전)
//other.mPtr = nullptr;
}
Dog& operator=(const Dog& other) //copy assignment
{
if (&other == this) //포인터를 이용한 리소스일 경우를 대비해서 만약 같은 객체를 copy하려가 한다면 자신의 객체를 바로 반환한다.
{
return *this;
}
mName = other.mName; //대입 연산자로 other의 이름과 나이를 copy함
mAge = other.mAge;
return *this;
}
Dog& operator=(Dog&& other) noexcept //move assignment
{
if (&other == this)
{
return *this;
}
mName = std::move(other.mName); //대입 연산자로 other의 이름의 소유권을 가져옴
mAge = other.mAge; //대입 연산자로 ohter의 나이를 단순히 복사
return *this;
}
void print()
{
std::cout << mName << " " << mAge << std::endl;
}
private:
std::string mName;
int mAge;
};
int main()
{
Dog choco{ "choco", 9 };
Dog mong{ "mong", 8 };
choco = choco;
choco = std::move(choco); //포인터를 이용한 리소스일 경우 문제가 생길 수 있다.
mong = std::move(choco);
mong.print();
choco.print();
}
Ref.
https://en.cppreference.com/w/cpp/language/rule_of_three
https://www.youtube.com/watch?v=aSrFrUOeQP0&list=PLDV-cCQnUlIYTBn70Bd822n2nlJwafCQE&index=7
'Modern C++' 카테고리의 다른 글
[C++] Class Keywords (클래스 키워드) : const, explicit (0) | 2021.08.19 |
---|---|
[C C++] Overloading (오버 로딩) (0) | 2021.08.19 |
[C++] Member Initializer List (멤버 초기화 리스트) (0) | 2021.08.18 |
[C C++] Static Member Function & Static Member Variable (정적 멤버 함수 및 변수) (0) | 2021.08.16 |
[C++] Memory Alignment (메모리 얼라인먼트) & Object In Memory (객체생성) (1) | 2021.08.16 |
[C++] Member Initializer List (멤버 초기화 리스트)
Member Initializer List (멤버 초기화 리스트)를 알아보기 전 먼저 Class의 Constructor (생성자), Destructor (소멸자)에 대해서 얘기해보자.
Class Dog
{
public:
Dog()
{
//std::cout << "Constructor" << std::endl;
}
~Dog()
{
//std::cout << "Destructor" << std::endl;
}
private:
};
Dog라는 Class를 생성했다. 위와 같이 클래스의 이름과 멤버 함수의 이름이 동일한 것을 볼 수 있다. Constructor는 클래스의 이름을 함수 이름으로 정의해주면 되고 Destructor의 경우 '~'를 붙여 정의해주면 된다.
Member Initializer List
먼저 멤버 초기화 리스트를 이용하지 않고 클래스를 구성해보면 아래와 같다.
Class Dog
{
public:
Dog(int age)
{
mAge = age;
}
~Dog()
{
//std::cout << "Destructor" << std::endl;
}
private:
int mAge;
};
필자도 원래 항상 이런 식으로 코드를 짜 왔다. 멤버 초기화 리스트는 기존 함수명 괄호 뒤에 세미콜론 :으로 표기한다. 그다음 초기화하고자 하는 변수들을 쉼표로 구분하고 괄호를 이용하여 초기화를 진행한다.
Class Dog
{
public:
Dog(int age) : mAge(age) {};
~Dog()
{
//std::cout << "Destructor" << std::endl;
}
private:
int mAge;
};
멤버 초기화 리스트를 하면 코드가 간략해지는 것 의외의 차이점을 못 찾을 수 있지만 아래의 예시를 보면 왜 멤버 초기화 리스트를 사용하는지 알 수 있다.
#include <iostream>
class Dog
{
public:
Dog() : mAge{ 1 } {};
Dog(int age) : mAge{ age } {};
private:
int mAge;
};
class Zoo
{
public:
//Zoo(int dogAge) : mong(Dog(dogAge)) {};
Zoo(int dogAge)
{
mong = Dog(dogAge);
}
private:
Dog mong;
};
int main()
{
Zoo cppZoo(5);
return 0;
}
Zoo라는 클래스를 추가로 생성하여 Zoo클래스에 Dog객체가 생성되도록 만들었다. Zoo의 생성자를 보게 되면 주석 처리된 코드는 Member Initializer List를 이용하여 객체의 생성자를 호출하고 주석이 아닌 Zoo생성자는 일반적으로 대입 연산자를 통해 Zoo클래스의 mong객체를 초기화한다.
위 코드를 어셈블리어로 확인을 하게 되면 둘의 차이를 명백하게 확인할 수 있다.
*참고로 필자는 아직 어셈블리어를 이해하지 못하고 어셈블리 코드를 확인하기 위해 Compiler explorer를 이용했다.
대입 연산자를 통해 mong객체를 초기화하는 코드를 어셈블리어 변환한 모습이다. 우측 어셈블리 코드를 확인해 보면 Zoo::Zoo(int)에 생성자 코드를 보게 되면 먼저 Dog::Dog()인 기본 Dog생성자가 1번 호출이 되고 Dog::Dog(int) 생성자가 한번 더 호출이 되게 된다. 이 뜻은 먼저 임시 객체가 생성이 되고 그 객체에 dogAge의 값으로 초기화가 되 후에 mong객체에 대입 (mov키워드) 이 되는 방식이다. 하지만 대입 초기화 연산자를 이용할 경우는 아래와 같다.
똑같이 우측 어셈블리 코드를 보면 Zoo::Zoo(int)에서 Dog::Dog(int) 생성자가 한번 호출이 되고 그다음 mov키워드가 없는 것을 확인할 수 있다. 이러한 이유 때문에 메모리 관점에서 효율적인 코드를 짜기 위해서 Member initializer list를 사용하는 것이다.
'Modern C++' 카테고리의 다른 글
[C C++] Overloading (오버 로딩) (0) | 2021.08.19 |
---|---|
[C++] Copy / Move Constructor & Assignment (복사, 이동 생성자와 대입 연산자) (0) | 2021.08.19 |
[C C++] Static Member Function & Static Member Variable (정적 멤버 함수 및 변수) (0) | 2021.08.16 |
[C++] Memory Alignment (메모리 얼라인먼트) & Object In Memory (객체생성) (1) | 2021.08.16 |
[C++] OOP Intro. 객체지향프로그래밍 (0) | 2021.08.16 |