복사 생성자

-> 생성자 : 객체 생성 시 객체를 받아온다.

-> 인자로 넘어오는 객체의 멤버 값을 복사 받는 것.

 

디폴트 복사 생성자

-> 사용자가 복사 생성자를 정의하지 않으면 컴파일러가 복사 생성자를 생성하여 호출한다.

-> 사용자가 복사 생성자를 정의하면 컴파일러는 복사 생성자를 생성하지 않는다.

 

 

 

복사 생성자의 호출 시점 3가지

#1. 먼저 생성한 객체를 나중에 생성하는 객체의 인자로 전달하는 경우

#2. 함수의 인자로 객체가 전달되는 경우

#3. 함수의 반환 값으로 객체가 반환되는 경우

 

 

복사 생성자의 인자로 레퍼런스 타입을 받는 이유

-> 생성자는 함수의 일종이므로 호출하면 stack 영역이 할당된다.

-> 생성자의 stack 영역에 지역 변수를 할당하고 인자로 넘겨주는 값을 복사 받을 때

-> 또 다시 복사 생성자를 호출해야 하는 문제가 발생한다.

-> 즉, 무한루프가 걸린다.

 

 

레퍼런스가 아닌 포인터로 받으면 어떻게 될까?

-> 다음과 같이 포인터 연산으로 인해서 문제(주소 값 변경)가 발생할 수도 있기 때문에 포인터로 받지 않는다.

int a = 10;
int* ptr = &a;
int* ptr2 = ptr;

ptr2 = ptr + 1;

 

디폴트 복사 생성자의 문제점

-> 얕은 복사의 사용

-> 얕은 복사는 단순 대입으로 인한 복사이다.

 

얕은복사

-> 얕은 복사는 객체가 가진 멤버들의 값을 새로운 객체로 복사하는데 만약 객체가 참조타입의 멤버를 가지고 있다면 참조값만 복사가 된다.

class CObj
{
public:
	CObj() : m_pBuff(nullptr) {}
	CObj(char* _pBuff)
	{
		m_pBuff = new char[strlen(_pBuff) + 1];
		strcpy_s(m_pBuff, strlen(_pBuff) + 1, _pBuff);
	}
	~CObj()
	{
		if (m_pBuff)
		{
			delete[] m_pBuff;
			m_pBuff = nullptr;
		}
		cout << "소멸자 호출" << endl;
	}


	얕은 복사 방식의 구현
	CObj(CObj& _obj)
	{
		// 얕은 복사
		m_pBuff = _obj.m_pBuff;	
	}

private:
	char*		m_pBuff;
};

void main()
{	
	CObj	obj1("Hello");
	CObj	obj2(obj1);
}

-> main 함수를 시작하면서 obj1 객체를 만든다.

-> 이후, obj2 객체를 만드는데 디폴트 복사 생성자를 호출하여 만든다.

-> 함수가 종료되면서 stack 영역을 정리할 때 문제가 발생한다.

-> obj2 객체가 소멸하면서 obj2.m_pBuff가 가지고 있는 Heap 영역의 주소를 해제하려 했더니

-> obj2가 해제하면서 이미 할당되지 않은 공간이 되었다.

-> obj2는 obj1의 주소를 참조하고 있는 상태에서 해당 주소를 할당 해제 하였기 때문이다.

 

이러한 문제를 해결하는 방법은 '깊은 복사'를 사용하는 것이다.

 

깊은 복사

-> 얕은 복사와는 달리 객체가 가진 모든 멤버(값과 참조형식 모두)를 복사한다..

객체가 참조 타입의 멤버를 포함할 경우 참조값의 복사가 아닌 참조된 객체 자체가 복사되는 것을 깊은 복사라 한다.

 

-> 복사 받을 때 객체 또한 동적할당을 진행한다.

-> 각 객체들이 서로 다른 공간을 참조하게 만든다.

class CObj
{
public:
	CObj() : m_pBuff(nullptr) {}
	CObj(char* _pBuff)
	{
		m_pBuff = new char[strlen(_pBuff) + 1];
		strcpy_s(m_pBuff, strlen(_pBuff) + 1, _pBuff);
	}
	~CObj()
	{
		if(m_pBuff)
		{
			delete[] m_pBuff;
			m_pBuff = nullptr;
			cout << "소멸자 호출" << endl;
		}
	}

	CObj(CObj& _obj)
	{
		// 깊은 복사 방식의 구현
		this->m_pBuff = new char[strlen(_obj.m_pBuff) + 1];
		strcpy_s(m_pBuff, strlen(_obj.m_pBuff) + 1, _obj.m_pBuff);
	}
private:
	char* m_pBuff;
};

void main()
{	
	CObj	obj1("Hello");
	CObj	obj2(obj1);
}

-> main 함수를 시작하면서 obj1 객체를 만든다.

-> 이후, obj2 객체를 만드는데 디폴트 복사 생성자를 호출하여 만든다.

-> 함수가 종료되면서 stack 영역을 정리한다.

-> obj2 객체가 소멸하면서 obj2.m_pBuff가 가지고 있는 Heap 영역의 주소를 해제한다.

-> 복사 생성자에서 m_pBuff를 함수의 인자(객체)를 전달하여 새로 할당한다.

 

'Programming > C++ Basic' 카테고리의 다른 글

C++ friend  (0) 2020.09.17
C++ this 포인터, extern 키워드  (0) 2020.09.16
C++ static과 클래스 , 멤버 함수  (0) 2020.09.15
C++ const 와 클래스, 멤버 함수  (0) 2020.09.15
C++ 파일 입출력(fopen_s, fread(), fwrite())  (0) 2020.09.13

static과 클래스

-> static 변수의 초기화는 전역에서 진행한다.

 

static 변수는 Data 영역에 등록이 된다.

-> 프로그램 시작 시 메모리에 등록된다.

-> 사용자가 객체를 몇개 만들지 알 수 없는 상황이기 때문에

-> 컴파일러가 static 변수를 여러개 만들 수 없다.

-> 그래서, 단 하나만 메모리에 등록시킨 후 해당 클래스로 만든 모든 객체가 static 변수를 공유한다.

 

static 변수의 초기화

class CObj
{
public:
	CObj() {}
	CObj(int _a) : m_a(_a) // 불가능
	{
	}

private:
	static int m_a;

};

static 변수는 이니셜라이즈를 통해 초기화가 불가능하다.

왜냐하면 static 변수는 프로그램 시작 시 Data영역에 메모리 할당이 되고,

생성자의 호출 시점은 런타임 중 객체를 만드는 시점에 메모리에 등록이 되기 때문이다.

 

static 변수의 초기화 방법은 다음과 같다.

class CObj
{
public:
	CObj() {}
	CObj()
	{
	}

private:
	static int m_a;
};

int CObj::m_a = 10; // static 변수의 초기화

static 변수는 전역 변수로 초기화를 진행한다.

 

static 멤버 함수

-> static 멤버 함수/변수는 객체를 생성하지 않아도 접근 지정자가 public이면

static 멤버 함수/변수의 접근이 가능하고, 일반 함수/멤버 변수의 접근이 불가능하다.

 

-> 단, 일반 멤버 함수 내부에서는 static 멤버 함수/변수 접근이 가능하다.

class CObj
{
public:
	CObj() {}

	static void Static_Func()
	{
		// 멤버 함수
		Static_Func();  // 가능
		Func();         // 불가능

		// 멤버 변수
		m_StaticA = 10; // 가능
		m_a = 10;       // 불가능
	}

	void Func()
	{
		// 멤버 함수
		Static_Func();  // 가능
		Func();		// 가능

		// 멤버 변수
		m_StaticA = 10; // 가능
		m_a = 10;       // 가능
	}
private:
	static int m_StaticA;
	int m_a;
};

 

static과 const의 차이점

 

static

-> 선언을 하게 되면 위치와 상관없이 프로그램의 시작부터 끝까지 메모리에 할당되어 있고,

-> 그 값을 마음대로 바꿀 수 있다. 
-> 위치에 대해서는 전역변수든 지역변수든 상관 없다.

 

const

-> 해당 변수를 초기화한 이후에는 절대로 바꾸지 못하도록 선언하는 것

-> 초기화가 되고나면 그 이후에는 값을 바꿀 수 없게 된다.

const와 클래스

 

const로 멤버 변수 선언 시 선언부에서 바로 초기화가 가능하다.

const int		m_a = 10;

단, 해당 클래스로 만든 모든 객체가 동일한 값을 가지게 된다.

그러므로 const로 멤버 변수 선언 시 초기화를 시키는 방법은 이니셜라이져를 사용하는 것이다.

 

이니셜라이져란?

-> 메모리 할당과 동시에 호출되는 것.

-> 메모리 할당과 동시에 호출되기 때문에 선언과 동시에 멤버 변수 초기화를 할 수 있다.

 

 

이니셜라이져 선언 방법

-> 생성자의 선언부와 정의부 사이에 : (콜론)으로 명시해준다.

class CObj
{
public:
	CObj(int _a, int _b)    
		// 이니셜라이져를 사용한다는 의미		
		: m_a(_a), m_b(_b)
	{		
		m_a = _a;
	}

private:
	const int		m_a;
	const int		m_b;

생성자 호출 순서
객체 생성 -> 메모리 할당 -> 생성자 호출
-> 메모리 할당 당시에 m_a는 쓰레기 값으로 초기화가 진행된다.
-> 이후 생성자에서 대입을 시도하는 것은 선언 후 초기화 형태이다.

 

이니셜라이져와 생성자의 차이점

// 이니셜 라이져
int a = 10;

// 생성자
int a;
a = 10;

 

const와 멤버 함수

-> const의 위치에 따라 상수화 되는 것이 다르다.

#1. 반환 타입 앞에 있는 const

#2. 반환 타입 뒤에 있는 const

 

반환타입 앞에 const가 붙으면 외부에서 수정이 불가능하다.

class CObj
{
public:
	CObj(){}

public:
	const void Const_Func()
	{		
		m_a = 10; // 불가능	
	}

private:
	const int m_a;
};

반환타입 뒤에 const가 붙으면 정의부가 상수가 된다.

-> 정의부 내부에서 멤버 변수들의 값 변경이 불가능하다. 

class CObj
{
public:
	CObj(){}

public:
	void Const_Func() const
	{		
		m_a = 10; // 불가능	
	}

private:
	int m_a;
};

단, 지역 변수들의 값 변경은 가능하다.

class CObj
{
public:
	CObj(){}

public:
	const void Const_Func() const
	{		
		int a = 10;
		a = 20;
	}
private:
	int m_a;
};

 

 const 멤버 함수 내부에서는 일반 멤버 함수 호출이 불가능하다.
 -> 일반 멤버 함수 내부에서 멤버 변수들의 값 변경 시도가 있을 수 있기 때문이다.

class CObj
{
public:
	CObj(){}

public:
	const void Const_Func() const
	{			
		Func();        // 불가능
		Const_Func();  // 가능
	}

	void Func()
	{
		int b = 10;
	}
};

 

const 객체

const CObj	obj;

-> 멤버 변수들이 모두 const화 된다.

-> 즉, 객체가 가지고 있는 멤버들의 값을 변경하지 못한다.

-> 이로 인해, const 멤버 함수만 호출이 가능하다.

-> 일반 멤버함수들의 호출은 불가능하다.

obj.Const_Func(); // 가능
obj.Func();       // 불가능

-> 단, :(이니셜라이져)를 이용한 변수 초기화는 가능하다.

 

원칙적으로 모든 곳에 const를 사용한다. 여기에는 지역 변수와 함수 매개 변수도 포함된다.

-> 개체를 수정하지 않는 멤버 함수에는 모두 const를 붙인다.

int GetNum() const;

값(value) 형식의 변수를 const로 반환하지 않는다. 포인터나 참조(reference)를 반환할 경우에만 const 반환을 한다.

 

단순 상수 변수에는 const 대신 constexpr 을 사용한다.

const int BUFFER_SIZE = 512;     // 적용전

constexpr int BUFFER_SIZE = 512; // 적용후

스트림 개방 함수

fopen_s()

errno_t

->스트림 개방 성공 시 0, 실패 시 이유에 따른 값을 반환

 

FILE** _Stream

-> 스트림을 저장할 변수의 주소를 전달한다.

-> 내부에서 동적할당해주기 때문에 이중 포인터를 사용한다.

 

const char* _FileName

-> 경로와 파일 이름, 확장자까지 전달한다.

 

const char* _Name

-> 읽기 / 쓰기, Text형식 / Binary형식 인지 전달한다.

 

 

스트림 소멸 함수

-> fclose()

 

쓰기

FILE* fp = nullptr;

 

절대 경로

errno_t err = fopen_s(&fp, "C:/Users/Desktop/Data/Text.txt", "wt");

상대 경로

errno_t err = fopen_s(&fp, "../Data/Text.txt", "wt");

 

스트림 개방

if (0 == err)
	{
		fputc('A', fp);
		fputc('B', fp);

		cout << "스트림 개방 성공" << endl;
		fclose(fp);
	}
	else
		cout << "스트림 개방 실패" << endl;

스트림 읽기

// 읽기
	char chA = 0, chB = 0;
	FILE*	fp = nullptr;

	errno_t err = fopen_s(&fp, "../Data/Text.txt", "rt");

	if (0 == err)
	{
		chA = fgetc(fp);
		chB = fgetc(fp);

		cout << "파일 개방 성공" << endl;
		fclose(fp);
	}
	else
		cout << "파일 개방 실패" << endl;

	cout << chA << ", " << chB << endl;

 

입출력 모드

r : Read의 약자. 읽기 모드일 때 사용한다. // 파일이 없을 경우 오류

w : Write의 약자. 쓰기 모드일 때 사용한다. // 파일이 없을 경우 생성

a : Append의 약자. 이어 쓰기 모드일 때 사용한다. // 파일이 없을 경우 생성

+ : 읽기 / 쓰기 모두 사용 가능

-> 좋은 모드라고 생각할 수 있으나, 읽기에서 쓰기로, 쓰기에서 읽기로 작업이 변경될 경우

-> 버퍼를 모두 비워주어야 하는 불편함이 있다.

 

데이터 종류 모드

t : Text의 약자. 사람이 읽고 쓰기 편한 모드.

b : Binary의 약자. 컴퓨터가 일고 쓰기 편한 모드.

 

binary 모드의 장점

#1. 속도가 빠르다.

#2. 그림, 영상, mp3 파일 등 읽기 어려운 파일의 입출력이 자유롭다.

#3. 배열 또는 구조체처럼 연속된 메모리를 사용하는 데이터일 경우 통째로 저장 및 불러오기가 가능하다.

 

binary 모드의 주의 사항

-> 저장하는 데이터 중 주소가 있을 경우 주소는 저장하면 안된다.

 

binary 모드 출력

fwrite()

const void* _Buffer

-> 저장할 메모리의 시작 주소


size_t _ElementSize

-> 얼마만큼 저장할 것인지 크기(byte단위)


size_t _ElementCount

-> 그 것들을 몇 개 저장할 것인지 전달


FILE* _Stream

-> 개방한 스트림을 전달

 

binary 모드 출력

int iArr[5] = { 1, 2, 3, 4, 5 };

FILE* fp = nullptr;
errno_t err = fopen_s(&fp, "../Data/Binary.txt", "wb");
if (0 == err)
{
	fwrite(iArr, sizeof(iArr), 1, fp);
	//fwrite(iArr, sizeof(int), 5, fp);

	cout << "파일 개방 성공" << endl;
	fclose(fp);
}
else
	cout << "파일 개방 실패" << endl;

 

binary 모드 입력

fread()

const void* _Buffer

-> 어떤 메모리에 읽어올 것인지 시작 주소


size_t _ElementSize

-> 얼마만큼 읽어올 것인지 크기를 전달(byte단위)


size_t _ElementCount

-> 몇 개를 읽을 것인지 전달


FILE* _Stream

-> 어떤 스트림을 이용할 것인지 전달

 

int iArr[5] = {};

FILE* fp = nullptr;
errno_t err = fopen_s(&fp, "../Data/Binary.txt", "rb");
if (0 == err)
{
	fread(iArr, sizeof(iArr), 1, fp);
	//fread(iArr, sizeof(int), 5, fp);
    
	cout << "파일 개방 성공" << endl;
	fclose(fp);
}
else
	cout << "파일 개방 실패" << endl;


for (int i = 0; i < 5; ++i)
	cout << iArr[i] << endl;

 

이미지 파일 복사 방법

void main()
{
	FILE*	fp_In = nullptr;
	FILE*	fp_Out = nullptr;

	errno_t err_In = fopen_s(&fp_In, "../Data/Image.jpg", "rb");
	if (0 == err_In)
	{
		//////////////////////////////////////////////////////////////////////
		errno_t err_Out = fopen_s(&fp_Out, "../Data/Copy.jpg", "wb");
		if (0 == err_Out)
		{
			char	ch = 0;
             
                        // 원본 파일의 사이즈 복사
			while (true)
			{
				int iCnt = fread(&ch, sizeof(char), 1, fp_In);

				if (1 > iCnt)
					break;

				fwrite(&ch, sizeof(char), 1, fp_Out);
			}

			cout << "복사 성공" << endl;
			fclose(fp_Out);
		}
		else
			cout << "복사 실패" << endl;
		//////////////////////////////////////////////////////////////////////


		cout << "불러오기 성공" << endl;
		fclose(fp_In);
	}
	else
		cout << "불러오기 실패" << endl;	
}

'Programming > C++ Basic' 카테고리의 다른 글

C++ static과 클래스 , 멤버 함수  (0) 2020.09.15
C++ const 와 클래스, 멤버 함수  (0) 2020.09.15
C++ 전방 선언  (0) 2020.09.13
C++ 생성자와 소멸자, explicit 키워드  (2) 2020.09.13
C++ 메모리 함수  (0) 2020.09.13

+ Recent posts