프로그래밍 공부

전체 글 700

카테고리 설명
프로그래밍 공부하는 블로그
  • C++에서 class가 생성될 때 기본적으로 만들어지는 것 또한 operator하여, 바꾸어줄 수 있다. class간의 복사, 이동, 대입, 소멸 을 조정하여 class의 값변경을 더 자유롭고 편하게 지정해주는 방법이다. #include using namespace std; class Test { int num1; int num2; public: // 클래스가 만들어졌을 때 기본으로 들어가는 것 // 기본 // 복사 // 이동 // 복사 대입 // 이동 대입 // 소멸 // 기본생성자. 로 취급되어 기본생성자는 사라진다. /*Test(int n1, int n2) : num1(n1), num2(n2) {}*/ Test(int n1, int n2) : num1(n1), num2(n2) {} }; class ..

  • 빌더패턴은 객체를 생성하는 과정을 캡슐화 하고, 객체 생성 과정의 다양한 단계에서 서로 다른 구현을 지원하는 디자인 패턴이다. 빌더패턴은 복잡한 객체를 생성하는 과정을 단순화 하고 유연성을 높여 객체를 생성하는 과정을 다양한 방법으로 구성할 수 있도록 한다. 이러한 빌더 패턴을 사용하는 이유는, 객체 생성 과정이 복잡해진다면 코드가 난잡해지기 때문에, 이러한 객체 생성과정을 더 간소화 하기 위해서 만든다. 예를들어, 객체 생성과정이 복잡할 때, 생성자를 사용하면 생성자의 매개변수가 많아지게 되어 필요없는 함수의 호출이 많아질 수 있고, 이를 관리하기도 어렵다. 또한, 생성자 오버로딩을 사용하여 매개변수 수를 줄이려고 하면 다양한 객체 생성방식을 적용하기 어려워진다. 이러한 문제점을 해결하기 위해 빌더 패..

  • C++에서는 smart_pointer이라는 기법을 활용하여 더 쉽고 간단하게 동적배열 선언 등 다양한 방법으로 사용이 가능하다. smart_pointer를 사용하면 new를 생성해주어도, 어떠한 함수 내부에서 종료되었을 때, 종료자가 스스로를 delete해주기 때문에, 따로 delete해줄 필요가 없어진다. 그럼으로 더 쉽고 간단하게 동적데이터 선언이 가능해진다. 다음은 코드 예시이다. #include class ptr { int x; int y; public: ptr(const int x, const int y) : x(x), y(y) {} auto Get_x() -> const int { return x; } auto Get_y() -> const int { return y; } friend ptr&..

  • 추상 팩토리 패턴( Abstract Factory Pattern ) 은 객체 생성을 추상화하는 디자인 패턴 중 하나로, 서로 관련된 객체의 집합을 생성하기 위한 인터페이스를 제공한다. 이 패턴은 추상화된 팩토리 클래스를 통해 관련된 객체를 생성하며, 각 객체는 추상 클래스나 인터페이스를 상속받아 구현된다. 이를 통해 클라이언트 코드는 실제 객체의 클래스에 대한 정보를 알 필요 없이 추상화된 인터페이스를 통해 객체를 생성할 수 있는 특징이 있다. 장점 유지보수성과 확장성이 높아진다. 클라이언트 코드와 객체 생성코드를 분리하기 때문에 유지보수가 쉬워지고 확장할때 편하다. 코드 의존성을 낮춘다. 코드 자체를 실제로 추가할 일이 크지 않을 수 있기 때문에, 어떠한 항목에 대하여 자세한 코드를 작성하지 않아도 되..

  • C++에서 operator을 활용하여 비교연산자를 오버로딩하고, 이를 활용해서 Class 안에 있는 모든 변수를 비교연산 해줄 수 있다. 보통, 비교연산을 위한 변수가 한개이거나, 모든 변수를 전부 비교해야 할 경우 사용하고, 여러개의 변수를 가지는 Class가 존재하고, 그 변수들 중 한개의 변수만 비교연산이 필요하다면, 그럴때 에는 operator의 사용을 권장하지 않는다. 비교연산자 같은 연산자 오버로딩 방식은, 어떠한 변수나 Class의 전체를 비교하거나 연산하는 방식으로 동작해야 하기 때문에 설계방식에서 벗어나기 때문이다. 다음은 비교연산자를 오버로딩하여 만든 코드 예시이다. 위 코드에서, 연산자 오버로딩을 활용하여 비교연산을 해줄 수 있는것을 볼 수 있다. 값의 변환을 유도하지 않기 때문에, ..

  • 일반적인 프로그래밍은 형식지정자를 사용하여 int나 flaot같은 형식을 지정해준다는 기준 하에 프로그래밍 하게된다. 그러나, 이러한 방식은 코드의 재사용성을 낮추고 객체지향적인 프로그래밍을 만드는데 방해물이 될 수 있다. 그래서 제네릭 프로그래밍 이라는 프로그래밍 패러다임이 등장하였다. 이는 알고리즘과 데이터구조를 타입에 독립적인 방식으로 작성하여, 재사용성을 높이고 코드의 중복을 줄이는 효과가 있다. 예를들어, C++의 STRL에서 vector나 list같은 배열컨테이너는 제네릭 프로그래밍의 대표적인 예시이다. 이러한 컨테이너는 타입에 관계없이 ( vector이나 list같은 타입을 형식에 관계하지 않고 생성시 선언으로 타입을 정해줄 수 있다 ) 정수나 문자열같은 다양한 타입의 데이터를 저장해줄 수 ..

작성일
2023. 5. 11. 18:33
작성자
WDmil
728x90

C++에서 class가 생성될 때 기본적으로 만들어지는 것 또한 operator하여, 바꾸어줄 수 있다.

 

class간의 복사, 이동, 대입, 소멸 을 조정하여 class의 값변경을 더 자유롭고 편하게 지정해주는 방법이다.

#include <iostream>

using namespace std;

class Test
{
	int num1;
	int num2;

public:
	// 클래스가 만들어졌을 때 기본으로 들어가는 것
	// 기본
	// 복사
	// 이동
	// 복사 대입
	// 이동 대입
	// 소멸
	
	// 기본생성자. 로 취급되어 기본생성자는 사라진다.
	/*Test(int n1, int n2) : num1(n1), num2(n2)
	{}*/

	
	Test(int n1, int n2) : num1(n1), num2(n2)
	{}
};

class Fraction
{
	int numerator;
	int denominator;
public:
	Fraction(int num = 0, int den = 0)
		: numerator(num), denominator(den)
	{
		cout << "constructor" << endl;
	}
	
	// 복사 연산자
	// 이건 임의적으로 동작을 막는 것 이다.
	/*explicit*/ Fraction(const Fraction& fraction)
	{
		cout << "copy constructor" << endl;
	}

	// 복사 대입 연산자
	Fraction& operator = (const Fraction& rhs)
	{
		cout << "copy assignment operator" << endl;
		return *this;
	}
};

Fraction CreateFraction()// 이름 있는 객체
{
	Fraction temp(5, 2);
	return temp;
}

int main()
{
	Test t1(1, 2);
	Test t2 = t1; // 이럴경우 복사생성자가 된다.
	// 아무것도 넣지 않았을 때 디폴트 복사생성자가 들어가서 생성되게 된다.

	Fraction frac(3, 5);
	Fraction copy1(frac); // 복사 생성자. 복사 초기화
	Fraction copy2 = frac; // 복사 생성자.

	copy2 = frac; // = operator= 이다. 복사대입 생성자.

	int a = 5; // 복사
	int b = a; // 복사 대입연산자.

	int a(5); // 복사 초기화는 이와 같다.

	CreateFraction();

	return 0;
}

위 방법대로, class 내부에 복사대입, 복사, 등 연산자를 추가하여 원래 추가되는 기본연산자를 바꾸고 더 쉽게 연산을 진행시켜줄 수 있다.


C++에서 RVO와 NRVO라는 개념이 있는데, 이는 함수에서 객체를 반환할 때 복사생성자 호출을 피하여, 성능을 향상시키는 최적화 기법이다. RVO는 객체를 반환하는 경우이고, NRVO는 명시적 이름이 지정된 지역변수를 반환하는 경우에 적용된다.

 

각 기법은 C++11부터 컴파일러가 자동최적화를 수행해준다.

 

각 설명은 다음과 같다.

  • RVO(Return Value Optimization)
    • 함수가 임시객체를 반환하는 경우에 발생한다.
    • 컴파일러는 함수 내에서 생성한 이름이 없는 임시객체를 호출한 코드가 있는 변수에 직접 생성하여 복사생성을 하지 않도록 한다.
    • 그리하여, 변수를 중복생성하지 않게하여 필요없는 데이터낭비를 피하게 된다.
  • NRVO(Named Return Value Optimization)
    • 함수에서 명시적으로 이름이 지정된 지역 변수를 반환하는 경우에 발생한다.
    • 지역변수에 값을 할당한 다음 반환하는 경우, 컴파일러는 이 변수를 호출한 코드가 있는 변수에 직접 생성하게 된다.
    • RVO의 경우와 마찬가지로 복사생성자 호출을 피하는 경우라고 말하면 된다.
    • 디버그 시에는 복사하고 대입하는 식으로 연산하지만, 릴리즈 시에는 원본대입 형식으로 최적화해준다.

 

#include <iostream>

/*
	RVO = Retrun Value Optimization
	생성할 때 이름없는 객체는 자동최적화. 복사가아니라 원본대입
	NRVO = Named Return Value Optimization
	이름있는 이름있는 객체를 RVO처럼 해주는것. 
	최적화 할때 사용하는 방법.

*/

using namespace std;

class Test
{
	int value1;
	int value2;

public:
	static int count;

	Test(int value)
		:value1(value),
		value2(count++) // 생성할 때마다 카운트를 넣어준다.
	{
		cout << "constructor : " << this->value1 << ", " << this->value2 << endl;
	}

	Test(const Test& rhs)
		:value1(rhs.value1), value2(count++)
	{
		cout << "copy constructor : " << this->value1 << ", " << this->value2 << endl;
	}

	~Test()
	{
		cout << "destructor : " << this->value1 << ", " << this->value2 << endl;
	}
};

int Test::count = 1;

Test MakeRVO(int value)
{
	return Test(value);
	// 임시객체의 생성이 이루어지지 않고 그냥 반환만된다.
}

Test MakeNRVO(int value)
{
	Test test(value);
	return test;
	// 임시객체의 생성이 이루어지고 반환된다. 복사생성자가 호출된다.
}

int main()
{
	cout << "---RVO---" << endl;
	Test t1 = MakeRVO(1);

	cout << "---NRVO---" << endl;
	Test t2 = MakeNRVO(1);
	// 컴파일러 에서만 이러한 현상이 나타나고, 릴리즈에서는 나타나지 않는다.
	return 0;
}

위에서 설명한, RVO방식과 NRVO방식에도 operator를 하여, 다양한 방식으로 활용할 수 있다.

 

생성자를 operator하여, 다양한 방식대로 생성하여 데이터를 기입할 수 있는데, 이 방식을 사용하여 자동형변환을 사용할 수 있다.

#include <iostream>

using namespace std;

class Fraction
{
	int numerator;
	int denominator;

public:
	Fraction(int num, int den)
		: numerator(num), denominator(den)
	{
		cout << "constructor" << endl;
	}

	Fraction(const Fraction& other)
		:numerator(other.numerator), denominator(other.denominator)
	{
		cout << "copy constructor" << endl;
	}
	
	// 생성자를 처음에쓰는것 이 아닌 것을 막아버린다.
	explicit Fraction(int a)
		: numerator(a), denominator(1)
	{
		cout << "conversion constructor" << endl;
	}

	friend std::ostream& operator << (std::ostream& out, const Fraction frac)
	{
		cout << frac.numerator << " /" << frac.denominator << endl;
		return out;
	}
};

void PrintFraction(Fraction frac)
{
	cout << frac << endl;
}

int main()
{
	Fraction frac1(10, 20);
	Fraction frac2(frac1);

	Fraction frac3(1);

	Fraction frac4('A'); // int형으로 자동 형변환이 일어나서 int형으로 들어간다.
	Fraction frac5(3.14f); // flaot에서 int로 바뀐다.

	PrintFraction(frac1);
	PrintFraction(frac2);
	PrintFraction(frac3);
	PrintFraction(frac4);
	PrintFraction(frac5);

	//PrintFraction('A');
	//PrintFraction(3.14);
	//PrintFraction(100);

	return 0;
}

위 코드에서 Fraction frac4와 frac5는 원래라면, 변수가 들어가면 안되지만, explicit을 뺀다면, int형을 받아서, 값이 들어가게 된다.


C++에서 복사생성을 할 때 얕은복사 와 깊은복사가 있다.

 

얕은복사는, 값을 가져와서 복사하는 방법으로 복사하게 되는데 여기서 동적생성한 변수를 얕은복사를 하게되면,

값의 복사가 아닌 값의 원본을 가져오는 경우가 생길 수 있다. 그래서 가져온 값을 사용하다가 delete하게 되었을 때 허상포인터 가 생길 수 있어 피치못할 오류에 취약할 수 있다.

 

그래서 동적생성한 변수나 복사가 아닌 원본값을 가져와서 사용해야할 경우, 다음과 같은 방법을 사용해야 한다.

#include <iostream>
#include <cassert>

using namespace std;

/*
	deep copy vs shallow copy
	기본 복사 생성자 = 맴버 와 맴버의 복사를 지원하는 얕은복사이다.
	이렇게 하면 맴버에 동적할당을 하게 되면, 문제가 생긴다.
	그렇기에 내부에서 동적생성을 따로 해주고 복사해야하는 문제가 생긴다.
*/

class Mystring
{
public:
	char* data = nullptr;
	int length = 0;

	Mystring(const char* const src = " ")
	{
		assert(src);

		length = strlen(src) + 1;
		data = new char[length];

		for (int i = 0; i < length; i++)
			data[i] = src[i];
		data[length - 1] = '\0';
	}

	Mystring(const Mystring& other)
	{
		this->length = other.length;
		if (other.data != nullptr)
		{
			this->data = new char[length];
			for (int i = 0; i < this->length; i++)
				this->data[i] = other.data[i];
		}
		else
		{
			data = nullptr;
		}
	}
	~Mystring()
	{
		if (data != nullptr)
		{
			delete[] data;
			data = nullptr;
		}
	}

	Mystring& operator =(const Mystring& other)
	{
		if (this == &other)
		{
			return *this;
		}

		if (this->data != nullptr)
		{
			delete[] this->data;
			this->data = nullptr;

			cout << "copy assignment operator" << endl;
			this->length = other.length;

			if (other.data != nullptr)
			{
				this->data = new char[length];
				for (int i = 0; i < this->length; i++)
					this->data[i] = other.data[i];
			}
			else
			{
				this->data = nullptr;
			}
		}
		return *this;
	}

	Mystring(Mystring&& other)
	{
		cout << "move constructor" << endl;
		this->data = std::move(other.data);
		this->length = std::move(other.length);

		other.data = nullptr;
	}

	Mystring& operator= (Mystring&& other)
	{
		cout << "Move assignment operator" << endl;

		if (this == &other) // prevent self-assignment
			return *this;
		
		if (this->data != nullptr)
		{
			if (other.data != nullptr && this->data != other.data)
			{
				delete[] this->data;
				this->data = nullptr;

				this->data = std::move(other.data);
			}
		}
		else
			this->data = std::move(other.data);

		this->length = std::move(other.length);

		other.data = nullptr;

		return *this;
	}

};

int main()
{
	cout << "#################1#################" << endl;
	Mystring str("Hello");
	cout << (int*)str.data << endl;
	cout << str.data << endl << endl;

	cout << "#################2#################" << endl;
	// copy
	{
		Mystring copy(str);
		copy = str;
		cout << (int*)copy.data << endl;
		cout << copy.data << endl << endl;
	}

	cout << "#################3#################" << endl;
	if (str.data != nullptr)
	{
		cout << (int*)str.data << endl;
		cout << str.data << endl << endl;
	}
	else
	{
		cout << (int*)str.data << endl << endl;
	}

	cout << "#################4#################" << endl;
	{
		Mystring copy(std::move(str));
		copy = std::move(str);

		cout << (int*)copy.data << endl;
		cout << copy.data << endl << endl;
	}

	cout << "#################5#################" << endl;
	if (str.data != nullptr)
	{
		cout << (int*)str.data << endl;
		cout << str.data << endl << endl;
	}
	else
	{
		cout << (int*)str.data << endl << endl;
	}

	return 0;
}

위 방법대로, operator하여, 생성자 를 정의해주고, 대입연산자와 복사연산자를 따로 만들어주어 깊은복사를 실행할 수 있게 한다.

728x90
카테고리
작성일
2023. 5. 10. 23:26
작성자
WDmil
728x90

빌더패턴은 객체를 생성하는 과정을 캡슐화 하고, 객체 생성 과정의 다양한 단계에서 서로 다른 구현을 지원하는 디자인 패턴이다.

 

빌더패턴은 복잡한 객체를 생성하는 과정을 단순화 하고 유연성을 높여 객체를 생성하는 과정을 다양한 방법으로 구성할 수 있도록 한다.

 

이러한 빌더 패턴을 사용하는 이유는, 객체 생성 과정이 복잡해진다면 코드가 난잡해지기 때문에, 이러한 객체 생성과정을 더 간소화 하기 위해서 만든다.

 

예를들어, 객체 생성과정이 복잡할 때, 생성자를 사용하면 생성자의 매개변수가 많아지게 되어 필요없는 함수의 호출이 많아질 수 있고, 이를 관리하기도 어렵다.

 

또한, 생성자 오버로딩을 사용하여 매개변수 수를 줄이려고 하면 다양한 객체 생성방식을 적용하기 어려워진다.

 

이러한 문제점을 해결하기 위해 빌더 패턴을 사용한다.

 

빌더패턴의 장점은 다음과 같다.

  • 복잡한 객체를 생성하는 과정을 단순화 한다.
  • 객체 생성 과정의 다양한 구현을 지원한다.
  • 객체 생성 과정에서 필요한 매개변수를 강제한다.
  • 객체 생성 과정에서 생길 수 있는 오류를 줄일 수 있다.

빌더패턴의 단점은 다음과 같다.

  • 객체 생성을 위한 별도의 빌더 클래스가 필요하다.
  • 객체 생성 과정을 단계별로 분리하기 때문에, 일부 구성 요소의 구현이 불가능할 수 있다.
#include <iostream>
#include <string>

class Person
{
public:
    std::string name;
    int age;
    std::string address;
    std::string phone;

    class Builder; // 전방 선언
};

class Person::Builder // 클래스 외부에 Builder 클래스 선언
{
private:
    Person person;

public:
    Builder() {}

    Builder& set_name(std::string name)
    {
        person.name = name;
        return *this;
    }

    Builder& set_age(int age)
    {
        person.age = age;
        return *this;
    }

    Builder& set_address(std::string address)
    {
        person.address = address;
        return *this;
    }

    Builder& set_phone(std::string phone)
    {
        person.phone = phone;
        return *this;
    }

    Person build()
    {
        return person;
    }
};

int main()
{
    Person::Builder builder;
    Person person = builder
        .set_name("Alice")
        .set_age(30)
        .set_address("123 Main St.")
        .set_phone("555-1234")
        .build();

    std::cout << "Name: " << person.name << std::endl;
}

위 코드는 빌더패턴을 사용하여 내용을 초기화 해주는 것 이다.

 

이는, 내부 클래스를 따로 선언하여, 하나의 클래스 내부의 변수를 수정해주는 방법을 사용한다.

 

즉, 변수는 외부클래스에 선언 한 뒤 내부클래스 에서 그 변수를 초기화해주는 것 이다.

 

이러한 방법으로 더 간단하고 직관적으로 객체를 초기화해줄 수 있고 수정또한 편하다는 장점이 있다.

728x90

'디자인 패턴' 카테고리의 다른 글

Obserever 옵저버 패턴  (0) 2023.11.14
싱글톤 패턴(Singleton)  (0) 2023.10.10
추상 팩토리 패턴( Abstract Factory Pattern )  (0) 2023.05.05
작성일
2023. 5. 10. 22:52
작성자
WDmil
728x90

C++에서는 smart_pointer이라는 기법을 활용하여 더 쉽고 간단하게 동적배열 선언 등 다양한 방법으로 사용이 가능하다.

 

smart_pointer를 사용하면 new를 생성해주어도, 어떠한 함수 내부에서 종료되었을 때, 종료자가 스스로를 delete해주기 때문에, 따로 delete해줄 필요가 없어진다.

 

그럼으로 더 쉽고 간단하게 동적데이터 선언이 가능해진다. 다음은 코드 예시이다.

#include <iostream>

class ptr
{
	int x;
	int y;
public:
	ptr(const int x, const int y) : x(x), y(y) {}
	auto Get_x() -> const int { return x; }
	auto Get_y() -> const int { return y; }
	
	friend ptr& operator* (ptr os) {
		return *os;
	}
};

class Smart_ptr
{
	ptr* ptrxy;
public:
	Smart_ptr(const int x, const int y) { ptrxy = new ptr(x, y); }

	friend std::ostream& operator<< (std::ostream& os, const Smart_ptr& sp) {
		os << '[' << sp.ptrxy->Get_x() << ", " << sp.ptrxy->Get_y() << ']';
		return os;
	}

	~Smart_ptr() { delete ptrxy; }
};

int main()
{
	Smart_ptr ptr1(1, 2);
	Smart_ptr ptr2(3, 4);
	Smart_ptr ptr3(5, 6);

	std::cout << ptr1 << ptr2 << ptr3 << std::endl;

	return 0;
}

위 코드처럼, Smart_ptr을 선언하여, 어떠한 포인터 데이터를 동적으로 쉽게 선언해주고, 만약 main문이 아니라 다른 함수에서 종료되었을 지라도 return되면 자동으로 종료자가 선언되기 때문에 delete를 따로 신경써주지 않아도 된다.

728x90
카테고리
작성일
2023. 5. 5. 12:30
작성자
WDmil
728x90

추상 팩토리 패턴( Abstract Factory Pattern ) 은 객체 생성을 추상화하는 디자인 패턴 중 하나로, 서로 관련된 객체의 집합을 생성하기 위한 인터페이스를 제공한다.

 

이 패턴은 추상화된 팩토리 클래스를 통해 관련된 객체를 생성하며, 각 객체는 추상 클래스나 인터페이스를 상속받아 구현된다. 이를 통해 클라이언트 코드는 실제 객체의 클래스에 대한 정보를 알 필요 없이 추상화된 인터페이스를 통해 객체를 생성할 수 있는 특징이 있다.

 

장점

  1. 유지보수성과 확장성이 높아진다.
    • 클라이언트 코드와 객체 생성코드를 분리하기 때문에 유지보수가 쉬워지고 확장할때 편하다.
  2. 코드 의존성을 낮춘다.
    • 코드 자체를 실제로 추가할 일이 크지 않을 수 있기 때문에, 어떠한 항목에 대하여 자세한 코드를 작성하지 않아도 되어 코드 의존성이 크게 낮아지고, 유연한 코드를 작성할 수 있게된다.
  3. 쉽게 객체생성 코드를 변경할 수 있다.
    • 위 항목에 근거하여, 새로운 구체 클래스를 추가하거나 기존 클래스를 변경하지 않고 객체 생성코드 를 변경할 수 있게된다.

단점

  1. 코드가 복잡해질 수 있다.
    • 다양한 경우의 수를 전부 구현해야 하기 때문에 코드가 의미없이 복잡해질 수 있다.
  2. 인터페이스와 구현클래스의 구현 난이도가 매우높다.
    • 위의 경우에 의거하여 모든 경우의 수를 전부 구현해야 하기 때문에 구현난이도가 매우 높다.

 

#include <iostream>
#include <memory>

// 추상화 클래스 A
class AbstractProductA {
public:
    virtual ~AbstractProductA() {} // 소멸자
    virtual void use() const = 0; // 사용시 0 으로
};

// 구체화 클래스 A1
class ConcreteProductA1 : public AbstractProductA { // class ConcreateProductA1은 AbstractProductA를 상속받는다. 자식클래스
public:
    void use() const override {
        std::cout << "Using ConcreteProductA1" << std::endl;
    }
};

// 구체화 클래스 A2
class ConcreteProductA2 : public AbstractProductA { // class ConcreateProductA1은 AbstractProductA를 상속받는다. 자식클래스
public:
    void use() const override { // 상속받은 use 를 대채한다.
        std::cout << "Using ConcreteProductA2" << std::endl;
    }
};

// 추상 클래스 B
class AbstractProductB {
public:
    virtual ~AbstractProductB() {}
    virtual void consume() const = 0;
};

// 구체화 클래스 B1
class ConcreteProductB1 : public AbstractProductB {
public:
    void consume() const override {
        std::cout << "Consuming ConcreteProductB1" << std::endl;
    }
};

// 구체화 클래스 B2
class ConcreteProductB2 : public AbstractProductB {
public:
    void consume() const override {
        std::cout << "Consuming ConcreteProductB2" << std::endl;
    }
};

// 추상 팩토리
class AbstractFactory {
public:
    virtual ~AbstractFactory() {}
    virtual std::unique_ptr<AbstractProductA> createProductA() const = 0;
    virtual std::unique_ptr<AbstractProductB> createProductB() const = 0;
};

// 구체화 팩토리 1
class ConcreteFactory1 : public AbstractFactory {
public:
    std::unique_ptr<AbstractProductA> createProductA() const override {
        return std::make_unique<ConcreteProductA1>();
    }

    std::unique_ptr<AbstractProductB> createProductB() const override {
        return std::make_unique<ConcreteProductB1>();
    }
};

// 구체화 팩토리 2
class ConcreteFactory2 : public AbstractFactory {
public:
    std::unique_ptr<AbstractProductA> createProductA() const override {
        return std::make_unique<ConcreteProductA2>();
    }

    std::unique_ptr<AbstractProductB> createProductB() const override {
        return std::make_unique<ConcreteProductB2>();
    }
};

// 클라이언트
class Client {
public:
    Client(std::unique_ptr<AbstractFactory> factory)
        : productA(factory->createProductA()), productB(factory->createProductB())
    {}

    void run() const {
        productA->use();
        productB->consume();
    }

private:
    std::unique_ptr<AbstractProductA> productA;
    std::unique_ptr<AbstractProductB> productB;
};

// 사용 예시
int main() {
    std::unique_ptr<AbstractFactory> factory1 = std::make_unique<ConcreteFactory1>();
    std::unique_ptr<AbstractFactory> factory2 = std::make_unique<ConcreteFactory2>();

    Client client1(std::move(factory1));
    Client client2(std::move(factory2));

    client1.run();
    client2.run();

    return 0;
}

위 코드는 추상 팩토리 방법을 기초적으로 구현해 놓은 코드이다.

 

이 코드를 해석해서 동작을 이어보면 다음과 같이 이어지게 된다.

 

main문에서 AbstractFactory 형태의 유니크주소 를 가진 factory1을 생성한다. 그리고 make_unique 를 통해 자식클래스의 ConcreteFactory1을 넣어준다. 이로써

factory1은 AbstractFactory 형식이지만, 실질적으로 자식클래스인 ConcreteFactory1의 형태를 띄게된다.

그 후 Client 형태의 clinet1변수를 선언하고 여기에 factory1의 데이터의 주소 소유권을 연결해주며 생성자에 매개변수로 전달한다.

 

매개변수로 전달받은 ConcreteFactory1은, Client의 생성자에 따라 각각 ConcreteFactory1에 해당하는 값들인
ConcreteProductA1 의 use()와 COncreteProductB1의 consume() 를 가지게된다

 

그 후 client1.run() 을 통해 가지게된 동작부의 주소값에 접속하여 동작을 실행하게 된다.

즉 이러한 추상 팩토리 기법은 인터페이스 -> 생성부 -> 동작부 로 연결되어 코드가 실행되게 된다.

 

Client에서만 생성자를 넣어서 Client에 연결된 데이터의 동작부를 Client에 끌어오게 되는것이다.

728x90

'디자인 패턴' 카테고리의 다른 글

Obserever 옵저버 패턴  (0) 2023.11.14
싱글톤 패턴(Singleton)  (0) 2023.10.10
빌더 패턴(Builder Pattern)  (0) 2023.05.10
작성일
2023. 5. 5. 09:11
작성자
WDmil
728x90

C++에서 operator을 활용하여 비교연산자를 오버로딩하고, 이를 활용해서 Class 안에 있는 모든 변수를 비교연산 해줄 수 있다.

 

보통, 비교연산을 위한 변수가 한개이거나, 모든 변수를 전부 비교해야 할 경우 사용하고, 여러개의 변수를 가지는 Class가 존재하고, 그 변수들 중 한개의 변수만 비교연산이 필요하다면, 그럴때 에는 operator의 사용을 권장하지 않는다.

 

비교연산자 같은 연산자 오버로딩 방식은, 어떠한 변수나 Class의 전체를 비교하거나 연산하는 방식으로 동작해야 하기 때문에 설계방식에서 벗어나기 때문이다.


 

다음은 비교연산자를 오버로딩하여 만든 코드 예시이다.

위 코드에서, 연산자 오버로딩을 활용하여 비교연산을 해줄 수 있는것을 볼 수 있다.

값의 변환을 유도하지 않기 때문에, 혹시나의 상황 또는 코드 자체의 직관성을 높여주기 위해 const 선언을 해주고

bool타입 반환을 해준다.

이 때, friend 선언으로 전역함수 화 시켜 밖에서 함수를 만들고 유도해줄 수 있다.


 

다음은, 메인문에서 구축된 비교연산자를 활용, if문을 동작시키는 코드이다.

위와 같은 코드로 보았을 때 52번째 줄 w1 과 w2의 비교연산은 원래라면 불가능하지만, Won 내부에 operator 선언을 해주었기 때문에 operator방식으로 비교연산자가 성립하여 가능해지는 것 을 볼수있다.


C++에서는 다양한 알고리즘 라이브러리를 지원한다. 이러한 라이브러리는

선언을 통해 사용할 수 있으며, 이중참조 포인터배열의 시작점과 끝, 그리고 sort와 shuffle 연산을 지원한다.

 

바로 위의 코드 이미지에서 sort와 random_shuffle 함수의 사용 예시를 보여주었다.

각각 이중참조 포인터배열의 시작과 끝을 나타내고, ( begin(), end() 를 활용하였다 )  넣어주면, 알아서 랜덤하게 섞어 동작하게 된다.

 

이 중 sort 활용 방법중 람다식 함수 를 사용하여 더 쉽고 간단하게 코드를 캡슐화 할 수 있는데, 다음 코드 예시를 살펴보자.

람다식 함수는 함수의 이름을 지정하지 않고 내부에서만 사용할 간단한 함수를 구축하는 방법이다.

쉽고 간단하게 즉석에서 붙여버릴 수 있음으로 매우 강력한 방법이지만,

 

외부에서 다시 사용이 불가능하다는 단점또한 존재한다.


C++에서 사용할 수 있는 operator중에 첨자 연산자 또한 가능하다. 이는 [] 로 이걸 활용하여 class를 리스트 처럼 만들어서 사용할 수 있다.

일반적인 리스트 연산은 범위값을 벗어났을 때, 쓰래기값이 찍히게된다.

위에서 출력한 arr의 -1과 3은 값을 찍을 수 없지만, 일단 참조가 가능함으로 찍게된다.

 

그러나, Class를 배열처럼 사용하면, 쓰레기값이 찍히면 lsit의 값을 리턴해주는것 이 아니라 어떠한 출력값을 대신해서 나타내줄 수 있음으로, 오류에 대한 연산처리나 컴파일러가 잡아내지 못한 버그를 쉽게 수정할 수 있게 된다.


C++에서 사용할 수 있는 operator중에 () 를 묶는 것으로 함수 처럼 Class를 사용할 수 있다. 이를 Functor이라고 한다. 이렇게 오버로딩을 하는 이유는, 객체를 함수처럼 사용하기 위해서 이다.

 

이렇게 객체를 함수처럼 사용함 으로 써 나타나는 장점은 다음과 같다.

  1. 유연성
    • Class 이기에 함수와 다르게 상태를 유지하면서 호출이 되기 때문에 같은 코드를 다양한 맥락에서 다양한 동작을 수행하는데 사용할 수 있다.
  2. 일반화
  3. 표현력
    • 펑터는 객체로서 표현력을 높이는데 사용된다. 특히, STL과 함께 사용될 경우 함수포인터 보다 코드 가독성이 크게 높아진다.

단점은 다음과 같다.

  1. 오버헤드 빈도가 높아진다.
    • 펑터를 사용하면, 일반적인 함수 포인터 보다 더 많은 오버헤드가 발생한다. 이는 성능이 낮을 수도 있는 임베디드 시스템에서의 성능저하로 이어질 수 있다.
  2. 구현이 복잡하다.
    • 펑터를 작성하는것은 함수포인터보다 훨씬 복잡하다. 이어지는 객체간의 상호작용도 신경써야 하기 때문에, 단순 함수를 호출해야할 경우 펑터를 사용하는것은 설계상 비효율적일 수 있다.

 

쉽게 설명하자면, 원래 함수에는 데이터가 저장이 불가능하나, Class 자체를 하나의 함수처럼 보고 이용하게 되면, 해당 함수가 가지고있는 중간 처리과정의 데이터변수를 호출하여 변경해줄 수 있게된다. 그렇기에 더 효율적인 프로그래밍이 가능해지는것이다.


다음은 코드예시이다.

위와 같이 () 로 묶는것처럼 함수를 호출하여, 해당 Class를 함수처럼 이용이 가능하게 한다.

위와같이, 출력부 함수를 같이 만들어 기입해주었기 때문에, 더 쉽게 class호출이 가능해지는것 이다.

또한, 이러한 방법도 함수 오버로딩의 방법중 하나이기 때문에, 다른 형식지정자를 기입하여, 일반적인 변수가 아닌, class를 참조하고, 이 class 내부의 좌표값 지정 호출방법을 따로 지정하여

 

출력부에 출력부를 넣는 방법으로 출력을 정의해줄 수 있다. ( 쉽게 설명하면. Point라는 class를 지정해주고, 이 class의 생성자가 내부변수를 초기화해준 뒤, Adder 에서 정의된 Point의 함수방식인 45번째 줄이 동작하여, Point 변수 끼리의 덧셈을 호출하고, Point간의 덧셈은 Point 내부에 정의되어 있기 때문에, 타고들어가서 정의된 Point 덧셈방식을 출력하게되는 과정을 도출한다. )

 

순서대로 다시 정리하면

Adder( 덧셈호출 ) -> ( 매개변수 두개 ) [Point A, Point B] -> Adder( Point 형식에 해당되는 오버로딩이 있는지 검사 ) -> Adder( Point 형식에 해당되는 operator를 실행 ) -> Adder(A + B 를 리턴함 ) -> 컴파일러( A + B의 형식을 검사하고 해당되는 operator가 정의되어있는지 확인 ) -> Point( 내부의 덧셈 operator가 선언시에 이미 컴파일러에 전달되어있음 ) -> 해당되는 operator의 연산과정을 도출하고 return을 반환함.

 

위 과정을 진행하여, 65번째 줄의 코드를 다시 정리하면

65번째 줄과 67번째 줄은 같다.


C++에서 형 변환을 하여 operator해줄 수 있다. 다음은 형변환으로. int형태로 데이터를 변환하여 출력하는 방식이다.

위 방법대로 원래는 w는 Won형식이라 Print 에서는 int형으로 받아와서, 출력이 안돼야 정상이다.

그럼에도 Won 내부에 int형식으로 받아올 시, 리턴되는 값을 정의해주었기 때문에 Print가 가능해진다.

 

이러한 방식으로 flaot이나 다른 형 지정자 또한 가능하다.

 

이런 방법을 응용해서,  단 한개의 값만 받아오고 다음값을 배열처럼 참조하여 출력하는 방식도 가능하다.

위와 같은 방식대로, 원래는 한개의 p의 x값의 주소만 가져가게 되나 Class의 같은 형식지정자는 연속된 데이터주소를 가짐으로 배열처럼 값을 도출하여 출력해줄 수 있다.


operator는 new와 delete에도 동작가능하여, 해당동작사항에 오버로딩을 해줄 수 있다.

그러나, 일반적으로 사용하는 방식은 아니고 사용할 일도 거의 없음으로 이러한 방식이 있다는것 만 알고있으면 된다.


C++에는 스마트 포인터 라는 개념이 존재한다. 이는 객체 자신의 자기주소값을 반환해주는 operator를 오버로딩하여, 값의 변환을 쉽게 해줄 수 있는 장점이 있다.

원래라면, num이라는 값은 포인터가 아님으로 연결되는 연산자는 . 이 맞으나, -> 연산자로 자기스스로가 포인터가 아니더라도, 자기스스로의 포인터 재참조가 가능해져서, -> 연산으로 동작을 구현해줄 수 있게되었다.

 

33번줄 34번줄은 각각 37번줄 38번줄과 같다.

728x90
작성일
2023. 5. 5. 08:27
작성자
WDmil
728x90

일반적인 프로그래밍은 형식지정자를 사용하여 int나 flaot같은 형식을 지정해준다는 기준 하에 프로그래밍 하게된다.

 

그러나, 이러한 방식은 코드의 재사용성을 낮추고 객체지향적인 프로그래밍을 만드는데 방해물이 될 수 있다.

그래서 제네릭 프로그래밍 이라는 프로그래밍 패러다임이 등장하였다. 이는 알고리즘과 데이터구조를 타입에 독립적인 방식으로 작성하여, 재사용성을 높이고 코드의 중복을 줄이는 효과가 있다.

 

예를들어, C++의 STRL에서 vector나 list같은 배열컨테이너는 제네릭 프로그래밍의 대표적인 예시이다. 이러한 컨테이너는 타입에 관계없이 ( vector이나 list같은 타입을 형식에 관계하지 않고 생성시 선언으로 타입을 정해줄 수 있다 ) 정수나 문자열같은 다양한 타입의 데이터를 저장해줄 수 있다.

 

또한, sort나 find같은알고리즘 함수들도 제네릭으로 작성되어있어, 서로 다른 타입의 컨테이너 에서도 동작하게 된다.

 

이러한 제네릭 프로그래밍을 함으로써 얻게되는 장점은 다음과 같다.

 

  • 코드의 재사용성이 증가한다.
    • 만든 함수나 Class에 다른 타입의 변수를 넣어도 스스로 지정하여 형식을 바꾸기 때문에 재활용하기 좋다.
  • 코드의 유연성이 증가한다.
    • 만든 함수나 Class등 코드의 내부에 다른 형식의 변수나, 아예 새로 만든 class를 선언하더라도 스스로 타입을 찾아서 기입하기 때문에 별도의 수정이 필요하지 않다.
  • 컴파일 시 오류발견에 유리하다
    • 형식지정자가 정의되어있지 않은 지정자 이면, 컴파일 시 오류가 나타나기 때문에 더 쉬운 오류발견이 가능해진다.

그러나, 이러한 편리성이 증가한 만큼 단점 또한 뚜렷하다.

 

  • 설계에 따라 코드 복잡도가 높아질 수 있다.
    • 제네릭 코드는 타입에 독립적으로 작성되어 있기 때문에 무의미한 코드의 반복이 이루어질 수 있으며 이로인해 꼭 필요하지 않은 분리가 발생하여 코드가 난잡해보일 수 있다.
  • 코드의 가독성이 떨어질 수 있다.
    • 위와 같은 사항에 따라 코드가 복잡해지면 자연스럽게 코드의 가독성이 떨어지게 된다.
  • 컴파일 시간이 더 많이 걸릴수 있다.
    • 분리되어있는 코드란, 컴파일 시 가져와야할 head의 숫자가 증가한다는 의미와도 같아지기 때문에, 이로인해 컴파일시간이 늘어나기도 한다.

이러한 제네릭 프로그래밍 패러다임을 활용한 간단한 vector 구현의 코드예시이다.

위 코드처럼, vector자체는 형식지정자가 따로 정해져있지 않기 때문에, 원하는 형식을 기입하여 배열을 구축할 수 있다.

이러한 vector의 탬플릿은 다음과 같다.

위와 같이, 어떠한 형식이 지정되어있는것 이 아닌, T라는 임의의 형식지정자를 사용하여, 원하는 값을 입력하면 T가 해당되는 형식으로 변환되기 때문에 이러한 가변적인 코드가 구현이 가능하다.

 

 

728x90

'컴퓨터 용어 정리' 카테고리의 다른 글

STL_AssociativeContainer  (0) 2023.05.28
STL_SequnceContainer  (0) 2023.05.28
C++ 람다식 함수 ( Lambda Function )  (0) 2023.05.05
C++ algorithm 라이브러리  (0) 2023.05.05
디자인 패턴(Design Pattern)  (0) 2023.04.28