11. 클래스와 동적 메모리 대입(1) - class 내 static 변수, 복사 생성자, 대입 연산자

WanJu Kim·2022년 12월 10일
0

C++

목록 보기
47/81

다음 코드를 보자.

StringBad.h
#pragma once
#include <iostream>

class StringBad
{
private:
	char* str;
	int len;
	static int num_strings;
public:
	StringBad(const char* s);
	StringBad();
	~StringBad();

	friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
};

StringBad.cpp
#include "StringBad.h"
using std::cout;

int StringBad::num_strings = 0;

StringBad::StringBad(const char* s)
{
	len = std::strlen(s);
	str = new char[len + 1];
	strcpy_s(str, len + 1, s);
	num_strings++;
	cout << num_strings << ": \"" << str << "\" 객체 생성\n";
}

StringBad::StringBad()
{
	len = 4;
	str = new char[4];
	strcpy_s(str, 4, "C++");
	num_strings++;
	cout << num_strings << ": \"" << str << "\" 디폴트 객체 생성\n";
}

StringBad::~StringBad()
{
	cout << "\"" << str << "\" 객체 파괴, ";
	--num_strings;
	cout << "남은 객체 수 : " << num_strings << "\n";
	delete[] str;
}

std::ostream& operator<<(std::ostream& os, const StringBad& st)
{
	os << st.str;
	return os;
}

main.cpp
#include "StringBad.h"

using namespace std;

void CallMe1(StringBad&);
void CallMe2(StringBad);

int main()
{
	{
		cout << "내부 블록 시작" << endl;
		StringBad headline1("Celery Stalks at Midnight");
		StringBad headline2("Lettuce Prey");
		StringBad sports("Spinach Leaves Bowl for Dollars");
		cout << "headline1: " << headline1 << endl;
		cout << "headline2: " << headline2 << endl;
		cout << "sports: " << sports << endl;
		CallMe1(headline1);
		cout << "headline1: " << headline1 << endl;
		CallMe2(headline2);
		cout << "headline2: " << headline2 << endl;
		cout << "하나의 객체를 다른 객체로 초기화:\n";
		StringBad sailor = sports;
		cout << "sailor: " << sailor << endl;
		cout << "하나의 객체를 다른 객체에 대입:\n";
		StringBad knot;
		knot = headline1;
		cout << "knot: " << knot << endl;
		cout << "이 블록을 빠져나온다.\n";
	}
	cout << "main()의 끝\n";	
}

void CallMe1(StringBad& rsb)
{
	cout << "참조로 전달된 StringBad:\n";
	cout << "     \"" << rsb << "\"\n";
}

void CallMe2(StringBad sb)
{
	cout << "값으로 전달된 StringBad:\n";
	cout << "     \"" << sb << "\"\n";
}

실행 결과.(마지막엔 에러가 떴다.)

왜 이런 결과가 나왔을까?

class 내의 static 변수

우선 클래스 정의 안에 있는 'static int num_strings'를 보자. 원래라면 static 변수는, 정의된 파일 내에서만 쓰이고 데이터가 프로그램 끝날 때까지 남아있는 변수를 의미한다. 클래스에서 사용하면 좀 변한다. 클래스 정의 내의 static 변수는 모든 클래스 객체가 공유한다. 예를 들어 객체1에서 num_strings를 1 증가시켰다? 그 변수가 객체2에서도 유효한 것이다.

이런 특징을 가져서 초기화 할 때도 좀 다르게 한다. cpp 파일의 외부에 떡하니 초기화 한다. 왜? 그렇지 않다면 정의할 곳이 없다. 클래스 선언 부분에 하자니, 여기는 메모리를 대입하는 곳이 아니라, 어떻게 대입할지 서술하는 곳이다. 객체를 만들어야 비로소 메모리가 생성된다. 그렇다고 생성자에서 하기에는 모든 객체가 공유하는 특징이 있는데 객체 만들 때마다 초기화하면 의미가 없어지기 때문이다.

int StringBad::num_strings = 0;	// static 변수 초기화하는 방법.

static 변수를 꺼내오려면 어떻게 해야 할까? static 멤버는 그 특징 덕분에 객체에 의해 호출되지 않는다. this 포인터도 갖지 않는다. 만약 public에 선언되었다면, 다음과 같이 호출이 된다.

StringBad::HowMany();
or
StringBad::num_strings;

// 함수 원형
static int HowMany() {return num_strings;}

static 메서드는 static 변수처럼 객체를 생성하지 않고 호출할 수 있다. 물론 public일 경우에 가능하다. 또한 static 멤버 변수만 사용할 수 있다. 왜? 객체에 의해 호출되는 것이 아니기 때문이다.

만약 클래스 내의 static 클래스 변수를 초기화 하려면? 클릭

복사 생성자

그 다음에 실행 결과를 보자. 코드를 잘 읽어보면 CallMe2 함수에서 뭔가 문제가 생겼음을 알 수 있다. CallMe2함수가 끝나고 원치않는 파괴자가 호출되었다.

또한 StringBad sailor = sports를 보자. StringBad knot; knot = headline1과 달리 선언과 동시에 초기화를 하니까 디폴트 생성자가 나오지 않았다. 그렇다고 매개변수를 사용한 생성자도 나오지 않았다. 그래서 num_strings도 변하지 않았다. 그럼 생성자를 생략했나? 사실 위와 같은 초기화는 다음과 같다.

StringBad sailor = sports;	// 이 구문은
StringBad sailor = StringBad(sports);	// 이 구문과 같다.

그리고 그에 알맞은 생성자는 다음과 같다.

StringBad(const StringBad &);

이처럼 프로그램에서 하나의 객체를 다른 객체로 초기화 하면 사용되는 생성자를 우리는 '복사 생성자'라고 부르기로 했다. 복사 생성자는 객체의 사본을 만든다. 다음은 복사 생성자를 호출하는 코드의 예시다.

ClassName object1(object2);
ClassName object1 = object2;
ClassName object1 = ClassName(object2);
ClassName* object1 = new ClassName(object2);

2,3 번째 선언은 C++ 시스템에 따라서 복사 생성자를 직접 사용해서 object1를 생성할 수도 있고 아니면 복사 생성자로 임시 객체를 만든 다음 대입할 수도 있다.

복사 생성자에서는 무슨 일이 일어날까? 디폴트 복사 생성자는 static 멤버를 제외한 멤버들을 멤버별로 복사한다.(혹은 얕은 복사라고도 한다.) 예를 들어

StringBad sailor = sports;

코드는 private 접근이 안되지만 접근이 된다고 가정하면

StringBad sailor;
sailor.str = sports.str;
sailor.len = sports.len;

와 같다. 그래서 클래스를 매개변수로 전달할 때는 값에 의한 전달보다는 참조에 의한 전달이 시간, 메모리 공간면에서 좋다.

그래서 다시 문제로 돌아와 보자. CallMe2 함수가 끝나고 파괴자가 왜 호출이 되었는가? 바로 복사 생성자가 사용되었기 때문이다. 파괴자는 생성자가 실행 되면 마지막에 무조건 실행된다. 위의 코드에서는 그 부분을 신경쓰지 못했다. 따라서 명시적인 복사 생성자를 만들어 주면 문제를 해결할 수 있다.

StringBad::StringBad(const StringBad& s)
{
	num_strings++;
	...
}

근데 이렇게 해도 문제가 생긴다. 왜? 복사 생성자의 내용 중 일부분이 동적 메모리를 담당하고 있기 때문이다. 그러니까 값을 복사하는 과정에서 '주소'를 복사하기 때문이다. 그게 왜 문제냐고? 파괴자가 호출이 되면 delete를 통해 이 메모리가 해제가 되는데, 문제는 이게 복사한 거라서, 나중에 한 번 더 해제를 해야 하기 때문이다. 해제를 하려고 하는데 해제할 것이 없다? 이건 굉장히 위험한 결과를 초래할 수 있다.

어쨌든 그러면 어떻게 해야 할까? 당연히(?) 복사할 때 다시 새로운 동적 메모리 주소를 만드면 된다. 이를 '깊은 복사'라고 한다. 단순히 값만 복사하는 '얕은 복사'와 대비되는 단어다.

StringBad::StringBad(const StringBad& s)
{
	len = s.len;
	str = new char[len + 1];
	strcpy_s(str, len + 1, s.str);
	num_strings++;
	cout << num_strings << ": \"" << str << "\" 객체 생성\n";
}

대입 연산자

근데 이렇게 해도 문제가 생긴다. (정말 코드 잘 만들었다.) 위의 코드에서는 대입 연산자가 사용되었다. 바로 이 문구에서다.

StringBad knot;
knot = headline1	// 대입 연산자 호출.

StringBad metoo = knot;	// 선언과 동시에 초기화는 복사 생성자 호출.

대입할 때도 복사 생성자에서와 같이 얕은 복사가 일어난다. 즉 그 다음의 파괴자 호출에서 같은 주소를 두 번 해제하려고 해서 문제가 생긴다. 이를 해결하기 위해서는 깊은 복사와 또 다른 게 필요하다.

  • 대입 받을 객체가 동적 메모리를 가지고 있다면, 미리 해제시켜야 한다. 왜? 그렇지 않으면, 대입 후 기존의 동적 메모리를 해제시킬 방법이 없다.
  • 자기 자신에게 대입하지 못하게 막아야 한다. 왜? 방금 첫 번째에 말했듯이, 기존에 가지고 있는 동적 메모리를 먼저 해제해야 하는데, 그 과정에서 대입 넣을 동적 메모리가 해제되기 때문이다.
  • 호출 객체에 대한 참조를 리턴해야 한다. 왜? 다음 코드를 작성하기 위해서.
S0 = S1 = S2;

이것을 함수형으로 표기하면

S0.operator=(S1.operator=(S2));

호출 객체에 대한 참조를 리턴해야 위의 식이 성립이 된다. 그래서 이런 수정 사항을 고려해서 대입 연산자를 작성해보겠다.

StringBad& StringBad::operator=(const StringBad& st)
{
	if (this == &st)
		return *this;
	delete[] str;		// 기존 메모리를 지워준다. 없을 수도 있지 않냐고? 디폴트 생성자조차도 메모리 할당 받았다.
	len = st.len;
	str = new char[len + 1];	// 새로운 메모리 대입.
	strcpy_s(str, len + 1, st.str);
	return *this;
}

이러면 드디어 모든 문제가 해결이 되었다.

profile
Question, Think, Select

0개의 댓글