'C/C++'에 해당되는 글 11건

  1. 2010.05.22 클래스 멤버 메소드를 콜백 함수로 사용하기
  2. 2010.05.13 메모리에 할당이 되지만 값을 변경할 수 없는 메모리 영역? 1
  3. 2010.04.04 구조체의 크기가 예상과 다른 이유는?
  4. 2010.04.03 포인터 문법에 관한 고찰
  5. 2010.04.03 동적 배열을 다른 클래스에서 사용하려면..?
  6. 2010.04.02 NULL 포인터에 대한 접근..
  7. 2010.04.02 문자열에 대한 레퍼런스는 어떻게 해야 할까?
  8. 2010.04.02 다음 예제에서 메모리 해제 에러가 왜 생길까요? 1
  9. 2010.04.02 문자열을 값에 의한 전달(Call By Value)을 어떻게 하는가?
  10. 2008.12.19 Name Mangling에 대한 고찰
2010. 5. 22. 17:34

클래스 멤버 메소드를 콜백 함수로 사용하기

이것은 초급자를 위한 팁임을 미리 말씀드립니다. 

 

1. Motivation

 우리는 개발한 상위 라이브러리에서 사용하고 싶은 이벤트 혹은 메시지를

 등록하고 그것을 사용자 정의 콜백 함수로 호출하는 구조를 사용자에게

 제공해 주고 싶은 경우가 있습니다.

 MFC와 같은 프레임워크처럼, ON_MESSAGE( UserMessage, OnUserFunction )로

 사용자 정의 메시지 및 이벤트 처리하게 해주는 메커니즘을 말이죠.

 

2. Solution 

우리가 개발한 라이브러리에서 다음과 같이 사용자 이벤트와 콜백 함수를

등록할 수 있는 함수가 있습니다.

RegisterEventHandler( ON_PMSG_CHAT, this, ClassType::OnPmsgChat );

이 구현을 위해 가장 고민해 보아야 할 문제는 여러 클래스 타입들의 메소드를

어떻게 콜백 함수로 등록할 수 있는가입니다. 다시 말해, 이것은 콜백 함수로

등록될 클래스의 타입과 클래스 메소드의 함수 포인터형을 알 수 없다는 것입니다.

MFC의 경우에는 이벤트 처리기와 메시지 맵은 CObject 이하의 수많은 클래스

타입들에 대한 오버로딩 메소드를 미리 정의하고 구현해 놓았지요.

 


3. Conclusion

아래 소스와 같은 구현으로 어떠한 클래스의 타입 메소드가 인수로 대입되더라도

콜백 함수로 등록하고 처리할 수 있습니다.

콜백 함수의 등록은 아래의 매크로 함수로 할 수 있습니다.

RegisterEventHandler( ON_PMSG_CHAT, this, ClassType::OnPmsgChat );

아래의 소스에서 매크로로 구현된 CMyClass::GetInstance()->AddEventHandler()는

우리가 개발한 라이브러리의 클래스 내에서 (싱글톤 패턴이라는 가정하에서는) Static

Object의 포인터를 저장하는 역할을 하는 메소드입니다.

 

// HandlerManager.h

#ifndef  HANDLER_MANAGER_
#define HANDLER_MANAGER_

#include <iostream>
#include <windows.h>

#define RegisterEventHandler(dwMessage, pInst, lpFunc);                                     \
           static CHandlerObject &lpfn##dwMessage = SetEventProc( pInst, &lpFunc );   \
           CMyClass::GetInstance()->AddEventHandler( &lpfn##dwMessage );

#define CALLBACKPROC(dwMessage, lpBuffer, dwSize);                                       \
           lpfn##dwMessage.CallBackProc( dwMessage, lpBuffer, dwSize );

 
class CHandlerObject
{
public:
    virtual LPVOID CallBackProc(DWORD dwMessage, LPVOID lpBuffer, DWORD dwSize) = 0;
};


template <class ObjectType, typename FuncPtrType>
class CHandlerManager : public CHandlerObject
{
private:
    ObjectType *m_pObject;
    FuncPtrType m_pFuncPtrType;

    int nCntHandler;

public:
    CHandlerManager(ObjectType* pObject, FuncPtrType pFuncPtrType) : m_pObject( pObject ),
                                                                                                 m_pFuncPtrType               
                                                                                                ( pFuncPtrType ) {}
    ~CHandlerManager() {}
 

    LPVOID CallBackProc(DWORD dwMessage, LPVOID lpBuffer, DWORD dwSize)
    {
               (m_pObject->*m_pFuncPtrType)( dwMessage, lpBuffer, dwSize );

               return NULL;
    }

};

 
template <class ObjectType, typename FuncPtrType>
CHandlerManager<ObjectType, FuncPtrType>
SetEventProc(ObjectType *pObject, FuncPtrType pFuncPtr)
{
               return CHandlerManager<ObjectType, FuncPtrType>(pObject, pFuncPtr);
}


#endif

 

Written By Simhyeon, Choe

2010. 5. 13. 20:46

메모리에 할당이 되지만 값을 변경할 수 없는 메모리 영역?


다음은 어떤 데브피아 유저가 질문한 내용에 제가 답변한 글입니다.

-------------------------------------------------------------------------------------

제가 보고 있는 c 언어 책의 포인터와 문자열 부분에서 ...

char *p = "abcde";

"abcde" 는 메모리 어딘가에 문자열 리터럴로 보관되어 있다.

그런데, 메모리에 할당하기는 하지만 값을 변경할 수 없는 메모리 영역에 보관된다고 하는데 ...

어떤 영역인가요 ?

제가 메모리 영역에 대해서 잘 몰라서 질문을 드립니다.

-------------------------------------------------------------------------------------

.rdata라는 섹션에 저장됩니다. PE File View 혹은 PE File Explorer라는 툴(실행 파일 분석툴)을

사용하여 해당 파일을 열어 보시면 .rdata라는 섹션에 저장되어 있습니다. 실행 파일은 여러 개의

섹션(영역)과 그 섹션에 대한 정보를 가지는 구조로 이루어져 있습니다. 또한 각각의 섹션은 초기화,

읽기, 쓰기, 데이터, 텍스트 등의 고유한 속성에 대한 정보를 가지고 있습니다.  실행 파일이 실행될

때, 파일의 이미지가 메모리에 동일하게 맵핑됩니다. .rdata 섹션의 속성은 READ ONLY, DATA로

설정되어 있기 때문에 해당 영역에 쓰려고 할 때, 커널은 속성을 체크하고 위배되면 엑세스 위반을

일으키게 됩니다. 물론, .rdata 섹션의 속성을 쓰기로 변경하면 쓰기도 가능합니다. 이렇게 읽기

전용 데이터 섹션을 설정한 이유는 동일한 상수를 여러 곳에서 참조하기 때문이지요. 이것은 아주

당연한 이유입니다. 한곳에서 수정이 가해지면 해당 리터럴 상수를 참조하고 있는 다른 곳에서도

영향을 주기 때문입니다. Win32 운영체제는 실행 파일을 .rdata, .data, .idata, .text, .rsrc 등의

여러가지 섹션으로 코드와 데이터를 나누어 관리하고 있습니다. 이렇게 섹션을 나누어 관리하는

이유는 로더가 각 영역을 커널에서 정한 레이아웃에서 엑세스하는 데이터 및 코드를 쉽게 맵핑하고

사용하기 위함입니다. 그리고 컴파일러는 리터럴 상수를 경우에 따라 프로세스를 생성시 매번

할당하는 것이 아니라, data 섹션이 아닌 text 섹션에 직접 쓰기도 합니다. 이것은 할당 및 엑세스

속도를 빠르게 하기 위함입니다. 여기서 정확한 표현은 Segment보다는 Section으로 보는 것이

옳습니다.
그리고 위에서 님이 말씀하신 것처럼 동일한 위치를 가리키는 이유는 단순히 컴파일러

입장에서 최적화를
위한 것입니다. 동일한 상수를 .rdata 영역에 여러번 할당하는 것이 낭비고 어차피
 
변경 불가능하고
참조만 되어지는 대상이기 때문에 두 개의 포인터가 하나의 리터럴 상수를 가리켜도

전혀 문제될 것이 없습니다.
두번째로 질문하신 내용에 대한 답변을 드리겠습니다.

디버기 프로세스가 되지 않고 파일 접근이 아닌 이상, 해당 프로세스에서 실제 그 영역 속성의 변경은

불가능하지만, 페이지 속성 변경 및 CopyOnWrite 메커니즘으로 .rdata에 쓸 수가 있습니다.

DWORD dwReadData = 0;

char *pString = "Hello";

VirtualProtect( pString, 4096, PAGE_READWRITE, &dwReadData );

*pString = 'P';


Written By Simhyeon, Choe

2010. 4. 4. 16:11

구조체의 크기가 예상과 다른 이유는?

이것도 예전에 데브피아에서 어떤 유저가 기고하였던 질문에 대한 저의 답변입니다.

------------------------------------------------------------------------------

구조체에서..
 
char a;
int b;
float c;

이것은 아는 대로 12바이트가 할당 됩니다.

char a;
int b;
double c;


이것은 16바이트가 할당이 되구요..

헌데, 

char a;
double b;
float c;

는 24바이트가 할당이 되네요.

구조체는 시스템의 메모리의 한 워드 단위로 할당이 되는거 아닌가요?

그러면 4바이트 단위니깐 16바이트가 맞는것 같은데..

답변 부탁드리겠습니다.

------------------------------------------------------------------------------

Structure Alignment는 시스템 cpu에 따라 의존적입니다.

이러한 구조체 정렬을 하는 근본적인 이유는 데이터 타입을

버스의 크기만큼 처리하는 것이 가장 효율적이기 때문이죠.

그러니까 예를 들면, 32Bit CPU에서는 데이터 버스도 32Bit의

크기를 가지는데, 한번에 처리할 수 있는 크기 단위로 처리하는

것이 성능면에서 당연히 효율적일 겁니다. 보다 작으면 데이터를

처리하기 위해서는 쉬프트 연산으로 인한 오버헤드가 크다는 겁니다.

차라리 몇 바이트를 낭비하는 대신, 성능상의 이점을 충분히 얻겠다는 의도입니다.

VC 컴파일러는 기본적으로 8Byte 정렬을 하는데요..

컴파일러 옵션으로 정렬 단위를 바꿔 줄 수 있습니다.

Structure Alignment는 그 구조체 멤버에서 가장 큰 데이터

타입을 기준으로 정렬하게 됩니다.

그럼 님께서 예를 들었던 구조체들을 다시 한번 살펴 보도록 하겠습니다.
 

char a;
int b;
float c;
 

1) 위의 구조체에서 가장 큰 멤버의 크기는 4Byte입니다.
 

■■■
■■■■
■■■■

그렇기 때문에 4 바이트를 기준으로 정렬하게 되며,

첫번째 멤버 char a는 3byte의 Empty Block이 생기게 되겠죠.
 

///////////////////////////////////////////////////////////////

 
char a;
int b;
double c;
 

2) 위의 구조체에서 가장 큰 멤버의 크기는 8Byte입니다.

 
■■■■■■■
■■■■■■■■
 

char a와 int b는 둘다 8Byte 블럭안에 포함될 수 있습니다.
 
단, 두번째 멤버 int b는 4Byte기 때문에, 앞의 공간을 사용한
 
char a에 의해서 3Byte 공간만 남아 정렬될 수 없기 때문에

int b 자신의 데이터 크기만큼 띄워 나머지 공간에 padding되게 됩니다.
 

///////////////////////////////////////////////////////////////

char a;
double b;
float c;

3) 위의 구조체에서 가장 큰 멤버의 크기는 마찬가지로 8Byte입니다.

■■■■■■■
■■■■■■■■
■■■■■■■■

첫번째 멤버 char a가 위치한 후에 8Byte에서 나머지 7Byte 공간에

double b가 들어갈 수 없기 때문에, 자신의 블럭 크기로 padding되게 됩니다.

마찬가지입니다. float c도 위의 8Byte(double b의 공간)이 모두 채워졌기

때문에 다시 8Byte만큼 정렬한 공간에서 나머지 4Byte크기로 padding되게 됩니다.

 
 

※ 메모리 위치가 수시로 변할 수 있는 근본적인 이유는 Allocation Segment Alignment 때문입니다.

    그러니까 힙 영역을 할당할 때, os에서 heap allocation을 처리하는 알고리즘에서 새로이 할당되는

    메모리 영역을 순차적으로 검색하다가 충분히 할당할 수 있는 공간이라고 판단되면, 이전에 할당된

    메모리 공간을 보다 앞으로 위치시킬 수 있는 그러한 이유가 있기 때문이죠.


Written By Sim-Hyeon, Choe
 

2010. 4. 3. 11:55

포인터 문법에 관한 고찰

아래의 문법은 컴파일하여도 전혀 문제가 되지 않습니다.

-----------------------------------------

int array[10];

int *ptr = array;

-----------------------------------------

2번째 라인을 int *ptr = &array;로 바꾸어 컴파일하면 에러가 발생합니다.

array는 포인터 상수로, &array와 같은 주소값을 가짐에도 불구하고 말입니다.


이유가 무엇일까요?

아주 예전에 제가 위의 문제로 고민을 한적이 있었습니다. 하하.

우리가 생각한대로 int *ptr = &array가 되어야 합리적으로 보여질 수 있습니다.

하지만 다음과 같은 에러를 출력하는 것을 보실 수 있습니다.

'initializing' : cannot convert from 'int (*)[10]' to 'int *'

참 이상하죠? 배열명 array 자체는 주소값을 의미하고 거기에 단지 &연산을 했을 뿐인데 말입니다.

독자는 분명히 array == &array 라는 사실을 의심치 않을 겁니다.

왜냐하면, printf( "%x", array  ); 와

            printf( "%x", &array ); 이 같고,

            printf( "%d", sizeof(array)  ); 와

            printf( "%d", sizeof(&array) ); 가 같은데 말이죠.

 즉, 대상체가 의미하는 주소값과 대상체의 크기는 모두 동일하다는 것입니다.

그럼 다시 위의 에러를 살펴 봅시다.

[배열 포인터]형을 [포인터]형으로 변환이 불가능하다는 의미가 될 겁니다.

그래서 다시, int (*ptr)[10] = &array; 해서 컴파일 해 보았습니다.

에러없이 컴파일이 잘 되는군요. 하지만 상식적으로 저 역시 이해가 되질 않습니다.

여전히 의문을 가질 수 밖에 없었습니다. 그렇다면 좀 더 시각을 달리해보는건 어떨까

합니다. 배열명 array는 우리가 알고 있는대로 [포인터 상수]입니다.

배열명 자체가 주소값을 의미합니다. 그렇다면 상수에 &연산이 가능하다는 것조차도

논리적으로 좀 이상하지  않겠습니까?

&연산은 변수의 선두 번지를 구하는 연산이라는 것을 독자들도 잘 아실 겁니다.

그렇지만 &연산은 가능합니다. 왜냐하면, printf( "%d", &array );가 무리없이 컴파일되니까

말입니다. 저는 사실 이것도 뭔가 찝찝하다고 생각하였는데 독자가 동의하실지 모르겠습니다.

상수에 &연산을 허용한다는 것은 결국 &(&array)도 허용해야 하지 않겠습니까?

하지만 이것은 컴파일 에러를 출력합니다. '&' requires l-value로 말이죠.

(VC++로 컴파일하였을 때 기준)

하지만 다른 여러 컴파일러에서는 int *ptr = &array;가 허용되는 것으로 알고 있습니다.
(리눅스의 gcc가 허용하는 것으로 알고 있습니다)

우리들이 의도하신대로 말이죠. 제가 말하고 싶은 결론은 컴파일러의 Dependent한 문제라고

볼 수 밖에 없습니다. 항상 우리가 생각하는 논리와 의도대로 모든 문법이 구성이 되어있지 않

습니다. 저 역시 이 부분은 개인적으로 굉장히 아쉬운 부분으로 생각하고 있습니다.

결국은 컴파일러 문제죠. 여기까지는 컴파일러가 정한 문법 규칙이라고 밖에 볼 수 없습니다.

제가 그렇게 확신하는 이유가 다음의 Disassembling 코드때문입니다.

 

9:        printf( "%d\n", array  );

0040D76B   lea           eax,[ebp-28h]
0040D76E   push        eax
0040D76F   push        offset string "%d" (0042201c)
0040D774   call          printf (0040d6c0)
0040D779   add          esp,8 

10:       printf( "%d\n", &array );

0040D77C   lea          ecx,[ebp-28h]
0040D77F   push        ecx
0040D780   push        offset string "%d" (0042201c)
0040D785   call          printf (0040d6c0)
0040D78A   add         esp,8

 
같은 Addressing Mode로 연산을 한다는 사실을 위의 코드로 증명하였습니다.

( [확장자 .c]로 하여 int *ptr = &array; 컴파일하면 에러는 나지 않고 경고만 출력하더군요. )

컴파일러에 의해서 어쩔수 없는 부분은 그려려니 인정하고 이해보다는 암기하여

사용할 수 밖에 없다는 것이라고 생각합니다.

Written By Sim-Hyeon, Choe

2010. 4. 3. 04:46

동적 배열을 다른 클래스에서 사용하려면..?


다음 예제를 살펴 봅시다.
-------------------------------------------------------------

class AAA
 

AAA::TestA()
{
    // 문자열 2차원 배열 선언
    char(* strResult)[15] = new char[500][15];
}

 

class BBB


#include "AAA.h"

AAA testa;  -> testa class 변수 선언

BBB::TestB()

{

    while( ... )
    {
        printf( "%s", testa.strResult[i] );  // 에러
    }

}

-------------------------------------------------------------

위의 예제에서 처럼 클래스 AAA에 선언된 배열 strResult를 어떻게 가져올 수 있을까요?

class AAA에서 다음과 같은 접근 제어 함수를 만들 수 있습니다.
 

"AAA.h" 

class AAA
{

public:

    char (*GetStrResult())[15];    // 2차원 배열 포인터를 리턴

}

"AAA.cpp"

// 클래스 AAA의 멤버인 2차원 배열 포인터를 아래와 같이 리턴할 수 있습니다.
char (*AAA::GetStrResult())[15]
{

    char (*strResult)[15] = new char[500][15]; 

    return strResult;

}

 

"BBB.cpp"

BBB::TestB()
{

    char (*ptr)[15] = NULL;

    while( ... )
    {

        ptr = testa.GetStrResult();


        printf( "%s", ptr + x );    // x는 출력할 문자열의 인덱스

    }

}

Written By Sim-Hyeon, Choe

2010. 4. 2. 15:18

NULL 포인터에 대한 접근..


*((char *)NULL) = 'A';

보통 초급자분들이 코딩을 하다보면 NULL 포인터를 참조하여 접근하는 경우를 흔히 볼 수 있습니다.

실수도 실수지만 호기심으로 위와 같은 문법을 작성하고 실행시킨 후에 참조 에러가 나는 원인을 궁금해

하는 분들이 많았습니다.


char *temp = NULL;

NULL 포인터는 메모리의 어느곳도 가리키지 않는 포인터를 의미합니다.

NULL 포인터를 Debugging해보면 0x00000000를 가리킨다는 사실을 확인하실 수 있는데,

그렇다고 하여도 널 포인터는 어느곳도 가리키지 않는 포인터를 의미합니다.

어느곳도 가리키지 않는곳에 문자 상수 'A'를 대입하려고 했으니 문제가 발생하겠지요.

윈도우즈 운영체제에서 유저가 NULL 포인터 영역의 접근을 제한하고 있는 것이 보다 정확한

의미가 되겠습니다.

보통 NULL 포인터의 용도는 리소스 할당 실패를 검출하거나, 일종의 포인터 초기화, 

메모리 할당 실패 정도로 말씀드릴 수 있겠습니다.

ex1) if( NULL == fopen(..., ...) )

ex2) struct DATA *link;

        link->next = NULL;

ex3) char *string = (char *)malloc( sizeof(20) * char );

       if( string == NULL )
       {
            printf( "Memory allocation error" );
       }


Written By Sim-Hyeon, Choe

2010. 4. 2. 15:05

문자열에 대한 레퍼런스는 어떻게 해야 할까?


보통 int형이나 float형등에 대한 레퍼런스는 쉽게 쓸 수 있습니다.

다음 예제를 살펴 봅시다.

wchar_t *abc = L"가나다라마바사";

wchar_t *abc2 = new wchar_t[256];

wchar_t abc3[256];

통상적으로 위와 같은 형태로 많이 사용합니다.

이때, 이 변수들에 대한 레퍼런스(문자열에 대한 레퍼런스)는 어떻게 할 수 있을까요?

물론, wchar_t &def = abc; 과 같은 형태는 불가능합니다.


문자열에 대한 레퍼런스를 할 수 있는 방법은 할당된 메모리 영역에 따라 두가지가 있습니다...

1) 스택 영역(Stack Segment)과 초기화되지 않은 데이터 영역(Uninitialized Data Segment)

아래는 스택 영역에 할당된 변수의 선언입니다.

int main(void)
{

    char string_1 = 'a';

    char &ref_str = string_1;

    return 0;

}

위에서 보시는 string_1은 char형이 1 byte기 때문에 레퍼런스를 string_1에 붙여줄 수 있습니다.

그 이유야 두말할 필요없이 'a'가 1 byte기 때문에 레퍼런스 선언이 가능합니다.

왜냐하면, 레퍼런스도 1 byte char형이기 때문이죠.

하지만, 다음 문법을 보겠습니다.

int main(void)
{

    char string[20] = "Hello World!!";

    char ???

}

위에서 선언된 string은 배열입니다. 좀 더 정확하게 말하자면, 배열은 포인터 상수입니다.

여기서 포인터의 상수란, 그 크기가 유동적인 것이 아니라 컴파일 타임에 정해졌다는 것을 의미합니다.

다시 말하면, 이미 string이라는 포인터가 가리키는 위치는 스택 영역 어딘가에 20byte로 할당된 메모리

상이라는 말인데, 여기서 중요한 점은 string이 가리키는 곳을 절대 바꿀 수 없다는 것이 포인터 상수의

특징입니다.

문제의 핵심은 컴파일러가 제공해주는 자료형이 char, short, int, long, float, double 등등..

문자열 상수 20 byte 모두 참조할 수 있는 자료형이 존재하지 않는다는 것이 핵심입니다.

그렇기 때문에, 결론은 20 byte 크기를 참조할 수 있는 사용자 정의 자료형을 사용하여야 한다는

것입니다.

아래가 스택 영역에 할당된 문자열 상수를 레퍼런스로 선언할 수 있는 해답입니다.

#include <iostream>

using std::cout;
using std::endl; 

struct String
{
    char str[20];
};

int main(void)
{

    String string;

    strcpy( string.str, "Hello World!" );

    String &ref_str = string;

    cout << ref_str.str << endl;

    return 0;
}

 

2) 초기화된 데이터 영역(Initialized Data Segment)

아마 위의 방법을 보시고 어딘가 좀 못마땅하고 문법적으로도 까다롭다는 것을 느끼실 겁니다...

하지만 초기화된 데이터 영역에 할당된 문자열을 레퍼런스로 선언/정의하는건 간단합니다..

해결책은 아래와 같습니다.

 

#include <iostream>

using std::cout;
using std::endl;

int main(void)
{

    char *string = "Hello World!";

    char *&ref_str = string;

    cout << ref_str << endl;

    return 0;

}

이것에 따른 의문도 있을거라고 생각되니 정리해 드리겠습니다.

char *string;에서 char *도 하나의 자료형입니다. "문자 포인터형"이지요..

마찬가지로 "문자 포인터형"의 레퍼런스를 하나 선언하여 string으로 초기화한다면 문제없습니다.

char *(&ref_str) = string;

※ 하지만 , 위에서 선언된 레퍼런스로 접근은 읽기는 가능하나 쓰기는 불가능합니다.

    왜냐하면, string이 가리키는 곳은 초기화된 데이터 영역에 할당된 문자열 상수이기 때문입니다.

    Initialized Data Segment는 오직 읽기만 가능하며, 쓰기는 불가능합니다.

    이 영역의 메모리 해제는 프로그램이 종료될 때, 해제가 됩니다.


Written By Sim-Hyeon, Choe

2010. 4. 2. 13:23

다음 예제에서 메모리 해제 에러가 왜 생길까요?

아래와 같이 코딩하였다고 가정하겠습니다.

-------------------------------------------

#include <iostream.h>

int main()
{
    char *i = new char[20];

    i = "asfdsadfsdaf";

    cout << i << endl;

    delete i;

    return 0;
}

--------------------------------------------

결과 출력 후, 확인하면 delete에서 Access Violation Error가 발생합니다.

이유는 무엇일까요?

char *i = new char[20];

위의 코드를 풀이해보면, 힙 영역 어딘가 char 형의 20 byte크기만큼 할당하고

할당한 것(정확하게 할당된 영역의 첫 주소값)을 포인터 i가 가리키는 형태입니다.

i = "asfdsadfsdaf";

위의 코드도 풀이하면, 메모리상 어딘가 존재하는 문자열 상수를 i가 가리키게 됩니다.

(여기서 할당된 메모리 위치를 이미 잃어버리게 됩니다. 다른곳의 주소를 가리키니까)

즉, i가 가리키고 있는 곳(new char[20])에 위의 문자열이 할당된게 아니라,

"asfdsadfsdaf"가 할당된 곳(초기화된 데이터 영역에 할당)의 첫주소를 가리키게 됩니다.

"asfdsadfsdaf"은 초기화된 데이터 영역(Initialized Data Segment)에 저장된 값은 일반 상수로

취급되며 쓰기는 불가능하고 오직 읽기만 가능합니다(.rdata 영역).

읽기가 가능하니 cout << i << endl; 하면 출력은 됩니다.

delete i; 라고 코딩되어 있는데,

결과적으로 i가 가리키는 곳은 초기화된 데이터 영역에 존재하는 "asfdsadfsdaf"이므로,

메모리의 특성상 쓰기가 불가능하니 Exception이 발생하는 것입니다. 

strcpy(i, "asfdsadfsdaf"); 가 되는데,

이것은 "i가 가리키고 있는 주소값을 참조하여 문자열 상수를 그곳으로 복사해라"는 의미로,

i가 가리키는 영역은 (커널에게 정상적인 메모리 할당을 허락받은) Heap 영역이니 가능하죠.

※ 초기화된 데이터 영역(Initialized Data Segment)은 프로그램 종료시, 자동으로 해제됩니다.

Written By Sim-Hyeon, Choe

2010. 4. 2. 13:04

문자열을 값에 의한 전달(Call By Value)을 어떻게 하는가?


다음 예제가 있다고 가정하겠습니다.

void main()
{
    char str = { "대한님국" };    

    fncA( str );
}

// 참조에 의한 전달

void fncA(char *str)
{

     ....

}

값에 의한 전달은 어떻게 할 수 있을까요?

천천히 살펴 봅시다.

char *str = "대한민국";

위의 선언을 풀이해보면, 메모리상 어딘가에 "대한민국"이라는 문자열 상수가

연속된 8byte 공간에 할당될 겁니다(한글 한자는 2byte기 때문에).

char *str;은 character형의 주소 가리킬 수 있는 포인터입니다.

char형은 1byte입니다. 1byte 메모리 공간에 대해서만 접근하고 참조할 수 있습니다.

함수의 인자를 받을때 char로 받으면 1byte 메모리 공간밖에 받을 수 없습니다.

결과적으로는 사용자 정의 자료형을 하나 선언해서 그 자료형을 넘겨 주어서 참조해야

합니다.

 

#include <stdio.h>
#include <string.h>

void fncA(struct string_t dest_string);

struct string_t
{
    char str[10];

};

int main(void)
{

    string_t string;

    strcpy( string.str, "대한민국" );

    fncA( string );

    printf( "%s", string.str );

    return 0;

}

void fncA(struct string_t dest_string)
{
    strcpy( dest_string.str, "우리나라" );

    printf( "%s\n", dest_string.str );
}

 

위와 같이 사용자 정의 자료형을 인자로 넘겨준다면, Call-By-Value가 성립됨을 알 수 있습니다.

※ 사용자 정의 자료형 내에서 맴버 str을 포인터형으로 줄 수 없는 이유가

   함수의 인자로 사용자 정의 자료형을 넘길 때, 맴버 대 맴버 복사가 이루어지는데,

   포인터 str 자체 즉, 4byte 공간만 복사될 뿐,

   맴버 포인터가 가리키는 문자열까지 복사가 불가능합니다.

   왜냐하면, 힙에 동적으로 할당된 공간자체가 맴버로 구조체에 포함되어 있지 않기 때문입니다.


Written By Sim-Hyeon, Choe

2008. 12. 19. 12:10

Name Mangling에 대한 고찰

Name Mangling에 대한 고찰 Written by Sim-Hyeon, Choe

32비트 컴파일러는 __declspec(dllexport) 지시자를 사용하여 DLL에서 데이터, 함수, 클래스

또는 클래스 멤버 함수를 내보낼 수 있습니다. __declspec(dllexport)는 Object 파일에 익스포트

지시문을 알아서 추가하기 때문에 별도의 모듈 정의파일(.def)를 사용할 필요가 없습니다.

그래서 __declspec(dllexport)를 정의함으로써 컴파일러에게 해당 데이터는 익스포트 될 것임을

알려주게 됩니다. 반대로 익스포트한 모듈의 함수를 사용하기 위해서는 __declspec(dllemport)로

정의하여 알려줘야 합니다.

한가지 예를 들어 보도록 하겠습니다.


MyLibrary.cpp,
MyLibrary.h

2개의 파일이 있다고 가정하겠습니다.

MyLibrary.cpp는 이 모듈의 기능을 정의한 파일입니다.

MyLibrary.h는 이 모듈의 기능에서 필요한 선언부를 담고 있는 헤더 파일입니다.


MyLibrary.h 소스의 선두
----------------------------------------
#ifndef  MYSYSAPI
#define MYSYSAPI __declspec(dllimport)
#endif

MYSYSAPI int __stdcall FuncA(int nA, int nB);
MYSYSAPI int __stdcall FuncB(int nA, int nB);
MYSYSAPI int __stdcall FuncC(int nA, int nB);
----------------------------------------

MyLibrary.cpp 소스의 선두
----------------------------------------
#define   MYSYSAPI __declspec(dllexport)
#include "MyLibrary.h"

int __stdcall FuncA(int nA, int nB)
{
       ... ...
}

int __stdcall FuncB(int nA, int nB)
{
       ... ...
}

int __stdcall FuncC(int nA, int nB)
{
       ... ...
}
----------------------------------------

위의 두 소스를 비교보면 알겠지만, MyLibrary.h는 MyLibrary.cpp와 반대로

__declspec(dllimport)를 사용한다. 소스를 컴파일한 내용으로 해석하면 MyLib

rary.cpp에서는 MYSYSAPI라는 매크로가 전처리하게 되는데, 이어 인클루드한

MyLibrary.h 헤더 파일의 내용을 전처리하는 과정을 따르게 됩니다.

MyLibrary.h 헤더 파일에서 보면, MYSYSAPI라는 매크로가 정의되어 있는지

확인하고 이미 정의되어 있기 때문에 #define MYSYSAPI __declspec(dllimport)

은 건너뛰게 됩니다. 결국 MyLibrary.cpp에서 선언 및 정의된 세 함수가 __declspec

(dllexport)로 사용되어지게 되는데요.

이 라이브러리를 Implicit Linking할 때에도 MyLibrary.h 헤더 파일을 포함해야 합니다.

하지만 여기서의 전처리 흐름은 MYSYS API 매크로가 정의된 적이 없기 때문에

__declspec(dllimport)가 되는 것입니다.

그렇다면 이렇게까지 복잡하게 전처리하고 익스포트 및 임포트하는 이유가 무엇일까요?

일단, 이렇게 정의함으로써 라이브러리를 링킹할 때에는 소스 파일에서 사용시 헤더 파일

에 컴파일러에게 일일이 __declspec(dllimport)으로 알릴 필요가 없게 되어 라이브러리

의 사용자가 더이상 관심을 가지지 않아도 될 것입니다. Win32 API를 사용하듯 필요한

함수만 호출해서 사용하면 되겠지요.

이미 위에서도 설명드렸습니다만, __declspec(dllexport)를 정의하면 별도의 .def 파일

을 생성하여 외부 정의 함수에 대한 정의도 따로 할 필요가 없게 됩니다.

그렇다면 __declspec(dllexport)로 정의한 함수의 심벌명은 어떻게 결정될까요?

Binary Dump로 익스포트 함수의 심벌 정보를 확인하면 다음과 같습니다.

_FuncA01@4
_FuncB02@4
_FuncC03@4



인간이란 망각의 동물이다.

자신의 부단한 노력으로 얻은 소중한 기억이 결국 시간이 지나갈 수록 잊혀

져버린다는 사실에 마음이 아프다.