ポインタの基本 編集

アドレス 編集

オブジェクトはメモリー上に領域が確保され、値が格納されます[1]。 メモリーをバイト単位の一次元配列と考えた場合、その配列の添え字がアドレスです。 バイト単位の一次元配列であることを強調するため、バイトアドレスとも呼ばれます。 1バイトは最小でも8ビットで、そのビット数はlimit.hのマクロCHAR_BITで定義されています[2]、10進数で256(28)通りの値を格納できます。256通り以上の値を格納するに連続した複数のバイトを一塊として使用します。

オブジェクトのアドレスを取得するには、オブジェクト名の前に&演算子(アドレス演算子)を付けます。

アドレス演算子
&オブジェクト名
オブジェクトaのアドレス
&a
変数のアドレスを表示する
#include <stdio.h>

int main(void) {
  int a = 1234;
  printf("変数aの値は%d\n", a);
  printf("変数aのアドレスは%p\n", &a);
  printf("main関数のアドレスは%p\n", &main);
}
実行結果
変数aの値は1234
変数aのアドレスは0x7ffe13618a64
main関数のアドレスは0x401130

この様に、オブジェクトには変数の他、関数も含まれます(アドレスの値は実行条件によって異なります)。

&main

は、main関数のアドレスを得る式で、単に

main

でも同じ意味です。この様に関数もメモリー上のオブジェクトなのでアドレスを持ちます。


基本例 編集

コード例
#include <stdio.h>

int main(void) {
  int i = 1234, *r = &i;

  printf("変数iは%d\n", i);
  printf("変数iのアドレスは%p\n", &i);
  printf("ポインタ変数rの値は%p\n", r);
  printf("iは%d\n", i);
  printf("*rは%d\n", *r);
}
実行結果
変数iは1234
変数iのアドレスは0x7ffe8d127c4c
ポインタ変数rの値は0x7ffe8d127c4c
iは1234
*rは1234
最後の*rのprintfの引数には、変数i は書かれてないのに、変数iと同じ値になっています。

このように、ポインタはきっちりとアドレスを受け継いでいることが分かります。 このことから「ポインタとは単にアドレスを格納するための型である」と思ってしまいがちですが、指し示す型の大きさや符号の有無(配列なら要素数や次数)などの特性おも示していることに、注意が必要です。

下記コードのように、ポインタ型変数を一切宣言しなくても、アドレスの情報をもとにして最初の数を復元することが可能です。 ポインタ型の知識はいったん置いといて、下記のコードを実行してみましょう。

コード例
#include <stdio.h>

int main(void) {
  int i = 1234;
       
  printf("変数iは%d\n", i);        
  printf("変数iのアドレスは%p\n", &i);
  printf("*(&i)は%d\n",*(&i) );
}
実行結果
変数iは1234
変数iのアドレスは0x7ffe31a1ffa4
*(&i)は1234

このように、ポインタ型変数を全く宣言しなくても、アドレス情報をもとにすることで最初の値(例では1234)を復元することが可能です。ポインタの学習で悩んだら、これらの基本例に立ち返りましょう。 上記例では説明の都合上、printfに引数に&演算と*演算を使いましたが、下記コードのように何もprintfの引数でなくとも、&演算と*演算は型宣言時に行わなくても使用可能です[3]

コード例
#include <stdio.h>

int main(void) {
  int i = 1234;

  printf("変数iは%d\n", i);
  printf("変数iのアドレスは%p\n", &i);
  printf("*(&i)は%d\n", *(&i));

  int v = *(&i);
  printf("vは%d\n", v);
}
実行結果
変数iは1234
変数iのアドレスは0x7ffef3fbc9c4
*(&i)は1234
vは1234

ポインタ変数の宣言 編集

ポインタの機能として、ポインタ変数を宣言する事によって、なんらかの別の変数のアドレスを格納する事ができるだけの領域を確保します。 ポインタ変数がその変数の値として格納するものは、指し示すべき対象のオブジェクトが格納されたアドレスです。 ポインタ変数の宣言では、指し示すべき対象のデータの型を指定します。 このようなメモリー上のデータの記憶位置を指定するデータである「アドレス」を格納するための変数のことをポインタ変数といいます。 上記コード例のようにC言語では、変数宣言において、変数名の直前に「*」を伴って宣言された変数は、(その*よりも前に書かれた型のデータを指し示す)ポインタ変数の宣言になります。 ポインタ変数の名前は、他の変数の名前と同様に有効な識別子でなければいけません。

ポインタ変数の初期化とNULL 編集

初期化を伴わないポインタ変数 pの宣言
    int *p;

ポインタpの指し示すオブジェクトの型は int であることは確定していますが、ポインタ変数pの値は不定で p を使って参照することは不正です。 そこでポインタ変数の宣言と同時に初期値を与えることが強く推奨されます。 初期値として適切な式が即座に思いつかない時にはNULLで初期化することが(ワーストケースを避けるため)妥当です。

NULLで初期化するポインタ変数 pの宣言
    int *p = NULL;

とすれば、p を NULL で初期化出来ます。 NULLは、どの様な型へのポインタとして有効な値で、なおかつ有効なアドレスではないことが言語仕様で保証されています。 このことで、NULLを使った参照を実行時に捕捉できる可能性が出てきます(実行時にNULLによる参照を捕捉できるかは言語処理系と実行環境依存です。一般に、UnixではSIGSEGVが、Windowsでは構造化例外処理による例外が上がりますが、0番地からどの程度離れた番地への参照を捕捉するかなどでバリエーションがあります。)。

    int *junk234;

では、ポインタ変数の名前は junk234 です。

コード例
#include <stdio.h>

int main(void) {
    int i = 1234,
       *p = &i;
    printf("整数変数iのアドレスは%p\n", &i);
    printf("ポインタ変数pの値は%p\n", p);
}
実行結果
整数変数iのアドレスは0x7ffcb0de444c 
ポインタ変数pの値は0x7ffcb0de444c

このように、ポインタ変数pの値として、整数変数iのアドレスが格納できている事が分かります。

基本的な用途 編集

アドレスの格納 編集

ポインタに変数を指させる
int main(void) {
	int i, *pi = &i;//piにiのアドレスを格納します。
}

ポインタへアドレスを格納

書式
ポインタ変数 = &オブジェクト;

ポインタを介した代入 編集

「*ポインタ名」を使って、そのアドレスの指している先の変数の値を書きかえることもできます。

ポインタを使って変数を書き換える場合、書き換えられる変数は、そのポインタに代入されているアドレスに対応する変数が書き換えられるのです。

変数の書き換え処理の記法は、具体的には後述するように代入を使って、書き換えが行われます。

さて、 ポインタを使って、 ポインタが指す変数に値を間接的に代入するには、次のように*演算子を用います。

変数の書き換え(書き換え前)。
#include <stdio.h>

int main(void) {
	int i = 55;
	int *pi = &i;
	//*pi = 1234; // コメントアウトした場合
	printf("%d\n", i); // 出力している変数は(piではなく) i 
}
実行結果
55

書き換え後

#include <stdio.h>

int main(void) {
	int i = 55;
	int *pi = &i;
	*pi = 1234; // コメントアウトしない場合
	printf("%d\n", i);
}
実行結果
1234

このように、確かに変数iを55から1234に書き換えています(printfで出力している変数はiです)。

ポインタを用いて変数に代入します。
#include <stdio.h>

int main(void) {
	int i, *pi = &i;
	*pi = 1234;
	printf("%d\n", i);
}
実行結果
1234

上記のプログラム中では、一度も「i = 1234」とは記述されていないにもかかわらず、実行結果では、printfによる変数iの表示で「1234」の表示が行われています。このことから、ポインタ(上記コードではpi)を使用して「*pi = 1234」のように書くことで、ポインタの指し示す変数(上記コードではi)に代入をする事ができる事が分かります。

ポインタを介した代入

書式
*ポインタ = ;

ポインタの参照 編集

ポインタを使って、 ポインタが指す変数の値を間接的に参照するには、次のように*演算子を用います。

ポインタを用いて変数を参照します。
#include <stdio.h>

int main(void) {
	int i = 1234,
      *pi = &i;
	printf("%d\n", *pi);
}
実行結果
1234

一般に、「*ポインタ名」は「変数名」と同じものを表しています。なので、上記のコードの場合、 printf("%d", *pi) とは、つまり、printf("%d", i) のことなので、よってprintfで「1234」を表示します。

書式
(ポインタの示すオブジェクトの参照)
*ポインタ名

ポインタとは 編集

「ポインタ」という用語は、文脈によってポインタ型、ポインタ型の変数、ポインタ型の変数の値のいずれかを指す。 世間ではそれらをまとめて「ポインタ」と呼んでいるので、何を指すかは文脈で判断します。

ポインタ型はある型から導出し、 その型の実体を参照するための値(一般にアドレス)を持つ。 つまり、ポインタ型は「ポインタ型」として単体であるのではなく、 あくまで他の型から導出して作られます。 ある型Tから導出したポインタ型を「Tへのポインタ」と呼びます。 つまり、intへのポインタや、doubleへのポインタなどがあるわけです。 [4]

ポインタ型の変数は、 別のオブジェクトへのアドレスを格納し、 そのアドレスの指している先のオブジェクトに、 間接的にアクセスできます。

ポインタ型の変数の値は、 前述したように一般にアドレスです。 ただしアドレスなら何でもいいわけではなく、 そのアドレスはオブジェクトを指している必要があります。

さて、ポインタの宣言の記述は次のようになっています。

ポインタのデータ型 *ポインタ名;

ポインタのデータ型とポインタ名は、 変数の宣言のときと同様です。 同じデータ型へのポインタは「,(コンマ)」で区切って一行で宣言できます。 その場合は、各々のポインタ名の前に*をつけること。

ポインタ変数の宣言
int main(void) {
	int *pi;//int型へのポインタpiを宣言します。
	return 0;
}

typedefをポインタに用いる 編集

複数のポインタを一行で宣言するとき、しばしば下の例のような誤りを犯す場合があります。

typedefを用いないポインタ変数の宣言
int main(void) {
	int* p1, p2;//int型へのポインタであるp1と、int型の変数であるp2が宣言されます。
	return 0;
}

上の例はtypedefを用いて、下の例のように記述することができます。

typedefを用いたポインタ変数の宣言
int main(void) {
	typedef int *PINT;
	PINT p1, p2;//int型へのポインタであるp1とp2が宣言されます。
	return 0;
}


アドレスの加減算 編集

アドレス値は加減算が可能です[5]。このことを利用して、計算によって目的のアドレス値を指定したり、配列の添字ベースの距離を求めたりできます。

アドレスの加減算
#include <stdio.h>
#include <stddef.h> // for ptrdiff_t

int main(void) {
  const int ary[] = {123, 55, 818, 1192, 777};

  const int *a = &ary[0];
  printf("&ary[0] = %p\n", a);

  const int *b = &ary[1];
  printf("&ary[1] = %p\n", b);

  ptrdiff_t delta = b - a; // 配列の要素間の距離(間にある要素の数)
  printf("&ary[1] - &ary[0] = delta = %td\n", delta);

  const int *v = ary + delta;
  printf("*vは%d\n", *v);
}
出力結果
&ary[0] = 0x7ffe0052c910
&ary[1] = 0x7ffe0052c914
&ary[1] - &ary[0] = delta = 1
*vは55
注目してほしいのは0番目の要素のアドレスと1番目の要素のアドレスの差は1ではなく4(0x7ffe0052c914 - 0x7ffe0052c910)だという点です。
他方、ポインター同士の引き算の結果は、1 となっています。
またポインター同士の引き算の値の型は int や long ではなく ptrdiff_t を使っています。
これは、環境や処理系により各々の型の大きさの差の影響を受けない様にするためです。

このように、ポインター同士のの引き算は、バイトアドレスの差ではなくポインターの指し示すオブジェクトの型を要素とする配列の添え字に相当する整数値をとります。

コラム:intにアドレス値を代入してはいけない! 編集

intにアドレス値を代入してはいけない!
intにアドレス値を代入してはいけません[6]
サンプルコード
#include <stdint.h> // uintptr_t の為
#include <stdio.h>

int main(void) {
  printf("sizeof(int*) = %zu\n", sizeof(int *));
  printf("sizeof(int) = %zu\n", sizeof(int));

  int ary[10];
  int x = (int)&ary;
  printf("&ary = %p, x = %#x\n", &ary, x);

  uintptr_t y = (uintptr_t)&ary;
  printf("&ary = %p, n = %#lx\n", &ary, y);
}
出力結果(amd64)
sizeof(int*) = 8
sizeof(int) = 4
&ary = 0x7ffc8f9293a0, x = 0x8f9293a0
&ary = 0x7ffc8f9293a0, n = 0x7ffc8f9293a0
このように sizeof(int) < sizof(int*) な実行環境では int 型の変数にアドレス値を保存する事ができません。
安全にアドレス値を保存できる整数型が必要な場合には、 stdint.h で定義されている uintptr_t を使います。
ただし、uintptr_t を使っても指し示す先の型情報は失われます。
uintptr_t を使っても指し示す先の型情報は失われる
#include <stdint.h> // uintptr_t の為
#include <stdio.h>

int main(void) {
  int ary[10];

  uintptr_t i = (uintptr_t)&ary[2];
  uintptr_t j = (uintptr_t)&ary[6];
  printf("&ary[6] - &ary[2] = %#ld\n", &ary[6] - &ary[2]);
  printf("j - i = %#ld\n", j - i);
}
出力結果(amd64)
&ary[6] - &ary[2] = 4
j - i = 16


関数へのポインター 編集

関数も変数などと同じメモリー上のオブジェクトなのでアドレスを持ち、そのアドレスをポインター変数に保存し、ポインター変数を介して呼び出すことができます。

関数へのポインター使用例
#include <stdio.h>

static int add(int x, int y) { return x + y; }
static int sub(int x, int y) { return x - y; }
static int mul(int x, int y) { return x * y; }
static int div(int x, int y) { return x / y; }
static int mod(int x, int y) { return x % y; }

int main(void) {
  struct {
    char *opr;
    int (*pfi)(int, int);
  } tbl[] = {
      {"+", add},
      {"-", sub},
      {"*", mul},
      {"/", div},
      {"%", mod},
      {.opr = NULL},
  };
  for (int i = 0; tbl[i].opr != NULL; i++)
    printf("45 %s 7 = %d\n", tbl[i].opr, tbl[i].pfi(45, 7));
}
出力結果
45 + 7 = 52
45 - 7 = 38
45 * 7 = 315
45 / 7 = 6 
45 % 7 = 3
単純に和差積商と剰余を表示するプログラムです。
演算子の記号と、「2つの引数を取り演算子を適用するだけの関数へのポインター」の対の構造体のテーブルを作りイテレーションしています。
関数へのポインター
        int (*pfi)(int, int);
本題の関数へのポインターの宣言です(この例では構造体定義の一部ですが)。
*pfi を囲む ( ) は必須で、ないと * と結合してポインターを返す関数の宣言になってしまいます。
関数へのポインターを保持する変数と、保持される関数のシグネチャーは一致している必要があります。
シグネチャー
関数の引数の型と順序及び戻り値の組合わせ。
C言語にはラムダ式はないので[7]、名前付き関数を定義してその名前(=アドレス)を使って関数へのポインターに代入または初期化します。
今回、関数へのポインターによって間接参照する関数には static 修飾子を付けファイルスコープであることを明示しました。

qsort 編集

標準ライブラリ関数の qsort() の最後の引数も関数へのポインターです。

qsort関数の使用例
#include <stdio.h>
#include <stdlib.h>

static int compare(const void *a, const void *b) {
  return *(int *)a - *(int *)b;
}

int main(void) {
  const int data[] = {8, 5, 3, 9, 1, 0, 7, 6, 4, 2};
  const int n = sizeof data / sizeof *data;

  qsort(data, n, sizeof *data, compare);
  for (int i = 0; i < n; i++)
    printf("%d ", data[i]);
  printf("\n");
}
出力結果
0 1 2 3 4 5 6 7 8 9

ポインタの応用 編集

ポインタを指すポインタ 編集

 
図2

別のポインタを指すポインタを作ることができます。

ポインタを指すポインタを宣言する時には、 ポインタ名の前に*単項演算子を1つ多くつけます。

ポインタを指すポインタが指しているポインタが指しているオブジェクトにアクセスするには、 ポインタを指すポインタ名の前に*単項演算子を1つ多くつけます。

ポインタを指すポインタの使用例
#include <stdio.h>

int main(void) {
  int i,
     *pi = &i,
    **ppi = &pi;

    **ppi = 1234;
  printf("%d", i);
}

ppiが間接的に指す変数iに値1234を代入します。

void型へのポインタ 編集

void型へのポインタを宣言することができます。 void型へのポインタは、他の型へのポインタを格納できます。 間接演算子を使うときは型キャストして用います。 void型へのポインタはインクリメントやデクリメントと整数との加減算は出来ないです。

void型へのポインタ
#include <stdio.h>

int main(void) {
	char c = 12, *cp = &c;
	int i = 34, *ip = &i;
	float f = 5.6, *fp = &f;
	double d = 7.8, *dp = &d;
	void *vp = cp;

	printf("vpが指すchar型の値は%i。\n", *(char*)vp);
	vp = ip;
	printf("vpが指すint型の値は%i。\n", *(int*)vp);
	vp = fp;
	printf("vpが指すfloat型の値は%f。\n", *(float*)vp);
	vp = dp;
	printf("vpが指すdouble型の値は%f。\n", *(double*)vp);
    return 0;
}

ポインタの演算 編集

ポインタは、4つの算術演算子(+, ++, -, --)を使って、整数を加算減算することができます(void型へのポインタは除く)。 ポインタにn加えると、ポインタのバイトアドレスは「そのポインタの型のサイズ*n」だけ進む。

ポインタの演算は、ポインタが配列や配列の要素を指す場合によく使われます。

ところで、ポインタが指しているオブジェクトには、ポインタを使って、普通の変数と同様に参照できます。 その際、演算の優先順位には注意が必要です。

*p++;

上の記述では、pをインクリメントし、 そのアドレスにある値を返す。

(*p)++;

上の記述では、pのアドレスにある値をインクリメントします。

ポインタの使用例 編集

swap関数 編集

返却値以外の方法でデータを返す 編集

関数の引数として配列を渡す 編集

動的可変長配列 編集

大きな構造体のコピー省略への応用 編集

定数とポインタ 編集

ポインタをconst修飾子による定数化と組み合わせて用いたい場合、少々、下記のような複雑な問題があります。

問題の概要は、

  • ポインタ変数自体を定数にしたいのか、それともポインタの参照先だけを定数にしたいのか、どちらを定数にしたいかの判断があることと、
  • たとえばconst int 宣言された変数はコンパイラ内部的にはint型ではなくconst int 型とでも言う様な別種の型のような状態でと保持される実装のコンパイラである場合があること、

などの事情です。

このため、下記のような通常のコードでも、コンパイラによっては警告文が出ます(コンパイル自体は可能です。参照先を書き換えようとはしていないので)。

const修飾子とポインタ
#include <stdio.h>

int main(void) {
  const int a = 3;
  const int b = 7;

  int* pa = &a; 
  printf("%d\n", *pa); //

  pa = &b; 
  printf("%d\n", *pa); //
}
実行結果
3
7


上記コードで警告が出る場合、MinGWの場合なら、それは下記のようにaとpaの型の不一致が原因です。 コンパイラ内部では、const int宣言された変数は、int型ではなくconst int型という別種の型で宣言されているからです。なのでポインタ pa のほうも const int 型で宣言しなければなりません。

ただしこの場合、ポインタ宣言時にconst をつけたにもかかわらず、下記のようにポインタの指し示す先を変更できてしまいます。しかも警告表示は一切出ません(MinGWで確認)。

const修飾子の修飾先はポインタ変数自身?参照先のオブジェクトか?
#include <stdio.h>

int main(void) {
  const int a = 3;
  const int b = 7;

  const int* pa = &a; // const がついている
  printf("%d\n", *pa); //

  pa = &b; 
  printf("%d\n", *pa); //
}
実行結果
3
7


ポインタ変数自体を書き換えたくない場合、int* const paのように int* と変数名のあいだにconstを書きます。

#include <stdio.h>

int main(void) {
  const int a = 3;
  const int b = 7;

  int* const pa = &a; // const がポインタ変数を修飾
  printf("%d ", *pa); //
  printf("\n");

  pa = &b; // const オブジェクトへの再代入
  printf("%d ", *pa); //
  printf("\n");
}
実行結果
※エラーになり、コンパイル不可能。

エラーになる理由は、定数として宣言されたポインタ変数 pa を書き換えようとしたからです。

上記コードでは、アスタリスク( * )の前にconstがあるのか、アスタリスクの後ろにconstがあるのかで、区別しています。

よって下記コードのように、int const *paのようにアスタリスクの後ろにconstをつければ、コンパイル可能です。

#include <stdio.h>

int main(void) {
  const int a = 3;
  const int b = 7;

  int const *pa = &a; // const が参照先を修飾
  printf("%d ", *pa); //
  printf("\n");

  pa = &b; 
  printf("%d ", *pa); //
  printf("\n");
}
実行結果
3
7


int aのようにaやbにconstをつけずにint const *paのように宣言した場合、ポインタ変数paだけが定数になるので、aやb自体の書き換えは可能です。ただし、コンパイラによっては動作が不安定かもしれないので、なるべくなら避けたほうが安全です。


#include <stdio.h>

int main(void) {
  int a = 3;

  int* const pa = &a; // const がポインタ変数を修飾
  printf("*paは %d ", *pa); //
  printf("\n"); 
  
  //pa = &b; // 定数paを書き換えようとしているのでエラーになるのでコメントアウト(狙い通りの正常なエラー)
  //printf("%d ", *pa); //
  //printf("\n");

  a = 15; // a自体はconst 宣言されてないので書き換え可能
  printf("aの書き換え後\n"); //
  printf("*paは %d \n", *pa); //

  printf("aは %d \n", a); 
}
実行結果
*paは 3
aの書き換え後
*paは 15
aは 15

脚註 編集

  1. ^ register 型修飾子を伴って宣言された変数は例外です。
  2. ^ N2596 working draft — December 11, 2020 ISO/IEC 9899:202x (E). ISO/IEC JTC1/SC22/WG14. p. 20, §5.2.4.2.1 Characteristics of integer types <limits.h>. http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2596.pdf. 
  3. ^ printfは命令でもキーワードでもなく組込み関数でもなく、単に標準ライブラリー関数の1つなので printf() の引数が特別扱いされる理由はありません。ただし、printf は引数の数が固定でない可変引数の関数なので、第一引数の文字列で指定した型指定子数や内容によって第二引数以降の型が違うという特性があります。
  4. ^ N2176 C17 ballot ISO/IEC 9899:2017 p31, §6.2.5 Types
  5. ^ 後述する関数へのポインターでは加減算は行なえません。
  6. ^ INT36-C. ポインタから整数への変換、整数からポインタへの変換”. 一般社団法人JPCERTコーディネーションセンター (2015年3月16日). 2021年11月10日閲覧。
  7. ^ 2022年6月現在策定中のC23にC++風のラムダ式のプロポーザルが上がりましたが、いまのところ working draft には含まれていません。

参考文献 編集

  • 日本工業標準調査会『JISX3010 プログラム言語C』2003年12月20日改正