C ++で配列を使用するにはどうすればよいですか?


480

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

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

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

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

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

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


ポインタは常にしかし彼らのターゲットの途中でどこかにするのではなく、初めに指摘している場合、彼らは...でも良いだろう
デュプリケータ

STLベクトルを使用すると、柔軟性が向上します。
Moiz Sajid 2015

2
std::arrays、std::vectors、およびgsl::spansを組み合わせて利用できるため、C ++で配列を使用する方法に関するFAQで、「今では、使用しないことを検討することができます。
アインポクルム2017年

回答:


302

型レベルの配列

配列型は、次のように表されるT[n]場合Tである要素のタイプ及びn正であるサイズ、アレイ内の要素の数。配列型は、要素型とサイズの積型です。これらの成分の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*、この変換の結果は配列の最初の要素へのポインタです。つまり、a T*が必要な場所ならどこでもを提供でき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]

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

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

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

  • int(*)[8] 8つの整数の配列へのポインタです。
  • int*[8]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からインデックス付け要素0にしますn-1。要素はありませんn。さらに、半開範囲(最初は包括的で最後は排他的)をサポートするために、C ++では(存在しない)n番目の要素へのポインターの計算を許可していますが、そのポインターを逆参照することは違法です。

   +---+---+---+---+---+---+---+---+....
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 ++では(C99ではなく)未定義の動作*(x+n)技術的に呼び出します。

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


配列がポインタに減衰しないケースは、参考のためにここ示されています。
legends2k 2014年

@fredoverflowアクセスまたは範囲の部分では、C配列がC ++ 11範囲ベースのforループで機能することを言及する価値があるかもしれません。
gnzlbg 2017年

135

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

多次元配列

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

名前付き多次元配列

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

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

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;

提案として:あなたは、その指摘しなければならないint connect_four[H][7];int connect_four[6][W]; int connect_four[H][W];などをint (*p)[W] = new int[6][W];してint (*p)[W] = new int[H][W];、有効な文、あるHWコンパイル時に知られています。
RobertSは

88

割り当て

特に理由もなく、配列を互いに割り当てることはできません。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[]られると考えることができます。この特別な規則は、配列とポインタに関する全体的な混乱の原因の1つです。他のすべてのコンテキストでは、配列またはポインタとして何かを宣言すると、大きな違いが生じます。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);
}

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


2
たとえvoid foo(int a[3]) a値が配列を渡しているかのように見えても、a内部をfoo変更すると元の配列が変更されることに注意してください。配列はコピーできないため、これは明らかですが、それを強化することは価値があるかもしれません。
gnzlbg 2017年

C ++ 20はranges::copy(a, b)
LF

int sum( int size_, int a[size_]);-(私は思う)C99以降
シェフGladiator

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およびVisual C ++ 10.0の両方でコンパイルおよびリンクします。

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

Windows 7クラッシュダイアログ

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

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

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

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

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

…配列オブジェクトの宣言では、主要な配列境界(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つの要素、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:error: '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を -pedantic-errors使用してコンパイルし、gcc.godbolt.org /のオンラインコンパイラを使用してテストします。また、clang 3.0およびclang 3.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 / 2 9 番目のダッシュ

条件式で eあるコア定数式の評価ない限り、e以下の式のいずれかを評価するであろう、抽象機械(1.9)の規則に従って、:
        ⋮

  • 参照に先行する初期化と以下のいずれかがない限り、参照タイプの変数またはデータメンバーを参照 する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_constant でので、std::integral_constantその結果を直接として表現されconstexpr、元の問題を再導入、値。Size_carrierクラスの代わりに、関数が配列への参照を直接返すようにすることができます。ただし、誰もがその構文に精通しているわけではありません。

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

うまくいけば、oops-there-was-a-reference-involved-in-your-のconstexpr問題はC ++ 17で修正されますが、それまでは、STATIC_N_ITEMS上記のようなマクロによって移植性が得られます。安全性。

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


1
+1素晴らしいCコーディングテスト:VC ++ 10.0とGCC 4.1.2を修正するために15分を費やしてきSegmentation faultました...説明を読んだ後、ようやく発見/理解しました!§5.2セクションをお書きください:-)乾杯
olibre

良い。1つのnit-countOfの戻り値の型は、ptrdiff_tではなくsize_tでなければなりません。C ++ 11/14ではconstexprでnoexceptでなければならないことは、おそらく言及する価値があります。
Ricky65、2014年

@ Ricky65:C ++ 11の考慮事項について言及していただきありがとうございます。これらの機能のサポートは、Visual C ++のサポートが遅れています。に関してはsize_t、それは現代のプラットフォームについて私が知っている利点はありませんが、CおよびC ++の暗黙の型変換規則のために多くの問題があります。つまりptrdiff_t、の問題を回避するために、非常に意図的に使用されていsize_tます。ただし、g ++には、配列サイズとテンプレートパラメータのマッチングに問題があることを認識しておく必要がありますsize_t(そうでない場合、このコンパイラ固有の問題size_tは重要ではないと思いますが、YMMV)。
乾杯とhth。-Alf

@アルフ。標準草案(N3936)で8.3.4読みました-配列の境界は...「型std :: size_tの変換された定数式で、その値はゼロより大きくなければなりません」。
Ricky65、2014年

@Ricky:矛盾を参照している場合、このステートメントは現在のC ++ 11標準には存在しないため、コンテキストを推測することは困難ですが、矛盾(動的に割り当てられた配列、C +ごとに0にバインドされる可能性があります) +11§5.3.4/ 7)は、おそらくC ++ 14では終了しないでしょう。下書きはそれだけです:下書き。代わりに「その」が何を指しているのかを尋ねる場合は、変換された式ではなく、元の式を指します。もしそのような文size_tが配列のサイズを示すために使用する必要があることを意味すると思うので、3番目にこれを言及した場合、もちろんそうではありません。
乾杯とhth。-アルフ

72

アレイの作成と初期化

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

自動配列

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

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

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

  • 場合TであるPOD(のようなint上記の例では)、全く初期化が行われません。
  • それ以外の場合、default-constructorはTすべての要素を初期化します。
  • Tアクセス可能なdefault-constructorを提供しない場合、プログラムはコンパイルされません。

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

    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では、匿名配列はとを介して作成されmallocます。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;

それぞれの匿名配列を1回だけ解放し、その後は再び触れないでください。まったく解放しないと、メモリリークが発生し(より一般的には、要素タイプによってはリソースリークが発生します)、複数回解放しようとすると、未定義の動作が発生します。配列を解放する代わりに非配列形式delete(またはfree)を使用することdelete[]未定義の動作です。


2
static名前空間スコープでの使用の廃止はC ++ 11で削除されました。
legends2k 2013年

newis演算子なので、参照によってすべての配列を返す可能性があります。意味がありません...
Deduplicator

@Deduplicatorいいえ、歴史的にnew参照よりもはるかに古いため、できませんでした。
fredoverflow 2014年

@FredOverflow:したがって、参照を返せなかった理由があります。それは、書かれた説明とは完全に異なります。
デデュプリケータ2014年

2
@Deduplicator不明な境界の配列への参照が存在するとは思わない。少なくともg ++はコンパイルを拒否しますint a[10]; int (&r)[] = a;
fredoverflow '27
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.