配列へのポインタとその周辺 #
概要 #
配列とは,同じ型の複数の変数をまとめて扱うためのデータ構造です.
ポインタ (pointer) とは,メモリアドレスを格納するためのオブジェクトのことです. C++ では,変数のメモリアドレスはもちろん,関数のエントリーポイントとなるメモリアドレスを格納することができます.
関数ポインタ の節では,関数へのポインタについて説明しました.本ページでは,C++における配列とポインタの関係を説明した後,配列型へのポインタ型について説明します.ここで,配列型へのポインタ型とは,そのポインタ型の変数に対して *
演算子で値を得ると,要素ではなく,配列そのものが得られるようなポインタ型を指すものとします.
配列とポインタ #
配列とは,同じ型の複数の変数をまとめて扱うためのデータ構造です.例えば,int
型の変数を10個まとめて扱うための配列は以下のように宣言できます.
int arr[10];
使いかたは以下のとおりです.
#include <iostream>
int main() {
int arr[10];
for (int i = 0; i < 10; i++) {
arr[i] = i * 2;
}
std::cout << arr[0] << "\n"; // 0
std::cout << arr[1] << "\n"; // 2
std::cout << arr[2] << "\n"; // 4
return 0;
}
配列変数は,変数からその要素数を取得する方法は,言語仕様としては提供されていません.ただし,C言語の時代から,配列変数の要素数を取得する方法として,以下の方法が広く知られています.
int arr[10];
int size = sizeof(arr) / sizeof(arr[0]);
これは,sizeof
演算子を利用して,配列全体のサイズを,配列の先頭要素のサイズで割れば,要素数が得られるという仕組みです.以下のように,関数形式マクロとして定義されることが多いです.
#include <iostream>
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
int main() {
int arr[10];
std::cout << ARRAY_SIZE(arr) << "\n"; // 10
return 0;
}
さて,ある型の配列変数は,同じ型のポインタ型変数に代入できます.
int arr[10];
int *p = arr;
このとき,変数 p
は arr
の先頭要素のアドレスを指します.つまり,p == &arr[0]
です.よって,*p
は arr
の先頭要素を表します.つまり,*p == arr[0]
です.
なお,第
\(i\)
要素のアドレスは p + i
と表せます.つまり,p + i == &arr[i]
です.よって,*(p + i)
は arr
の第
\(i\)
要素を表します.つまり,*(p + i) == arr[i]
です.
int
型へのポインタ型変数 p
について,*p
, *(p + i)
は p[0]
, p[i]
と表せます.逆に,int
型の配列変数 arr
について,arr[0]
, arr[i]
は *arr
, *(arr + i)
と表せます.よって,以下の関係が成り立ちます.
*p
==p[0]
==*arr
==arr[0]
*(p + i)
==p[i]
==*(arr + i)
==arr[i]
より正確には,組み込みの配列添字演算子 operator[]
は a[i]
を *(a + i)
と評価し,T
へのポインタ型変数または T
型の配列変数 a
に対し,a + i
は (a
のアドレス) + ((T
のサイズ)
\({}\times i\)
) を返すので,上記のような結果になります1
したがって,C++においては,配列とポインタは密接な関係にあるといえます.
配列とポインタの関係に関する注意 #
先述の通り,配列とポインタは密接な関係があるといえます.一方で,int
型へのポインタ型変数 p
に代入した場合,配列 arr
全体のサイズを得ることはできません.なぜなら,p
はあくまで int
型へのポインタ型なのでもとの配列の情報はもたないからです.
具体的には,
int arr[10];
int *p = arr;
std::cout << (sizeof(p) / sizeof(p[0])) << "\n";
とすると,環境によると思いますが,作者環境では int *
のサイズを int
のサイズで割った2が出力されました.
したがって,ふるまいとしては似ていますが,配列型とポインタ型は別物であることが確認できます.
また,配列型変数を引数とする関数についても注意が必要です.そのような関数には要素数の情報は渡らず,先頭アドレスだけが渡されます.よって,そのような関数の中で,先述の方法で要素数を得ることはできません2.
void func(int a[100]) {
std::cout << (sizeof(a) / sizeof(a[0])) << "\n"; // 2 (not 100)
}
配列を引数にもつ関数 #
一般に,配列型変数を引数にする場合,引数の要素数は使われないので省略可能です.
void func(int a[]) {
std::cout << (sizeof(a) / sizeof(a[0])) << "\n"; // 2
}
さらにいえば,引数として配列を受け取りたい場合は ポインタ変数で受け取れば十分ということになります.
void func(int *a) {
std::cout << (sizeof(a) / sizeof(a[0])) << "\n"; // 2
}
同様の理由で,引数で要素数を明示しても,要素数100以外の配列を渡すときにエラーは発生しません.
#include <iostream>
void func(int a[100]) {
std::cout << (sizeof(a) / sizeof(a[0])) << "\n"; // 2 (not 100 nor 50)
}
int main() {
int arr[50];
func(arr); // コンパイルエラーは発生しない
return 0;
}
配列型へのポインタ型 #
int
型配列を指すポインタとして,int
型へのポインタについて説明しましたが,これは先頭要素を指しているだけのため,*
演算子で得られるのは先頭要素だけで,特に,要素数の情報は失われるのでした.また,配列を引数にとる関数をつくろうとすると,先頭要素のポインタしか渡されないとわかりました.
そこで,int
型へのポインタではなく,配列型へのポインタ型というものがあれば,*
演算子で値を得ると,配列そのものが得られるので,要素数の情報も失われないと考えられます.
配列型へのポインタ型の宣言 #
クラス T
の要素数 N
の配列型へのポインタ変数 p_arr
の宣言は,
T (*p_arr)[N];
となります.T *p_arr[N]
とすると,T*
の N
個の要素をもつ配列の意味になってしまうので, (*p_arr)
の部分に括弧をつける仕様となっています.
using キーワードによるエイリアス宣言を使うと,少しだけすっきりさせることができます.例えば,上記 p_arr
の宣言は,
using p_arr_t = T (*)[N];
p_arr_t p_arr;
と書けます.あるいは,
using arr_t = T[N];
using p_arr_t = arr_t *;
p_arr_t p_arr;
や
using arr_t = T[N];
arr_t *p_arr;
でも同じことです.たとえば,要素数100の int
型配列のポインタ型の変数宣言は,それぞれ,
using p_arr_t = int (*)[100];
p_arr_t p_arr;
using arr_t = int[100];
using p_arr_t = arr_t *;
p_arr_t p_arr;
using arr_t = int[100];
arr_t *p_arr;
となります.
配列ポインタへの代入と要素の参照 #
int arr[100]
の配列ポインタへの代入と,要素の参照は,
p_arr_t p_arr = &arr;
std::cout << (*p_arr)[3] << "\n"; // arr[3]
となります.エイリアス宣言を使わなければ,
int (*p_arr)[100] = &arr;
std::cout << (*p_arr)[3] << "\n"; // arr[3]
となります.
先述の func
を配列型へのポインタ型を利用したものに置き換えると,以下のようになります.
#include <iostream>
void func(int (*pa)[100]) {
std::cout << (sizeof(*pa) / sizeof((*pa)[0])) << "\n"; // 100
}
int main() {
int arr[100];
func(&arr); // 100
return 0;
}
ちなみに,この場合,要素数の異なる配列のアドレスを渡すとエラーが発生します.
#include <iostream>
void func(int (*pa)[100]) {
std::cout << (sizeof(*pa) / sizeof((*pa)[0])) << "\n";
}
int main() {
int arr[50];
func(&arr); // コンパイルエラー
return 0;
}
応用:配列の要素を返す関数 #
既に ARRAY_SIZE
という関数形式マクロが定義できることを説明しましたが,配列変数を引数にとる関数としてしまうと,先頭要素のアドレスしか渡らないため,関数でなく関数形式マクロで実現しました.
しかし,配列型へのポインタ型を利用すると,関数形式マクロでなく,関数で実現することができます.たとえば,要素100の配列については,以下のようになります.
#include <cstddef>
std::size_t ArraySize(int (*pa)[100]) {
return sizeof(*pa) / sizeof((*pa)[0]);
}
ただし,この方法では,配列の型と要素数ごとに ArraySize
関数を定義する必要があります.
ところで,C++のテンプレートでは,テンプレート引数として,整数値をとることができます.これを利用すると,ArraySize
は関数テンプレートを使って以下のように書き直せます.
#include <cstddef>
template <class T, std::size_t N>
std::size_t ArraySize(T (*pa)[N]) {
return sizeof(*pa) / sizeof((*pa)[0]);
}
さらにいえば,この関数テンプレートの戻り値は,sizeof
演算子で計算するまでもなく,テンプレート引数の N
で決まっています.したがって,引数名も不要で,以下の実装で十分です.
#include <cstddef>
template <class T, std::size_t N>
std::size_t ArraySize(T (*)[N]) {
return N;
}
使い方は以下のとおりです.
#include <cstddef>
#include <iostream>
template <class T, std::size_t N>
std::size_t ArraySize(T (*)[N]) {
return N;
}
int main() {
int arr[100];
std::cout << ArraySize(&arr) << "\n"; // 100
double arr2[1024];
std::cout << ArraySize(&arr2) << "\n"; // 1024
return 0;
}
よって,関数形式マクロでなく,配列の要素数を返す関数(テンプレート)が作れました.
まとめ #
本ページでは,C++における配列とポインタの関係を説明した後,配列型へのポインタ型について説明しました.C++では配列とポインタの間には密接な関係がありますが,型としては同じものではありません.
一方で,配列型へのポインタ型を定義することができ,これを利用すると,関数形式マクロでなく,配列の要素を返す関数テンプレートが定義できることがわかりました.