C++ における動的メモリ割り当て

C++C++Beginner
今すぐ練習

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

この実験では、C++ における動的メモリ割り当ての扱い方を学びます。まずは new 演算子を使ってメモリを割り当て、その後 delete 演算子でメモリを解放します。また、動的配列の作成、shared_ptrunique_ptr のようなスマートポインタの使用、およびメモリリークの確認についても学びます。これらのスキルは、C++ プログラミングにおける効果的なメモリ管理に不可欠です。

この実験では、以下の主要なステップがカバーされます。new 演算子を使ったメモリ割り当て、delete 演算子を使ったメモリ解放、new[] を使った動的配列の作成、delete[] を使った配列の削除、shared_ptr スマートポインタの実装、排他的な所有権のための unique_ptr の使用、メモリリークの確認、およびメモリ割り当て失敗の処理。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL cpp(("C++")) -.-> cpp/BasicsGroup(["Basics"]) cpp(("C++")) -.-> cpp/OOPGroup(["OOP"]) cpp(("C++")) -.-> cpp/AdvancedConceptsGroup(["Advanced Concepts"]) cpp/BasicsGroup -.-> cpp/arrays("Arrays") cpp/OOPGroup -.-> cpp/classes_objects("Classes/Objects") cpp/OOPGroup -.-> cpp/constructors("Constructors") cpp/AdvancedConceptsGroup -.-> cpp/pointers("Pointers") cpp/AdvancedConceptsGroup -.-> cpp/exceptions("Exceptions") subgraph Lab Skills cpp/arrays -.-> lab-446081{{"C++ における動的メモリ割り当て"}} cpp/classes_objects -.-> lab-446081{{"C++ における動的メモリ割り当て"}} cpp/constructors -.-> lab-446081{{"C++ における動的メモリ割り当て"}} cpp/pointers -.-> lab-446081{{"C++ における動的メモリ割り当て"}} cpp/exceptions -.-> lab-446081{{"C++ における動的メモリ割り当て"}} end

new演算子を使ったメモリ割り当て

このステップでは、C++ における new 演算子を使って実行時に動的にメモリを割り当てる方法を学びます。動的メモリ割り当てにより、プログラム実行中にサイズが決定される変数や配列を作成でき、静的メモリ割り当てと比べてより柔軟性が高くなります。

WebIDEを開き、~/project ディレクトリに dynamic_memory.cpp という新しいファイルを作成します。

touch ~/project/dynamic_memory.cpp

dynamic_memory.cpp に以下のコードを追加します。

#include <iostream>

int main() {
    // newを使って整数型のメモリを動的に割り当てる
    int* dynamicInteger = new int;

    // 動的に割り当てた整数型に値を代入する
    *dynamicInteger = 42;

    // 動的に割り当てた整数型の値を表示する
    std::cout << "Dynamically allocated integer value: " << *dynamicInteger << std::endl;

    // 動的に割り当てた整数型のメモリアドレスを表示する
    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() {
    // 整数型のメモリを動的に割り当てる
    int* dynamicInteger = new int;

    // 動的に割り当てた整数型に値を代入する
    *dynamicInteger = 42;

    // メモリを解放する前の値を表示する
    std::cout << "Value before release: " << *dynamicInteger << std::endl;

    // deleteを使って動的に割り当てたメモリを解放する
    delete dynamicInteger;

    // 削除後にポインタをnullptrに設定する
    dynamicInteger = nullptr;

    // 安全なポインタの使用を示す
    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() {
    // 動的配列のサイズを宣言する
    int arraySize = 5;

    // new[]を使って動的な整数型配列を作成する
    int* dynamicArray = new int[arraySize];

    // 配列要素を初期化する
    for (int i = 0; i < arraySize; ++i) {
        dynamicArray[i] = i * 10;
    }

    // 配列要素を表示する
    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() {
    // 動的配列のサイズを宣言する
    int arraySize = 5;

    // new[]を使って動的な整数型配列を作成する
    int* dynamicArray = new int[arraySize];

    // 配列要素を初期化する
    for (int i = 0; i < arraySize; ++i) {
        dynamicArray[i] = i * 10;
    }

    // 削除前の配列要素を表示する
    std::cout << "Array Contents Before Deletion:" << std::endl;
    for (int i = 0; i < arraySize; ++i) {
        std::cout << "Element " << i << ": " << dynamicArray[i] << std::endl;
    }

    // delete[]を使って動的に割り当てた配列を解放する
    delete[] dynamicArray;

    // 削除後にポインタをnullptrに設定する
    dynamicArray = nullptr;

    // 安全なポインタの使用を示す
    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() {
    // MyClassへのshared_ptrを作成する
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(42);

    // 同じオブジェクトを指す別のshared_ptrを作成する
    std::shared_ptr<MyClass> ptr2 = ptr1;

    // 参照カウントを表示する
    std::cout << "Reference Count: " << ptr1.use_count() << std::endl;

    // shared_ptrを通じてオブジェクトにアクセスする
    std::cout << "Data from ptr1: " << ptr1->getData() << std::endl;
    std::cout << "Data from ptr2: " << ptr2->getData() << std::endl;

    // ポインタがスコープ外になると自動的にメモリを解放する
    return 0;
}

shared_ptr に関する重要なポイント:

  1. std::make_shared<MyClass>(42):
    • 動的メモリ割り当てを伴うsharedポインタを作成する。
    • コンストラクタ引数でオブジェクトを初期化する。
    • 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 は一度に1つのポインタのみがリソースを所有できることを保証します。

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は自動的に削除されます
}

int main() {
    // unique_ptrを作成する
    std::unique_ptr<Resource> ptr1 = std::make_unique<Resource>(42);

    // リソースデータを表示する
    std::cout << "Resource value: " << ptr1->getData() << std::endl;

    // std::moveを使って所有権を移す
    std::unique_ptr<Resource> ptr2 = std::move(ptr1);

    // ptr1は現在nullptrになっています
    if (ptr1 == nullptr) {
        std::cout << "ptr1 is now null after moving ownership" << std::endl;
    }

    // unique_ptrを関数に渡す(所有権が移されます)
    processResource(std::move(ptr2));

    return 0;
}

unique_ptr に関する重要なポイント:

  1. std::make_unique<Resource>(42):
    • 動的メモリ割り当てを伴うユニークポインタを作成します。
    • リソースの排他的所有権を保証します。
  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() {
    // これはメモリリークを引き起こします。リソースが削除されていません。
    Resource* leak = new Resource(42);
    // 欠落: delete leak;
}

int main() {
    // 複数のメモリリークをシミュレートします。
    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() {
    // 動的に割り当てられたリソースを適切に削除する
    Resource* leak = new Resource(42);
    delete 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 例外を投げます。この例外をキャッチして適切に処理することができます。

WebIDEを開き、~/project ディレクトリに memory_allocation.cpp という新しいファイルを作成します。

touch ~/project/memory_allocation.cpp

memory_allocation.cpp に以下のコードを追加します。

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

void demonstrateMemoryAllocation() {
    try {
        // 大量のメモリを割り当てようとする
        const size_t largeSize = 1000000000000; // 1兆個の整数
        int* largeArray = new int[largeSize];

        // 割り当てに失敗した場合、この行は実行されません
        std::cout << "Memory allocation successful" << std::endl;

        // 割り当てたメモリを解放する
        delete[] largeArray;
    }
    catch (const std::bad_alloc& e) {
        // メモリ割り当て失敗を処理する
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    }
}

void safeMemoryAllocation() {
    // 例外を投げないようにstd::nothrowを使用する
    int* safeArray = new(std::nothrow) int[1000000];

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

    // 割り当てたメモリを使用する
    std::cout << "Safe memory allocation successful" << std::endl;

    // 割り当てたメモリを解放する
    delete[] safeArray;
}

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

    // 方法1: 例外処理を使用する
    demonstrateMemoryAllocation();

    // 方法2: std::nothrowを使用する
    safeMemoryAllocation();

    return 0;
}

メモリ割り当て失敗の対処に関する重要なポイント:

  1. std::bad_alloc による例外処理:
    • try-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 などのスマートポインタの使用方法も扱います。最後に、メモリ割り当て失敗をチェックして対処する技術を学びます。