コンテンツにスキップ

配列と文字列

一言で言うと

  • 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)し、要素数の情報が消える
void Process(int arr[]) {          // 実際の型は int* — サイズは分からない
    // sizeof(arr) はポインタのサイズ(8)であって配列全体ではない!
}

現代の選択肢

#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++頻出バグの代表です(→ STLdangling)。

文字列

#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開発者が誤解しやすい点

  1. 「配列は安全なもの」— C#の常識。C++の生配列と v[i] は範囲チェックしません。デバッグビルドでチェック付きの at() やアサートを活用する
  2. 「文字列連結は罪(GC Alloc)」— C++ではGCはないが、再確保というコストはある。ループ内連結は reserve で緩和
  3. 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。役割が違う

関連項目

理解度チェック

  1. 生配列を関数に渡すと何が失われますか。
  2. vectorのpush_backで「無効化」されるものは何ですか。なぜですか。
  3. string_viewを引数に使ってよく、メンバ変数への保存に慎重になるべき理由は?

演習

samples/arrays_strings.cpp(検証済み)で、(a) vectorの容量(capacity)がpush_backでどう成長するか、(b) 再確保の前後で要素のアドレスが変わることを、出力で観察してください。


前: const と const correctness | カテゴリ目次 | 次: 構造体・クラス・アクセス指定子