본문으로 바로가기

동적인 메모리 디버깅하기

category 개발언어/c++ 2016. 7. 16. 18:55

메모리를 동적으로 할당하는 것은 잠재적인 버그의 가능성을 안고 있는 것이며, 아마도 이 상황에서 가장 흔한 버그는 메모리 유출일 것이다. 여러분에게 기억을 일깨우기 위해 메모리 유출이라는 것은 여러분이 new 연산자를 사용하여 메모리를 할당할 때 발생한다. 그러나 여러분이 그 메모리를 다 사용한 후에 그것을 다시 해제할 때 delete를 사용해서는 안된다. 여러분이 할당했던 메모리를 삭제하는 것을 잊어버리는 것은 별 문제로 하고, 여러분은 클래스 계층 구조 안에서 가상이 아닌 디스트럭터들도 그러한 문제를 발생시킬 수 있다는 사실을 특별히 명심해야 한다(왜냐하면, 우리가 보았듯이 그것은 하나의 객체가 파괴될 때 잘못된 디스트럭터가 호출되도록 할 수 있기 때문이다). 물론, 여러분의 프로그램이 종료될 때 모든 메모리는 해제된다. 그러나 프로그램이 실행되고 있을 때 그것은 여러분의 프로그램에 할당된 채로 남아 있게 된다. 메모리 유출은 대부분의 경우, 어떠한 명백한 증상도 나타내지 않는다. 어떤 경우에는 전혀 증상을 나타내지 않는다. 그러나 그것은 여러분 컴퓨터의 성능에 해로운 영향을 끼친다. 왜냐하면, 메모리가 어떤 좋은 목적으로 점유되지 않기 때문이다. 때로, 모든 사용 가능한 메모리가 할당되었을 때 그것은 큰 혼란을 야기시킬 수 있다.

여러분의 프로그램이 free store를 사용하는 것을 감시하기 위해서 비주얼 C++는 일련의 진단 루틴들을 제공한다(그것들은 free store의 특별한 디버그 버전을 사용한다). 그것들은 crtdbg.h라는 헤더 안에서 선언된다. 그러한 루틴들에 대한 모든 호출들은 여러분 프로그램의 릴리즈 버전으로부터 자동적으로 삭제된다. 그러므로 여러분은 그것들에 대해서 어떠한 전처리기 제어도 추가할 필요가 없다.

6.1 free store를 검사하는 함수들

 

free store를 검사하는 작업에서 어떠한 것들이 포함되는가에 대해서 전체적으로 살펴본다. 그런 다음, 어떻게 메모리 유출이 검출되는가에 대해서 살펴본다. crtdbg.h 안에서 선언되는 함수들은 _CrtMemState 형식을 갖는 구조 안에 저장되어 있는 free store의 상태에 대한 기록을 사용하여 free store를 검사한다. 이 구조는 비교적 간단한데, 다음과 같이 정의된다.

typedef struct _CrtMemState

{

    struct _CrtMemBlockHeader* pBlockHeader; // 최근에 할당된 블록에 대한 포인터들

    unsigned long lCounts[_MAX_BLOCKS]; //블록이 각 형식들에 대한 카운터

    unsigned long ISizes][_MAX_BLOCKS]; //각 블록 형식에 할당되어 있는 전체 바이트

    unsigned long lHighWaterCount; //현재까지 한번에 가장 많이 할당된 바이트

    unsigned long lTotalCount; //현재 할당된 전체 바이트

} _CrtMemState;

 

우리는 free store의 상태에 관한 세부적인 사항에 대해서는 직접적으로 살펴보지 않을 것이다. 왜냐하면, 우리는 정보를 좀더 읽기 쉬운 형식으로 나타내는 함수들을 사용할 것이기 때문이다. free store의 작업을 추적하는 매우 많은 함수들이 있다. 그러나 우리는 가장 살펴볼 만한 5개에 대해서만 알아볼 것이다.

그것들은 여러분에게 다음과 같은 기능들을 제공한다.

◇ 어떠한 지점에 있는 free store의 상태도 기록하는 기능

◇ free store의 두 가지 상태의 차이점을 알아내는 기능

◇ 상태 정보 출력하기

◇ free store 안에 있는 객체들에 관한 정보를 출력하는 기능

◇ 메모리 유출을 검출하는 기능

다음은 그러한 함수들의 선언들과 함께 그것들이 어떤 것을 하는가에 대한 간단한 설명 이다.

void _CrtMemCheckpoint (_CrtMemState* state);

 

이것은 free store의 현재의 상태를 _CrtMemState 구조 안에 저장한다. 여러분이 함수에 전달하는 인수는 상태가 기록될 _CrtMemState 구조에 대한 포인터이다.

int _CrtMemDifference (_CrtMemState* stateDiff,

const _CrtMemState* oldstate,

const _CrtMemState* newState);

 

이 함수는 세 번째 인수에 의해서 지정된 상태를 두 번째 인수 안에서 여러분이 지정한 이전의 상태와 비교한다. 그 차이점은 여러분이 첫 번째 인수 안에서 지정한 _CrtMemState 구조 안에 저장된다. 만약 상태들이 다르면 함수는 0이 아닌 값(true)을 리턴하고, 그렇지 않으면 0(false)을 리턴한다.

void _CrtMemDumStatistics(const _CrtMemState* state);

이것은 인수에 의해서 지정된 free store 상태에 관한 정보를 출력 스트림으로 전달한다. 인수에 의해서 가리켜지고 있는 상태 구조는 여러분이 _CrtMemCheckPoint()를 사용하여 기록했던 상태이거나, 또는 _CrtMemDifference()에 의해서 생성된 두 개의 상태의 차이점일 수도 있다.

void _CrtMemDumpAllObjectsSince(const _CrtMemState* state);

이 함수는 free store의 상태가 인수에 의해서 지정된 이후로 free store 안에서 할당된 객체들에 대한 정보를 나타낸다(이것은 여러분의 프로그램에서 _CrtMemCheckPoint()에 대한 이전의 호출에 의해서 기록될 것이다). 만약 여러분이 null을 함수에 전달하면 그것은 여러분의 프로그램이 실행된 이후에 할당된 모든 객체들에 대한 정보를 나타낸다.

int _CrtDumpMemoryLeaks();

이것은 우리의 샘플에 대해서 필요한 함수이다 왜냐하면, 이것은 메모리 유출을 검사하고, 검출된 모든 유출에 대한 정보를 나타내기 때문이다. 여러분은 이 함수를 어느 때고 호출할 수 있다. 그러나 매우 유용한 메커니즘을 사용하여 여러분의 프로그램이 종료될 때 그 함수가 자동적으로 호출되도록 할 수 있다. 만약 여러분이 그러한 메커니즘을 인에이블시켰다면 프로그램 실행 동안에 발생하는 어떠한 메모리 유출도 자동적으로 검출하게 될 것이다. 그러므로 우리가 어떻게 그렇게 하는가에 대해서 살펴본다.

6.2 free store 디버그 작업 제어하기

비트 필드

설명

_CRTDBG_ALLOC_MEM_DF

이 비트가 on되면 이것은 디버그 할당을 on시킨다. 그러므로 free store 상태를 추적할 수 있다.

_CRTDBG_DELAY_FREE_MEM_DF

이 비트가 on되면 이것은 메모리가 delete에 의해서 해제되지 않도록 한다. 그러므로 여러분은 메모리가 부족한 상황에서 어떤 것이 발생하는가를 알아 볼 수 있다.

_CRTDBG_CHECK_ALWAYS_DF

이 비트가 on되면 이것은 _CrtCheckMemory() 함수가 모든 new와 delete에 대해서 자동적으로 호출되도록 한다. 이 함수는 free store가 잘 보존되어 있는가를 검사한다. 예를 들어, 배열의 범위를 넘어서는 값을 저장함으로써 블록들이 덮어씌여지지 않았는가를 검사한다. 만약 어떤 결합이 발견되면 보고서가 출력된다. 이것은 실행을 느리게 한다. 그러나 에러를 재빨리 잡아낸다.

_CRTDBG_CHECK_CRT_DF

이 비트가 on되면 런 타임 라이브러리에 의해서 내부적으로 사용되는 메모리가 디버그 작업 등안에 추적된다.

_CRTDBG_LEAK_CHECK_DF

프로그램이 종료될 때 _CrtDumpMemoryLeaks()를 자동적으로 호출함으로써 유출 검사 작업이 실행되도록 한다. 만약 여러분의 프로그램이 할당된 모든 메모리를 해제하는 데 실패한 경우에만 출력을 얻게 된다.

여러분은 int 형식을 갖는 _crtDbgFlag 플래그를 설정함으로써 free store 디버그 작업을 제어한다. 이 플래그는 5개의 개별적인 제어 비트를 포함하는데, 그 중 하나는 자동 메모리 유출 검사를 인에이블하는 데 사용된다. 여러분은 그러한 제어 비트들을 다음의 식별자들을 사용하여 지정한다.

디폴트로, _CRTDBG_ALL0C_MEM_DF 비트가 on된다. 그리고 다른 모든 것들은 off된다. 이러한 비트들의 조합을 설정하고 해제하는 데 여러분은 반드시 bitwise 연산자를 사용해야 한다. _crtDbgFlag 플래그를 설정하기 위해서 여러분은 int 형식의 플래그를 여러분이 필요로 하는 지시자들의 조합을 구현하는 _CrtDbgFlag() 함수에 전달해야 한다. 여러분이 원하는 지시자들을 설정하는 한가지 방법은, 먼저 _crtDbgFlag 플래그의 현재 상태를 얻는 것이다. 여러분은 그것을 다음과 같이 _CrtSetDbgFlag() 함수를 _CRTDBG_REPORT_FLAG 인수와 함께 호출함으로써 할 수 있다.

int flag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); //현재 플래그를 얻는다.

그런 다음, 여러분은 bitwise 연산자를 사용하여 이 플래그를 갖는 개별적인 지시자들에 대한 식별자들을 결합시킴으로써 그러한 지시자들을 설정 또는 해제할 수 있다. 지시자를 on시키려면 여러분은 지시자 식별자를 플래그와 함께 OR 연산시켜야 한다. 예를 들어, 자동 메모리 유출 검사 지시자를 on시키려면 플래그 안에서 여러분은 다음과 같이 작성할 수 있다.

flag |= _CRTDBG_LEAK_CHECK_DF;

지시자를 off시키려면 식별자의 부정을 플래그와 함께 AND 연산시켜야 한다. 예를 들어, 라이브러리에 의해서 내부적으로 사용되는 메모리를 추적하는 것을 off시키려면 다음과 같이 작성할 수 있다.

flag &= ~_CRTDBG_CHECK_CRT_DF;

새로운 플래그가 효과를 발휘하게 하려면 플래그를 인수로 하여 _CrtSetDbgFlag()를 호출하면 된다.

_CrtSetDbgFlag(flag);

그 외에, 여러분이 원하는 지시자들에 대한 모든 식별자들을 OR 연산시켜서 그 결과를 _CrtSetDbgflag()에 대한 인수로서 전달할 수 있다. 만약 프로그램이 종료될 때 메모리 유출에 대해서만 검사하고자 한다면 다음과 같이 작성한다.

_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF |_CRTDBG_ALL0C_MEM_DF);

여러분의 프로그램에 있는 다양한 지점에서 비트들을 설정/해제하기보다는 지시자들의 특정한 조합을 원한다면 그렇게 하는 가장 쉬운 방법이다. 거의 우리는 동적 메모리 디버깅 기능을 샘플에 적용할 때에 도달했다. free store 디버깅 출력을 어디로 보낼 깃인가를 우리가 어떻게 결정하는가에 대해서 살펴보아야 한다.

6.3 free store 디버깅 출력

free store 디버깅 함수들로부터의 출력의 목적지는 디폴트로 표준 출력 스트림이 아니다. 그것들은 디버그 메시지 윈도우로 간다. 만약 그것들을 stdout에서 보기를 원한다면 우리가 반드시 그렇게 설정해야 한다. 이것에는 두 가지의 함수가 있다. _CrtSetReportMode()는 출력에 대한 일반적인 목적지를 설정하며, _CrtSetReportFile()은 특별히 스트림 목적지를 지정한다. _CrtSetReportMode()는 다음과 같이 선언된다.

int _CrtSetReportMode(int reportType, int reportMode);

리포트 형식

설명

_CRT_WARN

다양한 종류의 경고 메시지를 나타낸다. 메모리 유출이 발생했을 때의 출력이 하나의 경고이다.

_CRT_ERROR

복구할 수 없는 문제를 발생시키는 치명적인 에러

_CRT_ASSERT

assertion으로부터의 출력(우리가 이전에 설명했던 assert()는 함수로부터의 출력이 아닌)

free store 디버깅 함수들에 의해서 세 종류의 출력이 생성된다. _CrtSetReportMode() 함수에 대한 각각의 호출은 두 번째 인수에 의해서 지정된 목적지를 첫 번째 인수에 의해서 지정된 출력 형식으로 설정한다. 여러분은 리포트 형식을 다음에 있는 식별자들 중의 하나로 지정하게 된다.

crtdbg.h 헤더는 ASSERT와 ASSERTE, 두 개의 매크로를 정의하는데, 이것들은 표준 라이브러리에 있는 assert() 함수와 매우 동일한 방식으로 작동한다. 이 두 매크로 사이의 차이점은, ASSERTE는 실패가 발생할 때 assertion 표현을 보고하는 반면, ASSERT는 그렇지 않는다는 것이다.

여러분은 다음의 식별자들의 조합을 사용하여 리포트 모드를 지정한다.

리포트 모드

_CrtDbgReport

_CRTDBG_M0DE_DEBUG

이것은 디폴트 모드로서 디버거의 제어 아래에서 실행할 때 디버그 윈도우 안에서 여러분이 보게 될 디버그 문자열 안에 출력을 전달한다.

_CRTDBG_MODE_FILE

출력은 출력 스트림으로 전달된다.

_CRTDBG_MODE_WNDW

출력은 메시지 박스 안에 나타난다.

_CRTDBG_REPORT_MODE

만약 여러분이 이것을 지정하면 _CrtSetReportMode() 함수는 단지 현재의 리포트 모드를 리턴한다.

한 개 이상의 목적지를 지정하려면 여러분은 | 연산자를 사용하여 식별자들을 OR 연산시키면 된다. 여러분은 _CrtSetReportMode()에 대한 개별적인 호출을 통해서 각각의 출력 형식에 대한 목적지를 설정할 수 있다. 메모리 유출이 검출되었을 때 출력을 파일 스트림으로 돌리기 위해 우리는 다음의 문장을 통해서 리포트 모드를 설정할 수 있다.

CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);

이것은 목적지를 일반적으로 파일 스트림으로 설정한다. 목적지를 특별하게 지정하려면 여전히 _CrtSetReportFile() 함수를 호출해야 한다.

_CrtSetReportFile() 함수는 다음과 같이 선언된다.

_HFILE _CrtSetReportFile(int reportType, _HFILE reportFile);

여기서의 두 번째 인수는 파일 스트림에 대한 _HFILE 형식의 포인터이거나(이것에 대해서는 살펴보지 않을 것이다), 아니면 다음의 식별자들 중 하나이다.

리포트 파일

_CrtDbgReport 행동

_CRTDBG_FILE_STDERR

출력은 표준 에러 스트림인 stderr로 전달된다.

_CRTDBG_FILE_STDOUT

출력은 표준 출력 스트림인 stdout로 전달된다.

_CRTDBG_REPORT_FILE

만약 여러분이 이인수를 지정하면 _CrtSerReportFile() 함수는 현재의 목적지를 리턴할 것이다.

 

메모리 유출을 검출하는 출력을 표준 출력 스트림으로 전달하려면 다음과 같이 작성 한다.

_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STD0UT);

이제, 우리는 샘플에서 메모리 유출 검사 작업을 해보는 데 충분한 free store 디버그 루틴에 대한 지식을 갖게 되었다.

연습 - 메모리 유출 검사

심지어 우리가 프로젝트 설정 사항을 통하여 표준 출력 스트림을 하나의 파일로 가도록 했다고 해도 그것은 출력의 양을 감소시킬 것이다. 그러므로 우리는 이름들에 대한 배열의 크기를 5개 요소로 줄일 것이다. 다음은 일반적인 free store 디버그 기능과 특별한 메모리 유출 검출 기능을 사용하는 main()의 새로운 버전이다.

int main(int argc, char* argv[])

{

    // free store 디버깅 비트와 유출 검사 비트를 on시킨다.

    _CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF |_CRTDBG_ALLOC_MEM_DF);

 

    // stdout에 대한 직접적인 경고

    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);

    _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDOUT);

 

    Name myName("Ivor", "Horton");    // 한 개의 객체를 실험해 본다.

 

    // 로컬 char 배열에 있는 이름들을 검색하며 저장한다.

    char theName[12];

    cout << "\nThe name is " << myName.getName(theName);

 

    // free store 안에 있는 배열에 이름을 저장한다.

    char* pName = new char[myName.getNameLength()+1];

    cout << "\n The name is " << myName.getName(pName);

 

    const int arraysize = 5;

    Name names[arraysize];    // 한 개의 배열에 대한 작업

 

    // 이름들을 초기화한다.

    init(names, arraysize);

 

    // 비교 작업을 한다.

    char* phrase = 0;    //비교 문구를 저장한다.

    char* iName = 0;    //전체 이름을 저장한다.

    char* jName = 0;    //전체 이름을 저장한다.

 

    for(int i = 0; i < arraysize ; i++)    // 각 요소를 비교한다.

    {

        iName = new char[names[i].getNameLength()+1];    // 모든 다른 사람들의

     // 첫째 이름을 저장하는 배열

 

        for(int j = i+1 ; j<arraysize ; j++)

        {

            if(names[i] < names[j])

                phrase = "less than";

            else if(names[i] > names[j])

                phrase = " greater than ";

            else if(names[i] == names[j])    // 불필요한 것 - 그러나 그것은 연산자

         // 함수를 호출한다.

 

                phrase = " equal to ";

            jName = new char[names[j].getNameLength()+1];    // 두 번째의 이름을 저장하는 배열

            cout << endl << names[i].getName(iName) << " is" <<     phrase << names[j].getName (jName);

        }

    }

    court << endl;

    return 0;

}

 

여러분은 물론 crtdbg.h에 대한 #include를 파일에 추가해야 한다. 출력을 더 줄이려고 우리는 DebugStuff.h 헤더 안에 있는 제어 심볼들을 커멘트 처리함으로써 추적 기능에 대한 출력을 off시킬 수 있다.

// DebugStuff.h - 디버깅 제어

#ifndef DEBUGSTUFF_H

#define DEBUGSTUFF_H

 

#ifdef _DEBUG

 

//#define CONSTRUCTOR_TRACE    // 컨스트럭터를 추적한 것을 출력한다.

//#define FUNCTION_TRACE    // 함수 호출들을 추적한다.

#endif

 

#endif    //DEBUGSTUFF_H

 

여러분은 이것을 재컴파일하여 다시 실행시킬 수 있다.

작동하는 방식

이것은 기대한 대로 작동한다. 우리는 프로그램이 정말로 메모리 유출을 발생시킨다는 출럭을 얻게 된다. 그리고 프로그램의 끝부분에서 객체들의 리스트를 얻게 된다. free store 디버그 기능에 의해서 생성되는 출력은 다음과 같이 시작한다.

Detected memory leaks!

Dumping objects ->

{55} normal block at Ox007A1FE0,억 FEO, 16 bytes long.

Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

{54} normal block at OxO07AO030, 16 bytes long.

Data: <Emily Steinbeck > 45 6D 69 6C 79 20 53 74 65 69 6E 62 65 63 6B 00

{53} normal block at OxO07AO070, 13 bytes long.

Data: <Emily Miller > 45 6D 69 6C 79 20 4D 69 6C 6C 65 72 00

...

 

다음과 같이 끝난다.

...

{32} normal block at OxO07A05BO, 8 bvtes long.

Data: <Dickens > 44 69 63 6B 65 6E 73 00

{31} normal block at OxO07AOC90, 8 bytes long.

Data: <Charles > 43 68 61 72 6C 65 73 00

{30} normal block at Ox077AOCDO, 12 bytes long.

Data: <Ivor Horton > 49 76 6F 72 20 48 6F 72 74 6F 6E 00

{27} normal block at OxO07AOFOO, 7 bytes long.

Data: <Horton > 48 6F 72 74 6F 6E 00

{26} normal block at 0x007A0F40, 5 bvtes long.

Data: <Ivor > 49 76 6F 72 00

Object dump complete.

 

free store 안에 남아 있는 것으로 보고된 객체들은 가장 최근에 할당된 것이 먼저 나타나고, 가장 먼저 할당된 것이 가장 나중에 나타난다. 출력을 보면 Name 클래스가 그것의 데이터 멤버에 대해서 메모리를 할당하고는 릴리즈하지 않았다는 것이 분명하다. 마지막에 있는 세 개의 객체들은 main()에서 할당된 pName 배열과 myName 객체의 데이터 멤버들에 해당된다. 완벽한 이름들에 대한 블록은 main() 안에서 할당된다. 그리고 그것들도 역시 사용하고서 그대로 방치되고 있다. 클래스가 갖고 있는 문제는 메모리를 동적으로 할당하는 클래스와 관계된 기본적인 규칙들을 우리가 잊어버린 데서 발생하였다(그것들은 항상 디스트럭터, copy 컨스트럭터, 그리고 할당 연산자를 정의한다). 클래스는 다음과 같이 선언되어야 한다.

class Name

{

    public:

    Name();    //디폴트 컨스트럭터

    Name(const char* pFirst, const char* pSecond);    // 컨스트럭터

 

    Name(const Name& rName);    // Copy 컨스트럭터

 

    ~Name();    // 디스트럭터

 

    char* getName(char* pName) const;    // 전체 이름을 얻는다.

 

    int getNameLength() const;    // 전체 이름의 길이를 얻는다.

 

    // 이름들에 대한 비교 연산자들

    bool operator<(const Name& name) const;

    bool operator==(const Name& name) const;

    bool operator>(const Name& name) const;

 

    Name& operator=(const Name& rName);    // 할당 연산자들

 

private:

    char* pFirstname;

    char* pSurname;

};

 

다음처럼 copy 컨스트럭터를 정의할 수 있다.

Name::Name(const Name& rName)

{

    pFirstname = new char[strlen(rName.pFirstname)+1];    // 첫 번째 이름에 대한

     // 공간을 할당하여

    strcpy(pFirstname, rName.pFirstname);                // 그것을 복사한다.

    pSurname = new char[strlen(rName.pSurname)+1];        //성의 경우와 동일하다.

 

    strcpy(pSurname, rName.pSurname);

}

 

디스트럭터는 두 개의 데이터 멤버들에 대한 메모리를 해제해야 한다.

Name::~Name()

{

    delete() pFirstname;

    delete() pSurname;

}

할당 연산자에서 우리는 왼쪽과 오른쪽이 동일하게 되도록 준비해야 한다.

 

 

Name& Name::operator=(const Name& rName)

{

    if (this == &rName)    // Ihs가 rhs와 같다면

    return *this;            // 객체를 리턴한다.

 

    delete[] pFirstname;

    pFirstname = new char[strlen(rName.pFirstname)+1];    //첫번째 이름에 대한

     // 공간을 할당하여

    strcpy(pFirstname, rName.pFirstname);                // 그것을 복사한다.

 

    pSurname = new char[strlen(rName.pSurname)+1];        // 성의 경우와 동일하다.

 

    strcpy(pFirstname, rName.pSurname);

 

    return *this;

}

 

또한, 우리는 디폴트 컨스트럭터가 적절하게 작동하도록 해야 한다. 만약 디폴트 컨스트럭터가 free store 안에서 메모리를 할당하치 않으면 free store 안에서 할당되지 않은 메모리를 디스트럭터가 잘못하여 삭제하려고 할 것이다. 우리는 다음과 같이 그것을 수정해야 한다.

Name::Name()

{

#ifdef CONSTRUCTOR_TRACE

    // 컨스트럭터 호출들을 추적한다.

 

    cout << "\nDefault Name constructor called.";

 

#endif

 

    // 빈 문자열들에 대해 1의 배열을 할당한다.

    pFirstname = new char[1];

    pSurname = new char[1];

 

    pFirstname[0] = pSurname[0] ='\0';    // null 문자를 저장한다.

}

만약 여러분이 문장들을 main()에 추가하여 거기서 동적으로 할당된 메모리를 삭제하려고 한다면 프로그램은 메모리 유출에 관계된 어떠한 메시지도 없이 실행되어야 한다.