504

C++ は C から配列を継承し、事実上あらゆる場所で使用されています。C++ は、使いやすく、エラーが発生しにくい ( std::vector<T>C++98std::array<T, n>以降およびC++11以降) 抽象化を提供するため、配列の必要性は C ほど頻繁には発生しません。コードを作成したり、C で記述されたライブラリを操作したりする場合は、配列がどのように機能するかをしっかりと把握する必要があります。

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

  1. 型レベルの配列と要素へのアクセス
  2. 配列の作成と初期化
  3. 割り当てとパラメーターの受け渡し
  4. 多次元配列とポインターの配列
  5. 配列を使用する際のよくある落とし穴

この FAQ に何か重要な情報が欠けていると思われる場合は、回答を作成し、追加部分としてここにリンクしてください。

以下のテキストでは、「配列」は「C 配列」を意味し、クラス テンプレートではありませんstd::array。C 宣言子の構文に関する基本的な知識があることを前提としています。new以下に示すようにとを手動で使用するdeleteことは、例外に直面すると非常に危険であることに注意してください。ただし、それは別の FAQのトピックです。


(注: これはStack Overflow の C++ FAQへのエントリであることを意図しています。FAQ をこのフォームで提供するという考えを批判したい場合は、すべての始まりとなった meta への投稿がそれを行う場所になります。回答への回答その質問は、FAQ のアイデアが最初に始まったC++ チャットルームで監視されているため、アイデアを思いついた人にあなたの回答が読まれる可能性が非常に高くなります。)

4

5 に答える 5

314

型レベルの配列

配列型は次のように示されますT[n]。ここTで、 は要素の型nあり、正のsize、配列内の要素の数です。配列型は、要素型とサイズの積型です。これらの成分の 1 つまたは両方が異なる場合、異なるタイプが得られます。

#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*、この変換の結果が配列の最初の要素へのポインタになることです。つまり、 aT*が必要な場所ならどこでも a を指定できT[n]、コンパイラーは暗黙のうちにそのポインターを提供します。

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

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

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

コンパイラーは、配列の最初の要素へのポインターが有用であると見なされるときはいつでも、つまり、操作が配列で失敗し、ポインターで成功するときはいつでも、黙って生成します。結果のポインター値は単に配列のアドレスであるため、配列からポインターへのこの変換は自明です。ポインターは、配列自体の一部として (またはメモリ内の他の場所には) 格納されないことに注意してください。配列はポインターではありません。

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

配列がその最初の要素へのポインターに崩壊しない重要なコンテキストの 1 つは、&演算子が配列に適用される場合です。その場合、&演算子は、最初の要素へのポインターだけでなく、配列全体へのポインターを生成します。その場合、(アドレス) は同じですが、配列の最初の要素へのポインターと配列全体へのポインターは完全に異なる型です。

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]

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

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

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

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

要素へのアクセス

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

ポインター演算

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

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

配列と整数の追加はx意味がないため (配列にはプラス演算はありません)、配列からポインターへの減衰が開始されますが、ポインターと整数の追加は理にかなっています。

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

(暗黙的に生成されたポインタには名前がないので、x+0識別するために書きました。)

一方、が配列の最初の (またはその他の) 要素へのポインターxを示す場合、追加されるポインターが既に存在するため、配列からポインターへの減衰は必要ありません。i

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

描かれているケースでxは、 はポインター変数( の横にある小さなボックスで識別可能x) ですが、ポインター (または 型のその他の式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]一般的にはと同等ではありません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

最初の行で、コンパイラはポインタからポインタへの代入を検出しますが、これは簡単に成功します。2 行目では、配列からポインターへの代入を検出します。これは無意味なので (ただし、ポインターへのポインターの代入は理にかなっています)、配列からポインターへの減衰は通常どおり開始されます。

範囲

型の配列にはT[n]、からまでnのインデックスが付けられた要素があります。要素はありません。それでも、半分開いた範囲 (先頭が包括的で末尾が排他的) をサポートするために、C++ では (存在しない) n 番目の要素へのポインタの計算が許可されますが、そのポインタを逆参照することは違法です。0n-1n

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

たとえば、配列をソートする場合、次のどちらも同じように機能します。

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

&x[n]これは と同等であるため、2 番目の引数として指定することは違法であることに注意してください&*(x+n)。部分式は、技術的には C++ で未定義の動作*(x+n)を呼び出します(ただし、C99 ではそうではありません)。

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

于 2011-01-26T22:14:47.427 に答える
142

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

多次元配列

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

名前付き多次元配列

名前付き多次元配列を使用する場合、コンパイル時にすべての次元がわかっている必要があります。

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;
于 2011-01-26T22:15:30.997 に答える
91

割り当て

特に理由はありませんが、配列を互いに割り当てることはできません。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。迷ったら測る。

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

パラメーターの受け渡し

配列は値渡しできません。ポインターまたは参照によって渡すことができます。

ポインタ渡し

配列自体は値で渡すことができないため、通常は最初の要素へのポインターが代わりに値で渡されます。これは、「ポインタ渡し」と呼ばれることがよくあります。配列のサイズはそのポインターを介して取得できないため、配列のサイズを示す 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

残念ながら、配列パラメーターでサイズを指定することもできますが、これはコンパイラーによって黙って無視されます。つまり、コンパイラ エラーが示すように、次の 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);
}

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

于 2011-01-26T22:15:14.367 に答える
73

5.アレイを使用する際の一般的な落とし穴。

5.1落とし穴:タイプを信頼する-安全でないリンク。

OK、グローバル(変換ユニットの外部からアクセスできる名前空間スコープ変数)は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とVisualC++10.0の両方と正常にリンクします。

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

Windows7のクラッシュダイアログ

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

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

C++11§3.5/10によると、宣言の互換性のある型の要件について、コンパイラはこのエラーを診断しない権利を完全に有しています。

[N3290§3.5/10]
タイプIDに関するこのルールの違反は、診断を必要としません。

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

…配列オブジェクトの宣言では、バインドされた主要な配列の有無によって異なる配列タイプを指定できます(8.3.4)。

この許可されたバリエーションには、名前を1つの変換ユニットで配列として宣言したり、別の変換ユニットでポインターとして宣言したりすることは含まれていません。

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

まだ書かれていません

5.3落とし穴:Cイディオムを使用して要素の数を取得します。

深いCの経験があれば、書くのは自然なことです…

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

array必要に応じて最初の要素へのポインタに減衰するため、式は。sizeof(a)/sizeof(a[0])と書くこともできます sizeof(a)/sizeof(*a)。それは同じことを意味し、それがどのように書かれていても、それは配列の数要素を見つけるための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つの要素、ディスプレイの呼び出し...
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:\ count> _

仕組み:配列はへの参照によって渡される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で コンパイルされ、gcc.godbolt.org /-pedantic-errorsのオンラインコンパイラでテストされています。また、 clang3.0およびclang3.2でテストされていますが、clang 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ステートメントです。

C ++ 11 C ++ 14 $ 5.19 /29番目のダッシュ

条件式 eは、抽象マシン(1.9)の規則に従って、の評価が次の式のいずれかを評価しない限り、コア定数式です。         ⋮ e

  • 参照に先行する初期化があり、いずれかがない限り、参照型の変数またはデータメンバーを参照 するid式
    • 定数式で初期化されるか、
    • これは、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ソリュ​​ーションは、関数に結果を値としてではなく、関数の結果タイプを介して報告させることです。たとえば、次のようになります。

// 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_constantstd::integral_constantconstexprSize_carrier

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

うまくいけば、oops-there-was-a-reference-involved-in-your- constexprissueはC ++ 17で修正されますが、それまでは、STATIC_N_ITEMS上記のようなマクロは、たとえばclangおよびVisual C ++コンパイラーに対して、型を保持して移植性をもたらします。安全性。

関連:マクロはスコープを尊重しないため、名前の衝突を避けるために、名前のプレフィックスを使用することをお勧めしますMYLIB_STATIC_N_ITEMS

于 2011-09-16T01:31:10.727 に答える
73

配列の作成と初期化

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

自動配列

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

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

初期化は昇順で行われます。初期値は要素の型に依存することに注意してくださいT:

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

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

    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++ では空の配列初期化子 (すべての要素がゼロで初期化される) が許可されますが、C89 では許可されません (少なくとも 1 つの値が必要です)。また、配列初期化子は配列の初期化にのみ使用できることに注意してください。後で割り当てに使用することはできません。

静的配列

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

int global_static_array[8];

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

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

静的配列と自動配列の動作の違いは次のとおりです。

  • 配列初期化子のない静的配列は、潜在的な初期化の前にゼロで初期化されます。
  • 静的 POD 配列は1 回だけ初期化され、通常、初期値は実行可能ファイルに組み込まれます。この場合、実行時の初期化コストはありません。ただし、これは常に最もスペース効率の良いソリューションとは限らず、標準では必須ではありません。
  • 静的な非 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 では、無名配列はmallocand フレンドを介して作成されます。C++ では、匿名配列は、匿名配列new T[size]の最初の要素へのポインターを返す構文を使用して作成されます。

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

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

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

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

ここでは配列からポインターへの減衰は行われないことに注意してください。評価new int[size]は実際には整数の配列new int[size]を作成しますが、式の結果はすでに単一の整数 (最初の要素)へのポインターであり、整数の配列や未知のサイズの整数の配列へのポインターではありません。静的型システムでは配列サイズをコンパイル時の定数にする必要があるため、これは不可能です。(したがって、図の静的型情報で無名配列に注釈を付けませんでした。)

要素のデフォルト値に関して、無名配列は自動配列と同様に動作します。通常、匿名 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(または) を使用することも未定義の動作です。freedelete[]

于 2011-02-13T12:52:49.123 に答える