(C++) 멤버변수의 동적할당

프로그래밍을 하다보면 클래스 내에서 동적 할당이 필요한 경우가 생긴다.
문자열의 경우가 대표적인 예인데, 이땐 필요한 만큼 char 배열을 할당해서 사용해야 한다.

문제는 객체가 복사될 때 객체가 가져야 할 모든 정보가 복사되지 않고 동적 할당한 메모리를 가리키는 포인터 변수(char* 등)만 복사된다는 것이다.

그래서 복사된 하나의 객체 내용을 변경하면 나머지 다른 객체의 내용도 변하게 되고
뜻하지 않은 자료의 변형을 초래할 수 있다.
더군다가 이런 버그는 찾기도 힘드니 애초부터 습관을 잘 들여놔야한다.

잠깐 여기서 질문!
"그럼 const로 받으면 변형 못시키니 문제없지 않나요?"

물론 직접 접근을 사용한 변경은 할 수 없다.
하지만 멤버 함수에서 행하는 변경까지 막을 수는 없다 -_-;
그 예를 아래에서 살펴보자.



고객 관리 소프트웨어를 만드는 ABC사는 고객 정보를 저장하기 위해 Person 클래스를 만들었다.

class Person
{
 char *name; // 이름
 char *phone; // 전화번호
 int age; // 나이

public:
 void ShowData(); // 정보 출력 : 내용은 생략!
};

그리고 값을 입력하기 위해 생성자를 다음과 같이 선언하였다.
단, 이름과 전화번호의 길이가 일정하지 않기 때문에 동적 배열로 할당해서 저장하도록 하였다.

Person::Person(char* _name, char* _phone, int _age)
{
 name = new char[strlen(_name)+1]; // 길이만큼 배열 선언. +1은 널문자 공간
 strcpy(name, _name); // 입력받은 값을 복사해서 저장

 phone = new char[strlen(_phone)+1];
 strcpy(phone, _phone);

 age=_age;
}

동적 할당된 메모리는 사용 후 반드시 해제해줘야 한다.
(그렇지 않으면 프로세스가 종료될 때 까지 메모리 공간을 차지하게 된다)

클래스 내에서 사용된 메모리는 클래스가 닫힐 때 더이상 쓰지 않게 되므로
소멸자에서 delete 해주도록 하였다.

Person::~Person()
{
 delete []name; // 배열 해제
 delete []phone;
}

자 그럼 다 되었나?
언뜻 보기에 완벽하다.

그리고 대부분 프로그램에서는 문제없이 돌아갈 것이다.
ABC사는 고객 정보를 하나 둘씩 저장하며 소프트웨어를 유용하게 사용하고있다.

그런데 어느날, 고객 정보를 인터넷으로 전송하기 위해 다음과 같은 함수를 추가하게 되었다.

void Send( const Person p )
{
 // ...생략
}

const 선언으로 객체 내부 정보를 변경할 수 없고 전송 부분도 정확하게 구현하였다.
이 함수는 별 문제가 없어 보였다.

하지만 실제로는 아주 심각한 문제를 안고 있다.
바로 함수가 실행될 때마다 고객 정보가 지워지는 것이다!

이게 무슨 말인가...!

함수 내의 객체 p는 파라미터로 전달된 원본 객체의 내용을 복사해서 사용하고있다.
함수가 끝나면 객체 p는 자동으로 소멸된다.

바로 이때 소멸자를 호출하게 되는데 이것이 문제의 화근이었다.
멤버 변수가 기리키는 메모리 번지수는 원본 객체나 복사된 객체나 모두 동일하고
복사된 객체의 소멸자가 호출됨과 동시에 원본 객체의 내용을 모두 delete 해버린 것이다!

사용자 삽입 이미지


ABC사는 이 문제를 해결하기위해 복사 생성자를 사용하기로 하였다.
포인터 멤버변수가 가리키는 주소의 내용까지 모두 복사하도록 설정하면 이런 문제가 해결된다.

Person::Person(const Person& p) // 복사생성자 타입
{
 name = new char[strlen(p.name)+1]; // 원본과 동일한 크기의 배열을 만들고
 strcpy(name, p.name); // 원본 내용을 그대로 복사한다.

 phone = new char[strlen(p.phone)+1];
 strcpy(phone, p.phone);

 age = p.age;
}

이제 복사된 객체의 내용이 변경되어도 원본에는 영향을 미치지 않는다.
아래는 이 내용을 나타내는 전체 소스코드이다.
복사 생성자의 내용을 주석 처리하고 실행하면 데이터가 깨지는 것을 확인할 수 있다.


#include < iostream >
using namespace std;

class Person
{
public:
 char *name;
 char *phone;
 int age;

public:
 Person(char* _name, char* _phone, int _age);
 Person(const Person& p);
 ~Person();
 void ShowData();
};

Person::Person(const Person& p)
{
 name = new char[strlen(p.name)+1];
 strcpy(name, p.name);

 phone = new char[strlen(p.phone)+1];
 strcpy(phone, p.phone);

 age = p.age;
}

Person::Person(char* _name, char* _phone, int _age)
{
 name = new char[strlen(_name)+1];
 strcpy(name, _name);

 phone = new char[strlen(_phone)+1];
 strcpy(phone, _phone);

 age = _age;
}

Person::~Person()
{
 cout << "소멸자호출" << endl;
 delete []name;
 delete []phone;
}

void Person::ShowData()
{
 cout << "name: " << name << endl;
 cout << "phone: " << phone << endl;
 cout << "age: " << age << endl;
}

void TestFunc( const Person p )
{
 cout << "함수 호출" << endl;
}

void main()
{
 Person p1("KIM", "013-333-5555", 22);
 Person p2 = p1;

 TestFunc(p2);

 p1.ShowData();
 p2.ShowData();
 
 cout << "프로그램 끝" << endl;
}

출처: http://snbosoft.tistory.com/entry/

댓글

이 블로그의 인기 게시물

(네트워크)폴링방식 vs 롱 폴링방식

(ElasticSearch) 결과에서 순서 정렬

(18장) WebSocekt과 STOMP를 사용하여 메시징하기