C/C++メモリ領域の分布
C/C++プログラムのメモリ領域は、スタック、メモリマッピングセグメント、ヒープ、データセグメント、コードセグメントに分割されます。
- スタック: 非静的局所変数、関数引数、戻り値などが格納され、下向きに成長します。
- メモリマッピングセグメント: 共有動的メモリライブラリの読み込みに使用されます。
- ヒープ: プログラム実行時の動的メモリ割り当てに使用され、上向きに成長します。
- データセグメント: グローバル変数と静的データが格納され、静的領域とも呼ばれます。
- コードセグメント: 実行可能なコードと読み取り専用定数が格納され、定数領域とも呼ばれます。
#include
int global_var = 1;
static int static_var = 1;
void memory_test()
{
static int static_local = 1;
int local_var = 1;
int array1[5] = {1, 2, 3, 4, 5};
char string2[] = "test";
const char* pstring3 = "test";
int* ptr1 = (int*)malloc(sizeof(int) * 3);
int* ptr2 = (int*)calloc(3, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 5);
free(ptr1);
free(ptr3);
}
スタック内のデータ: local_var, array1, *array1, string2, *string2, pstring3, ptr1, ptr2, ptr3
ヒープ内のデータ: *ptr1, *ptr2, *ptr3
データセグメント内のデータ: global_var, static_var, static_local
コードセグメント内のデータ: *pstring3
C言語での動的メモリ管理
C言語では、動的メモリ管理にはmalloc、calloc、realloc、freeが使用されます。これらの関数はヒープ領域からメモリを確保・解放します。
C++でのメモリ管理
C++では、newとdeleteという2つのキーワードが使用されます。
1. 組み込み型でのnewとdeleteの使用
void dynamic_allocation_test()
{
// int型の領域を動的に確保
int* p1 = new int;
// int型の領域を動的に確保し、0で初期化
int* p2 = new int(0);
// 10個のint型の配列を動的に確保
int* p3 = new int[10];
// 10個のint型の配列を動的に確保し、一部を初期化
int* p4 = new int[10] {1, 2, 3};
delete p1;
delete p2;
delete[] p3;
delete[] p4;
}
組み込み型に対するnewは、型のポインタを返します。角括弧[]は配列の要素数を指定し、丸括弧()は単一要素の初期化、波括弧{}は配列要素の初期化に使用されます。
deleteはnewと対応して使用する必要があります。単一要素の確保・解放にはnew/deleteを使用し、複数要素の確保・解放にはnew[]/delete[]を使用します。
2. カスタム型でのnewとdeleteの使用
newとdeleteのC言語のmallocなどとの大きな違いは、カスタム型のコンストラクタとデストラクタを呼び出せる点です。
class SampleClass
{
public:
SampleClass(int val = 0)
: value(val)
{
std::cout << "SampleClass():" << this << std::endl;
}
~SampleClass()
{
std::cout << "~SampleClass():" << this << std::endl;
}
private:
int value;
};
int main()
{
// カスタム型
SampleClass* obj1 = (SampleClass*)malloc(sizeof(SampleClass));
SampleClass* obj2 = new SampleClass(42);
free(obj1);
delete obj2;
// 組み込み型
int* int_ptr1 = (int*)malloc(sizeof(int));
int* int_ptr2 = new int;
free(int_ptr1);
delete int_ptr2;
// カスタム型の配列
SampleClass* array1 = (SampleClass*)malloc(sizeof(SampleClass) * 5);
SampleClass* array2 = new SampleClass[5];
free(array1);
delete[] array2;
return 0;
}
mallocはコンストラクタを呼び出さず、freeはデストラクタを呼び出しませんが、newとdeleteはこれらを呼び出します。
operator newとoperator delete関数
newとdeleteはユーザーが動的メモリを確保・解放するための演算子ですが、operator newとoperator deleteはシステムが提供するグローバル関数であり、newとdeleteの下層実装です。
operator newはmallocを使用してメモリを確保し、失敗時にはユーザー設定のメモリ不足対策を実行します。operator deleteは最終的にfreeを使用してメモリを解放します。
newとdeleteの実装原理
1. 組み込み型
組み込み型の領域を確保する場合、newとmalloc、freeとdeleteは基本的に似ています。違いは、newが領域確保に失敗時に例外を投げる点です。
2. カスタム型
- new: まずoperator new関数で領域を確保し、その領域上でコンストラクタを実行します。
- delete: まず領域上でデストラクタを実行してオブジェクト内のリソースをクリーンアップし、operator delete関数でオブジェクトの領域を解放します。
- new[]: operator new[]関数で領域を確保し(実際には複数のoperator newを呼び出し)、その領域上で複数のコンストラクタを実行します。
- delete[]: まず領域上で複数のデストラクタを実行して複数のオブジェクト内のリソースをクリーンアップし、operator delete[]関数でオブジェクトの領域を解放します(複数のoperator deleteを呼び出します)。
配置new(placement new)式
配置new式は、既に割り当てられた原始メモリ空間でコンストラクタを呼び出してオブジェクトを初期化します。
形式: new(ポインタ) 型 または new(ポインタ) 型(初期化リスト)
使用場面: メモリプールと組み合わせて使用します。メモリプールが割り当てたメモリは初期化されていないため、カスタム型のオブジェクトの場合、配置new式を使用して明示的にコンストラクタを呼び出して初期化する必要があります。
class DataObject
{
public:
DataObject(int val = 0)
: data(val)
{
std::cout << "DataObject():" << this << std::endl;
}
~DataObject()
{
std::cout << "~DataObject():" << this << std::endl;
}
private:
int data;
};
int main()
{
// p1はDataObjectと同じサイズの空間を指しているが、オブジェクトではない(コンストラクタは実行されていない)
DataObject* p1 = (DataObject*)malloc(sizeof(DataObject));
// DataObjectのコンストラクタを実行
new(p1)DataObject;
p1->~DataObject();
free(p1);
DataObject* p2 = (DataObject*)operator new(sizeof(DataObject));
// 初化リストで初期化
new(p2)DataObject(100);
p2->~DataObject();
operator delete(p2);
return 0;
}
mallocとnew、freeとdeleteの違い
共通点: どちらもヒープから領域を確保し、ユーザーが手動で解放する必要があります。
相違点:
- mallocとfreeは関数、newとdeleteは演算子です。
- mallocは領域確保時に初期化しませんが、newは初期化します。
- mallocは領域サイズを手計算して渡す必要がありますが、newは型を指定すれば自動的に計算され、複数のオブジェクトの場合は[]内に個数を指定します。
- mallocの戻り値はvoid*で、使用時に型変換が必要ですが、newの戻り値は型のポインタ型(type*)で、型変換は不要です。
- mallocは領域確保に失敗時にNULLを返すため、使用時にNULLチェックが必要ですが、newは失敗時に例外を投げるため、例外処理が必要です。
- カスタム型の場合、mallocとfreeは領域の確保と解放のみ行い、newは領域確保後にコンストラクタを呼び出してオブジェクトを初期化し、deleteは領域解放後にデストラクタを呼び出してリソースをクリーンアップします。
メモリリーク
1. メモリリークの害
メモリリークとは、不注意やミスにより、プログラムが使用しなくなったメモリを解放しない状態を指します。アプリケーションが特定のメモリ領域を確保した後、設計上の誤りによりそのメモリへの制御を失い、そのメモリ領域を再利用できなくなる状態です。
長時間実行されるプログラム(サーバーなど)でメモリリークが発生すると、応答速度が低下し、最終的にメモリ不足でプログラムがフリーズする原因となります。
2. メモリリークの種類
- ヒープメモリリーク: malloc、calloc、reallocまたはnewでヒープから確保したメモリを、使用後にfreeまたはdeleteで解放しないこと。
- システムリソースリーク: プログラムがシステムから割り当てられたリソースを対応する関数で解放せず、システムリソースを浪費すること。
3. メモリリークの防止方法
- プロジェクト初期段階での良好な設計規範と、良好なコーディング習慣の徹底(確保したメモリは使用後に解放する)。
- RAII思想やスマートポインタを使用してリソースを管理する。
- 一部の企業内ライブラリには検出機能がある場合がある。
- メモリリーク検出ツールを使用する。
例外処理
例外処理にはtryとcatchのキーワードセットを使用します。tryは例外を投げる可能性のあるコードを囲み、catchはtryで投げられた例外を捕捉して処理します。
#include <stdexcept> // std::runtime_errorを含む
void exception_test()
{
try
{
throw std::runtime_error("ランタイムエラーが発生しました!");
// newは例外を投げる機能を持つ
}
catch (const std::runtime_error& e)
{
std::cerr << "例外を捕捉: " << e.what() << std::endl;
}
std::cout << "プログラムの実行を続行" << std::endl;
}
ここでcerrは標準エラー出力ストリームで、主にエラーメッセージの出力に使用されます(もちろんcoutでも可能です)。