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

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

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宣言子構文の基本知識があることを前提としています。以下に示すように、およびを手動で使用することは、new例外deleteに直面したときに非常に危険ですが、これはもう一つのよくある質問


(注:これはStack Overflow の C++ FAQこの形式でFAQを提供するというアイデアを批判したい場合は、このすべてを始めたのはメタへの投稿だったそれをする場所になるでしょう。その質問への回答は、C++ チャットルーム、FAQ のアイデアが最初に生まれた場所なので、あなたの回答はそのアイデアを思いついた人たちに読まれる可能性が非常に高くなります。

ベストアンサー1

型レベルの配列

配列型は と表されます。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*、この変換の結果が配列の最初の要素へのポインターになることです。つまり、 が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]

最初の要素へのポインターは 1 つの整数 (小さなボックスで表示) のみを指しているのに対し、配列全体へのポインターは 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]がありますが、要素はありません。しかし、半開範囲 (先頭は を含み、末尾は を含まない) をサポートするために、C++ では (存在しない) n 番目の要素へのポインターの計算が許可されていますが、そのポインターを逆参照することは違法です。n0n-1n

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

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

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

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

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

おすすめ記事