配列と文字列¶
一言で言うと¶
- C++の生配列
int a[10]は「メモリ上に連続して並んだ10個のint」そのもの。サイズ情報も境界チェックも付いていない - 実務では
std::array(固定長)とstd::vector(可変長)を使う - 文字列も同様に、Cスタイルの
char*ではなくstd::string(所有)とstd::string_view(参照)を使う
生配列とその問題¶
// C++20
int scores[5] = {10, 20, 30, 40, 50}; // スタック上に連続20バイト
int x = scores[7]; // コンパイルも実行も「通ってしまう」— 未定義動作(隣のメモリを読む)
- 境界チェックなし: 範囲外アクセスはエラーにならず、隣のメモリを破壊(→ buffer overflow)
- サイズを忘れる: 関数に渡すと配列はポインタに退化(decay)し、要素数の情報が消える
現代の選択肢¶
#include <array>
#include <vector>
std::array<int, 5> a{10, 20, 30, 40, 50}; // 固定長: 生配列と同じメモリレイアウト+サイズ情報
std::vector<int> v{10, 20, 30}; // 可変長: ヒープに確保、伸縮自在
a.size(); // サイズを持っている
v.push_back(40);
v.at(7); // atは境界チェックあり(例外)。v[7]はチェックなし(速いが自己責任)
メモリ上で起きること¶
std::array<int,5> a; スタックに20バイト直置き(生配列と同一。オーバーヘッドゼロ)
std::vector<int> v; スタックには3ポインタ分(先頭/末尾/確保末尾 — 典型実装、24バイト)
要素本体はヒープに連続配置
スタック ヒープ
┌──────────┐ ┌────────────────┐
│ data ────┼─────────>│ 10 20 30 (40)... │
│ size=3 │ └────────────────┘
│ cap =4 │ push_backで容量不足になると「より大きい領域を確保して全要素を引っ越し」
└──────────┘ → 古い要素へのポインタ/イテレータは全部無効化(頻出事故)
vector の再確保(reallocation)は要素のアドレスが変わることを意味します。「vectorの要素へのポインタを保存しておいたら、push_backの後で壊れた」はC++頻出バグの代表です(→ STL、dangling)。
文字列¶
#include <string>
#include <string_view>
std::string name = "Slime"; // 所有する文字列(可変・ヒープ確保しうる)
std::string_view sv = name; // 参照するだけ(所有しない・コピーしない・軽い)
void PrintName(std::string_view n); // 読むだけの引数はstring_viewが現代の定石
std::stringはほぼstd::vector<char>+文字列操作。SSO(小文字列最適化): 多くの実装は短い文字列(典型的に15文字程度まで — 実装依存)をヒープ確保せずstring本体内に格納するstring_viewは「先頭ポインタ+長さ」だけの軽量ビュー。参照先より長生きさせてはいけない(danglingの新たな入口)- リテラル
"Slime"は実行ファイルの読み取り専用領域に置かれる(→ メモリレイアウト)
C#との違い¶
| C# | C++ | |
|---|---|---|
| 配列 | int[] は参照型・境界チェックあり・Length持ち |
生配列は裸のメモリ。チェックなし |
| 範囲外 | IndexOutOfRangeException(安全に停止) | 未定義動作(何が起きるか不明) |
| 文字列 | string は不変(immutable)・参照型 | std::string は可変・値(コピーされる) |
| 文字列連結 | 新インスタンス生成(GC負荷) | 同一バッファ内で伸長(再確保はある) |
C#のstringは不変なので共有し放題ですが、C++のstringは可変な値です。「代入したら共有」ではなくコピーになる点、関数に値渡しするとコピーが走る点(だからconst参照/string_viewで渡す)がC#との大きな違いです。
Unity開発者が誤解しやすい点¶
- 「配列は安全なもの」— C#の常識。C++の生配列と
v[i]は範囲チェックしません。デバッグビルドでチェック付きのat()やアサートを活用する - 「文字列連結は罪(GC Alloc)」— C++ではGCはないが、再確保というコストはある。ループ内連結は
reserveで緩和 char*の文字列はNUL終端('\0')前提のC流儀。std::stringと混ぜるときのc_str()の寿命(元のstringが生きている間だけ有効)に注意
ゲーム開発での使用例¶
- 弾・敵・頂点などの大量データは
std::vector(連続メモリ=キャッシュ効率 → Data Locality) - 固定サイズが分かっているもの(装備スロット4個)は
std::array - アセットID・名前の受け渡しは
std::string_view(コピーゼロ)。保存するならstd::string(所有) - 毎フレームの文字列生成(スコア表示)はバッファ再利用で確保を回避
使う場面 / 使わない場面¶
std::vector: 迷ったらこれ(→ 配列かvectorか)std::array: コンパイル時にサイズ確定、スタックに置きたい- 生配列: C APIとの境界、意図的な低レベルコードのみ(理由をコメントで)
string_view: 読み取り引数。保存はしない(寿命の罠)
よくある誤解¶
- 「vectorは遅い」—
v[i]は生配列と同じ機械語になる(最適化時)。遅いのは誤った使い方(ループ内再確保、値コピー渡し) - 「string_viewは常にstringの上位互換」— 所有しないので、元が消えたら即dangling。役割が違う
関連項目¶
理解度チェック¶
- 生配列を関数に渡すと何が失われますか。
- vectorのpush_backで「無効化」されるものは何ですか。なぜですか。
- string_viewを引数に使ってよく、メンバ変数への保存に慎重になるべき理由は?
演習¶
samples/arrays_strings.cpp(検証済み)で、(a) vectorの容量(capacity)がpush_backでどう成長するか、(b) 再確保の前後で要素のアドレスが変わることを、出力で観察してください。
前: const と const correctness | カテゴリ目次 | 次: 構造体・クラス・アクセス指定子