본문 바로가기

C++

[TIL 22장] 동적할당(깊은 복사, 얕은 복사), 복사 생성자, extern, friend

오늘의 TIL 목차 (22.09. 02)

 

  • 깊은 복사 & 얕은 복사
  • 복사 생성자
  • extern
  • friend

깊은 복사 & 얕은 복사 ( 동적할당 )


[ 동적할당 - 깊은 복사, 얕은 복사 ]

: 동적할당 시 대입을 하는 과정에서 얕은 복사(reference)를 하면 문제가 발생, 깊은 복사(value)를 해야한다.

void main(void)
{
	int* a = new int(5); // 초기값 5 동적할당
    int* b = new int(3); // 초기값 3 동적할당
    int iA = 10, iB = 20;
    
    a = b; // 얕은 복사 (a의 주소에 b의 주소를 대입 - reference)
    *a = *b; // 깊은 복사 (a의 값을 b의 값으로 바꿔줌 - value)
    iA = iB; // 깊은 복사 (iB의 값을 iA에 대입함 - value)
    
    delete a;
    delete b;
}

 

■ 얕은 복사의 문제 ( 참조만 복사 )

1. a가 b의 주소를 가지게 되면서 기존 a의 주소 100번지를 더 이상 손 볼 수 없게 된다.

2. 동적 할당 해제 시 delete a;에서 200번지 주소를 해제하고, 다음 delete b;에서 200번지는 이미 해제됐기에 오류가 발생한다.

=> 결과적으로 5를 3으로 변경하기 위해  a=b를 하면 값은 변경되지 않고 주소가 대입되어 문제를 발생시킨다.

=> 깊은 복사(값 복사)를 해야 한다. 깊은 복사는 값 자체를 변경하기 때문에 이러한 오류가 발생하지 않기 때문.

동적할당된 포인터 a, b
얕은 복사로 인해 a의 주소가 b의 주소로 바뀌면서 생기는 문제

 

 

복사 생성자


[ 복사 생성자 ]

: 기본 생성자와 동일한 생성의 기능을 하지만 원본에 해당하는 사본을 생성할 때 호출되는 생성자

[ 디폴트 복사 생성자 형태 ] => 단순대입
// CDainn 클래스 내부, int m_iA와 char szName[5]를 멤버 변수로 가질 때,
CDainn(CDainn& _rhs) // 복사 생성자의 경우 생성자라 CDainn(클래스) 자체를 받을 수 없어 레퍼런스
{
	cout << "복사 생성자 호출" << endl;
	m_iA = _rhs.m_iA;
	m_szName = _rhs.m_szName; // 얕은 복사, szName의 값이 대입되는 것이 아니라 주소가 대입됨
	// 포인터, 사용자정의자료형의 경우 = 단순 대입으로 인해 얕은 복사 발생
    // 이 경우엔 깊은 복사를 위해 명시적으로 복사 생성자를 생성해야줘야 함
}

-

- 클래스에 생성자 CDainn() : m_iA(0), m_iB(0); 만 선언되어 있을 떄
void main(void)
{
	CDainn cd1; // 멤버 변수 m_iA, m_iB 기본 생성자에 의해 0 초기화
	CDainn cd2(cd1); // 복사 생성자에 의해 cd2의 멤버에도 각각 0초기화
    => 객체 생성 조건은 1. 메모리 할당 2. 생성자 호출
    1. cd2 객체를 생성하기 위해선 생성자가 호출되어야 한다.
    2. 우리는 객체(cd1)를 매개변수로 받는 생성자를 선언한 적이 없다.
    3. (디폴트) 복사 생성자에 의해 알아서 생성된 것
}

※ 복사 생성자 선언 시 무조건 생성자 선언해줘야함

     ☞ 복사 생성자도 생성자이기 때문에 선언 시 기본 생성자가 자동 생성되지 않기 때문

 

[ 복사 생성자 생성 조건 ]

복사 생성자 (미선언시 자동 실행되는 복사 생성자 코드)
- 각 매개변수에 원본 매개변수를 = 으로 대입해준다. (얕은 복사 발생)

[ 복사 생성자가 생성되는 경우 ]
1. 객체를 생성 시 생성자의 매개변수로 먼저 만들어진 원본 객체를 받는 경우
2. 함수의 매개 변수가 객체 타입인 경우
3. 함수의 반환 타입이 객체 타입인 경우

-

class CDainn
{
public:
	CDainn() : m_iA(0), m_iB(0) {}
	CDainn(int _iA, int _iB) : m_iA(_iA), m_iB(_iB) 
	{
		for (size_t i = 0; sizeof(iArray) > i; ++i)
		{
			iArray[i] = 97+i;
		}
	}
	
    // 깊은 복사를 위해 복사생성자를 명시적으로 선언한 경우
	CDainn(CDainn& _rhs) // 복사 생성자의 경우 생성하는 부분이라 CDainn(클래스) 자체를 받을 수 없어 레퍼런스
	{
		cout << "복사 생성자 호출" << endl;
		m_iA = _rhs.m_iA;
		m_iB = _rhs.m_iB;
		memcpy(iArray, _rhs.iArray, sizeof(_rhs.iArray)); // int형은 memcpy시 오류
	}

	void Print(CDainn& _rhs) // 객체 원본을 받아오므로 복사 생성자 실행 X
	{
		cout << _rhs.m_iA << endl;
		cout << _rhs.m_iB << endl;
	}

	CDainn operator+(CDainn& _rhs) // Temp는 소멸하므로 레퍼런스가 아닌 일반 객체로 반환
	{
		CDainn Temp(m_iA + _rhs.m_iA, m_iB + _rhs.m_iB);
		return Temp;
	}

private:
	int m_iA;
	int m_iB;
	char iArray[5];
};

※ 복사 생성자는 단순 1대1 대입

     ☞ 구조체와 포인터가 있는 경우 꼭 명시적으로 복사 생성자 선언 ( 단순 1대1 대입이 성립되지 않기 떄문 )

     ☞ 구조체의 경우 memcpy() 이용, 포인터의 경우 * 또는 strcpy_s 이용

 

■ 기능 / 주의

  • 복사 생성자 선언 시 (기본 또는 매개변수) 생성자 무조건 선언
    - 복사 생성자도 생성자이므로 복사 생성자 명시적 선언 시 기본 생성자 자동 생성 안됨 (존재 X)
  • 복사 생성자의 매개 변수로 원본 클래스 객체를 받아올 땐 레퍼런스로 받아올 것
    - 복사 생성자는 생성 부분이라 매개변수로 정의 중인 class타입을 받아올 수 없기 때문이다.
    - 복사 생성자를 제외한 메소드에서는 매개변수로 해당 class의 객체를 받아올 수 있다.
  • 복사 생성자는 기본 생성자와 별개로 사본 생성 시 기본 생성자가 아닌 (기본) 복사 생성자 실행
  • 디폴트 복사 생성자는 단순  1대1 대입을 하기 떄문에 구조체 또는 포인터가 있는 경우 명시적으로 코드 짜줘야함
    -  구조체와 포인터는 단순 1대1 대입 시 얕은 복사가 이뤄지기 떄문에 오류 발생
    - 깊은 복사를 위해 복사 생성자를 명시적으로 선언해줘야 한다.
  • 사본은 생성자만 복사 생성자를 실행할 뿐 소멸자는 똑같이 실행
  • 함수의 매개변수 또는 반환타입이 객체인 경우도 매번 복사 생성이 일어나므로 &로 원본값을 받아주도록 하자.
    - 함수는 매개변수를 받을 때와 값을 반환할 때 call by value가 이뤄지기 때문
    - 함수 반환 시 함수 안의 요소는 지역 특성 상 소멸되고 반환값은 임시 객체에 복사되어 해당 라인에 반환하고 소멸
    - 따라서 객체를 반환할 때도 복사가 일어나 복사 생성자 실행되고, 반환된 값은 실행 라인에서만 존재했다가 소멸.

 

■ 예시

- 코드

#include "stdafx.h"
#include <string>

class cDainn
{
public:
	/* 함수 오버로딩
	cDainn(char* str) 
	{
	cout << "cDainn(char* str) 생성자 호출" << endl;
	len = strlen(str);
	strData = new char[len + 1]; // null문자를 고려해 len+1만큼 할당
	cout << "strData 할당 :" << &strData << endl;
	strcpy_s(strData, len+1 , str);
	cout << strData << endl;
	}
	*/

	cDainn(string str) 
	{
		cout << "cDainn(string str) 생성자 호출" << endl;
		len = str.length();
		strData = new char[len + 1]; // null문자를 고려해 len+1만큼 할당 // strData는 멤버 변수로 이미 선언됐으니까 앞에 char로 선언 X
		cout << "strData 할당 :" << &strData << endl;
		strcpy_s(strData, len + 1, str.c_str()); // 문자열을 char배열 strData에 넣기 위해선 c_str()함수를 이용하여 char로 변환										
		cout << strData << endl;
	}

	// 복사 생성자는(자동 실행되는 복사 생성자 기준) 각 멤버 변수를 원본 멤버 변수로 대입하는 기능을 함
	// 이때 포인터(주소)가 있다면 얕은 복사가 이뤄지면서 오류 발생, 그래서 복사 생성자를 직접 선언해주자(깊은 복사 해주자)
	// 얕은 복사 시 소멸자가 실행 될 때 소멸자 안의 동적할당해제가 같은 주소를 2번 해제하면서 오류 발생
	// 레퍼런스로 받는 이유는 복사 생성자는	자기 클래스 자체를 매개변수로 받아올 수 없기 떄문에
	cDainn(const cDainn& rhs)
	{
		// rhs는 별칭이기 떄문에 s1.len과 같음 (s1은 현재 cDainn 클래스 객체)
		// 복사 생성자 미선언시 자동 실행되는 복사 생성자 코드
		/* strData = rhs.strData; // strData는 주소이기 떄문에 얕은 복사 이뤄져서 해당 문법 X
		len = rhs.len; // len은 일반 멤버 변수(값)기 떄문에 깊은 복사 */

		len = rhs.len;
		cout << "cDainn(cDainn& rhs) 복사 생성자 호출" << endl;
		strData = new char[len + 1]; // 복사 생성자 힙 메모리 공간 따로 할당(원본 객체의 len값을 참조)
		cout << "strData 할당 :" << &strData << endl;
		strcpy_s(strData, len + 1, rhs.strData); // 깊은 복사
		cout << strData << endl;
	}

	~cDainn() {
		cout << "~String() 소멸자 호출" << endl;
		delete[] strData;
		cout << "소멸된 strData 주소 : " << &strData << endl;
		strData = nullptr;
		//SAFE_DELETE_ARRAY(strData); 
	}
};

void main(void) {
	cDainn s1("dainn"); // "안녕"은 string과 char*중 char*로 오버로딩, string만 있을 경우 string 실행
	cDainn s2(s1); 
	// 복사 생성자는 기본 생성자와 별개로 개별적인 복사 생성자가 자동 실행이 됨
	// 소멸자는 동일하게 실행

private:
	char* strData;
	int len;
}

- 출력결과

cDainn(string str) 생성자 호출
strData 할당 :0135F9CC
dainn
cDainn(cDainn& rhs) 복사 생성자 호출
strData 할당 :0135F9BC
dainn
[Get 호출]
주소 : 0135F9CC
dainn
~String() 소멸자 호출 
소멸된 strData 주소 : 0135F9BC
~String() 소멸자 호출
소멸된 strData 주소 : 0135F9CC

// 소멸자 호출 순서는 거꾸로 (후입선출 느낌)

 

extern


[ extern ]

: 다른 파일에서 이미 이름이 같은 전역변수가 선언이 되었다는 의미로 존재를 알려주는 키워드

: 같은 이름의 사용권한을 얻어내는 문법, 각 cpp 파일이 합쳐지는 과정(링크)에서 같은 이름의 전역변수끼리 충돌 방지 

[ 1. cpp ]
int i = 200;

[ main.cpp ]
extern int i; // 1.cpp의 전역변수 i와 main.cpp의 전역변수 i는 같은 아이
void main() { cout << i << endl; } // 200 출력

-

* main이 아닌 곳에서 extern 시 인식 못하고 충돌
[ 1. cpp ]
extern int i = 200;

[ main.cpp ]
int i; // main.cpp에서 extern 안하면 충돌, 오류

* i는 동일한 전역변수이므로 선언과 동시에 초기화 중복 불가
[ 1. cpp ]
int i = 200;

[ main.cpp ]
extern int i = 50; // 한 파일에서만 선언과 동시에 초기화해줘야함, 오류
void main() { cout << i << endl; } // 200 출력

 

■ 기능 / 주의

  • extern은 각 .cpp에 같은 이름의 전역변수가 존재할 때 이미 존재함을 알려 충돌을 방지하는 키워드이다.
  • main을 실행하는 메인 cpp 파일에 extern을 붙여줘야 한다. ( 다른 cpp의 변수에 중복으로 extern 붙여도 됨 )
  • 선언과 동시에 초기화를 중복으로 할 수 없다. ( extern 시 해당 변수는 동일한 변수를 사용하는 것이기 떄문 )
  • extern 시 메모리 블럭을 추가로 할당하지 않는다.
    - 다른 .cpp 파일에서 선언된 전역변수를 같이 사용하는 것이기 때문
  • 같은 이름의 다른 자료형은 존재할 수 없다. ( 원래 존재 안됨 )

각 .cpp 파일은 오브젝트가 따로 생성되지만 링크(합치기) 과정에 의해 전역변수 a가 서로 충돌, 오류 발생

friend


[ friend ]

: friend(친구)로 지정한 클래스 또는 함수에게 자신의 모든 멤버에 접근 권한을 허용하는 것 ( private 포함 )

 

■ 기능 / 주의

  • A가 friend B를 하면 B는 A의 모든 멤버에 접근 가능하지만 A는 B의 멤버에 접근할 수 없다.
  • 클래스 내의 public, protected, private 어디에서든 friend를 지정할 수 있다.
  • friend 함수는 클래스 내에서 선언하지만 멤버가 아니다. 즉 객체 선언 또는 소속을 밝히지 않고 단독으로 사용된다. 
  • 객체 지향의 특징인 private을 파괴하는 문법이기 떄문에 잘 안 쓰임, private은 주로 Get() 함수로 접근하여 사용됨.
  • friend 함수는 몸체가 하나만 존재해야 한다. ( 클래스 밖에 구현, 선언과 동시에 정의부 구현 )
    - 선언과 동시에 정의부 구현 시 friend할 객체의 모든 정의를 알고 있는 가장 아래의 함수에서 구현해야 한다.
  • 파일이 다를 경우 전방선언만 한다면 존재만 알고 안의 내용을 몰라서 사용할 수 없다.

예시

 friend 함수

class CB; // 전방참조

class CA {
public:
	CA() {}
	CA(int _iA) : m_iA(_iA) {}

	int Get_A() { return m_iA; }

	friend int Get(CA _A, CB _B); // CB 클래스의 존재를 모르기 떄문에 위에 전방참조
	/* 몸체 구현 주의
	friend int Get(CA _A, CB _B) { return _A.m_iA + _B.m_iB; }
	여기서 선언 시 위에 CB를 전방 참조했어도 존재만 알뿐
	안의 내용물을 모르기 때문에 몸체 코드를 실행할 수 없음.
	정의부는 CA와 CB의 내용물을 모두 아는 CB 또는
	클래스 밖에 정의부를 구현해줘야 한다.
	*/
private:
	int m_iA;
};

class CB {
public:
	CB() {}
	CB(int _iB) : m_iB(_iB) {}

	int Get_B() { return m_iB; }

	friend int Get(CA _A, CB _B) { return _A.m_iA + _B.m_iB; } // 한 friend 함수에서만 정의해줘도 가능
	
private:
	int m_iB = 20;

};

// friend는 멤버가 아니기 때문에 따로 정의부 선언 시 소속 밝히지 X
//int Get(CA _A, CB _B) { return _A.m_iA + _B.m_iB; }

void main(void)
{
	CA ca(10);
	CB cd(20);

	cout << Get(ca, cd) << endl;
}

출력 결과 : 30 // 서로의 private 멤버 접근 성공, m_iA(10) + m_iB(20) = 30

※ friend 함수는 서로 friend를 해줘야 원활하게 사용 가능

 

▶ friend 클래스

class CDainn {
public:
	CDainn() {}
	CDainn(int _iA) : m_iA(_iA) {}

	friend class CC; // CC에게 자신의 모든 멤버 접근 허용
    
private:
	int m_iA;
};

class CC
{
public:
	CC() {}
    
	void Print(CDainn _D) { _D.m_iA = 100; cout << _A.m_iA << endl; } // private인 m_iA 접근 가능
    /*
	void Print(CDainn& _D) { _D.m_iA = 100;} // private인 m_iA 접근 가능
    만약 레퍼런스로 가져왔다면 실제 m_iA 값 100으로 변경,
    위의 CDainn은 call by value이므로 Print 함수 안에서만 100 적용, 원본값은 그대로
    */
}

void main(void) {
	CC cc;
    cc.Print(); // 100 출력 
}