C++メモリ管理の極意: ニュービーよりエキスパートへの道

C/C++メモリ構造解析

1.メモリ領域の構造

C++プログラムのコンパイルおよび実行時に、メモリは以下の領域に分割され、それぞれ異なる役割を担っています。C++における基本的なメモリ構造は以下の通りです:

1.スタック(Stack):

  • 用途:ローカル変数、関数パラメータ、リターンアドレスなどを格納。
  • 特性:スタックメモリの割り当てと解放はシステムが自動管理し、LIFO(後入れ先出し)方式で動作します。関数呼び出し時にスタックフレームがスタックにプッシュされ、関数終了時にポップされます。
  • 使用シーン:一時的な変数や関数パラメータの格納に適しています。

2.ヒープ(Heap):

  • 用途:実行時に動的にメモリを確保するための領域。newやmallocによるメモリ確保が可能です。
  • 特性:ヒープメモリはアドレス順に上昇して拡張され、スタックとは異なり自動解放されません。明示的にdeleteやfreeを呼び出す必要があります。それ以外にメモリリークが発生します。
  • 使用シーン:リストやツリーなどの動的構造や大規模なデータ構造に適しています。

3.データセグメント(Data Segment):

  • 用途:グローバル変数や静的変数を格納。プログラム開始時に割り当てられ、終了時に解放されます。
  • 特性:初期化済みデータセグメントと未初期化データセグメントに分かれます。初期化済みデータセグメントはプログラムロード時に指定値で初期化され、未初期化データセグメントはゼロに初期化されます。
  • 使用シーン:プログラムライフタイム全体で保持が必要な変数に適しています。

4.テキストセグメント(Text Segment):

  • 用途:実行可能なコードや定数文字列などを格納。
  • 特性:通常は読み取り専用で、コードの実行中の変更を防ぎ、セキュリティを強化します。
  • 使用シーン:実行命令や定数文字列の格納に適しています。
2.コード例

コード言語:C++

コード実行回数:0

実行

AIコード説明

#include <cstdlib>  // malloc, calloc, realloc, freeを含む
#include <iostream>

int globalData = 1;              // グローバル変数、初期化済みデータセグメントに格納
static int staticData = 1;  // 静的グローバル変数、初期化済みデータセグメントに格納

void MemoryTest() {
    static int staticLocal = 1;    // 静的ローカル変数、初期化済みデータセグメントに格納
    int tempVar = 1;            // ローカル変数、スタックに格納
    int array[10] = {1, 2, 3, 4}; // ローカル配列、スタックに格納
    char strArray[] = "abcd";       // 文字列配列、スタックに格納(文字列はスタックにコピーされる)
    const char* strPtr = "abcd"; // ポインタ変数はスタックに格納、文字列は定数領域に格納

    int* heapPtr1 = (int*)malloc(sizeof(int) * 4); // ヒープに動的に確保
    int* heapPtr2 = (int*)calloc(4, sizeof(int));  // ヒープに動的に確保し初期化
    int* heapPtr3 = (int*)realloc(heapPtr2, sizeof(int) * 4); // ヒープのメモリサイズを調整

    free(heapPtr1);  // ヒープメモリの解放
    free(heapPtr3);  // ヒープメモリの解放
}

int main() {
    MemoryTest();
    return 0;
}
3.詳細解説
  • globalData と staticData
  • これらの変数はグローバル変数と静的グローバル変数であり、初期化済みデータセグメントに格納されます。このセグメントはプログラム起動時にグローバル変数や静的変数が割り当てられ、プログラム終了まで存在します。
  • staticLocal
  • staticLocal は静的ローカル変数です。普通のローカル変数とは異なり、static修飾子により初期化済みデータセグメントに格納されます。関数が終了しても変数は解放されず、値が保持されます。再呼び出し時は再初期化されません。
  • tempVar と array
  • tempVar はローカル変数、array はローカル配列で、スタックに格納されます。スタックは関数呼び出し時に自動的にメモリが割り当てられ、関数終了時に解放されます。
  • strArray
  • strArray は文字列配列で、スタックに格納されます。コンパイラは文字列 "abcd" をスタックにコピーします。ローカル変数であるため、関数終了時にメモリが解放されます。
  • strPtr
  • strPtr はポインタ変数で、スタックに格納されます。ポインタが指す文字列 "abcd"定数領域(リードオンリーデータセグメント)に格納されます。
  • heapPtr1、heapPtr2、heapPtr3
  • これらのポインタ変数自体はスタックに格納され、指すメモリはヒープに格納されます。malloccallocrealloc でヒープメモリを確保します。ヒープメモリは手動で解放が必要で、free() で解放しないとメモリリークが発生します。

二、C言語での動的メモリ管理

詳細は前のブログ記事参照:C言語動的メモリ管理

C言語では以下のような関数で動的メモリ管理を行います:

1.malloc

malloc(memory allocation):指定されたサイズのメモリブロックを確保し、初期化は行われません。戻り値はvoid*型で、明示的なキャストが必要です。

例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

int* ptr = (int*)malloc(sizeof(int) * 5);  // 5個のint分のメモリを確保
2.calloc
  • calloc(contiguous allocation):連続したメモリを確保し、すべてのバイトをゼロに初期化します。戻り値はvoid*型です。

例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

int* ptr = (int*)calloc(5, sizeof(int));  // 5個のint分のメモリを確保し初期化
3.realloc

realloc(reallocation):既存のメモリブロックのサイズを調整します。拡張が失敗した場合、新しい場所にメモリを確保し、元の内容をコピーします。

例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

int* newPtr = (int*)realloc(ptr, sizeof(int) * 10);  // メモリを10個のint分に拡張
4.free

freemalloccallocreallocで確保したメモリを解放します。解放後はプログラムが管理しなくなります。

例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

free(ptr);  // 動的確保したメモリを解放

三、C++でのメモリ管理方法

C++はC言語のmalloccallocreallocfreeを引き継ぎつつ、newdelete演算子によるより柔軟なメモリ管理を提供します:

1.new 演算子
  • 用途:動的にメモリを確保し、基本型やカスタムオブジェクトを初期化します。
  • 特性:メモリ確保に失敗した場合、newstd::bad_alloc例外を投げ、mallocNULLを返すのとは異なります。
  • 構文new 型で単一オブジェクトを確保、new 型[数]で配列を確保します。

例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

// 動的にint型のメモリを確保、初期化なし
    int* ptr1 = new int;

// 動的にint型のメモリを確保し、10に初期化
    int* ptr2 = new int(10);

// 動的に3つのint型の連続メモリを確保、初期化なし
    int* ptr3 = new int[3];
2.delete 演算子
  • 用途newで確保したメモリを解放し、メモリリークを防ぎます。
  • 特性deleteは単一オブジェクトの解放に使用し、delete[]は配列の解放に使用します。
  • 構文delete ポインタで単一オブジェクトを解放、delete[] ポインタで配列を解放します。

注意:1. 単一要素のメモリ確保・解放にはnewdeleteを使用し、連続メモリの確保・解放にはnew[]delete[]を使用します。2.newdelete必ず一致して使用する必要があります

3.new と delete の利点

newdeleteは単なるメモリ確保・解放だけでなく、コンストラクタとデストラクタを自動的に呼び出すため、カスタム型の管理に最適です。

コード例:newとdeleteでカスタム型を操作

コード言語:C++

コード実行回数:0

実行

AIコード説明

#include <iostream>
#include <cstdlib>  // mallocとfreeを含む

using namespace std;

class Sample {
public:
    // コンストラクタ
    Sample(int val = 0) : value(val) {
        cout << "Sample() コンストラクタ呼ばれた、オブジェクトアドレス: " << this << endl;
    }

    // デストラクタ
    ~Sample() {
        cout << "~Sample() デストラクタ呼ばれた、オブジェクトアドレス: " << this << endl;
    }

private:
    int value;  // メンバ変数
};

int main() {
    // mallocでメモリを確保、コンストラクタは呼ばれない
    Sample* p1 = (Sample*)malloc(sizeof(Sample));  // メモリ確保のみ
    Sample* p2 = new Sample(1);               // メモリ確保とコンストラクタ呼び出し

    // mallocで確保したメモリを解放、デストラクタは呼ばれない
    free(p1);

    // deleteでメモリを解放、デストラクタを呼び出す
    delete p2;

    // 内部型の操作:mallocとnewは内部型に対してほぼ同じ挙動
    int* p3 = (int*)malloc(sizeof(int));  // メモリ確保のみ
    int* p4 = new int;                    // メモリ確保のみ
    free(p3);                             // mallocで確保したメモリを解放
    delete p4;                            // newで確保したメモリを解放

    // 配列の動的確保
    Sample* p5 = (Sample*)malloc(sizeof(Sample) * 10);   // メモリ確保のみ
    Sample* p6 = new Sample[10];                    // メモリ確保とコンストラクタ呼び出し

    // メモリ解放
    free(p5);      // メモリ解放のみ
    delete[] p6;   // 配列解放と各オブジェクトのデストラクタ呼び出し

    return 0;
}

注意:カスタム型のメモリ確保において、newはコンストラクタを呼び出し、deleteはデストラクタを呼び出しますが、mallocfreeは呼び出しません。

四、operator new と operator delete 関数

C++ではnewdelete演算子が動的メモリ管理に使用され、オブジェクトの作成・破棄時に自動的にコンストラクタとデストラクタを呼び出します。ただし、newdeleteは下層でグローバルのoperator newoperator delete関数に依存しています。これらをカスタマイズすることで、メモリプールなどのカスタムメモリ管理を実現できます。

1.operator new の実装原理

operator newはC++のグローバル関数で、メモリを確保します。デフォルトではmallocを使って指定サイズのメモリを確保します。メモリ確保に失敗した場合、std::bad_alloc例外を投げます。

コード例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

#include <new>   // bad_allocを含む
#include <cstdlib>  // mallocを含む
#include <iostream>
using namespace std;

// operator new: メモリ確保関数
void* operator new(size_t size) _THROW1(std::bad_alloc) {
    void* p;
    // mallocでメモリを確保、失敗時はハンドラを呼び出す
    while ((p = malloc(size)) == 0) {
        // ハンドラが0を返す場合、bad_alloc例外を投げる
        if (_callnewh(size) == 0) {
            static const std::bad_alloc nomem;
            _RAISE(nomem);  // メモリ不足例外を投げる
        }
    }
    return p;  // 成功した場合はメモリポインタを返す
}
2.operator delete の実装原理

operator deletenewに対応する関数で、メモリを解放します。freeを使ってnewで確保したメモリを解放します。newと同様に、ユーザーがカスタマイズして特定のメモリ管理を実現できます。

コード例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

#include <cstdlib>  // freeを含む
#include <iostream>
using namespace std;

// operator delete: メモリ解放関数
void operator delete(void* p) noexcept {
    // ポインタがNULLの場合、解放処理を実行しない
    if (p == NULL) return;

    // free関数を使ってメモリを解放
    free(p);
}
3.operator new[]operator delete[]

operator newoperator deleteと同様に、operator new[]operator delete[]も配列用に用意されています。これらは単一オブジェクト用のoperator newoperator deleteと同様の機能を持ちますが、配列向けに設計されています。

コード例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

// operator new[]: 配列用メモリ確保
void* operator new[](size_t size) {
    cout << "配列サイズ: " << size << "のメモリ確保" << endl;
    return malloc(size);  // mallocでメモリを確保
}

// operator delete[]: 配列用メモリ解放
void operator delete[](void* p) noexcept {
    cout << "配列メモリ解放" << endl;
    free(p);  // freeでメモリを解放
}

五、new と delete の実装原理

1. 内部型の newdelete 実装原理

内部型(int、doubleなど)の場合、newmallocdeletefreeの挙動は非常に似ています。主な違いは以下の通りです:

  1. 単一要素と配列new/deleteは単一オブジェクトを確保・解放し、new[]/delete[]は配列を確保・解放します。
  2. 例外処理
  • newはメモリ確保に失敗した場合std::bad_alloc例外を投げます。
  • mallocはメモリ確保に失敗した場合NULLを返します。
  1. 自動初期化
  • newはメモリを初期化できます。例えば、new int(5)は確保したintを5で初期化します。mallocはメモリを確保するだけで初期化は行いません。

コード例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

#include <iostream>
#include <cstdlib>  // mallocとfreeを含む
using namespace std;

int main() {
    // mallocでメモリを確保、初期化しない
    int* p1 = (int*)malloc(sizeof(int));

    // newでメモリを確保し、10で初期化
    int* p2 = new int(10);

    // mallocで確保したメモリを解放、freeを使う
    free(p1);

    // newで確保したメモリを解放、deleteを使う
    delete p2;

    return 0;
}

結論:

  • 内部型newmallocはメモリ確保の挙動は似ていますが、newは例外処理を提供し、mallocNULLを返します。
  • delete/freeはメモリ解放の挙動は基本的に同じで、単純な解放操作です。
2. カスタム型の newdelete 実装原理

カスタム型(クラスオブジェクトなど)の場合、newdeleteの挙動は内部型よりも複雑です。メモリ確保・解放だけでなく、コンストラクタとデストラクタも呼び出します。これはnew/deletemalloc/freeの主要な違いです。

カスタム型の new の作業フロー:

  1. operator newでメモリを確保
  • operator newmallocや他のメモリ確保関数を使ってオブジェクトのメモリを確保します。
  1. コンストラクタでオブジェクトを初期化
  • 確保されたメモリ上に、newはオブジェクトのコンストラクタを呼び出して初期化します。
  • このステップにより、オブジェクトのメンバ変数が正しく初期化されます。

カスタム型の delete の作業フロー:

  1. デストラクタを呼び出す
  • deleteはメモリ解放前にオブジェクトのデストラクタを呼び出して、リソースを解放します(例:動的に確保されたメモリ解放やファイルクローズなど)。
  1. operator deleteでメモリを解放
  • デストラクタ呼び出し後、operator delete関数が呼び出され、freeを使ってメモリを解放します。

コード例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

#include <iostream>
using namespace std;

class Sample {
public:
    // コンストラクタ
    Sample(int val = 0) : value(val) {
        cout << "Sample() コンストラクタ呼ばれた、値: " << value << endl;
    }

    // デストラクタ
    ~Sample() {
        cout << "~Sample() デストラクタ呼ばれた、値: " << value << endl;
    }

private:
    int value;  // メンバ変数
};

int main() {
    // newでSampleクラスオブジェクトを確保、コンストラクタを呼び出す
    Sample* p1 = new Sample(10);

    // deleteでSampleクラスオブジェクトを解放、デストラクタを呼び出す
    delete p1;

    return 0;
}

結論:

  • newdelete:メモリ確保・解放だけでなく、コンストラクタとデストラクタを呼び出すことで、カスタム型オブジェクトの正しく初期化とクリーンアップを行います。
  • malloc/free:カスタム型に対してはメモリ確保・解放のみを行い、コンストラクタとデストラクタは呼び出しません。そのため、オブジェクトライフタイムの管理には不向きです。

六、placement-new 表記(配置new)

placement-newは特殊なnewの構文で、指定されたメモリアドレスにオブジェクトを構築します。これはメモリプールなどの効率的なメモリ確保シーンで非常に有用です。

例:

コード言語:C++

コード実行回数:0

実行

AIコード説明

#include <new> // <new>ヘッダファイルを含む必要がある

class Sample {
public:
    Sample() { std::cout << "Sample コンストラクタ" << std::endl; }
    ~Sample() { std::cout << "Sample デストラクタ" << std::endl; }
};

int main() {
    char buffer[sizeof(Sample)];      // 足りるサイズのバッファを確保
    Sample* p = new(buffer) Sample;  // バッファ上にオブジェクトを構築

    p->~Sample();  // 明示的にデストラクタを呼び出す
    return 0;
}

七、malloc/free と new/delete の違いまとめ

C++ではmalloc/freenew/deleteの両方でヒープ上のメモリ確保と解放が可能です。共通点は動的メモリ確保を必要とする点ですが、重要な違いがあります:

  1. 関数 vs 演算子
  • malloc/free:C言語の関数で、メモリの確保と解放を行います。
  • new/delete:C++の演算子で、メモリの確保と解放を行い、コンストラクタとデストラクタを呼び出します。
  1. メモリ初期化
  • malloc:メモリを確保するだけで初期化は行いません。メモリの中身は未定義です。
  • new:メモリを確保するだけでなく、オブジェクトを初期化します。特にカスタム型ではコンストラクタが呼び出されます。
  1. メモリサイズ計算
  • malloc:ユーザーがメモリサイズを手動で計算してmallocに渡す必要があります。例:malloc(sizeof(int))
  • new:ユーザーはメモリサイズを計算する必要がなく、newは型に基づいて自動計算します。例:new intint型のメモリを自動的に確保します。
  1. 戻り値タイプ
  • mallocvoid*型のポインタを返します。使用する際には強制キャストが必要です。例:(int*)malloc(sizeof(int))
  • new:具体的な型のポインタを返します。強制キャストは必要ありません。例:new intint*型のポインタを返します。
  1. エラーハンドリング
  • malloc:メモリ確保に失敗した場合はNULLを返します。使用する際には戻り値がNULLであるかを手動で確認する必要があります。
  • new:メモリ確保に失敗した場合はstd::bad_alloc例外を投げます。使用する際には例外処理が必要です。
  1. コンストラクタとデストラクタ
  • malloc/free:メモリの確保と解放を行うだけで、コンストラクタとデストラクタは呼び出しません。カスタム型のオブジェクトには不向きです。
  • new/delete:メモリ確保時にコンストラクタを呼び出して初期化し、メモリ解放時にデストラクタを呼び出してリソースをクリーンアップします。カスタム型のオブジェクト管理に最適です。

メモリ管理は効率的なC++プログラミングの基礎です。スタック、ヒープ、動的メモリ管理方法を理解することで、C++の下層メカニズムを深く理解し、効率的なメモリ管理を実現できます。

タグ: C++ メモリ管理 動的メモリ確保 new/delete マルチスレッド

8月9日 05:37 投稿