ポインタの基礎:C言語

C言語を一通り学習した方を対象とした、ポインタの基礎の基礎をまとめました。

この投稿は、Quora に筆者自身が投稿した内容を、再編集したものです。

【目次】
1.ポインタとアドレス
1.1.ポインタとは
1.2.アドレスとは
2.ポインタの宣言文とポインタ演算
2.1.ポインタの宣言文
2.2.ポインタ演算
3.ポインタと演算子
4.ポインタと配列
5.まとめ

1.ポインタとアドレス

1.1.ポインタとは

ポインタは、変数で、その値がアドレスであるもののことを言います。
究極的には、ただそれだけのものです。

ポインタは、それ単独では、意味をなさない変数です。
ポインタ(ここで、pとします)には、基本的に、他の変数(同、xとします)のアドレス(&x)を入力(p=&x)し、ポインタpから変数xを操作したいときに使います。

1.2.アドレスとは

アドレスは、番地とも呼ばれ、メモリの位置を表す整数です。

例えば、メモリの先頭の位置は、0番地です。

アドレスはメモリの位置を表す整数なので、アドレスを値に持つ変数であるポインタのサイズは、メモリの最後の番地を表せるサイズです。

メモリのサイズは、処理系に左右され、32ビットとか64ビットなどがあります。
メモリのサイズが32ビットの処理系のとき、32ビットは(1バイト=8ビット)4バイトであるため、ポインタは4バイトの領域を使う変数ということになります。

2.ポインタの宣言文とポインタ演算

2.1.ポインタの宣言文

ご存知のように、C言語では、変数を必ず定義してから使います

変数の定義は、変数の型と独自の名前を付けて行います。
定義の情報は、まず、コンパイルするときに、メモリ上に変数を確保するのに使われます。

例えば、long int型のサイズが8バイト、ポインタのサイズが4バイトの処理系で、次の宣言を行ったとします。

long int a;

long int *p;

これをコンパイルすると、long int型の変数a用に8バイト、ポインタ変数p用には4バイトの領域が確保されます

ポインタpのために確保される領域のサイズは、指す先の型には左右されません。
同じ処理系では、指す先の型に関わらず、すべてのポインタが、同じサイズでメモリに確保されます。

2.2.ポインタ演算

では、ポインタ変数を定義するとき、なぜ、指す先の型を書く必要があるかを、次に説明します。簡単にするために、アドレス(メモリ上の位置)をここでは10進数で書きます。

上記のlong int型の変数aとlong int型のポインタ変数pの番地が、次のアドレスにあるとします・・・aは1000番地、pは2000番地。また、変数aには、123を代入します。

a=123;

では、次に、a+1の結果を考えます。変数aはlong int型の普通の変数なので、a+1の値は124となります。

次に、ポインタ変数pに変数aのアドレスを代入します。

p=&a;

ポインタ変数pには、変数aのアドレスが代入されたので、pの値は、1000になっています。

図1.

次に、p+1の結果を考えます。pの値が1000なので、1001と答えたくなりますが、ポインタに1を足すとは、ポインタを定義したときの指す先の型のサイズの1つ分後ろを指すことを意味します

ということで、この処理系ではlong int型1つ分は8バイトだったので、1008となります。
このようなポインタ変数への足し算引き算のことを、ポインタ演算と言います。

このポインタ演算のために、ポインタ変数の定義では、そのポインタが指す先の型を決めておく必要があったわけです

図1のポインタpが指す先は、ただの変数aなので、p+1、すなわち、1008番地から始まるメモリ内の値は不定です。
このとき、p+1の指す先の中身*(p+1)を参照することに意味はありません。

ポインタ演算が意味をなすのは、連続した領域に値を入れるためのもの、例えば、配列などの、要素の1つのアドレスをポインタに代入していたときです。
この例は、4.ポインタと配列で説明します。

3.ポインタと演算子

では、今度は、*p+1(*(p+1)ではありません)の結果を考えてみます。
<b>式*p+1は演算子の優先順位から、(*p)+1と解釈されます</b>。
*pは、pの指す先の中身という意味なので、1000番地が指す先の中身、すなわち、変数aの値です。
よって、*p+1は、123足す1で、この式全体の値は124となります。

例えば、次のような3つの変数の定義があったとします。

long int a;

long int *p;

long int **pp;

3番目の定義は、変数ppはポインタで、その値であるアドレスの指す先が、これまたアドレスであって、それが指す先が、long int型である変数です。
変数ppのサイズも、アドレスを保存するための変数なので変数pと同じように、この処理系の最大のアドレスを表現できるサイズである、4バイトです。

今、変数a、p、ppのアドレスが順に1000、2000、3000番地のとき、

a=123; p=&a; pp=&p;

上記の式を実行した後、変数aの値は123、pの値は1000番地、ppの値は2000番地です。
そして、*pはpの指す先の中身なので123、*ppはppの指す先の中身なので1000番地、**ppは、ppの指す先の中身の、さらに、指す先の中身なので123です。
また、&aはaのアドレスなので1000、&pはpのアドレスなので2000、&ppはppのアドレスなので3000番地です。

4.ポインタと配列

例えば、次のような、配列を含む、2つの宣言文があったとします。

long int b[10];

long int *p;

配列bは要素数10個のlong int型、ポインタ変数pは、指す先の型がlong int型です。
配列bが5000番地から始まり、ポインタ変数pが6000番地にあるとします。
このとき、配列bは、1つの要素で8バイトずつ必要なので、5080番地までメモリを使っています。

ご存知のように、C言語の配列名は、その配列の先頭アドレスを表します

そこで、配列bの先頭アドレスをポインタ変数pに代入するのは、次の式になります。

p=b

この式は、

p=&b[0]

すなわち、演算子の優先順位で次のように解釈されるので

p=&(b[0])

と同じです。

ポインタ変数pに配列bの先頭アドレスを代入した後は、ポインタ変数pを配列bの配列名のように使用することができます。
例えば、配列bの要素番号0(先頭の要素)の値は、b[0]ともp[0]とも表現できます。

また、配列名bをポインタ変数のように使用することもできます。
配列bの要素番号0(先頭の要素)の値は、*pとも、*bとも表すことができます。

ただし、配列名には変数のような値を保存する領域がないので、ポインタpのように配列名bに何かを代入することはできません。
よって、宣言文以外の場所で、b=〇とか、b++といった使い方はできません。
例外的に、宣言文では、配列の初期化に=を使います。

式b[1]、p[1]、*(b+1)、*(p+1)は、どれも、配列bの要素番号1(先頭から2番目の要素)の値を表します。

メモリの中の状態を図をご覧いただいてわかるように、ポインタと配列は全くの別物です。
式では一部同じような表現になることがあっても、ポインタ変数と配列名は全く違うものです。

5.まとめ

ここまでの説明にざっと目を通しただけでは、すぐに頭に入ってこなかったかもしれませんが、とにかく、伝えたかったことは、ポインタの扱いで、&や*をつけるつけない、ついているついていないを、パターンで覚えることは、危険だということです。

必ず、ポインタの宣言文とポインタに何を代入したかを確認してから、図で何を対象とするのかを確認し、さらに、それを式として正しく表現するために、&演算子、*演算子、[]演算子、その他の演算子を優先順位も考慮してプログラミングするようにします。

少し時間を多めにとって、ゆっくり読み返していただいてから、ご自分がお持ちの本などを改めて読んでいただくと、理解が進むかと思います。

ポインタなど面倒なものを使わずに、普通の変数だけを使ったプログラムを作ればいいと思う方も多いと思います。
それでも、文字列を出力するときはポインタの概念が必要なことなどもあり、完全に避けて通るのも難しいものです。
ポインタの基本的なことくらいまでは、とりあえず理解できるレベルになっておくとよいのではないでしょうか。

C言語のポインタについては、今回説明した内容の他にも、ポインタ配列やポインタ関数などがあります。
その他にも、ある型の値とポインタからなる構造体とmalloc関数(実行中の好きなときにメモリを確保できる、ライブラリ関数の1つ)などを使って、リンクリストと呼ばれる実行中の好きなときに要素数の増減をコントロールできる配列のようなもの(リンクリストは、厳密にはメモリ上で要素が連続して並んでいないので配列ではないのですが)を扱うこともできます(配列の要素数は、実行のはじめにメモリの領域を確保する必要性から、具体的な数で宣言します。そのため、どのような入力量にも対応できるように、要素数はかなり多めにしておくのが、一般的です。でも、配列の要素数を多めにするのは、あまり使われないメモリを増やしてしまうことになります。それを回避したいときに使うのが、リンクリストです)。

別の機会にこれらに関する投稿もしたいと思っています。

コメントを残す