C++ 동적 메모리 할당

C++Beginner
지금 연습하기

소개

이 랩에서는 C++ 에서 동적 메모리 할당을 처리하는 방법을 배우게 됩니다. 먼저 new 연산자를 사용하여 메모리를 할당한 다음, delete 연산자를 사용하여 메모리를 해제합니다. 또한 동적 배열 생성, shared_ptrunique_ptr과 같은 스마트 포인터 사용, 메모리 누수 확인도 탐구합니다. 이러한 기술은 C++ 프로그래밍에서 효과적인 메모리 관리에 필수적입니다.

이 랩은 다음 주요 단계를 다룹니다: new 연산자를 사용한 메모리 할당, delete 연산자를 사용한 메모리 해제, new[]를 사용한 동적 배열 생성, delete[]를 사용한 배열 삭제, shared_ptr 스마트 포인터 구현, 독점 소유권을 위한 unique_ptr 사용, 메모리 누수 확인, 그리고 메모리 할당 실패 처리.

new 연산자를 사용하여 메모리 할당

이 단계에서는 C++ 에서 런타임에 new 연산자를 사용하여 동적으로 메모리를 할당하는 방법을 배우게 됩니다. 동적 메모리 할당을 사용하면 프로그램 실행 중에 크기가 결정되는 변수와 배열을 생성할 수 있어 정적 메모리 할당보다 더 많은 유연성을 제공합니다.

WebIDE 를 열고 ~/project 디렉토리에 dynamic_memory.cpp라는 새 파일을 생성합니다.

touch ~/project/dynamic_memory.cpp

dynamic_memory.cpp에 다음 코드를 추가합니다.

#include <iostream>

int main() {
    // Dynamically allocate an integer using new
    int* dynamicInteger = new int;

    // Assign a value to the dynamically allocated integer
    *dynamicInteger = 42;

    // Print the value of the dynamically allocated integer
    std::cout << "Dynamically allocated integer value: " << *dynamicInteger << std::endl;

    // Print the memory address of the dynamically allocated integer
    std::cout << "Memory address: " << dynamicInteger << std::endl;

    return 0;
}

핵심 개념을 자세히 살펴보겠습니다.

  1. int* dynamicInteger = new int;:

    • new 연산자는 정수를 위한 메모리를 할당합니다.
    • 할당된 메모리에 대한 포인터를 반환합니다.
    • 스택이 아닌 힙에 변수를 생성합니다.
  2. *dynamicInteger = 42;:

    • 역참조 연산자 *를 사용하여 값을 할당합니다.
    • 동적으로 할당된 메모리에 값 42 를 저장합니다.

프로그램을 컴파일하고 실행합니다.

g++ dynamic_memory.cpp -o dynamic_memory
./dynamic_memory

예시 출력:

Dynamically allocated integer value: 42
Memory address: 0x55f4e8a042a0

new 연산자에 대한 주요 사항:

  • 런타임 중에 동적으로 메모리를 할당합니다.
  • 할당된 메모리에 대한 포인터를 반환합니다.
  • 메모리는 힙에 할당됩니다.
  • 크기는 런타임에 결정될 수 있습니다.
  • 보다 유연한 메모리 관리를 허용합니다.

참고: 다음 단계에서는 메모리 누수를 방지하기 위해 이 동적으로 할당된 메모리를 적절하게 해제하는 방법을 배우게 됩니다.

delete 연산자로 메모리 해제

이 단계에서는 C++ 에서 delete 연산자를 사용하여 동적으로 할당된 메모리를 적절하게 해제하는 방법을 배우게 됩니다. 메모리를 해제하지 못하면 메모리 누수가 발생할 수 있으며, 이는 프로그램의 성능 문제를 일으킬 수 있습니다.

WebIDE 를 열고 ~/project 디렉토리에 memory_release.cpp라는 새 파일을 생성합니다.

touch ~/project/memory_release.cpp

memory_release.cpp에 다음 코드를 추가합니다.

#include <iostream>

int main() {
    // Dynamically allocate an integer
    int* dynamicInteger = new int;

    // Assign a value to the dynamically allocated integer
    *dynamicInteger = 42;

    // Print the value before releasing memory
    std::cout << "Value before release: " << *dynamicInteger << std::endl;

    // Release the dynamically allocated memory using delete
    delete dynamicInteger;

    // Set the pointer to nullptr after deletion
    dynamicInteger = nullptr;

    // Demonstrate safe pointer usage
    if (dynamicInteger == nullptr) {
        std::cout << "Memory has been successfully released" << std::endl;
    }

    return 0;
}

delete 연산자에 대한 주요 사항:

  1. delete dynamicInteger;:

    • 이전에 new로 할당된 메모리를 해제합니다.
    • 메모리 누수를 방지합니다.
    • 메모리를 시스템에 반환합니다.
  2. dynamicInteger = nullptr;:

    • 삭제 후 포인터를 nullptr로 설정합니다.
    • 해제된 메모리의 우발적인 사용을 방지하는 데 도움이 됩니다.
    • 추가적인 안전 점검을 제공합니다.

프로그램을 컴파일하고 실행합니다.

g++ memory_release.cpp -o memory_release
./memory_release

예시 출력:

Value before release: 42
Memory has been successfully released

중요한 메모리 관리 규칙:

  • new로 할당된 메모리에는 항상 delete를 사용합니다.
  • 삭제 후 포인터를 nullptr로 설정합니다.
  • 삭제된 후에는 포인터를 사용하지 마십시오.
  • new에는 해당 delete가 있어야 합니다.

new[] 를 사용하여 동적 배열 생성

이 단계에서는 C++ 에서 new[] 연산자를 사용하여 동적 배열을 만드는 방법을 배우게 됩니다. 동적 배열을 사용하면 런타임에 여러 요소에 대한 메모리를 할당할 수 있어 정적 배열보다 더 많은 유연성을 제공합니다.

WebIDE 를 열고 ~/project 디렉토리에 dynamic_array.cpp라는 새 파일을 생성합니다.

touch ~/project/dynamic_array.cpp

dynamic_array.cpp에 다음 코드를 추가합니다.

#include <iostream>

int main() {
    // Declare the size of the dynamic array
    int arraySize = 5;

    // Create a dynamic integer array using new[]
    int* dynamicArray = new int[arraySize];

    // Initialize array elements
    for (int i = 0; i < arraySize; ++i) {
        dynamicArray[i] = i * 10;
    }

    // Print array elements
    std::cout << "Dynamic Array Contents:" << std::endl;
    for (int i = 0; i < arraySize; ++i) {
        std::cout << "Element " << i << ": " << dynamicArray[i] << std::endl;
    }

    return 0;
}

동적 배열에 대한 주요 사항:

  1. int* dynamicArray = new int[arraySize];:

    • 정수 배열에 대한 메모리를 할당합니다.
    • arraySize는 요소의 수를 결정합니다.
    • 메모리는 힙에 할당됩니다.
    • 크기는 런타임에 결정될 수 있습니다.
  2. 배열 초기화 및 접근:

    • 표준 배열 인덱싱 dynamicArray[i]를 사용합니다.
    • 일반 배열처럼 조작할 수 있습니다.
    • 런타임 조건에 따라 동적 크기 조정을 허용합니다.

프로그램을 컴파일하고 실행합니다.

g++ dynamic_array.cpp -o dynamic_array
./dynamic_array

예시 출력:

Dynamic Array Contents:
Element 0: 0
Element 1: 10
Element 2: 20
Element 3: 30
Element 4: 40

중요 고려 사항:

  • 동적 배열은 메모리 할당의 유연성을 제공합니다.
  • 크기는 런타임에 결정될 수 있습니다.
  • 할당에는 new[]를 사용하고, 할당 해제에는 delete[]를 사용하는 것을 잊지 마십시오.
  • 항상 할당 및 할당 해제 방법을 일치시키십시오.

delete[] 를 사용하여 배열 삭제

이 단계에서는 delete[] 연산자를 사용하여 동적 배열에 할당된 메모리를 적절하게 해제하는 방법을 배우게 됩니다. 적절한 메모리 관리는 메모리 누수를 방지하고 효율적인 리소스 활용을 보장하는 데 매우 중요합니다.

WebIDE 를 열고 ~/project 디렉토리에 delete_array.cpp라는 새 파일을 생성합니다.

touch ~/project/delete_array.cpp

delete_array.cpp에 다음 코드를 추가합니다.

#include <iostream>

int main() {
    // Declare the size of the dynamic array
    int arraySize = 5;

    // Create a dynamic integer array using new[]
    int* dynamicArray = new int[arraySize];

    // Initialize array elements
    for (int i = 0; i < arraySize; ++i) {
        dynamicArray[i] = i * 10;
    }

    // Print array elements before deletion
    std::cout << "Array Contents Before Deletion:" << std::endl;
    for (int i = 0; i < arraySize; ++i) {
        std::cout << "Element " << i << ": " << dynamicArray[i] << std::endl;
    }

    // Release the dynamically allocated array using delete[]
    delete[] dynamicArray;

    // Set the pointer to nullptr after deletion
    dynamicArray = nullptr;

    // Demonstrate safe pointer usage
    if (dynamicArray == nullptr) {
        std::cout << "Dynamic array has been successfully deleted" << std::endl;
    }

    return 0;
}

동적 배열 삭제에 대한 주요 사항:

  1. delete[] dynamicArray;:

    • 전체 배열에 대한 메모리를 적절하게 해제합니다.
    • new[]로 할당된 배열에는 반드시 delete[]를 사용해야 합니다.
    • 메모리 누수를 방지합니다.
    • 각 배열 요소에 대해 소멸자를 호출합니다.
  2. dynamicArray = nullptr;:

    • 삭제 후 포인터를 nullptr로 설정합니다.
    • 해제된 메모리에 대한 우발적인 접근을 방지합니다.
    • 추가적인 안전 점검을 제공합니다.

프로그램을 컴파일하고 실행합니다.

g++ delete_array.cpp -o delete_array
./delete_array

예시 출력:

Array Contents Before Deletion:
Element 0: 0
Element 1: 10
Element 2: 20
Element 3: 30
Element 4: 40
Dynamic array has been successfully deleted

중요한 메모리 관리 규칙:

  • new[]로 할당된 배열에는 항상 delete[]를 사용합니다.
  • deletedelete[]를 혼용하지 마십시오.
  • 삭제 후 포인터를 nullptr로 설정합니다.
  • new[]에는 해당 delete[]가 있어야 합니다.

스마트 포인터 shared_ptr 구현

이 단계에서는 자동 메모리 관리 및 참조 카운팅을 제공하는 C++ 표준 라이브러리의 스마트 포인터인 shared_ptr에 대해 배우게 됩니다. 스마트 포인터는 메모리 누수를 방지하고 메모리 관리를 단순화하는 데 도움이 됩니다.

WebIDE 를 열고 ~/project 디렉토리에 shared_pointer.cpp라는 새 파일을 생성합니다.

touch ~/project/shared_pointer.cpp

shared_pointer.cpp에 다음 코드를 추가합니다.

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int value) : data(value) {
        std::cout << "Constructor called. Value: " << data << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructor called. Value: " << data << std::endl;
    }

    int getData() const { return data; }

private:
    int data;
};

int main() {
    // Create a shared_ptr to MyClass
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(42);

    // Create another shared_ptr pointing to the same object
    std::shared_ptr<MyClass> ptr2 = ptr1;

    // Print reference count
    std::cout << "Reference Count: " << ptr1.use_count() << std::endl;

    // Access object through shared_ptr
    std::cout << "Data from ptr1: " << ptr1->getData() << std::endl;
    std::cout << "Data from ptr2: " << ptr2->getData() << std::endl;

    // Pointers will automatically free memory when they go out of scope
    return 0;
}

shared_ptr에 대한 주요 사항:

  1. std::make_shared<MyClass>(42):

    • 동적 메모리 할당으로 shared pointer 를 생성합니다.
    • 생성자 인수로 객체를 초기화합니다.
    • 별도의 newshared_ptr 생성보다 효율적입니다.
  2. ptr1.use_count():

    • 동일한 객체를 가리키는 shared_ptr 인스턴스의 수를 반환합니다.
    • 객체의 참조 횟수를 추적하는 데 도움이 됩니다.
  3. 자동 메모리 관리:

    • shared_ptr 이 객체를 참조하지 않을 때 메모리가 자동으로 해제됩니다.
    • 수동 메모리 관리 오류를 방지합니다.

프로그램을 컴파일하고 실행합니다.

g++ -std=c++11 shared_pointer.cpp -o shared_pointer
./shared_pointer

예시 출력:

Constructor called. Value: 42
Reference Count: 2
Data from ptr1: 42
Data from ptr2: 42
Destructor called. Value: 42

중요한 shared_ptr 특징:

  • 자동 메모리 관리
  • 참조 카운팅
  • 메모리 누수 방지
  • 스레드 안전한 참조 카운팅

독점 소유권을 위한 unique_ptr 사용

이 단계에서는 동적으로 할당된 리소스에 대한 독점적인 소유권을 제공하는 스마트 포인터인 unique_ptr에 대해 배우게 됩니다. shared_ptr과 달리 unique_ptr은 한 번에 하나의 포인터만 리소스를 소유할 수 있도록 보장합니다.

WebIDE 를 열고 ~/project 디렉토리에 unique_pointer.cpp라는 새 파일을 생성합니다.

touch ~/project/unique_pointer.cpp

unique_pointer.cpp에 다음 코드를 추가합니다.

#include <iostream>
#include <memory>

class Resource {
public:
    Resource(int value) : data(value) {
        std::cout << "Resource created. Value: " << data << std::endl;
    }

    ~Resource() {
        std::cout << "Resource destroyed. Value: " << data << std::endl;
    }

    int getData() const { return data; }

private:
    int data;
};

void processResource(std::unique_ptr<Resource> resource) {
    std::cout << "Processing resource with value: " << resource->getData() << std::endl;
    // Resource will be automatically deleted when function ends
}

int main() {
    // Create a unique_ptr
    std::unique_ptr<Resource> ptr1 = std::make_unique<Resource>(42);

    // Print resource data
    std::cout << "Resource value: " << ptr1->getData() << std::endl;

    // Transfer ownership using std::move
    std::unique_ptr<Resource> ptr2 = std::move(ptr1);

    // ptr1 is now nullptr
    if (ptr1 == nullptr) {
        std::cout << "ptr1 is now null after moving ownership" << std::endl;
    }

    // Pass unique_ptr to a function (ownership is transferred)
    processResource(std::move(ptr2));

    return 0;
}

unique_ptr에 대한 주요 사항:

  1. std::make_unique<Resource>(42):

    • 동적 메모리 할당으로 unique pointer 를 생성합니다.
    • 리소스에 대한 독점적인 소유권을 보장합니다.
  2. std::move(ptr1):

    • 리소스의 소유권을 이전합니다.
    • 원래 포인터는 nullptr이 됩니다.
    • 여러 포인터가 동일한 리소스를 소유하는 것을 방지합니다.
  3. 자동 메모리 관리:

    • unique_ptr 이 범위를 벗어날 때 리소스가 자동으로 삭제됩니다.
    • 수동 메모리 관리가 필요하지 않습니다.

프로그램을 컴파일하고 실행합니다.

g++ -std=c++14 unique_pointer.cpp -o unique_pointer
./unique_pointer

예시 출력:

Resource created. Value: 42
Resource value: 42
ptr1 is now null after moving ownership
Processing resource with value: 42
Resource destroyed. Value: 42

중요한 unique_ptr 특징:

  • 독점적인 소유권
  • 자동 메모리 관리
  • 복사할 수 없고, 이동만 가능
  • 리소스 누수 방지

메모리 누수 확인

이 단계에서는 강력한 메모리 디버깅 도구인 Valgrind 를 사용하여 C++ 에서 메모리 누수를 감지하고 방지하는 방법을 배우게 됩니다. 메모리 누수는 동적으로 할당된 메모리가 제대로 해제되지 않아 프로그램이 점점 더 많은 메모리를 소비하게 될 때 발생합니다.

먼저 터미널에서 Valgrind 를 설치합니다.

sudo apt update
sudo apt install -y valgrind

WebIDE 에서 ~/project 디렉토리에 memory_leak.cpp를 생성하여 메모리 누수 예제를 만듭니다.

touch ~/project/memory_leak.cpp

memory_leak.cpp에 다음 코드를 추가합니다.

#include <iostream>

class Resource {
public:
    Resource(int value) : data(value) {
        std::cout << "Resource created: " << data << std::endl;
    }

    ~Resource() {
        std::cout << "Resource destroyed: " << data << std::endl;
    }

private:
    int data;
};

void createMemoryLeak() {
    // This creates a memory leak because the resource is not deleted
    Resource* leak = new Resource(42);
    // Missing: delete leak;
}

int main() {
    // Simulate multiple memory leaks
    for (int i = 0; i < 3; ++i) {
        createMemoryLeak();
    }

    return 0;
}

디버깅 심볼과 함께 프로그램을 컴파일합니다.

g++ -g memory_leak.cpp -o memory_leak

Valgrind 를 실행하여 메모리 누수를 감지합니다.

valgrind --leak-check=full ./memory_leak

Valgrind 출력 예시:

==1988== Memcheck, a memory error detector
==1988== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1988== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==1988== Command: ./memory_leak
==1988==
Resource created: 42
Resource created: 42
Resource created: 42
==1988==
==1988== HEAP SUMMARY:
==1988==     in use at exit: 12 bytes in 3 blocks
==1988==   total heap usage: 5 allocs, 2 frees, 73,740 bytes allocated
==1988==
==1988== 12 bytes in 3 blocks are definitely lost in loss record 1 of 1
==1988==    at 0x4849013: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1988==    by 0x109241: createMemoryLeak() (memory_leak.cpp:19)
==1988==    by 0x109299: main (memory_leak.cpp:26)
==1988==
==1988== LEAK SUMMARY:
==1988==    definitely lost: 12 bytes in 3 blocks
==1988==    indirectly lost: 0 bytes in 0 blocks
==1988==      possibly lost: 0 bytes in 0 blocks
==1988==    still reachable: 0 bytes in 0 blocks
==1988==         suppressed: 0 bytes in 0 blocks
==1988==
==1988== For lists of detected and suppressed errors, rerun with: -s
==1988== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

보시다시피 Valgrind 는 3 개의 블록에서 12 바이트의 메모리 누수를 감지했습니다. createMemoryLeak()에서 생성된 Resource 객체가 삭제되지 않아 메모리 누수가 발생했습니다.

메모리 누수를 수정하려면 코드를 수정하여 리소스를 제대로 삭제합니다.

void createMemoryLeak() {
    // Properly delete the dynamically allocated resource
    Resource* leak = new Resource(42);
    delete leak;  // Add this line to prevent memory leak
}

Valgrind 로 프로그램을 다시 컴파일하고 실행하여 메모리 누수가 수정되었는지 확인합니다.

g++ -g memory_leak.cpp -o memory_leak
valgrind --leak-check=full ./memory_leak
==2347== Memcheck, a memory error detector
==2347== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2347== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==2347== Command: ./memory_leak
==2347==
Resource created: 42
Resource destroyed: 42
Resource created: 42
Resource destroyed: 42
Resource created: 42
Resource destroyed: 42
==2347==
==2347== HEAP SUMMARY:
==2347==     in use at exit: 0 bytes in 0 blocks
==2347==   total heap usage: 5 allocs, 5 frees, 73,740 bytes allocated
==2347==
==2347== All heap blocks were freed -- no leaks are possible
==2347==
==2347== For lists of detected and suppressed errors, rerun with: -s
==2347== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

메모리 누수에 대한 주요 사항:

  • 동적으로 할당된 메모리는 항상 delete합니다.
  • unique_ptrshared_ptr과 같은 스마트 포인터를 사용합니다.
  • Valgrind 를 사용하여 메모리 누수를 감지합니다.
  • 더 나은 디버깅 정보를 위해 -g 플래그로 컴파일합니다.

메모리 할당 실패 처리

이 단계에서는 C++ 에서 메모리 할당 실패를 처리하는 방법을 배우게 됩니다. 동적 메모리 할당이 실패하면 new 연산자는 std::bad_alloc 예외를 발생시키며, 이를 catch 하여 적절하게 처리할 수 있습니다.

WebIDE 를 열고 ~/project 디렉토리에 memory_allocation.cpp라는 새 파일을 생성합니다.

touch ~/project/memory_allocation.cpp

memory_allocation.cpp에 다음 코드를 추가합니다.

#include <iostream>
#include <new>
#include <limits>

void demonstrateMemoryAllocation() {
    try {
        // Attempt to allocate a large amount of memory
        const size_t largeSize = 1000000000000; // 1 trillion integers
        int* largeArray = new int[largeSize];

        // This line will not be reached if allocation fails
        std::cout << "Memory allocation successful" << std::endl;

        // Clean up allocated memory
        delete[] largeArray;
    }
    catch (const std::bad_alloc& e) {
        // Handle memory allocation failure
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    }
}

void safeMemoryAllocation() {
    // Use std::nothrow to prevent exception throwing
    int* safeArray = new(std::nothrow) int[1000000];

    if (safeArray == nullptr) {
        std::cerr << "Memory allocation failed silently" << std::endl;
        return;
    }

    // Use the allocated memory
    std::cout << "Safe memory allocation successful" << std::endl;

    // Clean up allocated memory
    delete[] safeArray;
}

int main() {
    std::cout << "Demonstrating memory allocation failure handling:" << std::endl;

    // Method 1: Using exception handling
    demonstrateMemoryAllocation();

    // Method 2: Using std::nothrow
    safeMemoryAllocation();

    return 0;
}

메모리 할당 실패 처리에 대한 주요 사항:

  1. std::bad_alloc을 사용한 예외 처리:

    • try-catch 블록은 메모리 할당 예외를 catch 합니다.
    • 자세한 오류 정보를 제공합니다.
    • 프로그램 충돌을 방지합니다.
  2. std::nothrow 할당:

    • 예외 발생을 방지합니다.
    • 할당 실패 시 nullptr을 반환합니다.
    • 조용한 실패 처리를 허용합니다.

프로그램을 컴파일하고 실행합니다.

g++ memory_allocation.cpp -o memory_allocation
./memory_allocation

예시 출력:

Demonstrating memory allocation failure handling:
Memory allocation failed: std::bad_alloc
Safe memory allocation successful

중요한 메모리 할당 전략:

  • 항상 할당 실패를 확인합니다.
  • 예외 처리 또는 std::nothrow를 사용합니다.
  • 대체 메커니즘을 구현합니다.
  • 매우 큰 메모리 블록 할당을 피합니다.

요약

이 랩에서는 C++ 에서 동적 메모리 할당을 처리하는 방법을 배우게 됩니다. 먼저, 런타임에 new 연산자를 사용하여 동적으로 메모리를 할당하는 방법을 배우고, 이를 통해 정적 할당에 비해 더 유연한 메모리 관리가 가능해집니다. 그런 다음, 메모리 누수를 방지하기 위해 delete 연산자를 사용하여 이 동적으로 할당된 메모리를 적절하게 해제하는 방법을 배우게 됩니다. 또한, new[]를 사용하여 동적 배열을 생성하고 delete[]로 삭제하는 방법을 살펴봅니다. 이 랩에서는 또한 shared_ptrunique_ptr과 같은 스마트 포인터를 사용하여 메모리 관리를 단순화하고 일반적인 함정을 피하는 방법을 다룹니다. 마지막으로, 메모리 할당 실패를 확인하고 처리하는 기술을 배우게 됩니다.