C++ 객체간 대입, 복사 과정에 대해서 질문드립니다.

richjaff의 이미지

#include <iostream>
#include <queue>
#include <sstream>
#include <windows.h>
 
using namespace std;
 
class Test
{
	private:
		int size;
		char* buff;
 
	public:
		Test(){
			size = 10;
			buff=new char[size];
		}
		~Test(){
			cout << "삭제" << endl;
			delete[] buff;
		}
		void operator=(Test& t){
			cout << "복사" << endl;
			this->size = t.size;
			this->buff = new char[this->size];
			memcpy(this->buff, t.buff, this->size);
		}
};
 
int main(int argc, char *argv[])
{
	//1.임시 객체 생성
	Test t;
	{
		Test a;
		t = a;	 // operator= 가 호출됨.
	}
 
	queue<Test> tq;
	{
		Test a;
		tq.push(a);  // operator= 가 호출되지 않고, 객체자체의 복사가 이루어집니다.
		// 따라서, 내부 맴버 buff의 주소값이 복사되어, 두개의 객체간에 buff 주소가 공유되어지며,
		// 임시 객체 a가 블록을 지나 자동소멸되면서,buff의 동적할당은 해제됩니다.
	}
 
	// 여기서 이미 객체 b 내부의 buff주소의 동적할당은 해제되어있는 상태이구요.
	Test& b = tq.front();
 
	// 위와 같은 문제때문에 객체를 사용함에 있어서 참조에러가 발생하게 됩니다.
 
	// 혹시나 해서 표준입출력에 사용되는 string 객체에 대해 동일한 테스트를 해본 결과... 
	queue<string> sq;
	{
		string str = "123";
		cout << "스트링 버퍼 주소값: " << (DWORD)str.c_str() << endl;
		sq.push(str);
		// str은 블록문을 지나는 순간 소멸이 되는데, 이때 string 객체 내부의 동적할당된
                   // 버퍼도 마찬가지로 할당해제가 이루어질 것으로 예상되는데, 이럴 경우 참조에러가 발생하지 않을까요?
	}
 
	// 위에서 string 임시 객체가 가지는 버퍼의 주소를 그대로 사용하고 있더군요.
        	cout << "큐에 위치한 스트링 버퍼 주소값: " << (DWORD)sq.front().c_str() << endl;
 
        // 위에서 string 객체의 소멸자가 호출되면서 내부 버퍼의 동적할당을 해제했다면, 참조에러가 되지만,
        // 표준라이브러리의 string을 그렇게 허접하게 만들었을리는 없을 것이고...
        // 어쨌든 버퍼의 주소는 같은 값을 사용하고 있으니, 위에서 사용한 임시 스트링 객체가 자동 소멸될때,
        // 내부 버퍼의 동적할당을 해제하지 않아야만, 아래처럼 안전하게 사용할 수 있다는 뜻인데,
        // 그렇다면 내부 버퍼는 위에서 임시 객체가 소멸되는 시점에서 할당해제되는게 아니라는 뜻이되니;;;
        // 그런거라면 메모리 누수나 마찬가지고 이건...
        // 혼란스럽네요.
 
 
        // 저는 스트링 복사를 실시할때, string 내부에 오퍼레이터=가 정의되어있어져서
         // 복사를 하는 과정에서 string 내부에 다시 새로운 버퍼를 만들고, 거기에 임시 string 객체의
         // 버퍼를 복사하기 때문에 주소값이 다르게 나올 것이라 예상했는데 아닌것 같더군요.
	string Str;
	{
		string temp = "test";
		Str = temp;
		cout << "스트링 버퍼 주소값: " << (DWORD)temp.c_str() << endl;
	}
	cout << "스트링 버퍼 주소값: " << (DWORD)Str.c_str() << endl;
 
	return 0;
}

string 객체에 대해서 조사해본 바에 의하면,

string 객체는 가변적인 문자열 데이터를 객체내에 직접 가지지 않으며 동적으로 
메모리를 할당하여 관리할 것임을 쉽게 추측할 수 있다. 생성자가 데이터 저장을 위해 
메모리를 할당하고 있으므로 파괴자에서는 당연히 이 메모리를 해제해야 한다. 
파괴자가 필요한 처리를 하므로 객체가 사라질 때 별도의 처리를 할 필요가 없으며 
지역 객체일 경우 쓰다가 그냥 버리기만 하면 된다. 예제의 s1~s6 객체들은 모두 
main 함수의 지역 객체이므로 별도의 정리 코드가 필요없다.

그렇다면 위에서 테스트 한 부분은 다소 문제의 여지가 있어보이는데,
이 부분에 대해서 명쾌하게 답을 내려주시면 정말 감사드리겠습니다.

첫번째 테스트

	{
		string k;
		string *o = new string;
		*o = "111";
		k = *o;
		cout << (DWORD)(*o).c_str() << endl;   // 메모리주소 A
		delete o;
	}
 
	{
		string *o = new string;
		*o = "111";
		cout << (DWORD)(*o).c_str() << endl;   // A와 같음 (주소를 재사용하고 있다는 뜻이므로, 위에서 할당해제되었음.)
	}

두번째 테스트
k가 *o로부터 받은 스트링의 주소를 보유하고 있기 때문에, 두번째 블록에서부터는 다시
메모리주소 A값에 대해서 메모리의 재활용이 이루어지지 않고 있습니다.
(이때, k = *o; 를 실시한 시점에서 두개의 객체가 가지는 buffer의 주소값은 같구요,
이때, 첫번째 블록을 지나는 순간 o가 delete 될때, k객체가 가지는 동일한 buffer주소에 대하여 할당해제를 실시 하지 않는데,
왜냐하면, k에서 해당 버퍼 주소값을 보유하고 있기 때문에 당연한 소리지만,
어떻게 해서 삭제가 안되는지 원리가 궁금합니다.
o가 소멸자를 호출할때 일일히, buffer를 사용하는 또 다른 객체가 있는지 확인하는 것은 아닐테고요...)

	string k;
	{
		string *o = new string;
		*o = "111";
		k = *o;
		cout << (DWORD)(*o).c_str() << endl;   // 메모리주소 A
		delete o;
	}
 
	{
		string *o = new string;
		*o = "111";
		cout << (DWORD)(*o).c_str() << endl;   // B
	}

세번째 테스트
메모리 주소가 같으므로, 할당해제된 메모리의 재사용이 이루어지고 있다는 뜻,
즉, 첫번째에서 k와 *o가 사용햇던 스트링 버퍼의 주소를 갖고 있는 객체가 블록을 지나는 순간 더이상 없으므로
다음 string 객체를 생성할때에서 볼 수 있듯이, 버퍼가 말끔히 할당해제 되었음을 알 수 있습니다.

	string k;
	{
		string *o = new string;
		*o = "111";
		k = *o;
		k="";  // k를 바꿈
		cout << (DWORD)(*o).c_str() << endl;   // 메모리주소 A
		delete o;
	}
 
	{
		string *o = new string;
		*o = "111";
		cout << (DWORD)(*o).c_str() << endl;   // A
	}

신기하네요.
어떤 원리로 저렇게 될 수 있는지 좀 알려주시면 정말 감사드리겠습니다.

결론적으로,

//========================== CODE 1 ====================================================/
	string k;
	{
		string *o = new string;
		*o = "111";
		k = *o;
		cout << (DWORD)(*o).c_str() << endl;   // 메모리주소 A
		delete o;
	}
 
	{
		string *o = new string;
		*o = "111";
		cout << (DWORD)(*o).c_str() << endl;   // 메모리주소 B (A는 k가 보유중.)
	}
 
 
//========================== CODE 2 ====================================================/
	string k;
	{
		string *o = new string;
		*o = "111";
		k = *o;
		k="";  // <---------- added
		cout << (DWORD)(*o).c_str() << endl;   // 메모리주소 A
		delete o;
	}
 
	{
		string *o = new string;
		*o = "111";
		cout << (DWORD)(*o).c_str() << endl;   // 메모리주소 A 
		//메모리 재활용 - 위에서 사용중인 객체 없음. 할당해제 되었다는 증거
	}

코드 1과 코드2가 가지는 차이점과 이유에 대해서 궁금합니다.
알려주시면 정말 감사드리겠습니다.

neogeo의 이미지

만드신 class 에 copy constructor 를 만드시면 됩니다. assign operator 로 복사가 되지 않고 copy constructor 가 불리워져서 복사가 됩니다.

예측하신대로 std::string 은 그런거 다 고려해서 만들어져 있으므로 문제가 되지 않습니다.

Neogeo - Future is Now.

Neogeo - Future is Now.

richjaff의 이미지

마지막 테스트는 도대체 어떤 원리로 저렇게 될 수 있는지 궁금합니다.
CODE 1과 CODE 2의 차이점에 대해서 혹시 아시면 설명좀 부탁드리겠습니다.

원래 궁금했던점은 답변자분께서 알려주신 답이었지만,
string을 추가적으로 테스트 하는 과정에서 의문사항을 유발하는 부분들이 추가적으로 나오더군요.

답변해주신 부분은 원래 질문의도에 맞는 답이었는데,
이 부분에 대해 알려주신것에 대해서는 정말 감사드립니다.

추가적으로 string에 대한 위의 의문점은 어찌 저렇게 될 수 있는지 궁금합니다.
또 테스트를 해본결과,

string* ss[10]={};
for(int i=0; i<10; i++){
  ss[i] = new string;
}
 
for(int i=0; i<10; i++){
  cout << (DWORD)ss[i]->c_str() << endl;  // 주소값이 전부 같음
}

string은 초기화 될때, 문자열 값으로 "" 완전한 공백을 가지게됩니다.
이때, 완전한 공백의 문자열로써, 가리키는 주소가 하나같이 같더군요.
그렇다면, string 내부에

class string
{
private:
  char* buffer; //내부문자열 포인터
  static const char blank[]="";
public:
  string() : buffer(blank){
  }
}

이와 비슷하게 정의되어있다는 것을 대강 추측해볼 수 있는데,
그렇기 때문에, 소멸자가 호출되는 순간에 무조건 buffer를 해제하는 구문이 있는것은 아닐테고,
(왜냐하면, 위에서 테스트 한것에서도 알 수 있듯이, string k = l; 이런식으로 대입할때,
메모리 자체가 복사되는게 아니라, k과 l이 같은 주소값을 가지는 것에서 알 수 있듯이,
주소값만 복사되고 있습니다.
그런데 이때, string k를 자동소멸 시킬때 소멸자 내부에서 buffer를 할당해제 시킨다면,
l이 가지고 있는 buffer는 참조에러를 유발하게 될 것입니다.
또는, 반대로 k를 남기고 l을 삭제해도 k는 참조 에러(구체적으로 내부에서 쓰는 스트링 buffer에 대한 참조에러)를 유발할 것입니다.
그렇기 때문에 이때, k혼자 삭제될 때에는 buffer주소를 l이 쓰고 있기 때문에 k혼자 삭제되는 시점에서는
할당해제가 이루어져서는 안되고, 역시나 테스트 해본결과 이때는 할당해제가 이루어지지 않습니다.
하지만 나머지 k도 삭제되면 그제서야 buffer에 대한 할당 해제가 이루어지고,
다음 블록에서부터는 할당해제가 이루어졌다는 증거로, 위에서 사용했던 버퍼 메모리주소에 대한 재활용이 이루어지게 됩니다.
즉, string에서 소멸자 호출시, buffer를 삭제하는 기준이 뭔지 궁금합니다.)

CODE 1, CODE 2의 차이점도 소멸자에서 buffer를 삭제하느냐, 마느냐를 판가름하는 기준이 있을 것 같다고 생각됩니다.
이 기준이 무엇인지 궁금합니다.

mithrandir의 이미지

추측컨데, buffer를 pointer로 가지고 있는게 아니라 reference가능한 객체로 들고있고, 그 객체엔 reference counter가 존재해서, 그 count가 0이 되면
해당 buffer를 삭제하지 않을까요? string의 구현을 한번 살펴보시는 것이 좋겠습니다.

언제나 삽질 - http://tisphie.net/typo/
프로그래밍 언어 개발 - http://langdev.net

언제나 삽질 - http://tisphie.net/typo/
프로그래밍 언어 개발 - http://langdev.net

M.W.Park의 이미지

copy on write 또는 implicit sharing으로 구글링해보세요.

-----
오늘 의 취미는 끝없는, 끝없는 인내다. 1973 法頂

-----
오늘 의 취미는 끝없는, 끝없는 인내다. 1973 法頂

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.