web-dev-qa-db-ja.com

C ++で配列を使用する方法

C++はCから配列を継承し、事実上どこでも使用されています。 C++は、使いやすく、エラーが発生しにくい抽象化を提供します(C++ 98以降のstd::vector<T>および C++ 11 以降の std::array<T, n> )。したがって、配列は不要C言語と同じくらい頻繁に発生します。しかし、レガシーコードを読んだり、C言語で書かれたライブラリと対話したりするときには、配列がどのように機能するのかをしっかり把握する必要があります。

これ FAQ は5つの部分に分かれています。

  1. 型レベルでの配列と要素へのアクセス
  2. 配列の作成と初期化
  3. 代入とパラメータ渡し
  4. 多次元配列とポインタの配列
  5. 配列を使うときのよくある落とし穴

このFAQに何か重要なことが欠けていると感じた場合は、答えを書き、ここに追加の部分としてリンクしてください。

次のテキストでは、「配列」はクラステンプレートstd::arrayではなく「C配列」を意味します。 C宣言子の構文に関する基本的な知識があることが前提です。以下に示すようにnewdeleteを手動で使用することは例外の点で非常に危険ですが、それが another FAQ のトピックです。

(注:これは Stack OverflowのC++ FAQ へのエントリになることを意図しています。あなたが FAQ を提供するという考えを批判したいのであればこのフォーム、そして これをすべて始めたmetaへの投稿 がそのための場所になるでしょう。その質問に対する答えは C++ chatroom でモニターされます - ここで FAQ アイデアは最初から作成されているので、アイデアを思いついた人にあなたの答えが読まれる可能性が非常に高いです。)

455
fredoverflow

型レベルの配列

配列型はT[n]として表されます。ここでT要素型、そしてnは正のsizeです。配列内の要素の数。配列型は要素型とサイズの積型です。これらの成分の一方または両方が異なる場合は、異なる種類があります。

#include <type_traits>

static_assert(!std::is_same<int[8], float[8]>::value, "distinct element type");
static_assert(!std::is_same<int[8],   int[9]>::value, "distinct size");

サイズは型の一部であることに注意してください。つまり、サイズが異なる配列型は互換性のない型で、互いにまったく関係がありません。 sizeof(T[n])n * sizeof(T)と同等です。

配列からポインタへの減衰

T[n]T[m]の間の唯一の "接続"は、両方の型が暗黙的にに変換され、T*になり、この変換の結果が配列の最初の要素を指すこと。つまり、T*が必要な場合はいつでもT[n]を指定でき、コンパイラはそのポインタを暗黙のうちに指定します。

                  +---+---+---+---+---+---+---+---+
the_actual_array: |   |   |   |   |   |   |   |   |   int[8]
                  +---+---+---+---+---+---+---+---+
                    ^
                    |
                    |
                    |
                    |  pointer_to_the_first_element   int*

この変換は「配列からポインタへの減衰」と呼ばれ、混乱の主な原因です。このプロセスで配列のサイズは失われます。これは、タイプ(T*)の一部ではなくなったためです。利点:型レベルで配列のサイズを忘れると、ポインタはanysizeの配列の最初の要素を指すことができます。欠点:配列の最初の(または他の)要素へのポインタが与えられた場合、その配列の大きさや、ポインタが配列の境界に対して正確に指している場所を検出することはできません。 ポインタは非常に愚かです

配列はポインタではありません

コンパイラは、配列の最初の要素が有用であると判断された場合、つまり配列に対して操作が失敗してポインタで成功した場合は常に、暗黙的にポインタを生成します。結果として得られるポインタvalueは単に配列のアドレスであるため、配列からポインタへのこの変換は簡単です。ポインタはnotで、配列自体の一部として(またはメモリ内の他の場所に)格納されていることに注意してください。 配列はポインタではありません。

static_assert(!std::is_same<int[8], int*>::value, "an array is not a pointer");

notが最初の要素へのポインタに崩壊する重要な状況の1つは、&演算子がそれに適用される場合です。その場合、&演算子は、最初の要素へのポインタではなく、entire配列へのポインタを返します。その場合、values(アドレス)は同じですが、配列の最初の要素へのポインタと配列全体へのポインタはまったく別の型です。

static_assert(!std::is_same<int*, int(*)[8]>::value, "distinct element type");

次のASCIIアートでこの違いについて説明しています。

      +-----------------------------------+
      | +---+---+---+---+---+---+---+---+ |
+---> | |   |   |   |   |   |   |   |   | | int[8]
|     | +---+---+---+---+---+---+---+---+ |
|     +---^-------------------------------+
|         |
|         |
|         |
|         |  pointer_to_the_first_element   int*
|
|  pointer_to_the_entire_array              int(*)[8]

最初の要素へのポインターは単一の整数(小さなボックスとして表示)を指すのに対して、配列全体へのポインターは8つの整数(大きなボックスとして表示)の配列を指すことに注意してください。

同じ状況がクラスでも起こり、おそらくもっと明白です。オブジェクトへのポインタとその最初のデータメンバへのポインタは同じvalue(同じアドレス)を持ちますが、それらは完全に異なる型です。

C宣言子の構文に慣れていない場合は、タイプint(*)[8]の括弧が不可欠です。

  • int(*)[8]は、8つの整数の配列へのポインタです。
  • int*[8]は8つのポインタの配列で、各要素はint*型です。

要素へのアクセス

C++は、配列の個々の要素にアクセスするための2つの構文上のバリエーションを提供します。どちらも他より優れているわけではないので、両方に慣れておく必要があります。

ポインタ演算

配列の最初の要素を指すポインタpを指定すると、式p+iは配列のi番目の要素へのポインタを生成します。後でそのポインタを間接参照することで、個々の要素にアクセスできます。

std::cout << *(x+3) << ", " << *(x+7) << std::endl;

xarrayを表す場合、配列と整数を追加しても意味がないので(配列に対するプラス演算はありません)、array-to-pointer decayが開始されます。しかし、ポインタと整数を追加することは意味があります。

   +---+---+---+---+---+---+---+---+
x: |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
     |           |               |
x+0  |      x+3  |          x+7  |     int*

(暗黙的に生成されたポインターは名前を持たないことに注意してください、それで私はそれを識別するためにx+0を書きました。)

一方、xが配列の最初の(または他の)要素へのpointerを示す場合、array-to-pointer decayは必要ありません。 iが追加されるポインタはすでに存在しています。

   +---+---+---+---+---+---+---+---+
   |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
   +-|-+         |               |
x: | | |    x+3  |          x+7  |     int*
   +---+

この例では、xはポインタvariablexの横にある小さなボックスで識別できます)ですが、関数が返された結果でもかまいません。ポインタ(またはT*型の他の式)。

インデックス演算子

構文*(x+i)は少しぎこちないので、C++は代替構文x[i]を提供します。

std::cout << x[3] << ", " << x[7] << std::endl;

加算は可換であるという事実のため、次のコードはまったく同じです。

std::cout << 3[x] << ", " << 7[x] << std::endl;

インデックス演算子を定義すると、次のような興味深い等価性が得られます。

&x[i]  ==  &*(x+i)  ==  x+i

ただし、&x[0]は通常、notで、xと同等です。前者はポインタ、後者は配列です。コンテキストが配列からポインタへの減衰を引き起こす場合に限り、x&x[0]を同じ意味で使用できます。例えば:

T* p = &array[0];  // rewritten as &*(array+0), decay happens due to the addition
T* q = array;      // decay happens due to the assignment

1行目で、コンパイラはポインタからポインタへの代入を検出しますが、これは簡単に成功します。 2行目では、arrayからポインタへの代入を検出しています。これは無意味なので(しかしポインタの代入に対するpointerは意味があります)、通常のようにarray-to-pointer decayが有効になります。

範囲

T[n]型の配列は0からn-1までのインデックスが付けられたn要素を持ちます。要素nはありません。それでも、半開きの範囲(開始はinclusive、終了はexclusive)をサポートするには、 C++では、(存在しない)n番目の要素へのポインタを計算することができますが、そのポインタを間接参照することは不正です。

   +---+---+---+---+---+---+---+---+....
x: |   |   |   |   |   |   |   |   |   .   int[8]
   +---+---+---+---+---+---+---+---+....
     ^                               ^
     |                               |
     |                               |
     |                               |
x+0  |                          x+8  |     int*

たとえば、配列を並べ替える場合は、次の両方が同じように機能します。

std::sort(x + 0, x + n);
std::sort(&x[0], &x[0] + n);

2番目の引数として&x[n]を指定するのは不正です。これは&*(x+n)と等価であり、部分式*(x+n)はC++では 未定義の動作 を呼び出しますが、C99では呼び出しません。

また、最初の引数として単にxを指定することもできます。これは私の好みではあまりにも簡潔すぎます。また、テンプレート引数の控除がコンパイラにとって少し難しくなります。その場合、最初の引数は配列ですが、2番目の引数はポインタです。 (やはり、配列からポインタへの減衰が始まります。)

288
fredoverflow

プログラマは、多次元配列とポインターの配列を混同することがよくあります。

多次元配列

ほとんどのプログラマーは、名前付きの多次元配列に精通していますが、多くのプログラマーは、多次元配列も匿名で作成できるという事実を知りません。多次元配列は、「配列の配列」または「true多次元配列」と呼ばれることがよくあります。

名前付き多次元配列

名前付き多次元配列を使用する場合は、コンパイル時にall次元を認識する必要があります。

int H = read_int();
int W = read_int();

int connect_four[6][7];   // okay

int connect_four[H][7];   // ISO C++ forbids variable length array
int connect_four[6][W];   // ISO C++ forbids variable length array
int connect_four[H][W];   // ISO C++ forbids variable length array

これは、名前付き多次元配列がメモリ内でどのように見えるかです。

              +---+---+---+---+---+---+---+
connect_four: |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+

上記のような2Dグリッドは、有用な視覚化にすぎないことに注意してください。 C++の観点から見ると、メモリは「フラットな」バイトシーケンスです。多次元配列の要素は、行優先の順序で格納されます。つまり、connect_four[0][6]connect_four[1][0]はメモリ内の隣接です。実際、connect_four[0][7]connect_four[1][0]は同じ要素を示しています!これは、多次元配列を取り、それらを大きな1次元配列として扱うことができることを意味します。

int* p = &connect_four[0][0];
int* q = p + 42;
some_int_sequence_algorithm(p, q);

匿名の多次元配列

匿名の多次元配列では、すべての次元最初を除くはコンパイル時に認識されている必要があります。

int (*p)[7] = new int[6][7];   // okay
int (*p)[7] = new int[H][7];   // okay

int (*p)[W] = new int[6][W];   // ISO C++ forbids variable length array
int (*p)[W] = new int[H][W];   // ISO C++ forbids variable length array

これは、メモリ内で匿名の多次元配列がどのように見えるかです。

              +---+---+---+---+---+---+---+
        +---> |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |
      +-|-+
   p: | | |
      +---+

配列自体はまだメモリ内の単一ブロックとして割り当てられていることに注意してください。

ポインターの配列

別のレベルの間接参照を導入することにより、固定幅の制限を克服できます。

ポインターの名前付き配列

これは、異なる長さの匿名配列で初期化される5つのポインターの名前付き配列です。

int* triangle[5];
for (int i = 0; i < 5; ++i)
{
    triangle[i] = new int[5 - i];
}

// ...

for (int i = 0; i < 5; ++i)
{
    delete[] triangle[i];
}

そして、これがメモリ内の様子です:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
triangle: | | | | | | | | | | |
          +---+---+---+---+---+

各行は個別に割り当てられるようになったため、2D配列を1D配列として表示することはできなくなりました。

ポインタの匿名配列

これは、異なる長さの匿名配列で初期化される5(または他の任意の数)のポインターの匿名配列です。

int n = calculate_five();   // or any other number
int** p = new int*[n];
for (int i = 0; i < n; ++i)
{
    p[i] = new int[n - i];
}

// ...

for (int i = 0; i < n; ++i)
{
    delete[] p[i];
}
delete[] p;   // note the extra delete[] !

そして、これがメモリ内の様子です:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
          | | | | | | | | | | |
          +---+---+---+---+---+
            ^
            |
            |
          +-|-+
       p: | | |
          +---+

コンバージョン数

配列からポインターへの減衰は、配列の配列およびポインターの配列に自然に拡張されます。

int array_of_arrays[6][7];
int (*pointer_to_array)[7] = array_of_arrays;

int* array_of_pointers[6];
int** pointer_to_pointer = array_of_pointers;

ただし、T[h][w]からT**への暗黙的な変換はありません。そのような暗黙の変換が存在した場合、結果はhの配列の最初の要素へのポインターTへのポインターになります(それぞれが元の2D配列の行の最初の要素を指します) 、しかしそのポインタ配列はまだメモリのどこにも存在しません。このような変換が必要な場合は、必要なポインター配列を手動で作成して入力する必要があります。

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = connect_four[i];
}

// ...

delete[] p;

これにより、元の多次元配列のビューが生成されることに注意してください。代わりにコピーが必要な場合は、追加の配列を作成し、自分でデータをコピーする必要があります。

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = new int[7];
    std::copy(connect_four[i], connect_four[i + 1], p[i]);
}

// ...

for (int i = 0; i < 6; ++i)
{
    delete[] p[i];
}
delete[] p;
132
fredoverflow

割り当て

特に理由はありませんが、配列を互いに割り当てることはできません。代わりにstd::copyを使用します。

#include <algorithm>

// ...

int a[8] = {2, 3, 5, 7, 11, 13, 17, 19};
int b[8];
std::copy(a + 0, a + 8, b);

これは、より大きな配列のスライスをより小さな配列にコピーできるため、真の配列割り当てが提供するものよりも柔軟性があります。 std::copyは通常、最大のパフォーマンスを得るためにプリミティブ型に特化しています。 std::memcpyのパフォーマンスが向上することはほとんどありません。疑わしい場合は、測定します。

配列を直接割り当てることはできませんが、canは、配列メンバーを含む構造体とクラスを割り当てることができます。これは、コンパイラによってデフォルトとして提供される割り当て演算子によって 配列メンバーがメンバーごとにコピーされる であるためです。独自の構造体またはクラスタイプに割り当て演算子を手動で定義する場合、配列メンバーの手動コピーにフォールバックする必要があります。

パラメータの受け渡し

配列を値で渡すことはできません。ポインタまたは参照で渡すことができます。

ポインターで渡す

配列自体は値で渡すことができないため、通常、最初の要素へのポインターは代わりに値で渡されます。これは多くの場合、「ポインタによる受け渡し」と呼ばれます。配列のサイズはそのポインターを介して取得できないため、配列のサイズを示す2番目のパラメーター(従来のCソリューション)または配列の最後の要素を指す2番目のポインター(C++イテレーターソリューション)を渡す必要があります:

#include <numeric>
#include <cstddef>

int sum(const int* p, std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

int sum(const int* p, const int* q)
{
    return std::accumulate(p, q, 0);
}

構文上の代替として、パラメータをT p[]として宣言することもできます。これはT* pとまったく同じことを意味しますパラメータリストのコンテキストのみ

int sum(const int p[], std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

コンパイラをT p[]からT *pに書き換えると考えることができますパラメータリストのコンテキストのみ。この特別な規則は、配列とポインターに関する混乱全体の一部を担っています。他のすべてのコンテキストでは、何かを配列またはポインターとして宣言すると、hugeの違いが生じます。

残念ながら、コンパイラーによって暗黙的に無視される配列パラメーターでサイズを指定することもできます。つまり、次の3つのシグネチャは、コンパイラエラーによって示されるように、まったく同じです。

int sum(const int* p, std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[], std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[8], std::size_t n)   // the 8 has no meaning here

参照渡し

配列は参照で渡すこともできます。

int sum(const int (&a)[8])
{
    return std::accumulate(a + 0, a + 8, 0);
}

この場合、配列サイズは重要です。正確に8つの要素の配列のみを受け入れる関数を作成することはほとんど役に立たないため、プログラマは通常、そのような関数をテンプレートとして作成します。

template <std::size_t n>
int sum(const int (&a)[n])
{
    return std::accumulate(a + 0, a + n, 0);
}

このような関数テンプレートは、整数へのポインタではなく、実際の整数の配列でのみ呼び出すことができます。配列のサイズは自動的に推測され、サイズnごとに、異なる関数がテンプレートからインスタンス化されます。また、要素タイプとサイズの両方から抽象化する 非常に便利 関数テンプレートを記述することもできます。

85
fredoverflow

アレイの作成と初期化

他の種類のC++オブジェクトと同様に、配列は名前付き変数に直接格納できます(その場合、サイズはコンパイル時定数である必要があります。 C++はVLA をサポートしません)。ヒープに匿名で格納し、ポインタを介して間接的にアクセスすることができます(その場合にのみ、サイズは実行時に計算されます)。

自動配列

自動配列(「スタック上に存在する」配列)は、制御の流れが非静的ローカル配列変数の定義を通過するたびに作成されます。

void foo()
{
    int automatic_array[8];
}

初期化は昇順で行われます。初期値は、要素型Tによって異なります。

  • TPOD の場合(上の例のintのように)、初期化は行われません。
  • そうでなければ、Tのdefault-constructorはすべての要素を初期化します。
  • Tがアクセス可能なデフォルトコンストラクタを提供しない場合、プログラムはコンパイルされません。

あるいは、初期値は、配列初期化子、中括弧で囲まれたコンマ区切りリストで明示的に指定することができます。

    int primes[8] = {2, 3, 5, 7, 11, 13, 17, 19};

この場合、配列初期化子の要素数は配列のサイズに等しいので、サイズを手動で指定することは冗長です。これはコンパイラによって自動的に推測されます。

    int primes[] = {2, 3, 5, 7, 11, 13, 17, 19};   // size 8 is deduced

サイズを指定し、より短い配列初期化子を提供することも可能です。

    int fibonacci[50] = {0, 1, 1};   // 47 trailing zeros are deduced

その場合、残りの要素は ゼロ初期化 です。 C++では空の配列初期化子(すべての要素はゼロで初期化される)が許可されています(少なくとも1つの値が必要です)。また、配列初期化子はinitialize配列にしか使えないことに注意してください。それらは後で割り当てに使用することはできません。

静的配列

静的配列(「データセグメント内に存在する配列」)は、staticキーワードで定義されたローカル配列変数、および名前空間スコープの配列変数(「グローバル変数」)です。

int global_static_array[8];

void foo()
{
    static int local_static_array[8];
}

(名前空間スコープの変数は暗黙的に静的であることに注意してください。staticキーワードをその定義に追加すると、 完全に異なる、非推奨の意味 になります。)

静的配列は自動配列とは異なる動作をします。

  • 配列初期化子のない静的配列は、それ以降の潜在的な初期化の前にゼロで初期化されます。
  • 静的POD配列は一度だけに初期化され、初期値は通常に実行可能ファイルに組み込まれます。この場合、実行時に初期化コストはかかりません。ただし、これは必ずしもスペース効率が最も高いソリューションとは限りません。また、標準では必須ではありません。
  • 静的非POD配列は初回で初期化され、制御の流れはそれらの定義を通過します。ローカルな静的配列の場合、関数が呼び出されないと、それは起こり得ません。

(上記のどれも配列に固有のものではありません。これらの規則は他の種類の静的オブジェクトにも同様に適用されます。)

配列データメンバ

配列データメンバーは、その所有オブジェクトが作成されたときに作成されます。残念ながら、C++ 03は メンバー初期化子リスト の配列を初期化する手段を提供していないので、初期化は代入で偽造しなければなりません:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        primes[0] = 2;
        primes[1] = 3;
        primes[2] = 5;
        // ...
    }
};

あるいは、コンストラクタ本体に自動配列を定義して、その上に要素をコピーすることもできます。

class Foo
{
    int primes[8];

public:

    Foo()
    {
        int local_array[] = {2, 3, 5, 7, 11, 13, 17, 19};
        std::copy(local_array + 0, local_array + 8, primes + 0);
    }
};

C++ 0xでは、 均一初期化 のおかげで配列をメンバ初期化子リストで初期化できます。

class Foo
{
    int primes[8];

public:

    Foo() : primes { 2, 3, 5, 7, 11, 13, 17, 19 }
    {
    }
};

これが、デフォルトコンストラクタを持たない要素型を扱う唯一の解決策です。

動的配列

動的配列には名前がありません。そのため、それらにアクセスする唯一の手段はポインタ経由です。それらには名前がないので、これからは「匿名配列」と呼びます。

Cでは、無名配列はmallocとその仲間を通して作成されます。 C++では、無名配列は、無名配列の最初の要素へのポインタを返すnew T[size]構文を使用して作成されます。

std::size_t size = compute_size_at_runtime();
int* p = new int[size];

次のASCII artは、実行時にサイズが8として計算された場合のメモリレイアウトを示しています。

             +---+---+---+---+---+---+---+---+
(anonymous)  |   |   |   |   |   |   |   |   |
             +---+---+---+---+---+---+---+---+
               ^
               |
               |
             +-|-+
          p: | | |                               int*
             +---+

明らかに、無名配列は、別に格納しなければならない追加のポインタのために、名前付き配列より多くのメモリを必要とします。 (フリーストアには追加のオーバーヘッドもあります。)

ここではnoarray-to-pointer減衰が行われていることに注意してください。 new int[size]を評価すると、実際にはarrayの整数が生成されますが、式new int[size]の結果はalready単一の整数へのポインタ(最初の要素)です。not整数の配列、またはサイズが不明な整数の配列へのポインタ。静的型システムでは配列サイズがコンパイル時定数である必要があるため、これは不可能です。 (それゆえ、私は匿名配列に図の静的型情報をアノテーション付けしませんでした。)

要素のデフォルト値に関しては、無名配列は自動配列と同じように振る舞います。通常、無名POD配列は初期化されませんが、値初期化を引き起こす 特殊構文 があります。

int* p = new int[some_computed_size]();

(セミコロンの直前の末尾の括弧に注意してください。)繰り返しますが、C++ 0xでは規則が単純化され、一様な初期化のおかげで無名配列の初期値を指定できます。

int* p = new int[8] { 2, 3, 5, 7, 11, 13, 17, 19 };

無名配列を使い終わったら、それをシステムに解放する必要があります。

delete[] p;

あなたはそれぞれの匿名配列を一度だけ解放しなければなりませんそしてそれ以降は絶対に二度と触れないでください。まったく解放しないとメモリリークが発生し(より一般的には、要素の種類やリソースリークによっては)、何度も解放しようとすると未定義の動作になります。 delete[]の代わりに非配列形式のdelete(またはfree)を使用して配列を解放することも、 未定義の動作 です。

70
fredoverflow

5.配列を使用する際のよくある落とし穴.

5.1落とし穴:型安全でないリンクを信頼する.

グローバル(翻訳単位の外側からアクセスできる名前空間スコープ変数)はEvil™であると言われたか、自分自身を発見しました。しかし、あなたは本当にEvil™がどれほど彼らなのか知っていましたか? 2つのファイル[main.cpp]と[numbers.cpp]からなる以下のプログラムを考えてください。

// [main.cpp]
#include <iostream>

extern int* numbers;

int main()
{
    using namespace std;
    for( int i = 0;  i < 42;  ++i )
    {
        cout << (i > 0? ", " : "") << numbers[i];
    }
    cout << endl;
}
// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

Windows 7では、これはMinGW g ++ 4.4.1とVisual C++ 10.0の両方で正常にコンパイルおよびリンクされます。

型が一致しないため、実行するとプログラムはクラッシュします。

The Windows 7 crash dialog

正式な説明:プログラムには未定義の動作(UB)があるため、クラッシュする代わりにハングアップするか、または何もしないか、アメリカ、ロシア、インドの大統領に脅迫的な電子メールを送信する可能性があります。中国とスイス、そして鼻のデーモンをあなたの鼻から飛び立たせる。

実際の説明:main.cppでは、配列はポインタとして扱われ、配列と同じアドレスに配置されます。 32ビット実行可能ファイルの場合、これは配列内の最初のint値がポインターとして扱われることを意味します。つまり、main.cppでは、numbers変数に(int*)1が含まれているか、含まれているように見えます。これにより、プログラムはアドレス空間の一番下のメモリにアクセスします。これは、通常は予約されており、トラップの原因となります。結果:あなたはクラッシュします。

C++ 11§3.5/ 10は、宣言のための互換型の要件について、次のように述べています。

[N3290§3.5/ 10]
型の同一性に関するこの規則の違反は、診断を必要としません。

同じ段落で、許可されているバリエーションについて詳しく説明します。

…配列オブジェクトの宣言は、主配列境界の有無によって異なる配列型を指定することができます(8.3.4)。

この許可されたバリエーションには、名前を1つの翻訳単位内の配列、および別の翻訳単位内のポインターとして宣言することは含まれません。

5.2落とし穴:時期尚早の最適化(memset&friends)を行う。

まだ書かれていません

5.3落とし穴:Cの慣用句を使用して要素数を取得する。

Cの経験が豊富なので、書くのは自然です…

#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

arrayは必要に応じて最初の要素を指すように減衰するので、式sizeof(a)/sizeof(a[0])sizeof(a)/sizeof(*a)と書くこともできます。それは同じことを意味し、それがどのように書かれていようとも、それはarrayの要素数を見つけるためのCの慣用句です。

主な落とし穴:Cの慣用句は型保証されていません。たとえば、コードは…

#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

N_ITEMSへのポインタを渡しているため、おそらく間違った結果が生成されます。 Windows 7で32ビットの実行可能ファイルとしてコンパイルされて生成されます…

7つの要素、displayを呼び出しています...
1つの要素.

  1. コンパイラはint const a[7]をちょうどint const a[]に書き換えます。
  2. コンパイラはint const a[]int const* aに書き換えます。
  3. そのためN_ITEMSはポインタで呼び出されます。
  4. 32ビット実行可能ファイルの場合、sizeof(array)(ポインタのサイズ)は4になります。
  5. sizeof(*array)sizeof(int)と同等です。これは32ビット実行可能ファイルの場合も4です。

実行時にこのエラーを検出するためにあなたは…

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7つの要素、displayを呼び出しています...
アサーションが失敗しました:( "N_ITEMSは引数として実際の配列を必要とします"、typeid(a)!= typeid(&* a))、ファイルruntime_detect ion.cpp、行16

このアプリケーションは、異常な方法でそれを終了するようにランタイムに要求しました。
詳細については、アプリケーションのサポートチームにお問い合わせください。

ランタイムエラーの検出は、検出されないことより優れていますが、わずかなプロセッサ時間、そしておそらくはるかに多くのプログラマ時間を浪費します。コンパイル時の検出に優れています。そして、もしあなたがC++ 98でローカルタイプの配列をサポートしないことに満足しているならば、あなたはそれをすることができます:

#include <stddef.h>

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

#define N_ITEMS( array )       n_items( array )

この定義をコンパイルして最初の完全なプログラムに置き換え、g ++で、私は手に入れました…

M:\ count> g ++のcompile_time_detection.cpp
compile_time_detection.cpp:関数 'void display(const int *)'において:
compile_time_detection.cpp:14:エラー: 'n_items(const int *&)'の呼び出しにマッチする関数がありません

M:\カウント> _

どのように機能するのか:配列は 参照 によってn_itemsに渡されるので、最初の要素へのポインタには減衰せず、関数は単に型で指定された要素数を返すことができます。

C++ 11ではこれをローカル型の配列にも使うことができ、配列の要素数を見つけるための型安全なC++イディオムです。 。

5.4 C++ 11およびC++ 14の落とし穴:constexpr配列サイズ関数の使用。

C++ 11以降ではそれは自然なことですが、危険に思われるかもしれませんが、C++ 03関数に代わるものです。

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

using Size = ptrdiff_t;

template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }

ここで重要な変更はconstexprの使用です。これにより、この関数はコンパイル時定数を生成できます。

たとえば、C++ 03関数とは対照的に、このようなコンパイル時定数を使用して、別のものと同じサイズの配列を宣言できます。

// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    constexpr Size n = n_items( x );
    int y[n] = {};
    // Using y here.
}

しかし、constexprバージョンを使用してこのコードを検討してください。

// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = n_items( c );     // Not in C++14!
    // Use c here
}

auto main() -> int
{
    int x[42];
    foo( x );
}

落とし穴:2015年7月の時点で、上記はMinGW-64 5.1.0と-pedantic-errorsを使用してコンパイルされています。また、 gcc.godbolt.org/ のオンラインコンパイラーを使用してテストされています。ただし、3.3、3.4.1、3.5.0、3.5.1、3.6(rc1)、または3.7(実験的)とは異なります。そしてWindowsプラットフォームにとって重要で、それはVisual C++ 2015ではコンパイルされません。その理由は、constexpr式での参照の使用についてのC++ 11/C++ 14ステートメントです。

番目

条件式 eは、 コア定数式 抽象マシン(1.9)の規則に従ったeの評価で、以下のいずれかの式が評価される場合を除きます。

  • id-expression 参照の前に初期化とのどちらかがない限り、参照型の変数またはデータメンバーを参照します。
    • 定数式で初期化されている
    • それは、その寿命がeの評価の範囲内で始まったオブジェクトの非静的データメンバーです。

より冗長なものをいつでも書くことができます

// Example 3  --  limited

using Size = ptrdiff_t;

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = std::extent< decltype( c ) >::value;
    // Use c here
}

…しかし、Collectionが生の配列ではない場合、これは失敗します。

非配列になることがあるコレクションを扱うためにはn_items関数のオーバーロード可能性が必要ですが、コンパイル時の使用のためには配列サイズのコンパイル時表現が必要です。そして、C++ 11とC++ 14でもうまく動作する古典的なC++ 03の解決策は、その結果を値としてではなく、関数の結果 type を使って報告させることです。例えばこんな感じ:

// Example 4 - OK (not ideal, but portable and safe)

#include <array>
#include <stddef.h>

using Size = ptrdiff_t;

template< Size n >
struct Size_carrier
{
    char sizer[n];
};

template< class Type, Size n >
auto static_n_items( Type (&)[n] )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

template< class Type, size_t n >        // size_t for g++
auto static_n_items( std::array<Type, n> const& )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

#define STATIC_N_ITEMS( c ) \
    static_cast<Size>( sizeof( static_n_items( c ).sizer ) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = STATIC_N_ITEMS( c );
    // Use c here
    (void) c;
}

auto main() -> int
{
    int x[42];
    std::array<int, 43> y;
    foo( x );
    foo( y );
}

static_n_itemsの戻り型の選択について:このコードはstd::integral_constantを使用しません。これは、std::integral_constantを使用すると、結果が直接constexpr値として表され、元の問題が再導入されるためです。 Size_carrierクラスの代わりに、関数が配列への参照を直接返すようにすることができます。しかし、誰もがその構文に精通しているわけではありません。

命名について:constexpr-参照による無効問題に対するこの解決策の一部は、コンパイル時定数の選択を明示的にすることです。

うまくいけば、あなたの-constexpr問題はC++ 17で修正されるでしょうが、それまでは上記のSTATIC_N_ITEMSのようなマクロが移植性をもたらします。型安全性を保持しながら、clangおよびVisual C++コンパイラに。

関連:マクロはスコープを尊重しないので、名前の衝突を避けるために名前の接頭辞を使うのは良い考えです。 MYLIB_STATIC_N_ITEMS

68