C++

동적할당

레나19 2022. 4. 1. 15:51

동적할당 #1, #2

#include <iostream>
using namespace std;
// 오늘의 주제 : 동적 할당

// 메모리 구조 복습
// - 실행할 코드가 저장되는 영역 -> 코드 영역
// - 전역(global) / 정적(static) 변수 -> 데이터 영역
// - 지역 변수/ 매개 변수 -> 스택 영역
// - 동적 할당 -> 힙 영역

// 지금까지 데이터 영역/ 스택 영역을 이용해서
// 이런 저런 프로그램을 잘 만들어왔다.
// 굳이 새로운 영역이 필요할까?

// 실제 상황
// - MMORPG 동접 1명~5만명, 몬스터 1마리~500만마리
// - 몬스터 생성 이벤트 -> 5분동안 몬스터가 10배 많이 나옴.

// - 스텍 영역
// 함수가 끝나면 같이 정리되는 불안정한 메모리
// 여기에 몬스터를 다량으로 넣는 게 적합하지는 않다.
// 잠시 함수에 매개변수 넘긴다거나, 하는 용도는 OK
// - 메모리 영역
// 프로그램이 실행되는 도중에는 '무조건' 사용되는, 항상 들고 있는 영역.

// 희망사항 )
// -필요할 때만 사용하고, 필요없으면 반납할 수 있는!
// - 그러면서도 (스택과는 다르게) 우리가 생성/소멸 시점을 관리할 수 있는!
// - 그런 아름다운 메모리 없나? -> HEAP 영역
// 동적할당과 연관된 함수/연산자 : malloc-free, new-delete, new[]-delete[]

// malloc
// - 할당한 메모리 크기를 건내준다.
// - 메모리 할당 후 시작 주소를 가리키는 포인터를 반환해준다. [메모리 부족시 NULL 반환해준다.]

// free
// - malloc ( 혹은 기타 calloc, realloc 등의 사촌) 을 통해 할당된 영역을 해제
// - 힙 관리자가 할당/ 미할당 여부를 구분해서 관리. 

class Monster
{
public:
	int _hp;
	int _x;
	int _y;

};

int main()
{
//	Monster monster[500 * 10000]; // 스택 오버 플로우 발생

	// 유저 영역 [메모장], [LOL 게임] [ 곰플레이어 ]  ,, 서로의 할당된 메모리 영역을 침범하지 않은채
	// 시행되고 있다.
	// ----------------------------------------------
	// 커널 영역 (Windows 등의 핵심 코드)
	
	//유저 영역) 운영체제에서 제공하는 API 호출
	//커널 영역) 메모리 할당해서 건내줌
	//유저 영역) ㄳㄳ 잘 쓸께요

	// C++ 에서는 기본적으로 CRT(C 런타임 라이브러리) 의 [힙 관리자]를 통해 힙 영역 사용
	// 단, 정말 원한다면 우리가 직접 API를 통해 힙을 생성하고 관리할 수도 있음(ex. MMORPG 서버 메모리 풀링)
	
	size_t; //마우스 커서 올리고 F12 누르면 비쥬얼스튜디오에서  더 자세하게 설명된 글을 볼 수 있다. 
	//typedef unsigned long long -> size_t 이다.
	// 메모리 할당받고 싶다는 요청을 하는 함수
	
	// 그런데 잠깐! void* ?? 무엇일까?
	// *가 있으니까 포인터는 포인터 (주소를 담는 바구니) => OK
	// 타고 가면 void 아무것도 없다? => NO
	// 타고 가면 void 뭐가 있는지 모르겠으니까 너가 적당히 변환해서 사용해라 => OK
	
	// void* pointer = malloc(1000);  // 1000바이트 힙 영역을 할당을 받았고 그 주소는 pointer
	// 위에 1000바이트는 너무 크게 할당받은 것 같으니까 딱 맞춰서 하고 싶으면 밑의 방식으로 해주면 된다. 
	
	// 메모리 pointer 디버깅해보면 위에 몇바이트 할당하고 있는 지 슬쩍 저장해놓는다. 헤더처럼 12바이트 저장해놓은 것을 확인할 수 있다.
	void* pointer = malloc(sizeof(Monster));
	Monster* m1 = (Monster*)pointer;
	m1->_hp = 100;
	m1->_x = 1;
	m1->_y = 2;
	

	// Heap Overflow
	// - 유효한 힙 범위를 초과해서 사용하는 문제
	// malloc으로 적은 메모리를 할당하고 밑에 사용할 때 더 많이 사용하면
	// Overflow 발생한다. 

	free(pointer); // 메모리 놔주는 상태
	pointer = nullptr;
	m1 = nullptr;
	//만약 free를 안해주면 어떤 일이 일어날까?
	//메모리 누수현상 발생.
	
	// free를 계속 해주면 어떻게 될까?
	//Double Free
	// 이건 대부분 그냥 크래시만 나고 끝난다.

	// 끔찍한 상황
	//Use-After-Free
	// Free 시켰는데 그 포인터를 다시 사용하는 경우, -> 엉뚱한 곳에 가서 데이터를 입력하는 상황 발생. 
	//m1->_hp = 100;
	//m1->_x = 1;
	//m1->_y = 2;
	// - 프로그래머 입장 : OMG 망했다....
	// - 프로그램 해킹하려는 해커 입장 : Oh yeah! 이거 써야지.


	return 0;

}

 

동적할당 #3

#include <iostream>
using namespace std;
// 오늘의 주제 : 동적 할당

// 메모리 구조 복습
// - 실행할 코드가 저장되는 영역 -> 코드 영역
// - 전역(global) / 정적(static) 변수 -> 데이터 영역
// - 지역 변수/ 매개 변수 -> 스택 영역
// - 동적 할당 -> 힙 영역

// 지금까지 데이터 영역/ 스택 영역을 이용해서
// 이런 저런 프로그램을 잘 만들어왔다.
// 굳이 새로운 영역이 필요할까?

// 실제 상황
// - MMORPG 동접 1명~5만명, 몬스터 1마리~500만마리
// - 몬스터 생성 이벤트 -> 5분동안 몬스터가 10배 많이 나옴.

// - 스텍 영역
// 함수가 끝나면 같이 정리되는 불안정한 메모리
// 여기에 몬스터를 다량으로 넣는 게 적합하지는 않다.
// 잠시 함수에 매개변수 넘긴다거나, 하는 용도는 OK
// - 메모리 영역
// 프로그램이 실행되는 도중에는 '무조건' 사용되는, 항상 들고 있는 영역.

// 희망사항 )
// -필요할 때만 사용하고, 필요없으면 반납할 수 있는!
// - 그러면서도 (스택과는 다르게) 우리가 생성/소멸 시점을 관리할 수 있는!
// - 그런 아름다운 메모리 없나? -> HEAP 영역
// 동적할당과 연관된 함수/연산자 : malloc-free, new-delete, new[]-delete[]

// malloc
// - 할당한 메모리 크기를 건내준다.
// - 메모리 할당 후 시작 주소를 가리키는 포인터를 반환해준다. [메모리 부족시 NULL 반환해준다.]

// free
// - malloc ( 혹은 기타 calloc, realloc 등의 사촌) 을 통해 할당된 영역을 해제
// - 힙 관리자가 할당/ 미할당 여부를 구분해서 관리. 

// new / delete
// - C++에 추가됨
// - malloc/free 함수! new/delete는 연산자(operator)

// new[] / delete[]
// - new가 malloc에 비해 좋긴 한데~ 배열과 같이 N개 데이터를 같이 할당하려면?

// malloc / free  vs  new / delete
// - 사용 편의성 -> new/delete 가 더 좋다.
// - 타입에 상관없이 특정한 크기의 메모리 영역을 할당받으려면? -> malloc / free 가 더 좋다. 

// 그런데 둘의 가장 근본적인 중요한 차이는?
// new / delete는 (생성타입이 클래스일 경우) 생성자/ 소멸자를 호출해준다!!
// malloc은 생성자 소멸자가 호출이 되지 않는다(그런 개념이 없다). 단지 할당된 메모리만 사용할뿐



class Monster
{
public:
	Monster()
	{
		cout << "Monster()" << endl;
	}

	~Monster()
	{
		cout << "~Monster()" << endl;
	}
	int _hp;
	int _x;
	int _y;
};

int main()
{
//	Monster monster[500 * 10000]; // 스택 오버 플로우 발생

	// 유저 영역 [메모장], [LOL 게임] [ 곰플레이어 ]  ,, 서로의 할당된 메모리 영역을 침범하지 않은채
	// 시행되고 있다.
	// ----------------------------------------------
	// 커널 영역 (Windows 등의 핵심 코드)
	
	//유저 영역) 운영체제에서 제공하는 API 호출
	//커널 영역) 메모리 할당해서 건내줌
	//유저 영역) ㄳㄳ 잘 쓸께요

	// C++ 에서는 기본적으로 CRT(C 런타임 라이브러리) 의 [힙 관리자]를 통해 힙 영역 사용
	// 단, 정말 원한다면 우리가 직접 API를 통해 힙을 생성하고 관리할 수도 있음(ex. MMORPG 서버 메모리 풀링)
	
	size_t; //마우스 커서 올리고 F12 누르면 비쥬얼스튜디오에서  더 자세하게 설명된 글을 볼 수 있다. 
	//typedef unsigned long long -> size_t 이다.
	// 메모리 할당받고 싶다는 요청을 하는 함수
	
	// 그런데 잠깐! void* ?? 무엇일까?
	// *가 있으니까 포인터는 포인터 (주소를 담는 바구니) => OK
	// 타고 가면 void 아무것도 없다? => NO
	// 타고 가면 void 뭐가 있는지 모르겠으니까 너가 적당히 변환해서 사용해라 => OK
	
	// void* pointer = malloc(1000);  // 1000바이트 힙 영역을 할당을 받았고 그 주소는 pointer
	// 위에 1000바이트는 너무 크게 할당받은 것 같으니까 딱 맞춰서 하고 싶으면 밑의 방식으로 해주면 된다. 
	
	// 메모리 pointer 디버깅해보면 위에 몇바이트 할당하고 있는 지 슬쩍 저장해놓는다. 헤더처럼 12바이트 저장해놓은 것을 확인할 수 있다.
	void* pointer = malloc(sizeof(Monster));
	Monster* m1 = (Monster*)pointer;
	m1->_hp = 100;
	m1->_x = 1;
	m1->_y = 2;
	

	free(pointer); // 메모리 놔주는 상태
	//만약에 free하지 않으면 메모리 누수

	Monster* m2 = new Monster;
	m2->_hp = 200;
	m2->_x = 2;
	m2->_y = 3;
	delete m2;

	Monster* m3 = new Monster[5];
	m3->_hp = 200;
	m3->_x = 2;
	m3->_y = 3;

	Monster* m4 = (m3 + 1);
	m4->_hp = 300;
	m4->_x = 4;
	m4->_y = 5;

	delete[] m3; // delete 꼭 이렇게 [] 표시해줘야함

	return 0;

}

 

생성해놓고 소멸을 잊어버려서 안하는 바람에 오류 발생하는 경우가 아주 많다.

 

 

타입 변환 #1

#include <iostream>
using namespace std;
// 오늘의 주제 : 타입 변환

// 지난 시간 : malloc -> void*을 반환하고, 이를 우리가 (타입 변환)을 통해 사용했었음


class Knight
{
public:
	int _hp = 10;
};

class Dog
{
public:
	int _age = 1;
	int _cuteness = 2;
};

class Cat
{
public:
	// 타입변환 생성자
	Cat(const Knight& knight)    //knight클래스를 parameter로 받는 생성자
	{
		_age = knight._hp;
	}
public:
	int _age = 1;
	int _cuteness = 2;
};

int main()
{
	// ---------------- 타입 변환 유형 (비트열 재구성 여부) ------------
	// [1] 값 타입 변환
	// 특징) 의미를 유지하기 위해서, 원본 객체와 다른 비트열 재구성

	{
		int a = 123456789; // 2의 보수
		float b = (float)a; // 부동 소수점 (지수 + 유효숫자)
		// a 숫자를 유지하기 위해서, float은 부동소수점을 이용하여 a의 비트를 그대로 복사하는 것이 아닌 비트열을 다르게 재구성하였다.
		// 디버깅을 통해 a와 b를 까보고, 계산기로 확인하여보자.
		// float 표현방식으로 비트를 완전히 재구성하여 123456789에 가깝게 구성해주었다.
        cout << b << endl;
	}
	// [2] 참조 타입 변환
	// 특징) 비트열을 재구성하지 않고, '관점'만 바꾸는 것.
	{
		int a = 123456789; //2의 보수
		float b = (float&)a; // 부동소수점 (지수+ 유효숫자)
		cout << b << endl;
		// a의 비트열 그대로 바라보는 타입만 바꾸어서 b에 들어갔다.
		// 디버깅을 통해 a와 b의 값을 확인하여보자. 
	}

	// ----------------- 안전도 분류 -------------

	// [1] 안전한 변환
	// 특징) 의미가 항상 100% 완전히 일치하는 경우
	// 같은 타입이면서 크기만 더 큰 바구니로 이동
	// 작은 바구니 -> 큰 바구니로 이동은 OK (업 casting)
	// ex) char -> short, short-> int, int->__int64
	{
		int a = 123456789;
		__int64 b = a;
		cout << b << endl;
	}


	// [2] 불안전한 변환
	// 특징) 의미가 항상 100% 일치한다고 보장하지 못하는 경우
	// 타입이 다르거나
	// 같은 타입이지만 큰 바구니 --> 작은 바구니 이동 (다운 casting)
	{
		int a = 123456789;
		float b = a; // 타입이 다름.
		short c = a; // 큰 바구니에서 작은 바구니로 이동.
		cout << b << endl;
		cout << c << endl;
	}

	// ------------- 프로그래머 의도에 따라 분류 -------------
	
	// [1] 암시적 변환
	// 특징) 이미 알려진 타입 변환 규칙에 따라서 컴파일러가 '자동'으로 타입 변환
	{
		int a = 123456789;
		float b = a; // 암시적으로 float b = (float) a; 이렇게 암시적으로 변환해줌
		cout << b << endl;
	}

	// [2] 명시적 변환
	{
		int a = 123456789;
		int* b = (int*)a; // 명시적. //int* b = a; 이렇게 하면 컴파일러에서 받아들이기를 거부한다. 컴파일 에러, 
        //정수 a는 주소값이 될 수 없다. 꼭 주소값처럼 쓰고 싶을 때 이렇게 명시적처럼 (int*) 을 넣어준다. 
		// 명시적 변환을 해주는것. 
		cout << b << endl;
	}

	// ----------------- 아무련 연관 관계가 없는 클래스 사이의 변환 ----------------
	
	//[1] 연관없는 클래스 사이의 '값 타입' 변환
	// 특징) 일반적으로 안 됨()
	{
		Knight knight;
	//	Dog dog = (Dog)knight;  // 안됨
		Cat cat = (Cat)knight; // 타입 변환 생성자로 가능. 
		// 일반적으로는 안되지만 타입 변환 생성자를 클래스에 만들어줘서 가능하게 한다.(예외. 타입 변환 생성자, 타입 변환 생성자)
	}
	
	
	return 0;

}

 

타입 변환 #2

#include <iostream>
using namespace std;
// 오늘의 주제 : 타입 변환

// 지난 시간 : malloc -> void*을 반환하고, 이를 우리가 (타입 변환)을 통해 사용했었음


class Knight
{
public:
	int _hp = 10;
};

class Dog
{
public:
	int _age = 1;
	int _cuteness = 2;
};

class Cat
{
public:
	Cat()
	{
		
	}
	
	// 타입변환 생성자
	Cat(const Knight& knight)    //knight클래스를 parameter로 받는 생성자
	{
		_age = knight._hp;
	}
	
	// 타입 변환 연산자
	operator Knight() // return type이 없다.
	{
		Knight knight;
		knight._hp = _age+_cuteness;
		return knight; 
	}


public:
	int _age = 1;
	int _cuteness = 2;
};

class TigerCat : public Cat
{
public:
	bool _siberia; //시베리아 호랑이	
};


int main()
{
	// ---------------- 타입 변환 유형 (비트열 재구성 여부) ------------
	// [1] 값 타입 변환
	// 특징) 의미를 유지하기 위해서, 원본 객체와 다른 비트열 재구성

	{
		int a = 123456789; // 2의 보수
		float b = (float)a; // 부동 소수점 (지수 + 유효숫자)
		// a 숫자를 유지하기 위해서, float은 부동소수점을 이용하여 a의 비트를 그대로 복사하는 것이 아닌 비트열을 다르게 재구성하였다.
		// 디버깅을 통해 a와 b를 까보고, 계산기로 확인하여보자.
		// float 표현방식으로 비트를 완전히 재구성하여 123456789에 가깝게 구성해주었다.
        cout << b << endl;
	}
	// [2] 참조 타입 변환
	// 특징) 비트열을 재구성하지 않고, '관점'만 바꾸는 것.
	{
		int a = 123456789; //2의 보수
		float b = (float&)a; // 부동소수점 (지수+ 유효숫자)
		cout << b << endl;
		// a의 비트열 그대로 바라보는 타입만 바꾸어서 b에 들어갔다.
		// 디버깅을 통해 a와 b의 값을 확인하여보자. 
	}

	// ----------------- 안전도 분류 -------------

	// [1] 안전한 변환
	// 특징) 의미가 항상 100% 완전히 일치하는 경우
	// 같은 타입이면서 크기만 더 큰 바구니로 이동
	// 작은 바구니 -> 큰 바구니로 이동은 OK (업 casting)
	// ex) char -> short, short-> int, int->__int64
	{
		int a = 123456789;
		__int64 b = a;
		cout << b << endl;
	}


	// [2] 불안전한 변환
	// 특징) 의미가 항상 100% 일치한다고 보장하지 못하는 경우
	// 타입이 다르거나
	// 같은 타입이지만 큰 바구니 --> 작은 바구니 이동 (다운 casting)
	{
		int a = 123456789;
		float b = a; // 타입이 다름.
		short c = a; // 큰 바구니에서 작은 바구니로 이동.
		cout << b << endl;
		cout << c << endl;
	}

	// ------------- 프로그래머 의도에 따라 분류 -------------
	
	// [1] 암시적 변환
	// 특징) 이미 알려진 타입 변환 규칙에 따라서 컴파일러가 '자동'으로 타입 변환
	{
		int a = 123456789;
		float b = a; // 암시적으로 float b = (float) a; 이렇게 암시적으로 변환해줌
		cout << b << endl;
	}

	// [2] 명시적 변환
	{
		int a = 123456789;
		int* b = (int*)a; // 명시적. //int* b = a; 이렇게 하면 컴파일러에서 받아들이기를 거부한다. 컴파일 에러, 
        //정수 a는 주소값이 될 수 없다. 꼭 주소값처럼 쓰고 싶을 때 이렇게 명시적처럼 (int*) 을 넣어준다. 
		// 명시적 변환을 해주는것. 
		cout << b << endl;
	}

	// ----------------- 아무련 연관 관계가 없는 클래스 사이의 변환 ----------------
	
	//[1] 연관없는 클래스 사이의 '값 타입' 변환
	// 특징) 일반적으로 안 됨()
	{
		Knight knight;
	//	Dog dog = (Dog)knight;  // 안됨
		Cat cat = (Cat)knight; // 타입 변환 생성자로 가능. 
		// 일반적으로는 안되지만 타입 변환 생성자를 클래스에 만들어줘서 가능하게 한다.(예외. 타입 변환 생성자, 타입 변환 생성자)
	
		Knight night2 = cat; //Knight 클래스에 만든 타입 변환 연산자 때문에 아무런  위화감없이 생성되었다. 
	
	}
	
	// [2] 연관없는 클래스 사이의 참조 타입 변환
	// 특징 ) 명시적으로는 된다. OK
	{
		Knight knight;
//		Cat& cat = knight; // knight는 cat 이 아니다보니까 컴파일러에서 거부를 한다.
		Cat& cat1 = (Cat&)knight;  // 이거는 컴파일러 에러 없이 잘 된다.  왜 그런 것일까?
		//참조라는 게 C++ 관점에서는 또 다른 이름을 짓겠다.라는 의미인데,, 어셈블리어 관점에서는 
		// 어셈블리어 관점에서는 포인터와 참조와 큰 차이가 없다. 
		// 주소를 담는 바구니이다. 
		// void* ,, 포인터같은 경우 실질적으로 어떤 데이터가 있는지 정해지지 않았지만 
		// 나중에 결정해서 사용하는 경우가 생기기 때문에 
		// 이런 경우를 봐도 참조나 포인터의 경우 그 자리에서 당장 데이터의 오류를 따지는 게 아니라
		// 이거를 타고 가면 어떠한 데이터가 있을 것이다 이 정도만 봐주는 것이다. 
		// 근데 이해 잘 안가네,, 나중에 알게 되겠지 머 
		// 객체를 바로 다루는 참조를 하면 에러가 나는데, 주소를 받는 식의 참조변환을 하면 말이 안되도 허용을  어느정도 해준다. 
		
	}
	
	// ---------------------- 상속 관계에 있는 클래스 사이의 변환 --------------
	
	// [1] 상속 관계 클래스의 값 타입 변환
	// 특징 ) 자식-> 부모 OK   /  부모 -> 자식 No
	{
	//	Cat cat;
	//	TigerCat tigercat = cat; // 자손 클래스 포인터는 조상클래스를 받지 못한다.
	//	TigerCat tigercat = (TigerCat)cat;
	TigerCat tigercat;
	Cat cat = tigercat; // Cat과 TigerCat이 가지고 있는 공통적인 것만 똑 갖고 TigerCat의 나머지는 지워진다.
	
	//위의 관계가 없는 클래스끼리는 명시적형변환이나 변환연산자를 만들어야 통과가 되었는데
	//상속관계에서는 한쪽방향으로는 통과가 된다. 
	
	}
	
	//[2] 상속 관계 클래스의 참조 타입 변환
	{
		Cat cat;
		
//		TigerCat& tigercat = cat;  // 컴파일러 에러 난다.
		
		TigerCat& tigercat = (TigerCat&)cat; // 포인터는 컴파일러에서 유연하게 처리해준다. 	}
	
		TigerCat tigercat2;
		Cat& cat = tigercat2; //허용이 잘 됨. 	
	
	}
	
   	//결론
	// [ 값 타입 변환 ] : 진짜 비트열도 바꾸고 - 원래의 값에 최대한 가깝게 변환을 해줌
	// [참조 타입 변환] : 비트열은 냅두고 우리의 '관점'만 바꾸는 변환
	// -땡깡 부리면 (명시적 요구) 해주긴 하는데, 말 안해도 암시적으로 해주는지는 안전성 여부와 연관 있음
	// -- 안전하다? (ex. TigerCat -> Cat&) 암시적으로 OK
	// -- 위험하다? (ex. Cat -> TigerCat&)
	// --- 메모리 침범 위험이 있는 경우는 암시적으로 해주지 않음. (위험함)
	// --- 명시적으로는 정말 하겠다고 최종 서명을 하면 컴파일러에서 통과는 해줌.
    
	return 0;
}

 

타입 변환 #3

#include <iostream>
using namespace std;

//오늘의 주제 : 타입 변환 (포인터)

class Item
{
public:
	Item()
	{
		cout << "Item()" << endl;
	}

	Item(const Item& item)
	{
		cout << "Item(const Item&)" << endl;
	}

	~Item()
	{
		cout << "~Item()" << endl;
	}

public:
	int _itemType = 0;
	int _itemDbId = 0;

	char _dummy[4096] = {}; // 이런 저런 정보들로 인해 비대해진 데이터 더미.
};

void TestItem(Item item)
{

}

void TestItemPtr(Item* item)
{

}

int main()
{
	// 복습
	{
		Item item; //객체생성 -> 메모리영역 : Stack에 올라가있는 상태이다.
		// Stack [type(4) dbId(4) dummy (4096 ] 이 있다.
		

		Item* item2 = new Item(); // 동적 할당을 이용한 객체 생성
		// Stack [ 주소(4~9바이트) ]  -> Heap 주소 [type(4) dbId(4) dummy (4096 ] 영역에 Item 객체가 저장되어있다.

		// 이 scope 를 벗어나면 자동으로 소멸자가 호출이 되는데, 위의 item은 자동으로 소멸자가 호출되지만
		// item2는 소멸자 호출을 안한다. stack의 주소는 사라지더라도 객체는 여전히 heap에 저장되어있디 때문에 
		// 정보가 닫히지 않았기 때문에 소멸자 생성을 하지 않는다. 

		// 그렇기 때문에 new로 생성했으면 delete로 지워야한다. delete를 누락하게 되면 메모리 누수가 일어난다.
		delete item2; // delete item2 이거 주석처리해서 디버깅으로 item1과 item2를 해보자. 
		// 메모리 누수(Memory Leak) -> 일어날 때마다 점점 가용 메모리가 줄어들어서 Crash가 나중에 발생하게 된다.
	
	
	}

	{
		Item item3;
		Item* item4 = new Item();

		TestItem(item3);
		TestItem(*item4);

		// 함수 만들 때 매개변수를 Item item 으로 하는 것과 Item^ item으로 하는 건 완전 다르다.
		// TestItem(item)과 TestItem(*item2)를 비교해보면 복사생성자를 이용해서 생성되고 없어지는 건 같은데
		// 디버깅으로 watch에 sizeof(item), sizeof(item2) 둘 다 해보면 차지하고 있는 데이터양이 완전 다르다는 것을
		// 확인할 수 있다.
		// 다시 돌아와서, 그러면 함수 호출할 때 복사생성자를 이용하여 객체를 복사하여 넘겨주는 방식인데
		// item을 복사하게 되면 엄청 큰 용량의 데이터가 복사되어서 전달이 된다.-> 속도가 느려짐
		// 그러나 item2를 복사하게 되면 4바이트만 복사되서 전달이 되는 거기 때문에 속도 부담이 현저히 줄어든다.


		TestItemPtr(&item3);
		TestItemPtr(item4);
		//하지만 포인터를 매개변수로 사용하는 함수는 생성자 소멸자가 호출이 되지 않고, 
		// 원격으로 heap에 approach하여 정보를 edit할 수 있다는 것을 알 수 있다.
		// 디버깅으로 위에부터 진행해보자. 생성자 소멸자가 호출되지 않는 것을 확인할 수 있을 것이다.


		delete item4;
	}

	// 배열
	{
		cout << "---------------------------------" << endl;

		//진짜 아이템에 100개 있는 것 [스텍 메모리에 올라와있는]
		Item item5[100] = {};
		//생성자 100개 생성
		
		cout << "---------------------------------" << endl;
		// 아이템이 100개 있을까?
		// 아이템을 가리키는 바구니가 100개. 실제 아이템은 1개도 없을 수도 있음.
		Item* item6[100] = {};
		// 생성자 반환이 없다.
		cout << "---------------------------------" << endl;

		for (int i = 0; i < 100; i++)
			item6[i] = new Item();

		cout << "---------------------------------" << endl;

		for (int i = 0; i < 100; i++)
			delete item6[i];
		cout << "---------------------------------" << endl;
	}
	return 0;
}

 

타입 변환 #4, #5

#include <iostream>
using namespace std;

//오늘의 주제 : 타입 변환 (포인터)
class Knight
{
public:
	int _hp = 0;
public:
};


class Item
{
public:
	Item()
	{
		cout << "Item()" << endl;
	}

	Item(int itemType) : _itemType(itemType) // 이거를 만들어줘야 상속받은 곳에서 Item(IT_WEAPON) 형식으로 초기화 가능
	{
		cout << "Item(int itemType)" << endl;
	}

	Item(const Item& item)
	{
		cout << "Item(const Item&)" << endl;
	}

	virtual ~Item()
	{
		cout << "~Item()" << endl;
	}

	void Test()
	{
		cout << "Test Item" << endl;
	}

public:
	int _itemType = 0;
	int _itemDbId = 0;

	char _dummy[4096] = {}; // 이런 저런 정보들로 인해 비대해진 데이터 더미.
};

enum ItemType
{
	IT_WEAPON = 1,
	IT_ARMOR = 2,
};

class Weapon : public Item
{
public:
	Weapon() : Item(IT_WEAPON)
	{
		cout << "Weapon()" << endl;
		_damage = rand() % 100;
	}

	~Weapon()
	{
		cout << "~Weapon()" << endl;
	}
	void Test()
	{
		cout << "Test Weapon" << endl;
	}
public:
	int _damage = 0;
};

class Armor : public Item
{
public:
	Armor() : Item(IT_ARMOR)
	{
		cout << "Armor()" << endl;
	}

	~Armor()
	{
		cout << "~Armor()" << endl;
	}

public:
	int _defence = 0;
};


void TestItem(Item item)
{

}

void TestItemPtr(Item* item)
{
	
}

int main()
{
	
	// 연관성이 없는 클래스 사이의 포인터 변환 테스트
	{
		// Stack [ 주소 ] -> Heap [_hp(4) }
		Knight* knight = new Knight();
		
	//	Item* item = knight;  // 이거는 에러난다.
	// Stack [ 주소 ] 
		Item* item = (Item*)knight;  //item이 knight를 참조하도록 만들어준다.
	//	암시적으로는 No, 명시적으로는 OK. 
	// Item* item이 knight 객체를 가리키는 묘한 상태가 되어있는 코드이다.
	// Item* item은 item 주소를 타고가면 Item클래스의 객체가 있다라는 선언이다.
		item->_itemType = 2;
		item->_itemDbId = 1;
	// 디버깅하면 itemType은 입력이 되는데 itemDbId는 별 말 없이 값 입력 안되고 그냥 통과된다.
	//	이유는 4바이트인 _hp 하나 있어서, 그 위치가 들어갈 수 있는 정보량인 itemType만 입력되고
	// itemDbId는 knight의 4바이트를 초과하기때문에 입력되지 않고 넘어간다.
	
	// 이런 식으로 타입 변환 잘못하면 정말 큰일이 벌어진다.
	// item은 포인터변환의 잘못된 예를 보여준다.

	// 58번줄, 62번줄, 63번줄 주석처리해주기. 
	
		delete knight;
	
	}

	// 부모 -> 자식 변환 테스트
	{
		Item* item = new Item();

//		Weapon* weapon = item; // 컴파일러 에러가 난다.
//		Weapon* weapon = (Weapon*)item;
//		weapon->_damage = 10; //이거 빌딩하면 에러난다. crash난다.
		//컴파일 에러는 없지만, 빌딩하면 crash발생.
		//127, 128번은 디버깅해보고 싶으면 주석처리 풀어줄것.
		delete item;
	}

	// 자식-> 부모 변환 테스트
	{
		Weapon* weapon = new Weapon();

		Item* item = weapon;

		delete weapon;

	}

	// 명시적으로 타입 변환 할떄는 항상 항상 조심해야 한다.
	// 암시적으로 될 때는 안전한가?
	// -> 평생 명시적으로 타입 변환(캐스팅) 안하면 되는 거 아닌가?

	Item* inventory[20] = {};

	srand((unsigned int)time(nullptr));

	for (int i = 0; i < 20; i++)
	{
		int randValue = rand() % 2; // 0-1 출력됨.
		
		switch (randValue)
		{
		case 0:
			inventory[i] = new Weapon();
			break;
		case 1:
			inventory[i] = new Armor();
			break;
		}
	}

	for (int i = 0; i < 20; i++)
	{
		Item* item = inventory[i];
		if (item == nullptr)
			continue;

		if (item->_itemType == IT_WEAPON)
		{
			Weapon* weapon = (Weapon*)item; //원래 원본이 weapon이었기 때문에 형변환 (Weapon*)item을 해도 이 경우에는 안전하다.
			cout << "Weapon Damage : " << weapon->_damage << endl;
		}
	}

	// ***************************매우 매우 매우 중요 ****************
	// 위의 원본을 weapon과 Armor로 만들어놓은 상황인데 이를 delete item 하면 어떤 일이 일어나는가?

	for (int i = 0; i < 20; i++)
	{
		Item* item = inventory[i];
		if (item == nullptr)
			continue;

		delete item; // 조상클래스 소멸자에 virtual 안해주면 item만 소멸되는 것을 볼 수가 있다.
//		delete item; // 이것만 호출해서 사용하면 디버깅해보면 item 소멸자만 호출된다.
		//weapon과 Armor의 소멸자도 호출되어야 하는데 그게 안된다. 그렇기 때문에 아래와 같이 해야한다.

		//if (item->_itemType == IT_WEAPON)
		//{
		//	Weapon* weapon = (Weapon*)item;
		//	delete weapon;
		//}
		//else
		//{
		//	Armor* armor = (Armor*)item;
		//	delete armor;
		//}
		
		//이거를 디버깅해보면 item weapon armor 모든 소멸자 호출되는 것을 확인할 수 있다.
		
		//그런데 조상 클래스 소멸자에 virtual을 붙여주면 소멸자 호출할 때 
		// delete item만 해도 armor와 weapon 다 지워지는 효과가 난다.
		//그래서 위처럼 저렇게 해줄 필요 없이 저거 202-210 주석처리하고
//		 delete item; // 이거 주석해제해서 하면 전부다 지워지는 것을 확인할 수 있다.
	}

	//정리
	// - 포인터 vs 일반 타입 매개변수의 차이를 이해하자
	// - 포인터 사이의 타입 변환(캐스팅)을 할 때는 매우매우 조심해야 한다.
	// - 부모- 자식 관계에서 부모 클래스의 소멸자에는 까먹지 말고 virtual을 붙이자!!

	return 0;
}

얕은 복사 vs 깊은 복사  (결과가 이상하다. 강의 다시 들어서 밑에 다시 코드를 만들 예정이다.)

#include <iostream>
using namespace std;

//오늘의 주제 : 얕은 복사 vs 깊은 복사

class Pet  // Knight를 따라다니는 Pet .
{
public:
	Pet()
	{
		cout << "Pet()" << endl;
	}
	~Pet()
	{
		cout << "~Pet()" << endl;
	}
	Pet(const Pet& pet) // 복사생성자
	{
		cout << "Pet(const Pet&)" << endl;
	}
};

class KnightWithPet
{
public:
	KnightWithPet()
	{
		_pet = new Pet();
	}
	~KnightWithPet()
	{
		delete _pet;
	}



public:
	int _hp = 100;
	Pet* _pet;
	;
};

class KnightCopy
{
public:
	KnightCopy()
	{
		_pet = new Pet();
	}
	KnightCopy(const KnightCopy& knight)
	{
		// 밑에 두 줄이 얕은 복사 방식
		/*_hp = knight._hp;
		_pet = knight._pet;*/

		// 깊은 복사 방식
		_hp = knight._hp;
		_pet = new Pet(*(knight._pet));
		// class Pet 안에 복사생성자는 const Pet& pet 참조를 받게 하기 위해.
		// new Pet(*(knight._pet)); 이거를 해주는거다.
		// 이해가 근데 잘 안간다.
		// 새로운 객체를 만들어줘서, 복사가 일어나게끔 해준다?
	}
	KnightCopy& operator=(const KnightCopy& knight)
	{
		// 이것도 깊은 복사가 됨. 
		_hp = knight._hp;
		_pet = new Pet(*(knight._pet)); 
		return *this;
	}
	~KnightCopy()
	{
		delete _pet;
	}

public:
	int _hp = 100;
	Pet* _pet;

};
	class Knight
	{
	public:

	public:
		int _hp = 100;
		//Pet _pet; // 이렇게 하면 Knight가 만들어지자마자 Pet이라는 애도 자동으로 만들어지게 되는거고
		// Knight가 소멸이 되자마자 Pet도 같이 소멸이 되게 된다.
		// 만약 Pet의 데이터가 크면, Knight에 대한 데이터도 같이 커지게 된다. 데이터 처리 부담이 생김.
		// 또한 만약에 Pet을 상속하는 자손 클래스 예를 들어, 토끼펫, 거북이펫, 용펫 등등 이 있으면
		// 그거를 상속하게 하는 게 어렵다.

		//그래서 아래와 같이 해준다.
		Pet* _pet; // pet의 포인터를 들고 있게끔 해줬다.

	};
	int main()
	{

		Pet* pet = new Pet();// 힙 영역에 생성

		Knight knight; //기본생성자
		knight._hp = 200;
		knight._pet = pet;

		Knight knight2 = knight; //knight와 똑같이 복사 knight2, 복사생성자
		// Knight knight3(knight); // 이렇게도 위 코드와 똑같이 복사된다. 
		//복사생성자로 만들어진다.

		Knight knight3; //기본생성자로 복사.
		knight3 = knight; // 기본생성자후 복사대입연산자를 통해 복사가 되는거다.

		// [복사 생성자], [복사 대입 연산자]
		// 둘 다 메모리에 있는 데이터를 그대로 복사해만들어준다.
		// 둘 다 안 만들어주면 컴파일러 '암시적으로' 만들어준다.

		// 중간 결론 ) 

		// 그런데 Knight 클래스 안에 변수가 포인터 변수가 들어가면 문제가 달라진다.
		// 위에 knight 객체를 만들었고 복사생성자로 knight2, knight3를 만들었는데
		// Pet* pet 으로 Knight class 안에 만들게 한 것 때문에 전부 같은 pet을 공유하게 된다.
		// 기사는 셋인데 펫은 하나가지고 전부 가지고 있는 셈.

		// 이런 식의 복사를 얕은 복사라고 한다.
		// 얕은 복사 Shallow Copy
		// 멤버 데이터를 비트열 단위로 '똑같이' 복사 (메모리 영역 값을 그대로 복사한다.)
		// 포인터는 주소값 바구니 -> 주소값을 똑같이 복사 -> 동일한 객체를 가리키는 상태가 됨. 
		// Stack : Knight [hp 0x1000] -> Heap 영역에 할당이 된 상태이다. 0x1000 Pet[  ] 
		// Stack : Knight2 [hp 0x1000] -> Heap 영역에 할당이 된 상태이다. 0x1000 Pet[  ] 
		// Stack : Knight3 [hp 0x1000] -> Heap 영역에 할당이 된 상태이다. 0x1000 Pet[  ] 

		// KnightWithPet 클래스를 만들었따.
		// KnightWithPet KWP1;
		// KnightWithPet KWP2= KWP1;
		// KnightWithPet KWP3;
		// KnightWithPet KWP3 = KWP1;
		// 이렇게 하면 crash 발생. 왜??
		// Pet이 하나의 주소로 복사가 되었는데 소멸시에는 3개다 delete _pet; 이게 세 번 작동함.
		// Pet은 하나인데 delete가 세번 작동해서 crash 발생

		// 그러면 어떻게 해줘야 하나? 깊은 복사 를 해줘야 한다.
		// [깊은 복사 Deep Copy]
		// 멤버 데이터가 참조(주소)값이라면, 데이터를 새로 만들어준다. (원본 객체가 참조하는 대상까지 새로 만들어서 복사)
		// 포인터는 주소값 바구니 -> 새로운 객체를 생성 -> 다른 객체를 가리키는 상태가 된다.
		// Stack : Knight1 [hp 0x1000] -> Heap 0x1000 Pet[  ] 
		// Stack : Knight2 [hp 0x2000] -> Heap 0x2000 Pet[  ] 
		// Stack : Knight3 [hp 0x3000] -> Heap 0x3000 Pet[  ] 


		KnightCopy copy1; //기본생성자
		copy1._hp = 200;
		copy1._pet = pet;

		KnightCopy copy2 = copy1; //knight와 똑같이 복사 knight2, 복사생성자
		// Knight knight3(knight); // 이렇게도 위 코드와 똑같이 복사된다. 
		//복사생성자로 만들어진다.

		KnightCopy copy3; //기본생성자로 복사.
		copy3 = copy1;
	


		return 0;
	}

 

얕은 복사 vs 깊은 복사 (위에꺼가 설명 잘 해놨다. 근데 결과가 이상해서 아예 코드 다시 적음.)

#include <iostream>
using namespace std;

// 오늘의 주제 : 얕은 복사 vs 깊은 복사

class Pet
{
public:
	Pet()
	{
		cout << "Pet()" << endl;
	}
	~Pet()
	{
		cout << "~Pet()" << endl;
	}
	Pet(const Pet& pet)
	{
		cout << "Pet(const Pet&)" << endl;
	}
};

class Knight
{
public:
	Knight()
	{
		_pet = new Pet();
	}

	Knight(const Knight& knight)
	{
		_hp = knight._hp;
		_pet = new Pet(*knight._pet);
	}
	Knight& operator=(const Knight& knight)
	{
		_hp = knight._hp;
		_pet = new Pet(*(knight._pet));
		return *this;
	}
	~Knight()
	{
		delete _pet;  // Knight와 Pet의 생성주기와 소멸주기가 같아짐.
	}
public:
	int _hp = 100;
	Pet* _pet;
};

int main()
{

	Pet* pet = new Pet();

	Knight knight; // 기본 생성자
	knight._hp = 200;
	knight._pet = pet;

	Knight knight2 = knight; // 복사 생성자
	//Knight knight(knight);

	Knight knight3; // 기본 생성자
	knight3 = knight; // 복사 대입 연산자
	
	// [복사 생성자] + [복사 대입 연산자]
	// 둘 다 안 만들어주면 컴파일러 '암시적으로' 만들어준다.
	
	// 중간 결론) 컴파일러가 알아서 잘 만들어준다? No.

	// 얕은 복사 Shallow Copy
	// 멤버 데이터를 비트열 단위로 '똑같이' 복사 (메모리 영역 값을 그대로 복사)
	// 포인터는 주소값 바구니 -> 주소값을 똑같이 복사 -> 동일한 객체를 가리키는 상태가 됨
	// Stack : Knight [hp 0x1000] -> Heap 영역에 할당이 된 상태이다. 0x1000 Pet[  ] 
	// Stack : Knight2 [hp 0x1000] -> Heap 영역에 할당이 된 상태이다. 0x1000 Pet[  ] 
	// Stack : Knight3 [hp 0x1000] -> Heap 영역에 할당이 된 상태이다. 0x1000 Pet[  ] 

	// [깊은 복사 Deep Copy]
	// 멤버 데이터가 참조(주소)값이라면, 데이터를 새로 만들어준다. (원본 객체가 참조하는 대상까지 새로 만들어서 복사)
	// 포인터는 주소값 바구니 -> 새로운 객체를 생성 -> 다른 객체를 가리키는 상태가 된다.
	// Stack : Knight1 [hp 0x1000] -> Heap 0x1000 Pet[  ] 
	// Stack : Knight2 [hp 0x2000] -> Heap 0x2000 Pet[  ] 
	// Stack : Knight3 [hp 0x3000] -> Heap 0x3000 Pet[  ] 

	return 0;
}

멤버변수에 포인터변수가있으면, 객체를 복사 생성자든 대입연산자든 복사가 이루어질때 (디폴트 생성자,연산자일때) 똑같은 주소값을 가지고 가르키기에 문제가 발생하고, 깊은 복사 (새로 복사 생성자, 대입연산자를 만들어줌)를 만들어주고, 새로 동적할당시키는건 이해했습니다. 

근데 마지막에 깊은복사 new Pet 동적할당할때 Pet의 복사 생성자를 왜 호출하는지 궁금합니다. 기본생성자를 호출해도 똑같이 동작하는데 의미의 차이일까요? 새로 동적할당하여 새로운 메모리를 할당받고, 원본 knight가 가진 pet의 멤버(만약 pet의 멤버가 존재했다면)의 값은 복사하여 전달한다. 라는 개념으로 보면 되나요?

00
Rookiss2021.02.15 AM 01:49

새로 동적할당하여 새로운 메모리를 할당받고, 원본 knight가 가진 pet의 멤버(만약 pet의 멤버가 존재했다면)의 값은 복사하여 전달한다. 라는 개념으로 보면 되나요?

-> 맞습니다.
기본 생성자가 '똑같이' 동작할 수도 있겠지만
Pet에도 이런 저런 정보 (위치라거나, 레벨이라거나, HP라거나...)가 있을 수도 있으니
기본 생성자가 아니라 그 상태를 복사하는 복사 생성자를 이용해서 만들어주는 것이죠.

 

 

얕은 복사 vs 깊은 복사 #2  -> 이건 너무 어려워서.. 중간에 이해 안됨. C++더 익숙해지면 다시 봐야겠다.

#include <iostream>
using namespace std;

// 오늘의 주제 : 얕은 복사 vs 깊은 복사

class Pet
{
public:
	Pet()
	{
		cout << "Pet()" << endl;
	}
	~Pet()
	{
		cout << "~Pet()" << endl;
	}
	Pet(const Pet& pet)
	{
		cout << "Pet(const Pet&)" << endl;
	}

	Pet& operator=(const Pet& pet)
	{
		cout << "operator=(const Pet&)" << endl;
		return *this;
	}
};

class Player
{
public:
	Player()
	{
		cout << "Player()" << endl;
	}
	//복사 생성자
	Player(const Player& player)
	{
		cout << "Player(const Player&)" << endl;
		_level = player._level;
	}
	//복사 생성 연산자
	Player& operator=(const Player& player)
	{
		cout << "operator=(const Player&)" << endl;
		_level = player._level;
		return *this;
	}

public:
	int _level = 0;
};

class Knight : public Player
{
public:
	Knight()
	{
		//_pet = new Pet();
	}
	//복사 생성자
	//Knight(const Knight& knight) //이렇게만 해주면 기본생성자만 호출이 되서 복사가 일어나지 않는다.
	Knight(const Knight& knight) : Player(knight), _pet(knight._pet)
	//조상클래스가 들고있던 복사생성자도 뒤에 써주고, _pet도 복사생성자 형식으로 써줘야 한다.
	{
		cout << "Knight(const Knight&)" << endl;
		_hp = knight._hp;
		
	}
	//복사 생성 연산자
	Knight& operator=(const Knight& knight)
	{
		cout << "operator=(const Knight&)" << endl;

		Player::operator=(knight);
		_pet = knight._pet;
		
		_hp = knight._hp;
		
		return *this;
	}
	~Knight()
	{
		
	}
public:
	int _hp = 100;
	Pet _pet;
};

int main()
{

	Knight knight; // 기본 생성자
	knight._hp = 200;

	cout << "----------------복사 생성자--------------" << endl;
	Knight knight2 = knight; // 복사 생성자
	//Knight knight(knight);

	
	Knight knight3; // 기본 생성자
	
	cout << "--------------복사 대입 연산자--------------" << endl;
	
	knight3 = knight; // 복사 대입 연산자
	
	// [복사 생성자] + [복사 대입 연산자]
	// 둘 다 안 만들어주면 컴파일러 '암시적으로' 만들어준다.
	
	// 중간 결론) 컴파일러가 알아서 잘 만들어준다? No.

	// 얕은 복사 Shallow Copy
	// 멤버 데이터를 비트열 단위로 '똑같이' 복사 (메모리 영역 값을 그대로 복사)
	// 포인터는 주소값 바구니 -> 주소값을 똑같이 복사 -> 동일한 객체를 가리키는 상태가 됨
	// Stack : Knight [hp 0x1000] -> Heap 영역에 할당이 된 상태이다. 0x1000 Pet[  ] 
	// Stack : Knight2 [hp 0x1000] -> Heap 영역에 할당이 된 상태이다. 0x1000 Pet[  ] 
	// Stack : Knight3 [hp 0x1000] -> Heap 영역에 할당이 된 상태이다. 0x1000 Pet[  ] 

	// [깊은 복사 Deep Copy]
	// 멤버 데이터가 참조(주소)값이라면, 데이터를 새로 만들어준다. (원본 객체가 참조하는 대상까지 새로 만들어서 복사)
	// 포인터는 주소값 바구니 -> 새로운 객체를 생성 -> 다른 객체를 가리키는 상태가 된다.
	// Stack : Knight1 [hp 0x1000] -> Heap 0x1000 Pet[  ] 
	// Stack : Knight2 [hp 0x2000] -> Heap 0x2000 Pet[  ] 
	// Stack : Knight3 [hp 0x3000] -> Heap 0x3000 Pet[  ] 


	// 실험)
	// - 암시적 복사 생성자 Steps
	//  1) 조상 클래스의 복사 생성자 호출
	//  2) 멤버 클래스의 복사 생성자 호출
	//  3) 멤버가 기본 타입일 경우 메모리 복사 (얕은 복사 방식 Shallow Copy)
	// 
	// - 명시적 복사 생성자 Steps   
	// 1) 부모 클래스의 기본 생성자 호출
	// 2) 멤버 클래스의 기본 생성자 호출
	// 명시적으로 선언해준 이상, 프로그래머가 전부다 통제해주어야 한다. 제대로 안해주면
	// 기본생성자가 호출이 되기 때문이다.
	// 
	// - 암시적 복사 대입 연산자 Steps
	// 1) 부모 클래스의 복사 대입 연산자 호출
	// 2) 멤버 클래스의 복사 대입 연산자 호출
	// 3) 멤버가 기본 타입일 경우 메모리 복사 (얕은 복사 Shallow Copy)
	//
	// - 명시적 복사 대입 연산자 Steps
	// 1) 컴파일러가 알아서 해주는 게 없다. 프로그래머가 다 해줘야 한다.

	// 왜 이렇게 혼란스러울까?
	// 객체를 '복사'한다는 것은 두 객체의 값들을 일치시키려는 것
	// 따라서 기본적으로 얕은 복사 (Shallow Copy) 방식으로 동작

	// 깊은 복사를 하기 위해서는 명시적 복사를 해야 하는데
	// 명시적 복사 -> [모든 책임], 모든 입력은 프로그래머한테 위임하겠다는 의미.

	return 0;
}

캐스팅 4총사

#include <iostream>
using namespace std;

// 오늘의 주제 : 캐스팅 (타입 변환)

class Player
{
public:
	virtual ~Player() { };
};

class Knight : public Player
{
public:

};

class Archer : public Player
{
public:

};

class Dog
{

};

void PrintName(char* str)
{
	cout << str << endl;
}


// 1) static_cast
// 2) dynamic_cast
// 3) const_cast
// 4) reinterpret_cast
// (int) 이렇게 캐스팅하는 건 옛날 C 방식. 요즘에는 위 네 개가지고 캐스팅한다.


int main()
{
	// static_cast : 타입 원칙에 비춰볼 때 상식적인 캐스팅만 허용해준다.
	// 1) int <-> float
	// 2) Player* -> Knight* (다운캐스팅)

	int hp = 100;
	int maxHp = 200;
	// float ratio = (float)hp/maxHp; 옛날 방식
	float ratio = static_cast<float>(hp) / maxHp;

	Player* p = new Knight();
	// Knight* k1 = p; 컴파일러 에러난다.
	// Knight* k1 = (Knight*)p;  //예전 방식
	Knight* k1 = static_cast<Knight*>(p);

	// dynamic_cast : 상속 관계에서의 안전 형변환 - virtual이 한번 이상 등장한다.
	// RTTI (RunTime Type Information)
	// 다형성을 활용하는 방식
	// -virtual 함수를 하나라도 만들면, 객체의 메모리에 가상 함수 테이블 (vftable) 주소가 기입된다.
	// - 만약 잘못된 타입으로 캐스팅을 했으면, nullptr 반환해준다. ***************
	// 이를 이용해서 맞는 타입으로 캐스팅을 했는지 확인을 하는데 유용하다.
	Knight* k2 = dynamic_cast<Knight*>(p);
	
	// const_cast : const를 붙이거나 떼거나~  ,, 그런데 실전에서 사용한 적은 거의 없다.
	// PrintName("DaewonKim"); // 안된다.
	PrintName(const_cast<char*>("DaewonKim"));

	// reinterpret_cast
	// 가장 위험하고 강력한 형태의 캐스팅
	// 're-interpret' : 다시-간주하다/ 생각하다
	// - 포인터랑 전혀 관계없는 다른 타입 변환 등
	// 말은 안되긴 하지만, 내가 뭐하는지 아니까 변환해줘~
	__int64 address = reinterpret_cast<__int64>(k2);

	// 말이 안되는 것도 형변환을 시켜버린다.

	Dog* dog1 = reinterpret_cast<Dog*>(k2);

	void* p = malloc(1000);
	//Dog* dog2 = (Dog*)p;
	Dog* dog2 = reinterpret_cast<Dog*>(p);
	return 0;
}

c스타일의 캐스팅은 reinterpret_cast이고

많은 형 변환중에 좀더 안전하게 쓸 수 있게 기능을 제한하고

사용자의 의도를 알수 있게 추가된?것이 

const_cast와 static_cast이며

마지막으로 가상함수테이블을 이용해서 추가적인 기능으로 더욱 안전하지만 

속도에 손해가 있는 dinamic_cast이다 라고 이해했는데요

혹시 틀린부분이 있나요?

물론 각각의 설명을 하자면

const_cast 상수를 뗴거나 붙여주는역할

static_cast 논리적으로 생각했을때 말이되는경우 사용가능, 단 안전은 보장해주지 않음.

dinamic_cast 가상함수테이블을 이용해 실제로 형변환이 가능한지 안전까지 보장해주지만 느림.

reinterpret_cast는 그냥 뭐든 변환해줌

 

답변 : 맞습니다.

'C++' 카테고리의 다른 글

C++ 알고리즘 서적 정리  (0) 2022.04.09
object  (0) 2022.03.27
  (0) 2022.03.20
포인터  (0) 2022.03.14
함수  (0) 2022.03.10