본문 바로가기

3.구현/C or C++

[c/c++] 구조체 복제에 대한 이야기

가지고 있는 자료을 똑같은 복제본을 만들어야 될때는 매우 많다. 수십가지 데이터 혹은 수십개의 트리로 구성된 구조체를 일일히 하나씩 값을 넣는 다는 것은 곤혹스러운 일이 아닐 수 없다.

다음은 구조체에 대한 것으로 구조체를 복사하는 것에 대한 내용을 다루겠다.

작성자: ospace (ospace114 at empal.com) ospace.tistory.com/

전형적인 구조체 복제

전형적인 구조체 복제 사용법은 다음과 같다.

struct tag_Sport
{
    TCHAR* pName;
    TCHAR  pPlayer[10];
    int      score;
};

int main()
{
    tag_Sport soccer0 = {NULL, _T("Ospace"), 0};
    tag_Sport soccer1;

    TCHAR name[] = _T("scoccer");
    // soccer0.pName = (char*)malloc(sizeof(name)); //예전 C 스타일
    soccer0.pName = new TCHAR[sizeof(name)]; // C++ 스타일
    _tcscpy_s(soccer0.pName, sizeof(name), name);

    // memcpy( &soccer1, &soccer0, sizeof(soccer0)); // C 함수
    CopyMemory( &soccer1, &soccer0, sizeof(soccer0));  // Win api


    //free(soccer0.pName);
    delete soccer0.pName;
}

위의 코드는 확인 안된 것이므로 틀릴 수 있다. ㅡ.ㅡㅋ (아마도 틀린 것 같다.)
구조체 멤버 중에서 포인터가 들어가면 절대 안된다. 동적 메모리 할당을 하면 다른 위치에 할당이 된다. 구조체에서 메모리할당은 연속적으로 되다는 가정하에서 메모리 복제가 이뤄지기 때문이다.

(물론, 해당 포인터도 추적하여 일일히 복제해주면 된다.)

즉, 다음과 같이 수정해야 한다.

struct tag_Sport
{
    TCHAR pName[10];
    TCHAR pPlayer[10];
    int      score;
};

그러면 pName에 대한 초기화 부분도 수정되야 한다.

만약 포인터에 대한 멤버 변수를 그대로 사용하고 싶다면 깊은복사(Shallow copy)를 사용하면 된다.
앞에 main함수가 다음과 같이 변경되면 된다.

int main()
{
    tag_Sport soccer0 = {NULL, _T("Ospace"), 0};
    tag_Sport soccer1;

    TCHAR name[] = _T("scoccer");
    // soccer0.pName = (char*)malloc(sizeof(name)); //예전 C 스타일
    soccer0.pName = new TCHAR[sizeof(name)]; // C++ 스타일
    _tcscpy_s(soccer0.pName, sizeof(name), name);

    // memcpy( &soccer1, &soccer0, sizeof(soccer0)); // C 함수
    //CopyMemory( &soccer1, &soccer0, sizeof(soccer0));  // Win api
    // 일단 soccer0의 값 모두를 soccer1으로 복사
    // 일반 변수는 복사된다. 참조가 아니다.
    // 단순 할당 연산자로 값이 복사 포인터 부분은 따로 값을 넣어줘야 한다.
    soccer1 = soccer0;
    soccer1.pName = new TCHAR[sizeof(soccer0.pName)];
    _tcscpy_s(source1.pName, sizeof(source0.pName), source0.pName);

    delete soccer0.pName;
    delete soccer1.pName;
}

위의 메모리 복제 함수가 CopyMemory, memcpy 두가지가 있다. 차이점은 없다. CopyMemory가 memcpy를 사용한다. 즉, CopyMemory가 memcpy로 선언되어 있다.

#define CopyMemory RtlCopyMemory // winbase.h
#define RtlCopyMemory(Destination,Source,Length) memcpy((Destination),(Source),(Length)) // winnt.h

구조체내 string형

전형적인 구조체 복제에는 데이터만 들어가고 포인터, 함수나 연산자가 없다. 이것이 중요하다. 그리고 구조체 내에 다시 구조체가 있는 경우도 고려되야 한다.
STL에 string과 MFC/ATC에 CString은 아래 구조체에서 저장되는 형태가 같다고 보면 된다.

다음예를 보자

#include <string>
using std::string;

struct tag_Sport
{
    string pName;  //(1)
    char  pPlayer[10]; //(2)
    int      score;
};

(1)은 string 형으로된 문자열이다. 내부적으로 배열로된 구조를 가지지만 별도의 주소를 가지게 된다. 즉 다른 곳에 메모리를 가지게 된다. 이것도 연속적인 메모리 공간이 아닌 다른 곳에 메모리를 할당하게 된다. 구조체 내에는 단지 주소만 가지고 있다.
이를 다음과 같이 복제해서 사용할 경우,

tag_Sport sport1 = {"Basketball", "Ospace", 60};
tag_Sport sport2;

CopyMemory(&sport2, sport1, sizeof(sport1)); //(1)
sport1.pName = "Baseball"; //(2)

위와 같이 되면 sport1과 sport2의 pName 결과가

(1) : sport1.pName => "Basketball", sport2.pName => "Basketball"  
(2) : sport1.pName => "Baseball",   sport2.pName => "Baseball"  

두 구조체가 똑 같이 변해버린다.
만약 sport1이 포인터로 초기화하고 sport2에 복제한 후 sport1를 제거했다면, sport2의 pName도 같이 제거되어 포인터 오류가 발생한다.
그러나 string이 컨테이너 내부에 들어가는 경우에는 이상없이 적용이 된다. 즉, 제대로 복사가 된다.
이는 나중에 컨터이너를 다루는 부분에서 더 자세히 보겠다.

구조체내 함수 사용

구조체 내에 함수를 정의해서 사용하는 경우가 있다.

struct tag_Sport
{
    TCHAR pName[10];
    TCHAR pPlayer[10];
    int GetString(TCHAR& str, int num);
    int      score;
};

int tag_Sport::GetString(TCHAR& str, int num)
{
    TCHAR buf[50];
    int nBuf = _stprintf_s(buf, sizeof(buf), _T("%s[%s]=>%d"), pName, pPlayer, score);
    CopyMemory(&str, num, buf);
    return nBuf;
}

위를 사용한 예제를 보면

int main()
{
    tag_Sport sport1 = {_T("Swimming"), _T("Ospace"), 80};
    tag_Sport sport2;

    CopyMemory(&sport2, &sport1, sizeof(sport1));
}

위의 예제는 이상없이 동작한다. 정확하게 작동한다. 즉 함수에 상관없이 작동이 된다. 실제 구조체 크기를 계산하면 UNICODE이면 44bytes가 나온다. 즉 함수에 대한 부분은 고려하지 않는다는 의미이다.
그러나, 가상함수 인경우는 44bytes에서 48bytes로 늘어난다. 구조체 앞쪽에 0xf8994e00으로 촛 8바이트이 vtbl(가상함수 테이블) 포인터 주소가 들어간다.
전에 있던 동적할당된 포인터를 해제해도 해당 가상 테이블은 그대로 살아 있기 때문에 실행하는데는 없지만, 해당 메모리는 해제되었기에 다른 클래스나 구조체에 의해서 덮어쒸어질 수 있기에 안전하지 못하다.

구조체에 함수 사용시 주의점은 가상함수를 사용해서는 안되지만, 일반 멤버 함수 사용에는 문제가 없다. 그러나 이것도 VC++에서 되는 것이지 다른 C++에서 된다는 보장이 없다.
(확인해봐야되는데, C++표준에는 어떻게 되었는지도 확인해봐야 하는뎅 ㅡ.ㅡ)

구조체 내에 구조체 복제

앞 내용을 잘 이해했으면 대충 어떻게 될지 짐작이 된다.

struct tag_Sport
{
    TCHAR m_name[10];
    int score;
    TCHAR m_player[10];
};

struct tag_Fitness
{
    tag_Sport m_member;
    TCHAR     m_org[10];
};

구조체가 위와 같을 경우 고려해보자.

int main()
{
    tag_Sport sport = {_T("Walking"), _T("Ospace"), 30};
    tag_Fitness fitness0 = {sport, _T("JJang")};
    tag_Fitness fitness1;

    CopyMemory(fitness1, fitness0, sizeof(fitness0));
}

위에서 구조체 끼리 인경우 그냥 대입하면 된다. (ㅡ.ㅡ; 사실 위와 같이 해줄 필요가 없다.)
그냥 다음과 같이 해주면 된다.

fitness0.m_member = sport;  

이것도 이상 없이 완벽하게 복제가 된다. UNICODE인 경우 구조체 크기는 64byte가 된다.

구조체 배열인 경우에 복제

이 경우는 구조체 배열이다. 어쩌면 이를 사용하기 위해 구조체 복제가 있을 것이다. 이 경우에 구조체 앞에 새로운 값이 들어간다.

struct tag_Sport
{
    TCHAR m_name[10];
    TCHAR  m_player[10];
    int    score;
};

int main()
{
    tag_Sport sport[2] = {
       {_T("Swimming"), _T("Ospace"), 60},
       {_T("Walking"), _T("Kida"), 80}};
    tag_Sport* pSport = NULL;
    tag_Sport sport1[2];

    CopyMemory(sport1, sport, sizeof(sport));
}

결과는 아래와 같다. 총 바이트 수는 88bytes이다. 연속적으로 값이 배열되었기 때문에 CopyMemory에 의해서 값 복제가 가능하다.

0x0012F0F0  53 00 77 00 69 00 6d 00 6d 00 69 00 6e 00 67 00  S.w.i.m.m.i.n.g.  
0x0012F100  00 00 00 00 4f 00 73 00 70 00 61 00 63 00 65 00  ....O.s.p.a.c.e.  
0x0012F110  00 00 00 00 00 00 00 00 64 00 00 00 57 00 61 00  ........d...W.a.  
0x0012F120  6c 00 6b 00 69 00 6e 00 67 00 00 00 00 00 00 00  l.k.i.n.g.......  
0x0012F130  4b 00 69 00 64 00 61 00 00 00 00 00 00 00 00 00  K.i.d.a.........  
0x0012F140  00 00 00 00 3c 00 00 00 cc cc cc cc cc cc cc cc  ....<...........  

포인터로 된 경우도 마찬가지이다. 다음과 같이 사용할 수 있다.

tag_Sport* pSport = new tag_Sport[2];

CopyMemory(pSport, sport1, sizeof(*pSport));

위에서 주의 할 점이 있다. "new tag_Sport[2]"에 의해서 할당되는 크기가 44bytes로 구조체 한개의 크기만 할당이 된다. 이렇게 되면 전체 구조체에 대한 정보를 담을 수 없게 된다. 이에 대해 주의해야 한다. 그러면 다음과 같이 할당할 수 있다.

tag_Sport* pSport = (BYTE*) new BYTE[sizeof(tag_Sport)*2];

여기서 sizeof에 의해서 해당 변수 크기를 알아내는데 변수명를 직접 넣은 경우가 있고 자료형을 넣은 경우가 있다. 예를 들어,

tag_Sport sport;
int n1 = sizeof(sport);
int n2 = sizeof(tag_Sport);

위의 둘가 44bytes로 같다.(내부에 선언된 문자열이 배열로 된경우)
그러나 내부에 포인터로 있는 경우는 달라질 수 있다. 즉 내부 포인터 주소 크기인 8byte로 변경되서 사용된다. 이부분에 대해서 주의해야 한다.

tag_Sport* pSport;
int n3 = sizeof(pSport);
int n4 = sizeof(tag_Sport);
int n5 = sizeof(*pSport);

위의 경우 n3는 8bytes로 나온다. 즉 주소 크기이다. n4와 n5가 제대로 44bytes 를 가진다.

구조체와 STL 컨테이너를 사용한 예제를 보자.

다음은 vector 컨테이너를 이용한 tag_Sport 구조체에 대한 처리이다.

std::vector<tag_Sport> vecSport1, vecSport2;
// 생략 : vecSport1에 데이터 추가
std::copy(vecSport1.begin(), vecSport1.end(), std::inserter(vecSport2, vecSport2.end()));

지금은 컨테이너가 vector형이기 때문에 표준에 의하면 Vector은 연속된 공간에 배열되기에 CopyMemory에 의해서 복사가 가능하다. 그러나 구조체 포인터가 들어가거나 다른 자료형에 대해서는 보장할 수 없다.

std::vector<tag_Sport*> pvecSport1, pvecSport2; // 컨테이너 내에는 포인터 주소만 저장된다.

컨테이너에 대한 복사는 아래 한승희님의 팁을 참조바란다.


한승희 님의 구조체 컨테이너 복사에 대한 팁

std::copy( vecTmp1.begin(), vecTmp1.end(), std::inserter(vecTmp2, vecTmp2.end())); 

그렇다면 구조체 내부에 컨터이너가 포함된다면 어떻게 될까?

tag_MemberShip {
    std::vector<tag_Sport1> vecSport;
    int num;
};

tag_MemberShip memberShip1, memberShip2;
CopyMemory(&memberShip2, &memberShip1, sizeof(tag_MemberShip));

다행 스럽게 위의 예제는 잘 동작한다. 컨테이너가 vector를 사용하기 때문이다. 모든 컨테이너에 적용되지 않기때문에 특정 자료형에서만 사용해야하는 점이 있다.
CopyMemory를 사용하지 않으려면 일일히 하나씩 복사해야 한다.

std::copy(membserShip1.vecSport.begin(), membserShip1.vecSport.end(),
             std::inserter(membserShip2.vecSport, membserShip2.vecSport.end()));
memberShip2.num = memberShip1.num;

이럴 경우는 함수를 따로 만들어서 사용하면 더 쉬울 것이다.

void copy(tag_MemberShip& srcMemberShip, tag_MemberShip& destMemberShip)
{
    std::copy(srcMemberShip.vecSport.begin(), srcMemberShip.vecSport.end(),
                 std::inserter(destMemberShip.vecSport, destMemberShip.vecSport.end()));
    memberShip2.num = memberShip1.num;
}

방금 시험하다가 안건데, 그냥 할당연산자(=)를 사용하면 끝이다.
예를 들어, 다음으로

std::copy(membserShip1.vecSport.begin(), membserShip1.vecSport.end(),
             std::inserter(membserShip2.vecSport, membserShip2.vecSport.end()));
memberShip2.num = memberShip1.num;

을 변경하면

memberShip2 = memberShip1; 

으로 하면 그냥 끝이다. 당연히 내부에 포인터 선언은 없는 것을 가정한다. 포인터가 있다면 새로 메모리를 할당하고 값을 복사하는 깊은 복사를 해줘야 한다.

구조체 안에 컨터이너가 혹은 구조체를 담은 컨테이너 혹은 양쪽 모두 인경우

더 고려해될 내용은 구조체 안에 컨터이너가 있고 그 구조체를 컨터에너에 담는경우..

struct tag_MemberShip {
    std::vector<tag_Sport1> vecSport;
    int num;
};
std::vector<tag_MemberShip> vecMemberShip1, vecMemberShip2;

위와 같은 경우인데 이것도 쉽다. 다음과 같이 한다.

vecMemberShip2 = vecMemberShip1; 

갑자기 허무하다. 위와 같이 하면 뭔가 문제가 생길 것 같은데, 그대로 복사해주고, 메모리 갑에도 문제가 없다. ㅡ.ㅡㅋ

할당연산자로 쉽게하기

추가로 문자열관련해서 컨테이너에 넣은 추가로 보자.

struct tag_Sport{
    std::vector<string> vecName;
    int num;
};

기존에는

struct tag_Sport{
    string strName;
    int num;
};

으로 되어 있었다. 컨테이거나 추가되었다고 해서 특별하게 달라질 것은 없다. 즉 실제 메모리에서 구조체 배열은 불연속적으로 구성되었다. 이말은 vecName과 strName은 메모리에서 구조체영역에 주소만 기록되어 있고 실제 값은 다른 곳에 저장되어 있다.

그렇기에 CopyMemory()에 의해서 복제된 것은 주소 값을 가져왔기에 vecName과 strName의 수정은 원복과 복사본 모두에게 영향을 미친다. CopyMemory에 의해서 복제되는 경우 vecName와 strName은 새로 값을 할당해서 새로운 주소로 넣어줘야 한다.

그러나 이렇게 복잡한 과정을 다음과 같이 한번에 없앨 수 있다. 그 것은 할당 연산자(=)를 사용한다.

tag_Member member1, member2;
member1.vecName.push_back(string("Ospace"));
member.num = 1;
member2 = member1;

member1.back().clear(); // 해당 vecMember에 문자열이 지워진다. (1)

앞에서 CopyMemory 경우는 복사에 의한 값이 서로 묶이기 때문에 같이 값이 변경이 된다.
할당 연산자(=)를 사용한 경우는 값이 그대로 복사되며, 참조에 의한 것이 아니기에 (1)에 의해서 문자열이 지워져도 member2에는 아무런 영향이 없게 된다.
그리고 문자열 형이 STL의 string이나 MFC/ATL의 CString에서도 같이 적용된다. 이 뜻은 CopyMemory에 의해서 나타난 결과와 할당 연산자(=)에 의해서 나온 결과는 앞의 내용과 같다는 의미이다.

허무~~ (ㅡ.ㅡ)ㅋ

그래도 할당 연산자보다 CopyMemory를 사용한 것이 속도 면에서 더 빠를 것이다. 단지 좀더 고려해야될 사항이 많아진다. 100% 정확하게 알지 못하면 그냥 안전하게 가는게 편하다.

구조체가 상속이된 경우

상속된 경우

struct tag_Sport
{
    TCHAR m_name[10];
    int    score;
};

struct tag_Fitness : public tag_Sport
{
    TCHAR m_companyName[10];
};

위와 같이 정의 되고,

_tcscpy_s(fitness.m_companyName, 10, _T("JJang0001"));
_tcscpy_s(fitness.m_name, 10, _T("Ospace01"));
fitness.score = 88;

로 초기화하면 다음과 같은 형태의 메모리 값을 가진다. 0x0012f260에서 시작하고 총 자료형의 크기는 44bytes가 된다.

0x0012F260  4f 00 73 00 70 00 61 00 63 00 65 00 30 00 31 00 O.s.p.a.c.e.0.1.  
0x0012F270  00 00 fd fd 58 00 00 00 4a 00 4a 00 61 00 6e 00  ....X...J.J.a.n.  
0x0012F280  67 00 30 00 30 00 30 00 31 00 00 00 cc cc cc cc  g.0.0.0.1.......  

상속을하여도 연속적으로 메모리가 배열된 것을 확인할 수 있다. 즉 포인터와 다른 가상함수 등을 사용하지 않으면 CopyMemory를 사용할 수 있다.
그리고 할당 연산자(=)를 사용하면 포인터에 상관없이 값을 복사할 수 있다. 보통 단일 구조체 처럼 사용하면 된다.
단지 값을 할당할 때 상속이 없는 구조체는 바로 값을 할당 가능하지만 상속된 구조체는 일일히 값을 넣어줘야 한다. (내가 착각할 수 도 있다.. 표준을 봐야하는데 ㅡ.ㅡ;)

추가로 더 고려해야될 내용

다음 고려사항은 클래스인 경우는 어떻게 될지 고려해봐야한다. 일단은 나의 지식으로는 CopyMemory는 안된다. 당연히 하나씩 복수해줘야하는데, 앞에 방법들 중에서 어느정도 적용될지 확인해봐야한다.
그리고, 일반 변수(int, float, show...)가 포인터 인경우도 확인해볼려 한다.

맺음말

나름대로 정리한 것인데 너무 허술한 것이 많네요.
바쁘다보니 일일히 정리라 든가 다시 확인하는 것은 어렵고...
만약 잘못된 부분이나 팁이 있으면 아래 메일로 보내주세요.
그러면 더욱더 많은 도움이 될것 같습니다.

반응형

'3.구현 > C or C++' 카테고리의 다른 글

Vectors in STL  (0) 2007.02.16
블록 메모리 복사 성능시험  (0) 2007.02.16
메모리 복사 성능시험 (memcpy)  (0) 2007.02.16
디버거 - 로그 윈도우 2.5 (Win32 디버그 지원)  (2) 2006.11.23
c언어에서 자료형 크기  (0) 2006.11.13