C/C++ でのポインタと配列表記の違い

Ilja Kostenko 2023年10月12日
  1. C++ 配列
  2. C++ ポインタ
C/C++ でのポインタと配列表記の違い

ポインタと配列は、間違いなく C++ の最も重要で複雑な側面の 1つです。リンクリストと動的メモリ割り当てをサポートし、関数が引数の内容を変更できるようにします。

C++ 配列

配列は、インデックスによってアクセスされる同じタイプの要素のセットです。つまり、配列内の要素の序数です。例えば:

int ival;

ivalint 型変数および命令として定義します。

int ia[10];

10 個の int オブジェクトの配列を設定します。これらの各オブジェクトまたは配列要素には、インデックスを取得する操作を使用してアクセスできます。

ival = ia[2];

変数 ival に、インデックス 2 の配列 ia の要素の値を割り当てます。同様に

ia[7] = ival;

インデックス 7 の要素に ival 値を割り当てます。

配列定義は、型指定子、配列名、およびサイズで構成されます。サイズは配列要素の数(少なくとも 1)を指定し、角括弧で囲まれます。配列のサイズは、コンパイル段階ですでにわかっている必要があるため、必ずしもリテラルで設定されているとは限りませんが、定数式である必要があります。

要素の番号付けは 0 から始まるため、10 個の要素の配列の場合、正しいインデックス範囲は 1〜10 ではなく、0〜9 です。これは配列のすべての要素を並べ替える例です。

int main() {
  const int array_size = 10;
  int ia[array_size];
  for (int ix = 0; ix < array_size; ++ix) ia[ix] = ix;
}

配列を定義するときは、要素の値を中括弧で囲んでコンマで区切ることにより、明示的に初期化できます。

const int array_size = 3;
int ia[array_size] = {0, 1, 2};

値のリストを明示的に指定する場合、配列のサイズを指定しない場合があります。コンパイラ自体が要素の数をカウントします。

C++ ポインタ

ポインタは、別のオブジェクトのアドレスを含み、このオブジェクトの間接的な操作を可能にするオブジェクトです。ポインタは通常、動的に作成されたオブジェクトを操作し、リンクリストや階層ツリーなどの関連データ構造を構築し、大きなオブジェクト(配列やクラスオブジェクト)をパラメータとして関数に渡すために使用されます。

各ポインタは、ある種のデータに関連付けられています。それらの内部表現は内部タイプに依存しません。ポインタタイプのオブジェクトが占めるメモリのサイズと値の範囲は同じです。違いは、コンパイラがアドレス指定可能なオブジェクトを認識する方法です。異なるタイプへのポインタは同じ値を持つ場合がありますが、対応するタイプのメモリ領域は異なる場合があります。

ここではいくつかの例を示します。

int *ip1, *ip2;
complex<double> *cp;
string *pstring;
vector<int> *pvec;
double *dp;

ポインターは、名前の前にアスタリスクで示されます。リストで変数を定義する場合は、各ポインターの前にアスタリスクを付ける必要があります(上記を参照:ip1 および ip2)。以下の例では、lp は long 型のオブジェクトへのポインタであり、lp2 は long 型のオブジェクトです。

long *lp, lp2;

次の場合、fp は float オブジェクトとして解釈され、fp2 はそれへのポインタです。

float fp, *fp2;

int 型の変数を指定します。

int ival = 1024;

以下は、intpi および pi2 へのポインターを定義および使用する例です。

[//] pi is initialized with the null address
int *pi = 0;
[//] pi2 is initialized with the address ival
int *pi2 = &ival;
[//] correct: pi and pi2 contain the ival address
pi = pi2;
[//] pi2 contains the null address
pi2 = 0;

アドレス以外の値をポインタに割り当てることはできません。

[//] error: pi cannot take the value int
pi = ival

同様に、次の変数が定義されている場合、別のタイプのオブジェクトのアドレスである 1つのタイプのポインターに値を割り当てることはできません。

double dval;
double *ps = &dval;

次に、以下に示す両方の割り当て式により、コンパイルエラーが発生します。

[//] compilation errors
[//] invalid assignment of data types: int* <== double*
pi = pd
pi = &dval;

変数 pi にオブジェクト dval のアドレスを含めることができないわけではありません。異なるタイプのオブジェクトのアドレスは同じ長さです。コンパイラによるオブジェクトの解釈はポインタのタイプに依存するため、このようなアドレス混合操作は意図的に禁止されています。

もちろん、アドレスが指すオブジェクトではなく、アドレス自体の値に関心がある場合もあります(このアドレスを他のアドレスと比較したいとします)。このような状況を解決するために、任意のデータ型を指すことができる特別な無効なポインターを導入できます。次の式が正しくなります。

[//] correct: void* can contain
[//] addresses of any type
void *pv = pi;
pv = pd;

void*が指すオブジェクトのタイプは不明であり、このオブジェクトを操作することはできません。このようなポインタでできることは、その値を別のポインタに割り当てるか、アドレス値と比較することだけです。

そのアドレスでオブジェクトにアクセスするには、アスタリスク(*)で示される間接参照操作または間接アドレス指定を適用する必要があります。例えば、

int ival = 1024;
, ival2 = 2048;
int *pi = &ival;

ポインターpi に逆参照演算を適用することにより、ival の値を読み取って格納できます。

[//] indirect assignment of the ival variable to the ival2 value
*pi = ival2;
[//] value indirect use of variable value and pH value value
*pi = abs(*pi); // ival = abs(ival);
*pi = *pi + 1; // ival = ival + 1;

int 型のオブジェクトにアドレス(&)を取る操作を適用すると、int 型*の結果が得られます。

int *pi = &ival;

同じ操作がタイプ int*(タイプ int C へのポインター)のオブジェクトに適用され、タイプ int へのポインターへのポインター、つまりタイプ int**を取得した場合。int**は、int 型のオブジェクトのアドレスを含むオブジェクトのアドレスです。

ppi を間接参照すると、ival アドレスを含む int*オブジェクトが取得されます。ival オブジェクト自体を取得するには、逆参照操作を PPI に 2 回適用する必要があります。

int **ppi = &pi;
int *pi2 = *ppi;
cout << "ival value\n"
     << "explicit value: " << ival << "\n"
     << "indirect addressing: " << *pi << "\n"
     << "double indirect addressing: " << **ppi << "\n"
     << end;

ポインタは算術式で使用できます。次の例に注意してください。この例では、2つの式がまったく異なるアクションを実行します。

int i, j, k;
int *pi = &i;
[//] i = i + 2
*pi = *pi + 2;
[//] increasing the address contained in pi by 2
pi = pi + 2;

ポインタに整数値を追加したり、ポインタから減算したりできます。ポインタに 1 を追加すると、対応するタイプのオブジェクトに割り当てられたメモリ領域のサイズだけその値が増加します。型 char が 1 バイト、int – 4 および double - 8 を占める場合、文字、整数、および double へのポインターに 2 を追加すると、それぞれ値が 2、8、および 16 増加します。

これはどのように解釈できますか?同じタイプのオブジェクトが次々にメモリ内にある場合、ポインタを 1 増やすと、次のオブジェクトを指すようになります。したがって、ポインタを使用した算術演算は、配列を処理するときに最もよく使用されます。それ以外の場合、それらはほとんど正当化されません。

イテレータを使用して配列要素を反復処理するときにアドレス演算を使用する典型的な例を次に示します。

int ia[10];
int *iter = &ia[0];
int *iter_end = &ia[10];
while (iter != iter_end) {
  do_the event_ with_(*iter);

関連記事 - C++ Pointer

関連記事 - C++ Array