1. ホーム
  2. c++

[解決済み] C++で配列はどのように使うのですか?

2022-03-20 07:48:40

質問

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

このFAQは5つのパートに分かれています。

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

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

以下の文章で、"array"はクラステンプレートではなく、"C array"を意味します。 std::array . C 言語の宣言文の構文に関する基本的な知識があることを前提としています。なお、手動で newdelete は、例外に直面すると非常に危険ですが、これは 別のFAQ .


(注) これは スタックオーバーフローのC++FAQ . もし、このような形でFAQを提供することを批判したいのであれば すべての始まりとなったmetaへの投稿 は、そのための場所でしょう。その質問に対する回答は C++チャットルーム そのため、あなたの回答は、このアイデアを思いついた人たちに読まれる可能性が非常に高いのです)。

どのように解決するのですか?

型レベルの配列

配列の型は、以下のように示されます。 T[n] ここで T 要素タイプ n は、正 サイズ は,配列の要素数である。配列の型は,要素の型とサイズの積の型になります.これらの成分の一方または両方が異なる場合、別個の型が得られます。

#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*

この変換はquot;array-to-pointer decay"として知られており、混乱の主な原因となっています。配列のサイズはこの処理で失われ、それはもはや型の一部ではないので ( T* ). Pro: 型レベルで配列のサイズを忘れることで、ポインタが配列の最初の要素を指し示すことができます。 任意の サイズになります。欠点:配列の最初の(あるいは他の)要素へのポインタが与えられた場合、その配列の大きさや、配列の境界に対してポインタが正確にどこを指しているのかを検出する方法がありません。 ポインターは非常に愚かである .

配列はポインタではない

つまり、ある操作が配列では失敗するがポインタでは成功するような場合、コンパイラは黙って配列の最初の要素へのポインタを生成するのです。この配列からポインタへの変換は些細なことで、結果として得られるポインタ は単に配列のアドレスです。なお,このポインタは ではなく は、配列自体の一部として(あるいはメモリ上の他の場所として)保存されます。 配列はポインタではありません。

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]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) というサブ式があります。 *(x+n) を呼び出します。 未定義の動作 C++では(C99では)ありません。

また、単純に x を第1引数として指定します。この場合、最初の引数は配列ですが、2番目の引数はポインタなので、コンパイラにとってテンプレート引数の控除は少し難しくなります。(この場合も、配列からポインタへの減衰が作用します)。