본문 바로가기
GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~

[C/C++ 프로그래밍] 15. 메모리 동적 할당

by GDNGY 2023. 5. 16.

Chapter 15. 메모리 동적 할당

메모리 동적 할당(dynamic memory allocation)은 C/C++ 프로그래밍에서 매우 중요한 주제입니다. 이는 실행 시간 동안 프로그램이 필요한 만큼의 메모리를 요청하고 해제할 수 있게 해주는 메커니즘입니다. C언어에서는 'malloc', 'calloc', 'realloc' 및 'free' 함수를 사용하여 동적 메모리를 할당하고 해제합니다. 'malloc'은 지정된 크기의 메모리 블록을 할당하고, 'calloc'은 지정된 크기의 메모리 블록을 할당한 후 0으로 초기화합니다. 'realloc'은 이미 할당된 메모리 블록의 크기를 변경하고, 'free'는 할당된 메모리를 해제합니다. 반면, C++에서는 'new'와 'delete' 연산자를 사용하여 메모리를 동적으로 할당하고 해제할 수 있습니다. 이는 객체 지향 프로그래밍의 일부로서 객체를 생성하고 소멸시키는 데 사용됩니다. 동적 메모리 할당은 프로그램의 효율성을 높여주지만, 사용이 제대로 이루어지지 않을 경우 메모리 누수와 같은 문제를 일으킬 수 있으므로 주의가 필요합니다.

 

반응형

 


[Chapter 15. 메모리 동적 할당]


15.1. 메모리 동적 할당의 이해
15.1.1. 메모리 동적 할당의 개념
15.1.2. 스택 메모리와 힙 메모리

15.2. C언어에서의 메모리 동적 할당
15.2.1. malloc 함수와 free 함수
15.2.2. calloc 함수와 realloc 함수
15.2.3. 메모리 누수와 그 방지법

15.3. C++에서의 메모리 동적 할당
15.3.1. new 연산자와 delete 연산자
15.3.2. 배열에 대한 동적 할당
15.3.3. 메모리 누수와 그 방지법

15.4. 메모리 동적 할당의 활용
15.4.1. 동적으로 생성된 배열 활용 예시
15.4.2. 동적으로 생성된 구조체 활용 예시

15.5. 메모리 동적 할당의 주의사항
15.5.1. 메모리 누수의 위험성
15.5.2. 메모리 접근 오류


15.1에서는 메모리 동적 할당

메모리 동적 할당은 프로그램 실행 중에 필요한 메모리 크기를 결정하고 할당하는 방법입니다. 이 방식은 프로그램의 유연성을 높이며, 데이터의 크기가 런타임에 결정될 때 유용합니다. 또한, 스택 메모리와 힙 메모리의 차이에 대해서도 배웁니다. 스택 메모리는 자동으로 관리되지만, 힙 메모리는 개발자가 직접 관리해야 하는 중요한 부분입니다.

15.1.1. 메모리 동적 할당의 개념

메모리 동적 할당(dynamic memory allocation)은 컴퓨터 프로그래밍에서 중요한 개념입니다. 그렇다면, 메모리 동적 할당이란 무엇일까요? 보통 프로그래밍에서 사용하는 변수나 배열은 컴파일 타임에 크기가 결정되는데, 동적 메모리 할당은 런타임 중에 메모리의 크기를 결정하고 할당하는 방식을 말합니다.

 

이 개념은 프로그램이 실행 중에 데이터의 크기가 변할 수 있는 경우에 매우 유용합니다. 예를 들어, 사용자로부터 입력을 받아 그 크기만큼의 배열을 생성하려는 경우, 동적 메모리 할당을 사용해야 합니다.

 

C 언어에서는 malloc 함수를 사용하여 메모리를 동적으로 할당할 수 있습니다. 아래에 간단한 예제를 들어보겠습니다:

 

[예제]

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = NULL;
    int size = 0;

    printf("Enter the size of the array: ");
    scanf("%d", &size);

    ptr = (int*)malloc(size * sizeof(int));
    if(ptr == NULL) {
        printf("Memory allocation failed.\n");
        return -1;
    }

    printf("Memory allocation succeeded.\n");

    /* Remember to free the allocated memory */
    free(ptr);

    return 0;
}


여기서 malloc 함수는 힙(heap) 메모리 영역에 원하는 크기만큼의 메모리를 할당하고, 그 메모리의 주소를 반환합니다. 반환된 주소는 포인터 변수 ptr에 저장되며, 이를 통해 할당된 메모리를 사용할 수 있습니다.

C++에서는 new 연산자를 사용하여 메모리를 동적으로 할당할 수 있습니다. C++의 경우에도 동일한 예제를 살펴보겠습니다.

[예제]

#include <iostream>

int main() {
    int *ptr = nullptr;
    int size = 0;

    std::cout << "Enter the size of the array: ";
    std::cin >> size;

    ptr = new int[size];
    if(ptr == nullptr) {
        std::cout << "Memory allocation failed." << std::endl;
        return -1;
    }

    std::cout << "Memory allocation succeeded." << std::endl;

    /* Remember to delete the allocated memory */
    delete[] ptr;

    return 0;
}


C++에서는 new 연산자를 사용하여 메모리를 할당하고, delete 연산자를 사용하여 할당된 메모리를 해제합니다. 위의 예제에서는 동적 배열을 생성하고 해제하는 방법을 보여줍니다.

 

이렇게 동적 메모리 할당을 통해 프로그램의 유연성을 높일 수 있지만, 메모리 관리를 주의 깊게 해야합니다. 동적으로 할당한 메모리는 반드시 해제해야 하며, 그렇지 않으면 메모리 누수가 발생하게 됩니다. 이는 프로그램의 성능을 저하시키는 주요 원인이 될 수 있습니다.

 

15.1.2. 스택 메모리와 힙 메모리

컴퓨터에서 메모리는 크게 스택 메모리(stack memory)와 힙 메모리(heap memory) 두 가지 영역으로 구분됩니다. 이 두 메모리 영역은 용도와 관리 방식이 다르며, 이를 이해하는 것은 메모리 동적 할당을 효과적으로 사용하기 위해 중요합니다.

 

스택 메모리는 컴파일 타임에 크기가 결정되는 변수들이 저장되는 공간입니다. 함수가 호출될 때마다 새로운 스택 프레임이 생성되며, 그 안에는 함수의 지역 변수, 매개변수 등이 저장됩니다. 함수가 종료되면 해당 스택 프레임은 소멸되며, 그 안의 변수들도 함께 소멸합니다. 즉, 스택 메모리는 컴파일러에 의해 자동으로 관리됩니다.

 

아래의 예제는 스택 메모리에 변수를 할당하는 C와 C++ 코드입니다.

 

[예제] C

#include <stdio.h>

void stack_function() {
    int stack_var = 10;  // This variable is stored in stack memory
    printf("Stack variable: %d\n", stack_var);
}

int main() {
    stack_function();
    return 0;
}


[예제] C++

#include <iostream>

void stack_function() {
    int stack_var = 10;  // This variable is stored in stack memory
    std::cout << "Stack variable: " << stack_var << std::endl;
}

int main() {
    stack_function();
    return 0;
}


반면에 힙 메모리는 런타임에 크기가 결정되는 데이터가 저장되는 공간입니다. 힙 메모리는 개발자가 직접 관리해야 하며, 필요한 크기의 메모리를 동적으로 할당하고 사용한 후에는 반드시 해제해야 합니다.

 

이전 섹션에서 살펴본 malloc, new 등의 함수나 연산자는 모두 힙 메모리에 메모리를 할당합니다. 반대로 free, delete 등은 힙 메모리의 할당을 해제합니다.

 

힙 메모리의 관리는 프로그래머의 책임입니다. 적절히 관리되지 않은 힙 메모리는 메모리 누수로 이어질 수 있으며, 이는 프로그램의 성능 저하나 충돌을 일으킬 수 있습니다. 따라서 동적 할당을 사용할 때는 항상 메모리 관리에 주의해야 합니다.

 

이것이 스택 메모리와 힙 메모리의 기본적인 차이점입니다. 이를 이해하는 것은 C/C++ 프로그래밍에 있어서 매우 중요합니다. 이러한 메모리 구조를 이해하면 프로그램의 효율성과 안정성을 높일 수 있습니다.

 


15.2. C언어에서의 메모리 동적 할당

C언어에서 동적 할당을 어떻게 처리하는지 살펴봅니다. 이는 프로그램 실행 중에 필요한 만큼의 메모리를 동적으로 할당하고 해제하는 방법을 포함합니다. 우리는 malloc, calloc, realloc과 같은 함수를 사용하여 메모리를 할당하고, free 함수를 사용하여 할당된 메모리를 해제합니다. 또한 메모리 누수와 그것을 방지하는 방법에 대해서도 배웁니다. 이 모든 내용은 프로그램의 효율성과 성능을 향상하는 데 중요한 역할을 합니다.

15.2.1. malloc 함수와 free 함수

C언어에서는 메모리를 동적으로 할당하기 위해 malloc 함수를 사용합니다. malloc 함수는 메모리 할당에 성공하면 해당 메모리의 시작 주소를 반환하고, 실패하면 NULL을 반환합니다. 이 함수는 stdlib.h 헤더 파일에 선언되어 있습니다.

 

malloc 함수의 기본 구조는 다음과 같습니다:

 

[예제]

void* malloc(size_t size);

 

여기서 size_t는 양의 정수를 나타내는 데이터 타입으로, 할당하고자 하는 메모리의 크기(바이트 단위)를 지정합니다.

 

다음은 malloc 함수를 사용하여 정수를 저장할 수 있는 메모리를 할당하는 C 코드의 예입니다:

 

[예제]

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int*)malloc(sizeof(int)); // Allocate memory for an integer

    if (ptr == NULL) {  // Check if the memory allocation was successful
        printf("Memory allocation failed!\n");
        return 1;
    }

    *ptr = 10;  // Store a value at the allocated memory
    printf("Value: %d\n", *ptr);

    free(ptr);  // Deallocate the memory
    return 0;
}


위의 코드에서는 malloc 함수를 사용하여 정수를 저장하기 위한 메모리를 동적으로 할당합니다. 할당된 메모리는 ptr이라는 포인터 변수를 통해 접근합니다. malloc 함수가 메모리 할당에 실패하면 NULL을 반환하므로, 메모리 할당 후에는 항상 성공 여부를 확인해야 합니다. 이후에는 할당된 메모리에 원하는 값을 저장하고 사용할 수 있습니다.

 

할당된 메모리는 사용이 끝난 후에는 반드시 해제해야 합니다. 이를 위해 C언어에서는 free 함수를 제공합니다. free 함수는 할당된 메모리를 해제하며, 이 함수에는 할당된 메모리의 주소를 인자로 전달합니다. 메모리 누수를 방지하기 위해서는 malloc 함수로 할당된 메모리는 반드시 free 함수로 해제해야 합니다.

 

free 함수를 사용하여 메모리를 해제하는 것은 매우 중요합니다. 메모리를 해제하지 않으면 메모리 누수가 발생하여 프로그램의 성능이 저하될 수 있습니다. 메모리 누수는 특히 오랫동안 실행되는 프로그램에서 심각한 문제를 일으킬 수 있습니다. 따라서 malloc 함수로 할당한 메모리는 사용이 끝났을 때 free 함수로 꼭 해제해야 합니다.

 

15.2.2. calloc 함수와 realloc 함수

이전 섹션에서는 malloc과 free 함수를 사용하여 메모리를 동적으로 할당하고 해제하는 방법에 대해 알아보았습니다. 이번 섹션에서는 calloc과 realloc라는 두 가지 추가적인 메모리 할당 함수를 살펴보겠습니다. 이 두 함수는 stdlib.h 헤더 파일에 선언되어 있습니다.

 

calloc 함수

calloc 함수는 malloc 함수와 유사하게 동적 메모리 할당을 수행하지만, 두 가지 주요 차이점이 있습니다. 첫째, calloc은 요청한 메모리 공간을 0으로 초기화합니다. 둘째, calloc은 할당할 개체의 수와 각 개체의 크기를 별도의 매개변수로 받습니다. 이것은 다음과 같은 형태로 표현됩니다:

 

[예제]

void* calloc(size_t num, size_t size);

 

다음은 calloc 함수를 사용하여 메모리를 할당하는 예제 코드입니다:

[예제]

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int*)calloc(4, sizeof(int)); // Allocate memory for four integers

    if (ptr == NULL) {  // Check if the memory allocation was successful
        printf("Memory allocation failed!\n");
        return 1;
    }

    for (int i = 0; i < 4; i++) {
        printf("Value at index %d: %d\n", i, *(ptr + i));  // Print the initial values
    }

    free(ptr);  // Deallocate the memory
    return 0;
}

 

위의 코드에서는 calloc 함수를 사용하여 4개의 정수를 저장할 수 있는 메모리를 동적으로 할당하고 있습니다. 할당된 메모리는 ptr이라는 포인터 변수를 통해 접근합니다. calloc 함수로 할당된 메모리의 초기 값은 모두 0입니다.

 

realloc 함수

realloc 함수는 이미 할당된 메모리의 크기를 변경하는 데 사용됩니다. 이 함수는 새로운 크기를 인자로 받아 원래 메모리 블록의 크기를 변경하고, 변경된 메모리 블록의 주소를 반환합니다. 이것은 다음과 같은 형태로 표현됩니다:

 

[예제]

void* realloc(void* ptr, size_t newSize);

 

다음은 realloc 함수를 사용하여 메모리를 재할당하는 예제 코드입니다:

 

[예제]

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int*)malloc(2 * sizeof(int)); // Allocate memory for two integers

    if (ptr == NULL) {  // Check if the memory allocation was successful
        printf("Memory allocation failed!\n");
        return 1;
    }

    ptr[0] = 1;
    ptr[1] = 2;

    ptr = (int*)realloc(ptr, 4 * sizeof(int)); // Increase the memory size to hold four integers

    if (ptr == NULL) {  // Check if the memory allocation was successful
        printf("Memory reallocation failed!\n");
        return 1;
    }

    ptr[2] = 3;
    ptr[3] = 4;

    for (int i = 0; i < 4; i++) {
        printf("Value at index %d: %d\n", i, ptr[i]);  // Print the values
    }

    free(ptr);  // Deallocate the memory
    return 0;
}

 

위의 코드에서는 malloc 함수를 사용하여 먼저 2개의 정수를 저장할 수 있는 메모리를 할당합니다. 그 다음 realloc 함수를 사용하여 메모리의 크기를 4개의 정수를 저장할 수 있도록 늘립니다. realloc 함수를 사용하면 원래 메모리 블록의 내용은 새 메모리 블록으로 복사됩니다.

 

주의사항

malloc, calloc 및 realloc 함수는 성공적으로 메모리를 할당하거나 크기를 변경할 수 없는 경우 NULL을 반환합니다. 따라서 메모리 할당 후에는 항상 포인터가 NULL인지 확인해야 합니다. 또한, 메모리를 더 이상 사용하지 않게 되면 free 함수를 사용하여 반드시 메모리를 해제해야 합니다. 그렇지 않으면 메모리 누수가 발생할 수 있습니다.

 

15.2.3. 메모리 누수와 그 방지법

메모리 누수는 프로그램에서 동적으로 할당된 메모리를 필요하지 않은 상태임에도 불구하고 해제하지 않는 현상을 말합니다. 이 현상은 프로그램이 실행되는 동안 메모리 사용량을 불필요하게 증가시키고, 궁극적으로는 시스템의 전반적인 성능을 저하시키는 결과를 초래할 수 있습니다.

 

메모리 누수의 예시

#include <stdlib.h>

int main() {
    int *ptr = (int*)malloc(sizeof(int));
    // Do some operations with ptr
    // Forget to call free(ptr)
    return 0;
}

 

위 코드에서, ptr로 할당된 메모리는 free가 호출되지 않아 메모리 누수를 일으킵니다.

 

메모리 누수 방지

C언어에서 메모리 누수를 방지하는 가장 기본적인 방법은 프로그램이 동적으로 할당한 모든 메모리를 해제하는 것입니다. 이는 free 함수를 사용하여 수행됩니다.

 

[예제]

#include <stdlib.h>

int main() {
    int *ptr = (int*)malloc(sizeof(int));
    // Do some operations with ptr
    free(ptr);  // Remember to free the allocated memory
    return 0;
}

 

메모리 누수의 위험성

메모리 누수가 발생하면 시스템이 점점 더 많은 메모리를 소비하게 되며, 이로 인해 시스템 성능이 저하되거나, 심각한 경우에는 시스템이 강제 종료될 수도 있습니다. 이러한 문제는 프로그램의 실행 시간이 길거나, 많은 양의 메모리를 동적으로 할당해야 하는 프로그램에서 특히 심각합니다.

 

메모리 누수 탐지

메모리 누수를 탐지하는 일은 쉽지 않습니다. 복잡한 프로그램에서는 수많은 메모리 할당과 해제가 발생하며, 이 모든 것을 수동으로 추적하는 것은 매우 어렵습니다. 이런 경우에는 메모리 누수를 자동으로 탐지해 주는 도구를 사용하는 것이 유용합니다.

 

C와 C++ 프로그램에서는 Valgrind와 같은 도구를 사용하여 메모리 누수를 탐지할 수 있습니다. 이러한 도구는 프로그램이 메모리를 적절히 관리하고 있는지를 검사하고, 메모리 누수가 발견되면 이를 알려줍니다. 이는 프로그래머가 메모리 누수를 빠르게 찾아 수정할 수 있도록 도와줍니다.

 

또한, 최신의 개발 환경에서는 디버깅 도구를 사용하여 메모리 누수를 탐지할 수도 있습니다. Visual Studio와 같은 통합 개발 환경(IDE)는 메모리 사용을 추적하고 문제를 식별하는 도구를 내장하고 있습니다.

 

핵심은 모든 동적 메모리 할당에 대해 free 함수를 호출해야 하며, 복잡한 시스템에서는 도구를 활용하여 메모리 누수를 탐지하고 수정하는 것입니다.


15.3. C++에서의 메모리 동적 할당

C++에서는 메모리 동적 할당을 위해 'new'와 'delete' 연산자를 사용합니다. 'new'는 필요한 메모리 공간을 할당하고, 할당된 메모리의 주소를 반환합니다. 반면에 'delete'는 'new'에 의해 할당된 메모리를 해제합니다. 이는 C 언어의 'malloc/calloc'과 'free'와 유사한 역할을 하지만, C++에 특화된 기능을 제공하며, 객체 지향 프로그래밍에 보다 적합합니다.

15.3.1. new 연산자와 delete 연산자

C++에서 메모리 동적 할당을 담당하는 것은 'new'와 'delete' 연산자입니다. 이 두 연산자는 C 언어의 'malloc'과 'free'와 유사한 역할을 하지만, 더욱 간결하고 객체 지향적인 방식을 제공하며 C++ 프로그래밍에서 광범위하게 사용됩니다.

 

'new' 연산자는 필요한 메모리 공간을 할당하고, 해당 메모리 공간의 주소를 반환하는 역할을 합니다. 사용 방법은 다음과 같습니다.

 

[예제]

int* p = new int;  // 정수를 저장할 수 있는 메모리 공간을 할당하고, 그 주소를 p에 저장한다.

 

이렇게 할당된 메모리는 'delete' 연산자를 사용하여 해제할 수 있습니다.

 

[예제]

delete p;  // p가 가리키는 메모리 공간을 해제한다.

 

'new'는 단일 변수뿐만 아니라 배열도 동적으로 할당할 수 있습니다. 배열의 경우에는 'delete[]'를 사용하여 메모리를 해제합니다.

 

[예제]

int* arr = new int[5];  // 정수 5개를 저장할 수 있는 메모리를 할당하고, 첫 번째 원소의 주소를 arr에 저장한다.
delete[] arr;  // arr이 가리키는 배열을 메모리에서 해제한다.

 

'new'와 'delete'는 C++의 중요한 요소 중 하나입니다. 특히, 객체 지향 프로그래밍에서는 객체를 동적으로 생성하고 해제하는 데 'new'와 'delete'가 광범위하게 사용됩니다.

 

[예제]

class MyClass {
    public:
    MyClass() {
        cout << "Object created!" << endl;
    }
    ~MyClass() {
        cout << "Object destroyed!" << endl;
    }
};

int main() {
    MyClass* obj = new MyClass;  // 객체를 동적으로 생성한다. 생성자가 호출된다.
    delete obj;  // 객체를 해제한다. 소멸자가 호출된다.
    return 0;
}

 

이러한 방식으로, 'new'와 'delete'는 C++에서의 메모리 관리에 있어 핵심적인 역할을 합니다. 하지만 동적 메모리를 사용할 때는 'delete'를 통해 꼭 메모리를 해제해 주어야 메모리 누수를 방지할 수 있습니다.

 

'new'와 'delete'를 사용하는 방법은 다양한 상황에서 유연하게 메모리를 관리할 수 있게 해주지만, 잘못 사용하게 되면 메모리 누수(memory leak)와 같은 문제를 일으킬 수 있습니다. 메모리 누수는 프로그램이 사용하는 메모리를 계속 증가시키는 현상으로, 결국 프로그램의 성능을 저하시키고 시스템 전체에 문제를 일으킬 수 있습니다.

 

아래는 메모리 누수를 일으키는 코드의 예시입니다:

 

[예제]

for(int i = 0; i < 1000000; i++) {
    int* p = new int;
}

 

이 코드는 1,000,000개의 정수를 동적으로 할당하지만, 할당한 메모리를 해제하지 않습니다. 이로 인해 메모리 누수가 발생하게 됩니다.

 

이러한 문제를 피하려면 'new'로 할당한 메모리는 반드시 'delete'로 해제해야 합니다. 예를 들어, 위 코드를 수정하여 메모리 누수를 방지할 수 있습니다:

 

[예제]

for(int i = 0; i < 1000000; i++) {
    int* p = new int;
    delete p;  // 할당한 메모리를 해제한다.
}

 

또한, 동적 할당을 사용할 때는 포인터 변수를 적절히 관리하는 것이 중요합니다. 포인터 변수가 할당된 메모리의 주소를 잃어버리게 되면, 그 메모리를 해제할 방법이 없어지므로 메모리 누수가 발생하게 됩니다.

 

동적 할당과 관련된 더 많은 사항들이 있습니다만, 여기서는 가장 기본적인 내용들만 소개했습니다. 이해를 돕기 위해 실제 코드를 작성하면서 'new'와 'delete'를 사용해 보는 것이 좋습니다. 이 과정에서 중요한 것은 항상 'new'로 할당한 메모리를 'delete'로 해제하는 것입니다. 이것은 좋은 프로그래밍 습관이며, 메모리 누수와 같은 문제를 방지하는 데 중요합니다.

 

15.3.2. 배열에 대한 동적 할당

C++에서는 'new' 연산자를 이용해 배열의 크기를 동적으로 할당할 수 있습니다. 동적 배열 할당은 실행 시간에 배열의 크기를 결정할 수 있으므로, 프로그램의 유연성을 높여줍니다.

 

[예제]

int* arr = new int[10];  // 10개의 정수를 저장할 수 있는 배열 동적 할당

 

이런 식으로 'new'를 사용하면 메모리에 10개의 int형 데이터를 저장할 공간을 할당받고, 이 메모리의 주소를 반환하여 포인터 변수 'arr'에 저장합니다. 이제 'arr'은 배열의 이름처럼 사용될 수 있습니다.

 

[예제]

for(int i = 0; i < 10; i++) {
    arr[i] = i;
}

 

이 코드는 각 배열 요소에 그 인덱스 값을 할당하는 방법을 보여줍니다. 배열 요소에 접근할 때는 일반적인 배열과 마찬가지로 대괄호([])를 사용합니다.

 

하지만 동적으로 할당된 메모리는 반드시 'delete'를 이용해 해제해야 합니다. 배열을 동적으로 할당한 경우, 해제할 때는 'delete[]'를 사용해야 합니다.

 

[예제]

delete[] arr;  // 동적으로 할당한 배열 메모리 해제

 

이렇게 동적 할당된 배열은 프로그래머의 책임하에 메모리가 관리됩니다. 동적 할당을 통해 크기가 큰 데이터 구조를 만들거나, 실행 중에 메모리 요구량을 변경할 수 있게 됩니다. 하지만 할당된 메모리를 해제하지 않으면 메모리 누수가 발생하게 됩니다.

 

그리고 'new'와 'delete'를 사용하는 동안 에러가 발생하면 bad_alloc 예외가 발생합니다. 이러한 예외를 처리하려면 try-catch 문을 사용할 수 있습니다.

 

[예제]

try {
    int* myarray= new int[1000];
}
catch (std::bad_alloc& ba) {
    std::cerr << "bad_alloc caught: " << ba.what() << '\n';
}

 

이 코드는 'new' 연산자가 메모리를 할당하지 못할 때 발생하는 예외를 처리합니다. 예외가 발생하면, catch 블록 내의 코드가 실행되어 에러 메시지를 출력합니다. 이렇게 동적 할당된 메모리 관리에 주의를 기울이면서 사용하면, C++의 효율적인 메모리 관리 기법을 최대한 활용할 수 있습니다.

 

15.3.3. 메모리 누수와 그 방지법

C++에서 동적 할당은 프로그램의 유연성을 높여주는 강력한 도구입니다. 하지만 이러한 동적 할당된 메모리는 프로그래머가 직접 관리해야 하며, 잘못 관리하면 메모리 누수(memory leak)라는 문제가 발생합니다. 메모리 누수는 프로그램이 동적으로 할당한 메모리를 제대로 해제하지 않아, 필요 없는 메모리가 계속 쌓이는 현상을 말합니다. 이는 프로그램의 성능 저하를 초래하며, 심한 경우에는 프로그램이나 시스템이 멈추는 등의 치명적인 문제를 야기할 수 있습니다.

 

따라서 메모리 누수를 방지하는 방법을 알아두는 것이 중요합니다. C++에서는 'new'로 할당한 메모리는 반드시 'delete'로 해제해야 합니다. 특히, 배열을 동적 할당한 경우에는 'delete[]'를 사용해야 합니다. 아래는 간단한 예제 코드입니다.

 

[예제]

int* arr = new int[10];  // 배열 동적 할당

// 코드 작성...

delete[] arr;  // 할당된 배열 메모리 해제

 

하지만 이렇게 모든 동적 할당 메모리를 수동으로 관리하는 것은 복잡하고 버그가 발생하기 쉽습니다. 특히 예외가 발생하거나, 함수에서 빠져나올 때 등에는 메모리 해제를 잊어버리기 쉽습니다.

 

이를 해결하기 위해 C++에서는 스마트 포인터(smart pointer)라는 개념을 도입했습니다. 스마트 포인터는 동적 할당된 메모리를 자동으로 관리해주는 객체입니다. C++11부터는 표준 라이브러리에서 unique_ptr, shared_ptr, weak_ptr 등의 스마트 포인터를 제공합니다.

 

[예제]

std::unique_ptr<int[]> arr(new int[10]);  // unique_ptr로 동적 배열 관리

 

위와 같이 unique_ptr을 사용하면, unique_ptr 객체가 범위를 벗어날 때 자동으로 메모리를 해제해줍니다. 따라서 프로그래머가 직접 'delete'를 호출할 필요가 없습니다.

 

스마트 포인터를 사용하면 메모리 누수를 크게 줄일 수 있지만, 완벽히 방지할 수는 없습니다. 예를 들어, 순환 참조(circular reference) 같은 문제는 스마트 포인터만으로는 해결하기 어렵습니다. 그러므로 항상 메모리 관리에 주의를 기울이는 습관을 들이는 것이 중요합니다. 메모리 누수는 프로그램의 성능을 크게 저하시킬 뿐만 아니라, 다른 버그를 야기하기도 하므로, 메모리 관리에 신경 쓰는 것이 좋습니다.

 


15.4. 메모리 동적 할당의 활용

메모리 동적 할당은 프로그램이 실행 중에 정확한 메모리 요구사항을 알 수 있을 때 매우 유용합니다. 예를 들어, 사용자로부터 입력을 받아야하는 데이터의 양이 실행 시점에 결정되는 경우, 동적 할당을 사용해 정확한 양의 메모리를 할당할 수 있습니다. 또한, 크기가 변경 가능한 데이터 구조(예: 링크드 리스트, 트리, 그래프 등)를 구현할 때도 이를 활용할 수 있습니다. 이런 구조들은 메모리의 일정 부분을 추가하거나 제거해야 할 때 동적 할당을 사용합니다. 즉, 메모리 동적 할당은 프로그램의 유연성과 효율성을 향상하는 중요한 도구입니다.

15.4.1. 동적으로 생성된 배열 활용 예시

동적으로 생성된 배열은 프로그램 실행 도중에 정확한 크기를 결정할 수 있게 해주는 유용한 도구입니다. 이번 섹션에서는 C와 C++에서 동적으로 생성된 배열을 어떻게 사용하는지에 대한 예제를 살펴볼 것입니다.

 

먼저, C언어에서의 예시를 살펴보겠습니다.

 

[예시] C

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n;
    printf("배열의 크기를 입력하세요: ");
    scanf("%d", &n);

    int* arr = (int*)malloc(n * sizeof(int));

    if (arr == NULL) {
        printf("메모리 할당 실패");
        return 1;
    }

    for (int i = 0; i < n; i++) {
        arr[i] = i;
    }

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }

    free(arr);
    return 0;
}

 

위 예제에서는 동적 메모리 할당을 사용하여 사용자로부터 입력받은 크기만큼의 정수 배열을 생성합니다. 메모리 할당 후에는 반드시 메모리 할당이 성공했는지 확인해야 하며, 마지막에는 free()를 사용하여 할당된 메모리를 해제합니다.

 

다음으로 C++에서 동적으로 생성된 배열을 사용하는 예제를 살펴보겠습니다.

 

[예시] C++

#include <iostream>

int main() {
    int n;
    std::cout << "Enter the size of the array: ";
    std::cin >> n;

    int* arr = new int[n];

    for (int i = 0; i < n; i++) {
        arr[i] = i;
    }

    for (int i = 0; i < n; i++) {
        std::cout << arr[i] << " ";
    }

    delete[] arr;
    return 0;
}

 

이 C++ 코드는 앞서 살펴본 C 코드와 매우 유사합니다. 차이점은 C++에서는 new와 delete 연산자를 사용하여 동적 메모리를 할당하고 해제한다는 것입니다.

 

이러한 동적 배열은 필요한 만큼의 메모리만 할당하여 프로그램의 효율성을 높이고, 실행 시점에 데이터의 크기가 결정되는 상황에서 유용합니다. 단, 항상 동적으로 할당된 메모리는 사용 후에 해제해주어야 메모리 누수를 방지할 수 있습니다.

 

15.4.2. 동적으로 생성된 구조체 활용 예시

구조체(structure)는 여러 변수를 한 곳에 묶을 수 있는 방법으로, 서로 다른 데이터 유형을 하나의 묶음으로 만들 수 있습니다. 이번 섹션에서는 동적 메모리 할당을 사용하여 구조체를 생성하고 활용하는 방법에 대해 설명하겠습니다.

 

먼저 C언어에서 동적으로 생성된 구조체를 사용하는 예제를 살펴보겠습니다.

 

[예시] C

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

typedef struct {
    char name[30];
    int age;
} Person;

int main() {
    Person *p = (Person*)malloc(sizeof(Person));
    if (p == NULL) {
        printf("메모리 할당 실패\n");
        return 1;
    }

    strcpy(p->name, "John Doe");
    p->age = 30;

    printf("Name: %s\n", p->name);
    printf("Age: %d\n", p->age);

    free(p);
    return 0;
}


이 코드는 동적 메모리 할당을 사용하여 Person 구조체 인스턴스를 생성하고 있습니다. malloc() 함수를 사용하여 Person 크기만큼의 메모리를 할당하고, 이후에는 -> 연산자를 사용하여 구조체 멤버에 접근합니다. 이 프로그램은 구조체 인스턴스를 성공적으로 사용한 후 free() 함수를 호출하여 메모리를 반환합니다.

이번에는 C++에서 동적으로 생성된 구조체를 사용하는 방법을 살펴보겠습니다.

[예시] C++

#include <iostream>
#include <string>

struct Person {
    std::string name;
    int age;
};

int main() {
    Person *p = new Person;

    p->name = "John Doe";
    p->age = 30;

    std::cout << "Name: " << p->name << std::endl;
    std::cout << "Age: " << p->age << std::endl;

    delete p;
    return 0;
}

 

C++에서는 new 키워드를 사용하여 동적으로 메모리를 할당합니다. 이 경우에도, -> 연산자를 사용하여 구조체의 멤버에 접근할 수 있습니다. 메모리가 더 이상 필요하지 않게 되면 delete 키워드를 사용하여 메모리를 반환합니다.

 

이와 같이, 동적 메모리 할당은 단일 변수 뿐만 아니라 복잡한 구조체와 같은 데이터 구조에 대해서도 사용될 수 있습니다. 이러한 방법은 프로그램 실행 중에 데이터 구조의 크기를 결정해야 할 때 매우 유용합니다. 그러나 항상 주의해야 할 점은 동적으로 할당된 메모리는 사용 후 반드시 해제해야 한다는 것입니다. 그렇지 않으면 메모리 누수가 발생할 수 있습니다.

 


15.5. 메모리 동적 할당의 주의사항

메모리 동적 할당은 유연성을 제공하지만 주의해야 할 부분들이 있습니다.

1. 할당한 메모리는 반드시 해제해야 합니다. 메모리를 해제하지 않으면 메모리 누수가 발생하여 시스템의 성능을 저하시킵니다.
2. 이미 해제한 메모리에 접근하려고 하면 오류가 발생합니다. 이를 dangling pointer라고 합니다.
3. 할당된 메모리 범위를 벗어나 데이터를 쓰려고 하면 오류가 발생합니다. 이를 buffer overflow라고 합니다.
4. 충분한 메모리가 없을 때 메모리 할당이 실패하면 NULL이 반환됩니다. 이런 경우를 항상 체크해야 합니다.​

15.5.1. 메모리 누수의 위험성

C/C++에서 메모리 관리는 매우 중요한 요소 중 하나입니다. 특히, 동적으로 할당한 메모리를 제대로 해제하지 않는다면, 이는 메모리 누수(memory leak)로 이어질 수 있습니다. 메모리 누수는 애플리케이션의 성능을 저하시키며, 심각한 경우에는 시스템 전체의 안정성에 영향을 미칠 수 있습니다.

 

메모리 누수는 프로그램이 실행되는 동안에 발생하는데, 프로그램이 할당한 메모리를 해제하지 않아서 메모리 공간이 점차 줄어드는 현상을 말합니다. 이러한 메모리 누수는 프로그램의 실행 시간이 길어질수록 메모리 사용량이 불필요하게 늘어나게 만들며, 결국에는 프로그램이나 시스템 전체의 성능을 떨어뜨리게 됩니다.

 

다음은 메모리 누수를 일으키는 간단한 C++ 코드 예제입니다:

 

[예시] 

#include <iostream>

int main() {
    while(true) {
        int* ptr = new int[10000]; // 이 라인에서 메모리를 할당하지만
        // delete[] ptr; // 이 라인이 주석 처리되어 메모리가 해제되지 않습니다.
    }
    return 0;
}

 

이 코드는 무한 루프 안에서 계속해서 메모리를 할당합니다. 그러나 할당된 메모리를 해제하는 부분이 주석 처리되어 있어 메모리 누수가 발생하게 됩니다. 이 프로그램을 실행하면 메모리 사용량이 계속 증가하는 것을 볼 수 있습니다.

 

메모리 누수를 방지하는 가장 기본적인 방법은 '동적으로 할당한 메모리는 반드시 해제한다'는 원칙을 지키는 것입니다. 이를 위해서는 동적 할당과 메모리 해제를 짝을 이루도록 코드를 작성해야 합니다. 예를 들어, 함수 내에서 동적 할당을 수행한다면, 같은 함수 내에서 해제를 수행하거나, 해제해야 할 메모리에 대한 정보를 반환하여 호출한 쪽에서 해제하도록 코드를 작성해야 합니다.

 

[예시]

void someFunction() {
    int* arr = new int[100];
    // 배열 사용
    delete[] arr; // 사용이 끝난 후 해제
}

 

하지만 이 방법은 복잡한 프로그램에서는 어려움이 따를 수 있습니다. 특히 예외 처리나 여러 함수를 거치며 메모리를 전달할 때는 메모리를 언제, 어디서 해제할지를 정확히 관리하는 것이 어렵습니다. 이런 상황에서는 스마트 포인터(smart pointer)와 같은 메모리 관리 기법을 활용할 수 있습니다. C++에서는 unique_ptr, shared_ptr, weak_ptr 등 다양한 스마트 포인터를 제공하며, 이들을 활용하면 메모리 누수를 효과적으로 방지할 수 있습니다.

 

15.5.2. 메모리 접근 오류

C/C++ 언어에서는 개발자가 직접 메모리를 관리하므로, 이를 제대로 처리하지 않을 경우 다양한 문제가 발생할 수 있습니다. 특히 메모리 접근 오류는 가장 흔히 발생하는 문제 중 하나입니다. 메모리 접근 오류는 보통 할당되지 않은 메모리 영역에 접근하려고 할 때, 또는 이미 해제된 메모리 영역에 접근하려고 할 때 발생합니다. 이러한 오류는 프로그램의 비정상적인 종료나 예상치 못한 동작을 일으킬 수 있습니다.

 

다음은 메모리 접근 오류를 발생시키는 C++ 코드의 예시입니다:

 

[예시]

int main() {
    int* ptr = new int[10];
    delete[] ptr;
    ptr[5] = 20; // 이미 해제된 메모리 영역에 접근하는 경우
    return 0;
}

 

이 코드에서는 동적으로 배열을 할당한 후, 메모리를 해제합니다. 그런데 메모리를 해제한 뒤에 다시 그 메모리 영역에 접근하려고 하므로, 메모리 접근 오류가 발생하게 됩니다.

 

이외에도 배열의 범위를 넘어서 접근하는 경우도 메모리 접근 오류를 발생시킵니다. 예를 들어, 다음과 같은 코드가 있습니다:

 

[예시]

int main() {
    int* arr = new int[10];
    arr[15] = 30; // 할당된 범위를 넘어서 접근하는 경우
    delete[] arr;
    return 0;
}

 

이 코드에서는 10개의 원소를 가진 배열을 동적으로 생성하였지만, 15번째 인덱스에 접근하려고 하므로 메모리 접근 오류가 발생합니다.

 

이런 메모리 접근 오류를 방지하기 위해선 두 가지 원칙을 지켜야 합니다. 첫째, 항상 할당된 메모리의 범위 내에서만 접근해야 합니다. 둘째, 메모리를 해제한 후에는 해당 메모리에 접근하지 않아야 합니다. 메모리를 해제한 후에는 반드시 포인터를 null로 설정하여, 더 이상 접근할 수 없도록 하는 것이 좋습니다.

 

[예시]

int main() {
    int* ptr = new int[10];
    delete[] ptr;
    ptr = nullptr; // 메모리 해제 후 포인터를 null로 설정
    return 0;
}

 

이렇게 포인터를 null로 설정하면, 실수로 해제된 메모리에 접근하는 것을 방지할 수 있습니다. 이 외에도 메모리 할당과 해제를 철저히 관리하면서 프로그래밍하는 습관을 들이는 것이 중요합니다.

 

 

 

2023.05.16 - [GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~] - [C/C++ 프로그래밍] 14. 예외 처리

 

[C/C++ 프로그래밍] 14. 예외 처리

Chapter 14. 예외 처리 C/C++에서 예외 처리는 프로그램에서 예기치 않은 이벤트나 오류가 발생했을 때 이를 효과적으로 처리하는 방법을 말합니다. 이런 오류들은 파일을 열 수 없거나, 메모리를 할

gdngy.tistory.com

 

반응형

댓글