반응형

오늘은 복사생성자에 대해 알아보겠습니닷
이 글은 포인터를 잘 모르시면 이해가 안될 수도 있습니다.
바로 ㄱㄱ싱


해당 코드를 실행하면 에러가 납니다. 왜 그런지 한번 생각해보시죠 ㅇㅅㅇ
 

#include <iostream>

class MyClass
{
public:
	// 기본 생성자
	MyClass() : m_data(nullptr) { std::cout << "기본 생성자 호출 ㅇㅅㅇ" << '\n'; }
	// 파라미터 생성자
	MyClass(const char* _str) {
		int len = strlen(_str);
		m_data = new char[len + 1];
		strcpy_s(m_data, len + 1, _str);
		std::cout << "파라미터 생성자 호출 ㅇㅅㅇ" << '\n';
	}
	// 소멸자
	~MyClass()
	{
		delete[] m_data;
		std::cout << "소멸자 호출 ㅇㅅㅇ" << '\n';
	}
	void Show() { std::cout << m_data << '\n'; }
private:
	char* m_data;
};

int main()
{
	MyClass mc1("Hello");
	MyClass mc2 = mc1;
	MyClass mc3;
	mc3 = mc1;

	mc1.Show();
	mc2.Show();
	mc3.Show();

	return 0;
}

 
해당 코드의 문제점을 찾으셨나요?
문제를 30초만에 바로 찾으셨다면 당신은 코딩고수....
 


뭐가 문제인데?

해당 코드의 문제는 mc1, mc2, mc3 총 3개의 객체들의 멤버변수인 m_data가 같은 메모리를 참조하고 있다는 것입니다. 즉 객체만 다르고 m_data는 같은 포인터를 공유하고 있다는 것임니다.... 

이런 느낌 ㅇㅇ...(ㅋㅋ)

"니가 뭔데? 증거있음?" 이라고 하실까봐 위의 코드에서 m_data의 주소를 출력해 보겠읍니다..

// 출력 결과
파라미터 생성자 호출 ㅇㅅㅇ
기본 생성자 호출 ㅇㅅㅇ
Data : Hello, Address : 000001696D8337F0
Data : Hello, Address : 000001696D8337F0
Data : Hello, Address : 000001696D8337F0
소멸자 호출 ㅇㅅㅇ

말했듯이 같은 주소를 가리키고 있고, 소멸자도 전부 호출하지 못하며, 에러가 나고 꺼진 것을 볼 수 있습니다...
따라서 프로그램이 종료되기 직전 소멸자가 호출할 때, mc1의 소멸자가 호출되면서 m_data를 delete해주지만, mc2, mc3의 m_data도 같은 주소를 가리키기 때문에? mc1이 삭제된 직후 mc2, mc3가 삭제될때 이미 삭제된 메모리 삭제를 시도하기 때문에 터진답니다? ㅇㅅㅇ


어째서 같은 주소를 가리키는 것임?

그건 복사 생성자와 복사 대입 연산자의 결함때문인데요?
그것은 바로 사용자가 복사생성자를 명시적으로 정의하지 않으면 내부의 기본적으로 정의되어 있는 복사생성자를 호출하게 된답니다?

기본적으로 구현되어있는 생성자와 연산자들은 멤버를 각각 복사 대입하도록 구현되어 있기 때문인데요...

 

대체 어떻게 생겼길래?

복사 생성자의 내부 구현은 어떻게 되어있을까용

class MyClass {
    char* m_data;
};

// 기본 복사 생성자의 내부 동작
MyClass::MyClass(const MyClass& _other) 
{
    // 이러면 m_data의 주소 값이 대입되므로 같은 주소를 가리킨다.
    this->m_data = _other.m_data;
}

이런식으로 구현되어 있을거임(?) ㅇㅅㅇ

 

복사 대입 연산자도 이와 비슷한 맥락으로 이렇게 구현되어 있답니다?

class MyClass {
    char* m_data;
};

// 기본 복사 생성자의 내부 동작
MyClass& MyClass::operator=(const MyClass& _other) 
{
    if (this != &other) // 자기 자신을 대입하는지 검사
    {
         // 위와 동일. 이러면 m_data의 주소 값이 대입되므로 같은 주소를 가리킨다.
         this->m_data = _other.m_data;
    }
    return *this;
}

복사 생성자와 복사 대입 연산자는 대체 언제 호출됨?

바로 이때!

둘의 차이는 생성 당시에 (=)연산자를 붙였는가 안붙였는가의 차이인듯 합니다람쥐


그렇다면 이런 현상을 어떻게 방지할 수 있는데?

그 해답은 바로 복사 생성자와 복사 대입 연산자의 재정의입니닷

	// 복사 생성자 재정의
	MyClass(const MyClass& _other)
	{
		CopyData(_other);
	}
	// 복사 대입 연산자 재정의
	MyClass& operator=(const MyClass& _other)
	{
		// 복사 대상이 같으면 복사 ㄴㄴ
		if (this != &_other)
		{
			CopyData(_other);
		}
		return *this;
	}
	// 멤버의 메모리 할당 후에 대상 카피
	void CopyData(const MyClass& _other)
	{
		int len = strlen(_other.m_data);
		m_data = new char[len + 1];
		strcpy_s(m_data, len + 1, _other.m_data);
	}

저같은 경우에는 이런 식으로 재정의해봤습니다 ㅇㅅㅇ
 
그럼 이제 제대로 출력되겠지?
바로 출력을 해보자.

// 출력 결과
파라미터 생성자 호출 ㅇㅅㅇ
복사 생성자 호출 ㅇㅅㅇ
기본 생성자 호출 ㅇㅅㅇ
복사 대입 연산자 호출 ㅇㅅㅇ
Data : Hello, Address : 00000177881C3700
Data : Hello, Address : 00000177881C40B0
Data : Hello, Address : 00000177881C3750
소멸자 호출 ㅇㅅㅇ
소멸자 호출 ㅇㅅㅇ
소멸자 호출 ㅇㅅㅇ

3개의 데이터가 의도대로 다른 주소를 가리키고, 에러 없이 소멸자도 정상적으로 잘 호출되는 모습이쥬?


오늘도 제가 꿀팁을 알려드렸는데요?
좋으면 구독과 좋아요 알림설정까지 꾸욱 ㅇㅅㅇ

반응형
반응형

잠깐 알아가보기 전에 잡설을 해보겠다....

보고싶은 분은 접은글을 봐주시길....★

더보기

불과 몇달 전에 한 프로젝트 중에 이런 난잡한 코드를 썼다.

 

void InGameHumanBox::Update()
{
	if (human)
	{
		if (Utillity.CheckBaseClass<TutorialHuman>(human))
		{
			m_name->SetText(human->gameObject->GetName());
			m_profile->index = (int)human->GetHorrorLV();
			m_gauge->index = ((int)human->GetHorrorScore() / 10) + 25;
		}
		if (Utillity.CheckBaseClass<Woman01>(human))
		{
			m_name->SetText(human->gameObject->GetName());
			m_profile->index = (int)human->GetHorrorLV();
			m_gauge->index = ((int)human->GetHorrorScore() / 10) + 25;
		}
		if (Utillity.CheckBaseClass<Woman01>(human))
		{
			m_name->SetText(human->gameObject->GetName());
			m_profile->index = (int)human->GetHorrorLV();
			m_gauge->index = ((int)human->GetHorrorScore()/10) + 25;
		}
		if (Utillity.CheckBaseClass<Woman02>(human))
		{
			m_name->SetText(human->gameObject->GetName());
			m_profile->index = (int)human->GetHorrorLV() + 8;
			m_gauge->index = ((int)human->GetHorrorScore() / 10) + 25;
		}
		if (Utillity.CheckBaseClass<Woman03>(human))
		{
			m_name->SetText(human->gameObject->GetName());
			m_profile->index = (int)human->GetHorrorLV() + 16;
			m_gauge->index = ((int)human->GetHorrorScore() / 10) + 25;
		}
		if (Utillity.CheckBaseClass<Man01>(human))
		{
			m_name->SetText(human->gameObject->GetName());
			m_profile->index = (int)human->GetHorrorLV() + 4;
			m_gauge->index = ((int)human->GetHorrorScore() / 10) + 25;
		}
		if (Utillity.CheckBaseClass<Man02>(human))
		{
			m_name->SetText(human->gameObject->GetName());
			m_profile->index = (int)human->GetHorrorLV() + 12;
			m_gauge->index = ((int)human->GetHorrorScore() / 10) + 25;
		}
		if (Utillity.CheckBaseClass<Man03>(human))
		{
			m_name->SetText(human->gameObject->GetName());
			m_profile->index = (int)human->GetHorrorLV() + 20;
			m_gauge->index = ((int)human->GetHorrorScore() / 10) + 25;
		}
	}
}

 

CheckBaseClass()가 무슨 메소드냐면....

대상이 P클래스를 상속받았으면 true를 반환

그렇다 그냥 인자로 받은 human친구가 템플릿 인자로 받은 클래스를 상속받았으면 true를 반환해주는 클래스다.

하지만 이런 삽질을 간단하게 만들어주는 편한 메소드를 찾아냈다...!

is_base_of 클래스 | Microsoft Learn

 

is_base_of 클래스

자세한 정보: is_base_of 클래스

learn.microsoft.com

오늘은 이 클래스에 대해 소개해보겠슴덩

template <class Base, class Derived>
struct is_base_of;

말그대로 Derived클래스가 Base클래스를 상속받은건지 알려주는 클래스인데요?

이걸로 편하게 상속받은 클래스를 확인해보시죠 ㅇㅅㅇ


근데 이렇게만 쓰면 섭섭하죠?

특정 객체와 클래스를 인자로 넘겨주면 해당 클래스를 상속받은건지 확인해주는 메소드를 작성해보겠습니다.

#include "pch.h"
#include <type_traits>

struct Human{};
struct Human01 : public Human{};
struct Human02 : public Human{};
struct Human03 : public Human{};

// P클래스를 상속받은 클래스면 true를 반환한다.
template <typename P, typename C>
bool CheckBaseClass(C _dest) {
    return std::is_base_of<P, C>::value;
}

int main()
{
    Human   _human;
    Human01 _human01;
    Human02 _human02;
    Human03 _human03;
    std::cout << std::boolalpha;
    std::cout << "human01 is base of Human : " << CheckBaseClass<Human>(_human01) << '\n';
    std::cout << "human02 is base of Human : " << CheckBaseClass<Human>(_human02) << '\n';
    std::cout << "human03 is base of Human : " << CheckBaseClass<Human>(_human03) << '\n';
    std::cout << "=================================" << '\n';
    std::cout << "human01 is base of Human01 : " << CheckBaseClass<Human01>(_human01) << '\n';
    std::cout << "human02 is base of Human01 : " << CheckBaseClass<Human01>(_human02) << '\n';
    std::cout << "human03 is base of Human01 : " << CheckBaseClass<Human01>(_human03) << '\n';

    return (0);
}

출력결과

human01 is base of Human : true
human02 is base of Human : true
human03 is base of Human : true
=================================
human01 is base of Human01 : true
human02 is base of Human01 : false
human03 is base of Human01 : false

그럼 여기서 궁금한 점 하나...

dynamic_cast와 is_base_of 와 성능차이는 얼마나 날까요? 성능차이가 나지 않으면 굳이 쓸 이유가 없는 코드죠?

그래서 코드로 확인을 해봤습니다...

더보기
#include "pch.h"
#include <iostream>
#include <type_traits>
#include <chrono>

struct Human {};
struct Human01 : public Human {};
struct Human02 : public Human {};
struct Human03 : public Human {};

template <typename P, typename C>
bool CheckBaseClass(C* _dest) {
    return std::is_base_of<P, C>::value;
}

template <typename P, typename C>
bool CheckDynamicCast(C* _dest) {
    P* temp = dynamic_cast<P*>(_dest);
    return temp != nullptr;
}

int main()
{
    Human01* _human01 = new Human01();
    Human02* _human02 = new Human02();
    Human03* _human03 = new Human03();

    const int iterations = 100000000;  // 반복 횟수 설정

    // std::is_base_of 시간 측정
    auto startIsBaseOf = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) {
        CheckBaseClass<Human>(_human01);
    }
    auto endIsBaseOf = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsedIsBaseOf = endIsBaseOf - startIsBaseOf;
    std::cout << "std::is_base_of time: " << elapsedIsBaseOf.count() << " seconds\n";

    // dynamic_cast 시간 측정
    auto startDynamicCast = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) {
        CheckDynamicCast<Human>(_human01);
    }
    auto endDynamicCast = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsedDynamicCast = endDynamicCast - startDynamicCast;
    std::cout << "dynamic_cast time: " << elapsedDynamicCast.count() << " seconds\n";

    delete _human01;
    delete _human02;
    delete _human03;

    return 0;
}

결과 (4번 시행)

1트
2트
3트
4트

2트때 차이가 너무 안나서 엥? 했다가 여러번 해보니까 나름 격차가 좀 있네요 ㅇㅅㅇ


그래서 어따씀?

에 대한 의문이면 게임엔진으로 따져보면 AddComponent같은 곳에 쓰지 않을까요?

 

오늘도 개꿀팁(?)을 알려드렸는데요?

이상으로 글을 마쳐보겠습니다. 감사합니다?

 

반응형
반응형

생성자 상속이라는 문법이 있다.

https://learn.microsoft.com/ko-kr/cpp/cpp/constructors-cpp?view=msvc-170#inheriting_constructors

 

class Base {
public:
	Base()
	{
		std::cout << "Call : Base()" << '\n';
	}
	Base(int a)
	{
		std::cout << "Call : Base(int)" << '\n';
	}
	Base(const std::string& a)
	{
		std::cout << "Call : Base(const std::string&)" << '\n';
	}
};

class Derived : public Base {
public:
	using Base::Base;
};

int main()
{
	Derived d1;
	Derived d2(0);
	Derived d3("0");
	return 0;
}

원래 C++은 클래스를 상속할 경우 부모생성자의 호출을 위해 자식클래스의 생성자를 명시적으로 적을 필요가 있었다.

하지만 생성자 상속이란 문법을 사용해서 생략할 수 있다 (ㄷㄷ)

위 코드에서 Derived클래스는 원래 같았으면 부모생성자의 생성자를 명시적으로 적어줘야 했지만 using Base::Base;를 써줌으로써 기본 생성자, int형을 받는 생성자, const std::string&를 받는 생성자가 자동으로 정의된다.

 

-실행결과

 

잘쓰면 코드가독성에 매우 도움이 될 것 같다? 

반응형
반응형
#include <iostream>
#include <assert.h>

using namespace std;

class String
{
private:
	char* str;
	int lenght;
	int capacity;
public:
	String() : lenght(0), capacity(0) {}
	String(const char* _str)
		: lenght(strlen(_str)),
		capacity(lenght * 2 + 1)
	{
		str = new char[capacity];
		strcpy_s(str, lenght + 1, _str);
	}
	String(const String& _str)
		: lenght(strlen(_str.str)),
		capacity(lenght * 2  + 1)
	{
		str = new char[capacity];
		strcpy_s(str, lenght + 1, _str.str);
	}

	~String()
	{
		delete[] str;
	}
	
	int Size() { return lenght;  }
	int Capacity() { return capacity; }

	bool operator==(const char* _dest)
	{
		return (strcmp(this->str, _dest) == 0);
	}
	bool operator==(const String& _dest)
	{
		return (strcmp(this->str, _dest.str) == 0);
	}

	void operator=(const char* _str)
	{
		int len = strlen(_str);
		if (len + 1 >= capacity)
		{
			delete[] str;
			capacity = len * 2;
			str = new char[capacity];
		}
		strcpy_s(str, len + 1, _str);
		lenght = len;
	}
	void operator=(const String& _str)
	{
		int len = _str.lenght;
		if (len + 1 >= capacity)
		{
			delete[] str;
			capacity = len * 2;
			str = new char[capacity];
		}
		strcpy_s(str, len + 1, _str.str);
		lenght = len;
	}

	String& operator+=(const char* _str)
	{
		int len = strlen(_str);
		if (lenght + len + 1 > capacity)
		{
			capacity = (lenght + len) * 2 + 1;
			char* tempStr = new char[capacity];
			strcpy_s(tempStr, capacity, str);
			delete[] str;  // 기존 메모리 해제
			str = tempStr; // 새로운 메모리로 교체
		}
		strcat_s(str, capacity, _str); // str에 _str 추가
		lenght += len;
		return *this;
	}

	String& operator+=(const String& _str)
	{
		int len = _str.lenght;
		if (lenght + len + 1 > capacity)
		{
			capacity = (lenght + len) * 2 + 1;
			char* tempStr = new char[capacity];
			strcpy_s(tempStr, capacity, str);
			delete[] str;  // 기존 메모리 해제
			str = tempStr; // 새로운 메모리로 교체
		}
		strcat_s(str, capacity, _str.str); // str에 _str 추가
		lenght += len;
		return *this;
	}

	String operator+(const char* _str)
	{
		char* string = new char[lenght + strlen(_str) + 1];
		strcpy_s(string, lenght + 1, str);
		strcat_s(string, lenght + strlen(_str) + 1, _str);
		String temp = string;
		delete[] string;
		return temp;
	}
	String operator+(String& _str)
	{
		char* string = new char[lenght + _str.lenght + 1];
		strcpy_s(string, lenght + 1, str);
		strcat_s(string, lenght + strlen(_str.str) + 1, _str.str);
		String temp = string;
		delete[] string;
		return temp;
	}

	char& operator[](int _index)
	{
		if (_index > lenght) assert(false && "IndexError");
		return str[_index];
	}

	friend ostream& operator <<(ostream& _outStream, const String& _str);
};

ostream& operator <<(ostream& _outStream, const String& _str) {
	cout << _str.str;
	return _outStream;
}

int main()
{
	String s1 = "abc";
	cout << "s1 : " << s1 << '\n';

	String s2 = "def";
	cout << "s2 : " << s2 << '\n';

	String s3 = s1 + s2;
	cout << "s3 : " << s3 << '\n';

	String s4 = "";
	s4 += s3;
	cout << "s4 : " << s4 << '\n';

	String s5;
	s5 = s4 + "qwe";
	cout << "s5 : " << s5 << '\n' << '\n';

	cout << "s1 == s2 : " << (s1 == s2) << '\n';
	cout << "s3 == s4 : " << (s3 == s4) << '\n';
	cout << "s1 == abc : " << (s1 == "abc") << '\n';

	cout << "s1[0] ~ s1[3] : ";
	for (int i = 0; i < s1.Size(); i++)
	{
		cout << s1[i];
		if (i != s1.Size() - 1) cout << ", ";
	}

	return 0;
}

 

과제하는겸 string클래스를 구현해보았다.

메모리 할당 delete하는데 에러가 떠서 좀 고생함

반응형

+ Recent posts