LoginSignup
6
2

More than 1 year has passed since last update.

C++11, C++14, C++17 での新しいメモリ管理と定数 (変数・定数・クラスのインスタンス化・配列)

Last updated at Posted at 2021-08-06

C++ の新機能を含め、変数等のメモリ管理や定数に関わるものをまとめてみました。

※本記事で言っている「安全」は、「ソースコードに人為的ミスがあったとしてもメモリリークを起こさない」という意味で書いています。

0. まとめ

  • const / constexpr 等を使う
  • 速度よりも安全性が大事なら new / delete使わない
    • malloc() / free() も基本的には使わない
    • C++ 標準コンテナやスマートポインタを使う
  • C++ 標準コンテナへの要素追加・挿入は emplace 系メソッドによる直接構築を用いる
  • コンテナ等のメモリ解放で Swap 技法 (std::vector<T>(v).swap(v); 等) は使わない
    • shrink_to_fit() を使う
  • 安全性よりも速度が大事なら基本的に new / delete を使うが、メモリリークに注意する
  • 配列サイズ取得に sizeof(array) / sizeof(array[0])std::extent<decltype(array), i>使わない
    • 同一スコープ内の従来の静的配列は std::size(array) を使う
    • std::array 等の C++ コンテナの使用を検討する
  • その他
    • auto など

1. 基礎

1.1. 変数と従来の配列変数

※細かいことはここでは省略。
※変数の宣言については少し後述。
※ C++ 標準コンテナについては少し後述。

// 変数
int foo = 23;

// 従来の配列変数
int bar[] = {0, 1, 2, 3, 4};
// or
int bar[] {0, 1, 2, 3, 4};

1.2. ポインタと参照

ポインタは変数のメモリ上の場所を示す値で、参照は変数の別名のようなものです。

ポインタや参照について詳しく説明している記事は他にたくさんありますので、ここでは簡単な情報のみ書きます。

従来の代入 (コピー代入)
int foo = 23;

// 
int bar = foo; // foo の値が bar にコピーされる (ここでは bar == 23)

bar = 42; // foo == 23, bar == 42

// メモ: 関数の引数でも同様
// メモ: 構造体やオブジェクト、std::array でも同様にコピー代入できる
// メモ: 従来の配列ではコピー代入不可
ポインタ
// 
int foo = 23; // int 型

&foo; // アドレス演算子 & で、変数 foo のポインタを得られる。&foo は int * 型

// 
int *bar = &foo; // bar は int * 型のポインタ変数

*bar; // 間接演算子 * で、bar が示すメモリ上の値 (ここでは foo の値) を得られる。*bar は int 型

*bar = 42; // foo == 42 になる

// 
int **baz = &bar; // ポインタのポインタも扱える。baz は int ** 型

**baz = 1729; // foo == 1729 になる

// メモ: *baz は int * 型、**baz は int 型
// メモ: 関数の引数でも同様
// メモ: 構造体やオブジェクトでも同様
参照
int foo = 23; // foo は左辺値

// 従来の参照 (左辺値参照)
int& bar = foo; // bar は左辺値 foo の別名

bar = 42; // foo == 42 になる

// 右辺値参照
// 
// メモ: 右辺値参照はムーブの機能のために使用します (※ここでは詳細略)
const auto f = [] { return 23; }; // 戻り値やリテラル値は右辺値

int&& baz = f(); // 右辺値を束縛

// メモ: 関数の引数でも同様
// メモ: 構造体やオブジェクトでも同様

※アドレス演算子 & と参照宣言子 & は別物です。
※ポインタの操作では &* を付けるたびに値の実体の型が変わりますが、参照は値の実体の型は変わらず別名を作成するような操作になります (言語上は参照でも型が区別されます) 。
※当たり前ながら、乗算演算子の *、比較演算子の &&、ビット演算の & も別物です。
※ちなみに、C や C++ にはべき乗演算子 ** はありません。

変数宣言時の &* の位置に関してはメリットとデメリットがあるため、型名側に寄せるか変数名側に寄せるか一概には言えませんが、同一のプロジェクト内では統一すべきかと思います。

参考「ポインタ型記法のススメ ─ int* p; int *p; 空白をどちらに挿入するか | MaryCore」(※特に「有名プロジェクトでの使用例」が参考になります)

右辺値参照やムーブ代入については参考記事等を参照。

参考「右辺値参照・ムーブセマンティクス - cpprefjp C++日本語リファレンス
参考「値のカテゴリ: 左辺値と右辺値 (C++) | Microsoft Docs
参考「右辺値参照宣言子: && | Microsoft Docs
参考「vector::operator= - cpprefjp C++日本語リファレンス
参考「unique_ptr::operator= - cpprefjp C++日本語リファレンス
参考「move (utility) - cpprefjp C++日本語リファレンス

2. 定数定義、メンバ変数の書き換えを制限

プログラミング全般で言えることですが、変数はバグの原因になりやすいため、値を書き換えないことが分かっているものは定数にすべきです。

2.1. 定数定義

コンパイル時に値を決定する定数
// 従来の書き方
#define FOO 23

// C++11
constexpr int foo = 23;

// メモ: 状況に応じて使い分ける
実行時に値を決定する定数
// foo の書き換え不可
const int foo = 23;

int const foo = 23;

// baz の書き換え不可
const int baz = bar;

int const baz = bar;

// 実体 *baz, bar の書き換え不可
const int *baz = &bar;

int const *baz = &bar;

// ポインタ baz の書き換え不可
int * const baz = &bar;

// bar を書き換えを不可な baz にする (const 参照)
const int& baz = bar;

constconstexpr は他にも様々なルールがあるため注意 (各自で確認してください) 。

関数の戻り値に const を付けた場合等の説明はここでは略。

参考「constexpr - cpprefjp C++日本語リファレンス
参考「const - C++ 入門
参考「その17 constのあれこれ2

2.2. メンバ変数の書き換えを制限

クラスのメンバ変数にに関しても同様に const を指定できますが、それとは別に、メンバ関数の宣言の後ろconst を付けることで、そのメンバ関数がメンバ変数の値を書き換えないことを指定できます。

class Foo {
private:
    int _bar;
public:
    int bar() const; // ★
};

int Foo::bar() const { // ★
    return this->_bar;
}

const メンバ変数や、mutable メンバ変数、メンバ関数の戻り値に const を付けた場合等の説明はここでは略。

参考「const - C++ 入門
参考「その17 constのあれこれ2

2.3. 関数の引数では必ずしも const を付けるとは限らない

関数の引数でも同様に const を付けた方が関数内でバグが発生する可能性を下げられますが、引数の「変数そのもの」は const にしないのが一般的です。

関数の引数の「変数そのもの」に const を付けた場合
// メモ: 関数内でバグが発生しにくくなるが、実際には一般的にこのような書き方はされない
int f(const int foo, const int * const bar) {
    // ...
}

関数の引数は関数の「外」から見えるものであり、関数の引数の変数そのものが const であるかどうかは「外から見たら意味がない」情報なので、通常の変数のように定義することが多いです (それでも付けた方が安全な気もしますが…) 。

一方で、ポインタや参照の「示す先」に関して const を付けることはよく使われる手法です。

関数の引数が「示す先」に const を付けた場合
// メモ: bar は書き換え可能だが、*bar や bar[n] は書き換え不可
int f(int foo, const int *bar) {
    // ...
}

ポインタや参照の「示す先」がその関数によって変更されるかどうかは、外から見ても重要だからです。

3. 変数定義、クラスのインスタンス化

3.1. ポインタ型でない従来の自動変数を使う場合

// プリミティブ型の変数定義
int foo;

// 引数なしコンストラクタを呼ぶ
Foo foo;

// 引数付きコンストラクタを呼ぶ
Foo foo(param);

// メモ: 静的配列も同様

プリミティブ型の変数は、関数が終わる等でスコープから出ると自動で解放されます。

クラスのインスタンス化については、上記の書き方では変数のスコープから出るとデストラクタが自動で呼ばれ、メモリが解放されます。

3.2. C++ 標準コンテナを用いる場合

C++ では可変長配列の std::vector、C++11 からは固定長配列の std::array 等、多くのコンテナを利用できます。
文法的にコンテナを用いた変数を自動変数として宣言することで、自動でデストラクタが呼ばれるようになるため、ポインタ変数で管理するよりも安全です。

参考「コンテナライブラリ - リファレンス - cpprefjp C++日本語リファレンス
参考「C++ コンテナ クラス入門

3.2.1. コンテナへの要素追加・挿入は emplace 系メソッドによる直接構築を用いる

C++11 以降、クラスをインスタンス化してコンテナに追加する際に、emplace 系メソッドを使用することで無駄なコピー・ムーブが行われなくなり、効率が良くなります。

std::vector であれば push_back() / insert() の代わりに emplace_back() / emplace()std::unordered_map では insert() の代わりに emplace() / emplace_hint() / try_emplace() を使用します。

※各コンテナのリファレンス等参照。

3.2.2. 明示的なメモリ解放は shrink_to_fit() (メモリ解放の保証はなし)

※ 2021/04/17 現在、shrink_to_fit() を持つクラスは std::vector / std::deque / std::basic_string (std::wstring 等含む) です。

過去の手法として、std::vector 等のメモリを明示的に解放するために swap() (入れ替え) するというものがありましたが、元々 swap() はメモリを解放するための機能ではなく、要素がある場合にはコピーが発生して効率も悪いため、C++11 から追加された、shrink_to_fit() を利用します。
ただし shrink_to_fit() はあくまで「メモリサイズの縮小をリクエストする」だけで、絶対にメモリが解放される保証はありません。

参考「vector::shrink_to_fit - cpprefjp C++日本語リファレンス

std::wstringstd::basic_string<wchar_t> のエイリアスです。

参考「basic_string - cpprefjp C++日本語リファレンス

3.3. スマートポインタを用いる場合

ポインタ変数を用いて new / delete で管理すると、delete を忘れるとデストラクタが呼ばれなくなり、メモリリークの原因になります。

スマートポインタを使うことで、メモリリークを防止できます。

スマートポインタを使うには #include <memory> が必要です。

3.3.1. スマートポインタ変数の宣言

その変数のスコープ内だけで用いる場合には unique_ptr を使用します。
(「ムーブ」(後述) して、変数の寿命を延ばすことも可。)

ポインタに関わる各演算子がオーバーロードされているため、通常のポインタ変数と同じように扱うことができます。

従来のポインタ変数によるメモリ管理
// プリミティブ型の変数定義
int * const foo = new int;

delete foo;

// 引数なしコンストラクタを呼ぶ
Foo * const foo = new Foo;

delete foo;

// 引数付きコンストラクタを呼ぶ
Foo * const foo = new Foo(param);

delete foo;
スマートポインタによるメモリ管理 (new を使用する場合)
// プリミティブ型の変数定義
const std::unique_ptr<int> foo(new int);

// 引数なしコンストラクタを呼ぶ
const std::unique_ptr<Foo> foo(new Foo);

// 引数付きコンストラクタを呼ぶ
const std::unique_ptr<Foo> foo(new Foo(param));

// メモ: いずれも delete 不要
スマートポインタによるメモリ管理 (std::make_unique<T>() を使用する場合)
// メモ: C++14 から利用可

// プリミティブ型の変数定義
const std::unique_ptr<int> foo = std::make_unique<int>();

// 引数なしコンストラクタを呼ぶ
const std::unique_ptr<Foo> foo = std::make_unique<Foo>();

// 引数付きコンストラクタを呼ぶ
const std::unique_ptr<Foo> foo = std::make_unique<Foo>(param);

// メモ: いずれも delete 不要

std::make_unique<T>() を使用する場合には、new / delete はどちらも不要になります。

※ポインタまたはスマートポインタが const でも、元の実体の値は書き換え可能です (別途定数の宣言をしない場合) 。

スマートポインタの詳細や、shared_ptr 等については参考記事参照。

参考「C++11スマートポインタ入門 - Qiita」(※ std::make_unique::reset() は代入演算子 = で置き換え可能。後述)
参考「std::make unique - C++入門
参考「c++ - make_uniqueの利点 - スタック・オーバーフロー
参考「C++11スマートポインタで避けるべき過ち Top10 | POSTD

3.3.2. スマートポインタ変数の代入、ムーブ (所有権の譲渡)

std::unique_ptr に関して「コピー代入」は禁止されていますが、それ以外の代入はできます。

また、std::unique_ptr では代入演算子 = がオーバーロードされ、std::make_unique::reset() を含んだ挙動をします。

(shared_ptr 等に関してはもっと複雑なため注意。ここでは説明略。)

後から初期化 (しているように見える)
// メモ: 実際にはこの時点でインスタンス化されている (所有権なし) 。
//       nullptr を代入してもインスタンス化される。
std::unique_ptr<int> foo;

// メモ: 新たに所有権を譲渡。
foo = std::make_unique<int>(23);
代入とムーブ
std::unique_ptr<int> foo = std::make_unique<int>(23);
std::unique_ptr<int> bar = std::make_unique<int>(42);

// メモ: 直前のリソースを解放してから新たに所有権を譲渡。
std::wcout << *foo << std::endl; // 出力: 23

foo = std::make_unique<int>(1729);

std::wcout << *foo << std::endl; // 出力: 1729

foo = std::move(bar);

std::wcout << *foo << std::endl; // 出力: 42

conststd::unique_ptr は譲渡することもされることもできません。

参考「unique_ptr::operator= - cpprefjp C++日本語リファレンス
参考「unique_ptr::reset - cpprefjp C++日本語リファレンス

3.3.3. 明示的な解放

明示的な解放
std::unique_ptr<int> foo = std::make_unique<int>(23);

foo = nullptr;
// or
// foo.reset();

// 所有権の有無を確認
std::wcout << std::boolalpha << ((bool) foo) << std::endl; // 出力: false

3.4. newmalloc() 等を用いて、従来のポインタでメモリを管理する場合

  • 安全性よりも速度を重要視する場合は、基本的には new を使う
  • 解放し忘れ (メモリリ-ク) に注意
  • 配列の new に対しては delete[] で解放する

C++ ではプリミティブ型でも new によってメモリを確保でき、そのプログラムをスマートポインタに置き換えることもできることから、基本的には malloc() は理由がなければ使わない方が安全といえます。

new / delete に対して、malloc() / free() ではコンストラクタ・デストラクタが呼ばれないというデメリットもあります。

より細かいことは別記事にまとめました。

参考「C++ での動的メモリ確保: new, malloc(), スマートポインタ等の使い分け - Qiita

4. 変数の初期化

C++ での変数の初期化の方法は多くあるため、詳しくは参考記事等参照。

ここでは、特に便利そうなものだけまとめます。

(丸カッコによる値初期化等、基本的な内容については省略。)

参考「初期化子 | Microsoft Docs
参考「C++ の初期化 - プログラミングの教科書を置いておくところ
参考「C++の初期化は分かりにくい - ぷろみん

4.1. new 配列での初期化

変数の初期値を指定しない場合、プリテミティブ型ではゼロ 0 やポインタ型なら nullptr 等、クラスではデフォルトコンストラクタで初期化されます。

new で従来の配列を確保する場合、丸カッコ () を付けることで全ての要素が初期化されます。付けない場合は値が初期化されません。

new に対する丸カッコ () ではデフォルト初期化しかできず、引数付きコンストラクタや値で初期化するには波カッコ {} を使用する必要があります。

値が初期化されない例
int * const array = new int[10]; // メモ: 各要素の値不定

// ...

delete[] array;
値が初期化される例
int * const array = new int[10](); // メモ: int 型配列の場合、各要素の値はゼロ 0

// ...

delete[] array;

詳しい仕様は参考記事等参照。

参考「初期化子 | Microsoft Docs

4.2. 統一初期化記法

従来の C / C++ では集約型 (配列型、構造体、共用体) のみで波カッコによる値の初期化ができましたが、C++11 から一般的なクラス (標準コンテナ含む) でも波カッコで初期化できるようになりました。

int array[] = {0, 1, 2, 3, 4};

std::vector<int> v = {0, 1, 2, 3, 4};

std::array<int, 5> a = {0, 2, 4, 6, 8};

統一初期化記法を使用すべきか、= を付けるか等は状況に応じて使い分けてください。

※波カッコの前に型名を書かない場合、イコール = を記述していても、代入でもコピーコンストラクタでもなく、通常のコンストラクタが呼ばれます。
※波カッコの前に型名を書いた場合、イコール = を記述すると、仕様論上は代入でなくコピーコンストラクタが呼ばれますが、コンパイラの最適化によって通常のコンストラクタのみになる場合があります。

参考「統一初期化構文 · C++11 and C++14 additional features handbook.
参考「初期化子リスト - cpprefjp C++日本語リファレンス
参考「C++11 Universal Initialization は、いつでも使うべきなのか - Qiita

4.3. 構造化束縛

C++17 から、JavaScript で言うところの分割代入である「構造化束縛」が利用できるようになりました。

※参照を用いてコピー代入を無くしたり元の値を書き換えることも可能。

// 
const int array[] = {23, 42, 1729};

const auto [foo, bar, baz] = array; // foo == 23, bar == 42, baz == 1729

// 
const std::pair<int, std::wstring> pair = {2, L"にゃ"};

const auto [key, value] = pair; // key == 2, value == L"にゃ"

参考「構造化束縛 - cpprefjp C++日本語リファレンス

4.4. 指示付き初期化

C++20 から、集成体である構造体、共用体でメンバを指定して初期化できるようになりました。

ただし、C++20 では配列型での指示付き初期化はできません

struct Foo {
    int x;
    int y;
    int z;
};

const Foo foo = {
    .x = 23,
    .y = 42,
    .z = 1729
};

詳しくは参考記事等参照。

参考「指示付き初期化 - cpprefjp C++日本語リファレンス
参考「集成体初期化 - cppreference.com

5. 配列サイズ取得

5.1. 従来の静的配列の場合は std::size() (配列で sizeof() を使わない)

配列の要素数を求める従来の方法として sizeof(array) / sizeof(array[0]) のようなものがありましたが、C++17 から std::size() が使えるようになったため、こちらを使用します。
単に可読性が向上するだけではなく、コンパイル時の扱いの違いによってバグが発生することを、未然に防ぐメリットがあります

sizeof() ではバグを引き起こす可能性がある
int sArray[10];

int * const dArray = new int[10];

// 
sizeof(sArray) / sizeof(sArray[0]); // 結果: 10

sizeof(dArray) / sizeof(dArray[0]); // 結果: 2 = 8 / 4 (64bit 環境用にコンパイルした場合)

// 
delete[] dArray;

上記の例ではソースコードがシンプルなので間違えにくいと思いますが、実際の長いソースコード中で後に静的配列から動的配列に書き換えたりすると、それだけでプログラムが正しく動作しなくなります (コンパイラが警告を出すことはあります) 。

std::size() はポインタに対して使用するとコンパイルエラーとなり、より安全です。

std::size() ではポインタ変数のコンパイルが通らないため、安全
int sArray[10];

int * const dArray= new int[10];

// 
std::size(sArray); // 結果: 10

// std::size(dArray); // コメントをはずすとコンパイルエラー

// 
delete[] dArray;

参考「size - cpprefjp C++日本語リファレンス

5.2. 従来の多次元静的配列の場合も std::size() (std::extentdecltype を使わない)

C++11 以降で std::extent<decltype(array), i> によって多次元配列の要素数を取得できるようになりましたが、こちらもポインタに対して使用すると期待通りの動作をしない (0 を返す) ため、std::size() の方が安全です (C++17 以降) 。

std::extent<> と decltype() ではバグを引き起こす可能性がある
int sArray[10][3];

int ** const dArray = new int *[10];

// 
std::extent<decltype(sArray), 0>::value; // 結果: 10
std::extent<decltype(sArray), 1>::value; // 結果: 3

std::extent<decltype(dArray), 0>::value; // 結果: 0

// 
delete[] dArray;
std::size() ではポインタ変数のコンパイルが通らないため、安全
int sArray[10][3];

int ** const dArray = new int *[10];

// 
std::size(sArray);    // 結果: 10
std::size(sArray[0]); // 結果: 3

// std::size(dArray); // コメントをはずすとコンパイルエラー

// 
delete[] dArray;

参考「size - cpprefjp C++日本語リファレンス

6. その他

6.1. C++ 型推論 auto

※ C の auto と C++ の auto は別物です。

C++11 から変数の型名を省略して auto で書けるようになりました。

ラムダ式を使用するとき等で使用します。

詳細は参考記事等参照。

参考「auto - cpprefjp C++日本語リファレンス
参考「C++ autoの使いどころ・使わない方が良い場面 - uchan note

6.2. おまけ: 可変長配列 VLA

本来 C++ では自動変数の配列の長さに変数を指定できませんが、g++ 等では可変長配列 Variable Length Array として定義することができます。

int something(int n) {

    int array[n]; // VLA

    // ...

}
6
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2