ポインターを理解しよう目指せ! Cプログラマ(11)

ポインターは、ほかの型と違って、ほかのオブジェクトを参照する値を入れておくためのものです。ポインターの理解はCの学習の肝となる部分です

» 2012年08月08日 12時00分 公開
[長沼立巳, 小山博史SSS(G)/ガリレオ]

ポインターはオブジェクトを参照する

 今回説明するポインターは、型の1つです。

 型にはいろいろありますが、例えばint型のオブジェクトにはint型の値を、double型のオブジェクトにはdouble型の値を入れることができました。ポインターのオブジェクトには、他のオブジェクトを参照するための値を入れることができます。

 ひとくちにポインターと言ってもいろいろな型があります。いずれも他のオブジェクトを参照しますが、その参照先のオブジェクトの型によりポインターの型が定まります。例えば参照先のオブジェクトの型がintとすると、そのオブジェクトのポインターは「intへのポインター」という型になります。

 「intへのポインター」という型は、int * と書きます。

int a; // int型のオブジェクト宣言
int *pa; // 「intへのポインター」型のオブジェクト宣言

 変数aを参照するための値は、アドレス演算子(単項&演算子)&演算子)を使って&aとすると得られます。aの型がintであるとすると、&aの型はint *になります。

 つまり次のように書くことで、オブジェクトaを参照するための値(アドレス)がpaに入ります。

int a;
int *pa;
pa = &a; // aを参照するための値がpaに入る

 ポインターが参照するオブジェクトの値を取り出すには、間接演算子(*演算子)*演算子)を使います。

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

 ポインターpaには、オブジェクトaを参照する値を入れましたので、*pa と書くことでオブジェクトaの値を取り出すことができます。

 なお、ポインターもオブジェクトですので、ポインターのポインターの型も表現できます。例えばint **は「intへのポインターへのポインター」という型になります。

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

コラム●“int *pa”か、“int* pa”か

 ポインターを宣言するときに使う int *pa という書き方は、int* pa のように、アスタリスク(*)の位置を変えても同じように宣言できます。型としてはintへのポインターですから、int* paの方がいいようにも思えます。

 ところが、次のように書くときにすっきりしないことがあります。

int* p1, p2;

 ぱっと見たところp1もp2もintへのポインターのようですが、p2はポインターではなくint型の変数宣言になってしまっているのです。p2もintへのポインターにするには、次のように書く必要があります。

int* p1, *p2;

 しかしこれでは読みづらいですね。というわけで、intと*の間に空白を入れて、次のように書く人もいます。

int *p1, *p2;

 とはいえ、そもそも一度に2つ宣言しようとしなければ問題にならないわけです。

int* p1;
int* p2;

 これならどちらもintへのポインターです。アスタリスクの前後に空白を入れて、

int * p1;

 と書く場合もあります。

 結局のところ、どの書き方をしても問題ありません。試行錯誤して自分のスタイルを決めていけばよいでしょう。


なぜポインターが必要なのか

 ところで、なぜポインターというものが必要なのでしょうか。

 ポインターは他のオブジェクトを参照します。この参照するという特性を利用すると、次のようなことが簡単に行えるようになるのです。

メリット1:相対位置による操作ができる

 ポインターによりオブジェクトの位置を渡すと、その位置を基準として離れた位置のデータを簡単に参照することができます。つまり、基準からどれくらい離れた位置にほしいデータがあるかさえ知っていれば、すぐにその位置のデータを読み書きできます。

メリット2:大きなオブジェクトを簡単に扱える

 ポインターを渡すことでデータのある場所を渡すことができますので、データそのものをコピーすることなく、複数の場所から同じデータを参照することができるようになります。

メリット3:操作を指定することができる

 ポインターは関数を参照することができます。ある操作に対して関数をポインターで渡すことにより、その操作を指定することができます。

NULLとvoid *

 前述のメリットを、ポインターを使うことでどうやって実現するのか、これから説明します。その前に、ポインターの基本についてもう少しおさえておきましょう。

 ポインターには voidへのポインター(void *)void *) という特別な型のポインターがあります。voidへのポインターは他の型を参照するポインターと相互に変換することができるという特徴があります。

 異なる型をもつオブジェクト同士で値をやりとりするには、特別なルールがあるか、なければ強制型変換(キャスト)が必要でした。ポインターでも同じで、int * を double * へ型変換するためには強制型変換が必要です。しかし、int * を void * へ、あるいは void * を double * へ、という変換はキャストしなくても可能です。

int i;
int *pi = &i; // int* を int* へ。同じ型なので問題なし。
double *pd = &i; // エラー。int* を double* へ変換できない。強制型変換が必要。
void *pv = &i; // int* を void* へ。問題なし。

 また、ポインターには、空ポインター定数(NULLポインター定数)NULLポインター定数) という特別な定数値があります。この値は、オブジェクトを参照しているポインターと比較すると絶対に等しくなりません。空ポインター定数は「ポインターがなんのオブジェクトも参照していない」ことを表すために使います。この空ポインター定数を持つポインターを 空ポインター(NULLポインター)NULLポインター) と呼びます。

 空ポインター定数は、コード上では NULL として表現されます。実際には NULL の値は、((void*)0) もしくは 0 と定義されます。このため 0 を使いたくなりますが、コードから空ポインターであることがわかるように NULL を指定するように心がけましょう。

int i;
int *pi = &i;
if (pi == NULL) { /* piとNULLが等しくなることはない */ }
if (pi == 0) { /* 上に同じ */ }

 if文では、条件式の値が0のときは偽、 0 以外では真とみなされます。ポインターの値が 0 (つまり NULL)ということは有効なポインターではないということになりますので、次のような書き方も使われます。

if (pi) { /* pi はおそらく有効なオブジェクトを参照している */ }
if (! pi) { /* pi は有効なオブジェクトを参照していない(NULLである) */ }

 ただし、ポインターの値が NULL でないからと言っても、必ずしも有効なオブジェクトを参照しているとは限りません。例えばポインターを宣言したときに初期化を行わなければ、他の変数と同じように不定な値になり、その参照先に有効なオブジェクトがあるかどうかは実行してみるまで分かりません(おそらくないでしょう)。

 有効なオブジェクトを参照していないポインターを扱うのは、おそらくみなさんが想像する以上に危険ですので、そうならないようなプログラミングを心がけましょう。そのためには有効なオブジェクトを参照していないポインターには NULL を入れておくことです。

配列とポインター

 配列は同じ型のオブジェクトが、いくつも隙間なく並んだ構造をしています。例えば、型intをもつオブジェクトを3つ持つ配列は、次のように定義します。

int ary[] = { 1, 2, 3 };
// 上は次のように書いた場合と同じです。
int ary[3];
ary[0] = 1;
ary[1] = 2;
ary[2] = 3;

 ここでポインターが「相対位置による操作ができる」ことを思い出せば、ポインターと配列の相性が良いことに気がつくはずです。配列では先頭のオブジェクトから各要素が相対的にどの位置にあるか簡単にわかるからです。配列をポインターで扱うときは、配列の要素をポインターで表して使います。

 まず、配列の先頭要素へのポインターの書き方を見てみましょう。int型の配列における要素の型はすべてintですので、先頭要素へのポインターの型は int * です。配列 ary の先頭要素は ary[0] ですから、そのアドレスはアドレス演算子を用いて次のように書くことができます。

int *p = &(ary[0]);

 このように書いても全く問題ありませんが、配列名である ary が式の中に出てきたときには配列の先頭要素を参照するポインターとして扱われるというルールがありますので、次のように簡単に書くこともできます。

int *p = ary;

 ポインターで配列の要素を参照することもできます。ary は先頭要素へのポインターですから、先頭要素を参照するには *ary と書くことができます。

 先頭要素へのポインター ary は、ary + 0 と書いても同じことです。先頭要素は *(ary + 0) と書けます。同じように、次の要素へのポインターは ary + 1、その参照先の要素は *(ary + 1) と書けます。

// 配列の先頭要素へのポインター
&(ary[0])
ary
ary + 0
// 配列先頭要素
ary[0]
*ary
*(ary + 0)
// 配列の先頭の次の要素へのポインター
&(ary[1])
ary + 1
// 配列の先頭の次の要素
ary[1]
*(ary + 1)
// 以下同じ

 ところで、配列へのポインターはどのように書くのでしょうか。例えば、次のように書いたら、変数pの型はどうなるでしょうか。

int (*p)[3] = &ary;

 これは、“「int型の3つの要素を持つ配列」へのポインター”pを宣言して、aryで初期化しています。とはいえ、慣れないうちは読みづらいと思いますので、1つずつ追いながら読んでみます。

 まず右側です。&ary は配列 ary のアドレスになります。ary の型はint型の3つの要素を持つ配列ですので、int [3] となります。このアドレスですから、&ary の型はint (*)[3] となります。なぜ * を()で囲う必要があるかというと、int *[3] では「int *型の要素を持つ3つの配列」という別の意味になってしまうからです。

 次に左側を見てみましょう。まず注目すべきは、p という識別子です。p の型を見るには、近いところにある記号から見ていきます。pの最も近くにあるのは()内にある * です。型の中に出てくる * はポインターを表しますから、p は何かを参照するポインターであることがわかります。ここで () 内を外してみると、int [3] になります。これはint型の要素を3つもつ配列です。これらをまとめて読むと、p は“「int型の要素を3つもつ配列」へのポインター”であることがわかります。

 つまり、型の一部を取り除くと、取り除いた部分の型が残るのです。また、左側から識別子pを取り払ってみると、int (*)[3]になります。これは、右側の型 int (*)[3]と同じです。まずは識別子を取り除くだけでも、どんな型になるかわかりやすくなります。このことを覚えておくと、見慣れない型に出会っても、落ち着いて対処できるはずです。

コラム●配列名とポインター生成

 配列名は「配列の先頭要素を参照するポインターとして扱われる」と書きましたが、もう少し細かい説明を加えると、次のようになります。

 より厳密な定義では、“〜型の配列”をもつ式は、“〜型へのポインター”の式に型が変換されることになっています。例えば、本文中の ary は、もともとint型の3つの要素を持つ配列ですから、型は int [3] です。これがint型へのポインター、つまり int * に変換されます。

 そして、そのポインターは配列の先頭要素を指します。つまり配列名 ary を書いた時には、この型が int * で、先頭要素へのポインターとなるわけです。このルールを「ポインター生成」と呼ぶこともあります。

 ただしこのルールにはいくつか例外があります。1つは、本文中にも出てきた &ary という書き方です。この場合には型の変換は発生せず、そのまま“int型の配列へのポインター”になります。他には sizeof ary と書いた場合があります。この場合もそのまま“int型の配列”のサイズになります。


構造体とポインター

 構造体は、さまざまな型が集まってできています。int型のメンバー width とdouble型のメンバー height をメンバーにもつ構造体 struct size は、次のように宣言します。

struct size {
  int width;  // 幅
  int height; // 高さ
};

 構造体 struct size へのポインターの型は struct size * です。

struct size s = { 2, 3 };
struct size *ps = &s1;

 psのように、構造体へのポインターの型である変数を使っているときは、構造体のメンバーを参照するのに -> 演算子を使います。

// . 演算子を使った参照
print("%d, %d\n", s.width, s.height); // 2, 3
// -> 演算子を使った参照
print("%d, %d\n", ps->width, ps->height); // 2, 3

 構造体のメンバーへのポインターとなる変数を用意したいときは、次のようにして構造体のメンバーを参照するための値(アドレス)を取得します。

int *pw = &s.width;

 構造体などのメンバーにアクセスする . 演算子は、アドレス演算子(&)よりも優先順位が高いので、&s.width は &(s.width) と同じです。型は、メンバーの型を参照するポインターの型となります。つまり、int width というメンバーへのポインターは int * 型です。

 なお、構造体のビットフィールド メンバーにはアドレスがありませんので、& 演算子でアドレスを求めることができません。アドレスが必要な場合には、共用体を使ってビットフィールドではない変数とメモリーを共有し、その変数のアドレスを使うなどの方法があります。

const 修飾子とポインター

 const 修飾子をつけた変数は値の変更ができなくなります。

const int a = 3;
a = 4; // エラー。変更できない。

 参照しているオブジェクトの値を変更できないポインターを用意したい場合は、次のように const をつけます。

int a = 3, b = 4;
const int *p = &a;
p = &b; // 問題なし。
*p = 5; // エラー。変更できない。

 オブジェクト p の宣言は const int *p ですから、p の型は、そこから p を取り除いた const int * です。これはどんな型か読んでみましょう。まず識別子 p とそれに最も近い記号である *p に着目すると、p はポインターであることがわかります。次に *p を取り除いてみると残るのは const int ですから、全体で見ると p は const int へのポインターであることがわかります。

 なお、const int *p は、int const *p と書いても意味は同じです。

 次に、ポインターに const 修飾子をつけて変更できないようにしたい場合は次のように書きます。

int a = 3, b = 4;
int * const p = &a;
p = &b; // エラー。変更できない。
*p = 5; // 問題なし。

 今度の p の型は int * const です。まず識別子 p に一番近い部分を読むと const p です。これで p が const であることがわかります。つまり、オブジェクト p 自体が const になっているため、p の値を変更しようとするとエラーになります。const p を取り除いた残りの部分は int * ですので、p は const であり、intへのポインターであることがわかります。

 ポインターとその参照先の、両方とも const である場合には、次のように書きます。

const int * const p = &a;

今回学んだこと

  • ポインターはオブジェクトを参照します。
  • 参照先の変数や値の型に応じてポインターの型を決めます。
  • ポインターのメリットは次の3つです
  • メリット1:相対位置による操作ができる
  • メリット2:大きなオブジェクトを簡単に扱える
  • メリット3:操作を指定することができる
  • 他の型と相互に変換できるvoid *というポインターがありますvoid *というポインターがあります
  • 何も参照していないことを示すNULLという定数がありますNULLという定数があります
  • 配列や構造体とポインターの関係を学びました
  • ポインターをconstで変更できないようにする方法を学びました

Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

AI for エンジニアリング
「サプライチェーン攻撃」対策
1P情シスのための脆弱性管理/対策の現実解
OSSのサプライチェーン管理、取るべきアクションとは
Microsoft & Windows最前線2024
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。