오늘은 C++의 아이템구매와 인벤토리의 생성에 대한 수업이 진행 되었다.
아이템을 구매하고 구매한 아이템을 인벤토리에 복사하는 과정이 있었다.
같은 클래스의 객체로부터 정보를 복사하여 새로운 객체를 생성하는 복사 생성자에 대하여 조사해본다.
우선 코드를 가지고 예를 들어 보도록 하자.
●일반 생성자 & 복사 생성자 <코드 예제1>
#include<iostream>
using namespace std;
class Sample
{
private:
int num1;
int num2;
public:
Sample(int a, int b); // 일반 생성자 선언
Sample(const Sample& Sp); // 복사 생성자 선언
};
Sample::Sample(int a, int b) // 일반 생성자 구현
{
cout << "일반 생성자" << endl;
num1 = a;
num2 = b;
}
Sample::Sample(const Sample& Sp) // 복사 생성자 구현
{
cout << "복사 생성자" << endl;
num1 = Sp.num1;
num2 = Sp.num2;
}
위와 같이 Sample 클래스를 생성하였다.
복사생성자는 Sample::Sample(const Sample& Sp) 의형태는 복사 생성자의 표준 정의이다.
정의의 형태를 보자면 우선 Sample 이라는 클래스의 생성자는 똑같이 이름이 Sample 이어야하며
const 는 상수선언이다. 그리고 & 는 복사생성자에 꼭 필요하다. 가장뒤의 Sp는 사용자지정이름이다.
다음으로는 클래스선언과 생성자 생성 이후
메인 부분을 더하여 출력 결과를 확인하도록 하자.
●일반 생성자 & 복사 생성자 <코드 예제2>
#include<iostream>
using namespace std;
class Sample
{
private:
int num1;
int num2;
public:
Sample(int a, int b);
Sample(const Sample& Sp);
};
Sample::Sample(int a, int b) // 일반 생성자
{
cout << "일반 생성자" << endl;
num1 = a;
num2 = b;
}
Sample::Sample(const Sample& Sp) // 복사 생성자
{
cout << "복사 생성자" << endl;
num1 = Sp.num1;
num2 = Sp.num2;
}
int main()
{
Sample sp1(3, 3);
Sample sp2(sp1);
Sample sp3 = sp2;
return 0;
}
int main() 부분에 세가지의 방법 (sp1, sp2, sp3)의 방식으로 객체를 생성하였다.
복사 생성자의 매개 변수는 오직 하나이며 자기 클래스에 대한 참조로 선언된다.
또한 클래스에서 오직 한 개만 선언이 가능하다.
●일반 생성자 & 복사 생성자 <코드 예제2의 실행결과>
출력결과 위와 같은 결과를 볼수 있다.
복사 생성자는 생성자의 인수로 다른 객체를 상수(const)로 레퍼런스(주소)로 받기 때문에 생성자 내부에서 해당 객체의 정보를 변경할 수 없고 복사만 가능하다.
이때 const 키워드를 붙이는 이유는 함수 내에서 변경되지 않는 객체라는 것을 명시적으로 나타내 주기 위함이다.
●디폴트(default) 복사 생성자
첫번째와 두번째 라인은 위에서 클래스를 정의할 때 함께 정의한 생성자와 생성자 오버로딩인데
세번째라인은 왜 오류가없이 실행 되었을까? 정답은 C++ 컴파일러의 기본 복사 생성자 기능 때문이다.
주의사항은 디폴트 복사 생성자는 '얕은복사(shallow copy)'를 수행한다. 이는 어떤 멤버 변수의 값고 주소가 모두 같다는 것이다. 만일 일반 생성자에서 동적 할당을 수행하고, 해당 메모리를 포인터로 가리키고 있는 경우 일반적으로 소멸자에서 해당 메모리를 해제하게 되는데, 디폴트 복사 생성자는 얕은 복사만을 수행하므로 복사받은 객체가 삭제되고 동적 할당된 메모리도 해제된다면 복사된 객체가 해동 주소에 접근하면 런타임 오류가 발생한다.
그렇기 때문에 이런 경우에는 복사 생성자를 개발자가 직접 지정해 새롭게 동적 할당을 수행해 주어야 한다.
●생성자의 인자로 레퍼런스( & )를 받는 이유
복사 생성 시 인자로 레퍼런스가 아닌 객체를 그대로 받게 되는 경우 함수 내부에 진입하면서 해당 객체를 복사하여 가져오게 된다. 복사 생성자는 복사 대상의 정보를 변경하지 않는 인자가 const 타입인 함수이다.
따라서 새로운 객체를 생성해 오버헤드가 생기는 것을 방지하기 위하여 레퍼런스로 인자를 받는 것이다.
이와같은이유로 다음과 같은 경우에도 새로운 객체가 필요하므로 복사 생성자가 호출된다.
- (default) 복사 생성자로 대입 연산을 수행
- 매개변수로 들어갈 때
- 반환값으로 주어질 때
반드시 새로운 객체를 생성해야 하는 경우가 아니고, 값을 참조하거나 단순히 값을 변경하는 경우에는 레퍼런스를 사용한다면 *오버헤드가 사라진다.
*오버헤드 란?
프로그램이 실행되는 중에 다른 위치의 코드를 실행시켜야 할 때, 간접적으로 시간,메모리,자원 등이 사용되는 현상.
반드시는 아니고 간접적인 것.
외부 함수 사용시 함수 사용을 위해 스텍메모리를 할당하고 함수에 따라 여러가지 연산 등이 일어난다.
이는 예상치 못한 자원들이 사용되는 것이고 이를 오버헤드로 볼 수 있다.
이를 줄이기위해서는 매크로, 인라인 함수 등을 사용하고 최적화가 필요하다.
●팩토리 패턴
우선 팩토리 패턴을 알아보기전 생성자의 단점에 대하여 알아본다.
생성자에게는 두 가지 단점이 있다고 한다.
- 첫번째. 메소드(함수) 이름이 항상 타입과 같은 이름을 가져 이름에 추가적인 정보를 표시할수 없다는점
- 두번째, 생성자는 반환값이 없기 때문에 객체 생성을 실패했을 때 이를 알려줄 방법이 예외 뿐이라는 것
우선 첫번째 단점에대한 예제 코드를 보도록한다.
다음 예제는 좌표점을 나타내기 위해 Point 클래스를 생성하고 직교 좌표계와 극 좌표계를 모두 지원하려 하는 코드이다.
#include<iostream>
using namespace std;
class Point
{
float x, y;
float distance, radian;
public:
// 직교 좌표계 생성자
Point(float x, float y) :x{ x }, y{ y }
{
}
//극 좌표계 생성자
Point(float distance, float radian) :distance{ distance }, radian{ radian }
{
}
};
Point p(3.5, 4.2) // 에러 : 어떤 생성자를 호출할 것인가?
// 직교 ? 극 ? 두가지다 이름과 매개변수가 같기 때문에 모른다.
위의 예제를 본다면 생성자의 이름과 매개변수의 형이 전부 동일하기때문에 컴파일러가 모호하다며 에러가 된다.
함수의 서명이 같기때문에 추가적인 매개변수를 이용하거나 상속을 이용한다면 코드는 복잡해 질것이다.
이것이 첫번째 단점이다.
이어서 두번째 단점에 대한 예제 코드를 보도록 한다.
#include<iostream>
using namespace std;
class A
{
int* container = new int[10];
public:
virtual ~A()
{
delete[] container;
container = nullptr;
}
};
class B : public A
{
A* obj = new A;
public:
B()
{
throw std::exception("Error!");
}
// 생성자에서 예외를 발생시켰다면
// 객체 생성이 실패했으므로
// 당연히 소멸자가 불려지지 않는다.
~B()
{
delete obj;
obj = nullptr;
}
};
위의 코드는 아무 이상없는 듯 보일수 있으나 B의 생성자에서 예외가 발생하게 되어, B의 멤버 변수들을 정리할 때 메모리 누수가 발생하게 된다. 팩토리 패턴은 생성자의 이러한 당점을 보완한 디자인 패턴이다.
위의 예제 코드에서 nullptr 이란? 쉽게 말해서 null(=0) pointer 이다. 자세한사항은 접은글 참조 사이트를 보자!
자 이제 팩토리 패턴에 대하여 알아보도록 한다.
●팩토리 / 팩토리 패턴 / 팩토리 메소드
생성자의 문제점을 다시한번 보자면,
메소드의 이름이 타입의 이름과 같기때문에 호출이 모호할수 있다는점
반환 값이 없기 때문에 객체 생성이 실패할때 예외를 던지는 외에는 달리 방법이 없다는 것 이다.
이에대한 간단한 해결책은 객체를 만드는 함수를 만드는 것이다.
팩토리 메소드(factory method) 혹은 가상 생성자(virtual constructor)라고 불리는 이 함수는
객체를 대신 생성하여 전달한다는 기능을 한다. 생성자보다 좀 더 명확한 명명이 가능하고, 반환값을 이용해 자연스러운 객체 실패도 가능하다. 위의 Point 예제를 팩토리 메소드를 사용하여 바꾸어 보도록 한다.
#include<iostream>
using namespace std;
/////////////////////////// 변경 전 ///////////////////////////
class Point
{
float x, y;
float distance, radian;
public:
// 직교 좌표계 생성자
Point(float x, float y) :x{ x }, y{ y }
{
}
//극 좌표계 생성자
Point(float distance, float radian) :distance{ distance }, radian{ radian }
{
}
};
/////////////////////////// 변경 후 ///////////////////////////
class Point
{
float x, y;
protected: // 프로텍티드의 정보는 비공개이지만 자식클래스에게만 공개된다
Point(float x, float y) : x{ x }, y{ y } {}
public:
static Point CreateCartesian(float x, float y)
{
return { x,y };
}
static Point CreatePolar(float distance, float radian)
{
return { distance * cos(radian), distance * sin(radian) };
}
};
변경전과 변경후를 본다면 static 으로 Point 의 클래스는 고정시키고 이름을 바꾸어 생성하는 것이 무엇인지 좀더 확실하게 판별이 가능해진다.
위으 코드를 좀 더 분명히 하기 위하여 따로 클래스로 모아두는 방법이 있다.
이를 팩토리(Factory) 라고 한다. 팩토리는 내부팩토리와 외부팩토리로 나뉠 수 있다.
●내부 팩토리
내부팩토리는 Point 클래스안에 또다른 클래스인 Factory를 만들고 그내부에 static Point 함수들을 넣었다.
이처럼 클래스 내부에 팩토리를 만들어놓으면 API 사용성이 좋아진다.
만일 여러 타입을 이용해야 한다면 이때는 팩토리를 외부에 만드는 것이 좋다.
#include<iostream>
using namespace std;
class Point
{
float x, y;
protected: // 프로텍티드의 정보는 비공개이지만 자식클래스에게만 공개된다
Point(float x, float y) : x{ x }, y{ y } {}
public:
class Factory
{
static Point CreateCartesian(float x, float y)
{
return { x,y };
}
static Point CreatePolar(float distance, float radian)
{
return { distance * cos(radian), distance * sin(radian) };
}
};
};
Point p = Point::Factory::CreateCartesian(3.4, 4.5);
●외부 팩토리
클래스 내부에 있는 생성자를 접근해줘야 하기 때문에 friend 선언이 불가피하다.
이러한 외부 팩토리를 매개변수 기반 팩토리 메소드(parametrized factory method)라고도 한다.
class A; class B; class C;
class D
{
friend class Factory;
A a;
B b;
C c;
protected:
D(A a,B b,C c);
};
class Factory
{
public:
static D createD(A a, B b, C c)
{
return { a,b,c };
}
};
●추상 팩토리
추상 팩토리는 어려 타입의 군(family)을 생성할 때 사용한다.
어떤 타입을 만들 것인지 지칭하지 않아도 연관된 혹은 독립된 객체들의 군을 생성할 수 있는 인터페이스를 가지고있다.
복잡한 시스템 에서 유용하다. 다음 예제는 음료를 만드는 예제 이다.
추상 팩토리를 사용할 때 주의사항은 구체 팩토리를 만들어야만 사용이 가능하다는 점이다.
실제로 사용하는 케이스는 많지 않다고 한다.
class Beverage // Beverage = 물 외의 음료
{
public:
virtual void Prepare() = 0;// Prepare = 준비하다
};
// 아래 두 클래스는 Prepare()을 적절히 재정의 했다고 가정
class Coffe : public Beverage {};
class Tea : public Beverage {};
// 추상 팩토리 - 실제로 사용하려면 구체 팩토리가 있어야 함
class BeverageFactory
{
public:
unique_ptr< Beverage> Make() const = 0;
};
// 구체 팩토리
class CoffeFactory : public BeverageFactory {};
class TeaFactory : public BeverageFactory {};
// 추상 팩토리 사용예
class Store
{
unordered_map<string, unique_ptr<BeverageFactory>> beverageFactories;
public:
Store()
{
beverageFactories["Coffe"] = make_unique<CoffeFactory>();
beverageFactories["Tea"] = make_unique<TeaFactory>();
}
unique_ptr<Beverage> GetDrink(const string& kind)
{
return beverageFactories[kind]->Make();
}
};
●함수형 팩토리
팩토리 메소드는 함수형으로 만들수 있다. 호출 객체 (callable object)를 이용하면 된다.
class Store
{
unordered_map<string, unique_ptr<BeverageFactory>> beverageFactories;
public:
Store()
{
beverageFactories["Coffe"] = [] {return make_unique<Coffe>(); }
beverageFactories["Tea"] = [] {return make_unique<Tea>(); }
}
unique_ptr< Beverage> GetDrink(const string& kind)
{
return beverageFactories[kind]->Make();
}
};
●요약 및 장단점 정리
팩토리는 생성자 대신에 객체 생성을 해주는 디자인 패턴이다.
어떤 구체적 클래스의 인스턴스가 생성되는지 캡슐화 해주며 언제, 누가, 어떻게 생성하는지 숨긴다.
팩토리 패턴의 장단점을 확인하자.
장점
- 객체 생성과 관련된 복잡한 과정을 추상화 할 수 있다.
- 객체 생성과 관련된 다양한 최적화와 필요한 제한을 할 수 있다.
- 명명을 통해 가독성이 향상 된다.
- 한 종류의 객체가 아니라 인자에 따라 여러 종류의 객체중 하나를 생성할 수 있다.
- 팩토리의 응집성이 매우 높다.
- 사용하는 제품군의 변경을 용이하게 해준다.
단점
- 팩토리와 대상과의 결합도가 높아진다.
- 새로운 종류의 객체를 생성해야 할 때, 기존의 코드를 수정해야 할 수 있다.
- 추상 팩토리에서 제품군에 새 종류의 제품을 추가하는 것이 어렵다.
'공부' 카테고리의 다른 글
C++ Lvalue & Rvalue (0) | 2022.11.29 |
---|---|
C++ 인벤토리 판매기능 추가 (0) | 2022.11.28 |
C++ 상점.인벤토리(추상화,가상함수 사용하기) (0) | 2022.11.27 |
C++ 가상함수 & 재정의 (0) | 2022.11.24 |
C++ 코드작동 확인하는 참고 사이트 (0) | 2022.11.24 |
댓글