C++において、「配列」と「ポインタ」は明確に異なる概念です。配列は同一の型の要素が連続して並んだメモリ領域そのものであり、ポインタはそのメモリ領域のアドレス(番地)を格納するための変数です。
しかし、C言語から受け継いだ仕様により、この2つは非常に密接な関係を持っており、特定の文脈ではポインタが配列のように、配列がポインタのように振る舞います。この関係を理解することは、C++の基礎を深める上で重要です。
配列名の「ポインタへの縮退」
C++には、「配列名は、式の中で使用されると、その配列の先頭要素へのポインタとして解釈される」という重要なルールがあります。これを配列の「ポインタへの縮退(decay)」と呼びます。
#include <iostream>
int main() {
// 4つのdouble型要素を持つ配列
double sensorReadings[4] = {12.5, 13.0, 12.8, 13.1};
// 配列名 'sensorReadings' は、その先頭要素 (&sensorReadings[0]) への
// ポインタ (double*) として解釈される
double* ptr = sensorReadings;
std::cout << "配列の先頭要素のアドレス: " << &sensorReadings[0] << "\n";
std::cout << "ポインタ ptr の値: " << ptr << "\n";
// &sensorReadings[0] と ptr の値は一致する
return 0;
}
double* ptr = sensorReadings; という代入が可能なのは、右辺の sensorReadings が &sensorReadings[0] と同じ意味の double* 型の値に「縮退」しているためです。
ポインタ演算(アドレスの算術)
ポインタ変数が配列の要素を指している場合、そのポインタに対して整数を加算・減算(+ または -)できます。これをポインタ演算と呼びます。
ptr + i という式は、ptr が指すアドレスから「i 要素分だけ後方」のアドレスを計算します。
重要: これは、ptr のアドレス値に i を足すのではなく、ptr のアドレス値に i * sizeof(型) を足す計算を意味します。double* 型のポインタであれば、i * sizeof(double) バイト分だけアドレスが進みます。
, arr[1], arr[2]]
#include <iostream>
int main() {
double sensorReadings[4] = {12.5, 13.0, 12.8, 13.1};
double* ptr = sensorReadings; // ptr は sensorReadings[0] を指す
// ポインタ演算は、配列のインデックスアクセスと等価なアドレスを指す
for (int i = 0; i < 4; ++i) {
std::cout << "要素 " << i << " のアドレス:\n";
std::cout << " &sensorReadings[" << i << "]: " << &sensorReadings[i] << "\n";
std::cout << " ptr + " << i << ": " << (ptr + i) << "\n";
}
return 0;
}
実行結果(一例):
要素 0 のアドレス:
&sensorReadings[0]: 0x7ffee5a9e5c0
ptr + 0: 0x7ffee5a9e5c0
要素 1 のアドレス:
&sensorReadings[1]: 0x7ffee5a9e5c8
ptr + 1: 0x7ffee5a9e5c8
要素 2 のアドレス:
&sensorReadings[2]: 0x7ffee5a9e5d0
ptr + 2: 0x7ffee5a9e5d0
要素 3 のアドレス:
&sensorReadings[3]: 0x7ffee5a9e5d8
ptr + 3: 0x7ffee5a9e5d8
double 型が8バイトの環境であるため、アドレスが8ずつ増えていることがわかります。
添字演算子 [] の正体
前述の「ポインタ演算」と「間接演算子 *」を組み合わせると、C++の添字演算子 [] の本質が見えてきます。
ptr が配列の先頭を指すとき、 *(ptr + i) という式は、「ptr から i 要素分進んだアドレス」にある値を取得します。
C++では、ptr[i] という式は、*(ptr + i) の糖衣構文(シンタックスシュガー)、つまり「より読みやすくするための別表記」として定義されています。
ptr[i] は *(ptr + i) と等価です。
これは、配列名 sensorReadings 自体にも当てはまります。sensorReadings[i] という式は、
sensorReadingsが先頭要素へのポインタ&sensorReadings[0]に縮退します。*(sensorReadings + i)として解釈されます。
#include <iostream>
int main() {
int arr[4] = {10, 20, 30, 40};
int* ptr = arr;
std::cout << "--- ポインタによるアクセス ---\n";
std::cout << "ptr[2]: " << ptr[2] << "\n"; // 30
std::cout << "*(ptr + 2): " << *(ptr + 2) << "\n"; // 30
std::cout << "--- 配列名によるアクセス ---\n";
std::cout << "arr[2]: " << arr[2] << "\n"; // 30
std::cout << "*(arr + 2): " << *(arr + 2) << "\n"; // 30
// (補足) 加算は交換可能なので *(i + ptr) も等価
// これにより、文法的には 2[ptr] という奇妙な表記も可能
std::cout << "*(2 + ptr): " << *(2 + ptr) << "\n"; // 30
std::cout << "2[ptr]: " << 2[ptr] << "\n"; // 30
return 0;
}
ポインタ同士の演算
ポインタ同士の加算はできませんが、同じ配列を指しているポインタ同士であれば、以下の演算が可能です。
- 減算 (
-): 2つのポインタ間の「要素数」の差を返します。 - 関係演算 (
>,<,>=,<=,==,!=): アドレスの位置(前後関係)を比較します。
配列とポインタの重要な違い
両者は似ていますが、決定的に異なります。
- メモリの実体:
int arr[4];は、int4個分のメモリ領域(実体)を確保します。int* ptr;は、アドレス値1個分(例: 8バイト)のメモリ領域(実体)を確保します。
sizeofの振る舞い:sizeof(arr)は、配列全体のバイト数(sizeof(int) * 4)を返します。sizeof(ptr)は、ポインタ変数自体のサイズ(例: 8バイト)を返します。
- 代入(再束縛):
ptrは変数なので、ptr = &someOtherVariable;のように、後から別のものを指すように再代入が可能です。arrは配列(メモリブロックそのもの)を指す名前であり、変数ではありません。arr = ...のような再代入はできません。
まとめ:現代C++での推奨
この記事で解説したCスタイル配列と生のポインタの振る舞いは、C言語との互換性のために存在する、C++の低レベルな側面です。
ptr[i] が *(ptr + i) と等価であるという知識は重要ですが、ポインタ演算を直接使用することは、配列の範囲外にアクセスしてしまう「バッファオーバーラン」などの深刻なバグの温床となります。
現代のC++プログラミングでは、生のポインタとCスタイル配列の直接的な操作は可能な限り避けるべきです。
代わりに、以下のような、より安全で高機能な機能の使用が強く推奨されます。
std::array(固定長配列)std::vector(可変長配列)- イテレータ (コンテナの要素を指す安全なポインタのようなもの)
- 範囲ベース
forループ (for (auto& item : my_vector))
これらの機能は、開発者がポインタ演算の複雑さやメモリ管理の危険性を意識することなく、ロジックに集中できるように設計されています。
