配列へのポインタとその周辺

配列へのポインタとその周辺 #

概要 #

配列とは,同じ型の複数の変数をまとめて扱うためのデータ構造です.

ポインタ (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;

このとき,変数 parr の先頭要素のアドレスを指します.つまり,p == &arr[0] です.よって,*parr の先頭要素を表します.つまり,*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++では配列とポインタの間には密接な関係がありますが,型としては同じものではありません.

一方で,配列型へのポインタ型を定義することができ,これを利用すると,関数形式マクロでなく,配列の要素を返す関数テンプレートが定義できることがわかりました.


  1. したがって,言語仕様上は p[i] == *(p + i) == *(i + p) == i[p] が成り立ちます. ↩︎

  2. これが,先述の ARRAY_SIZE を関数形式マクロで定義した理由です. ↩︎


This work is licensed under CC BY 4.0