- - PR -
ポインターはオブジェクトを参照する
今回説明するポインターは、型の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
へのポインターは他の型を参照するポインターと相互に変換することができるという特徴があります。
異なる型をもつオブジェクト同士で値をやりとりするには、特別なルールがあるか、なければ強制型変換(キャスト)が必要でした。ポインターでも同じで、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
の値は、((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 *
というポインターがあります - 何も参照していないことを示す
NULL
という定数があります - 配列や構造体とポインターの関係を学びました
- ポインターをconstで変更できないようにする方法を学びました
Coding Edgeお勧め記事 |
いまさらアルゴリズムを学ぶ意味 コーディングに役立つ! アルゴリズムの基本(1) コンピュータに「3の倍数と3の付く数字」を判断させるにはどうしたらいいか。発想力を鍛えよう |
|
Zope 3の魅力に迫る Zope 3とは何ぞや?(1) Pythonで書かれたWebアプリケーションフレームワーク「Zope 3」。ほかのソフトウェアとは一体何が違っているのか? |
|
貧弱環境プログラミングのススメ 柴田 淳のコーディング天国 高性能なIT機器に囲まれた環境でコンピュータの動作原理に触れることは可能だろうか。貧弱なPC上にビットマップの直線をどうやって引く? |
|
Haskellプログラミングの楽しみ方 のんびりHaskell(1) 関数型言語に分類されるHaskell。C言語などの手続き型言語とまったく異なるプログラミングの世界に踏み出してみよう |
|
ちょっと変わったLisp入門 Gaucheでメタプログラミング(1) Lispの一種であるScheme。いくつかある処理系の中でも気軽にスクリプトを書けるGaucheでLispの世界を体験してみよう |
|
TechTargetジャパン
- ポインターを理解しよう (2012/8/8)
ポインターはオブジェクトを参照する値を入れておくためのものです。ポインターの理解はCの学習の肝となる部分です - Railsのコントローラをテストする (2012/8/6)
RSpecを使ってコントローラのテストを作成する。テストもコードなので便利なGemや機能を使ってDRYに書こう - 派生型でもっと便利にデータを扱う (2012/4/26)
基本型を組み合せて使える派生型を学びます。派生型には構造体、共用体、配列などがあります - 実例で学ぶRailsアプリのテスト方法 (2011/12/22)
具体的なWebアプリを例に簡単なテストを使ったリファクタリングについ
て解説する
|
|
キャリアアップ
スポンサーからのお知らせ
イベントカレンダー
お勧め求人情報
転職/派遣情報を探す
**先週の人気講座ランキング**
〜 Android編 〜
ホワイトペーパー(TechTargetジャパン)
「ITmedia マーケティング」新着記事
第1回 レポート分析のプロトタイピングで意思決定フローを作る
ページビュー、ユーザー数、広告のビューやクリック数……。Webのアクセス解析で一般的な...
#2 BtoBのマーケティングは営業と「ニギ」れ!
購買プロセスが複雑な法人向け商材≒BtoBの会社ではマーケティングは軽視されがちだとい...
第3回 成功事例で考えるマーケティング領域のビッグデータ活用
ビッグデータがマーケティング分野に及ぼす影響を、先行事例をもとに解説。例えば、クー...