ポインタの基礎
Cでは、ポインタは最も重要な概念の1つである。しかし、同時に最も難解な部分でもある。
アドレス
まずは、Cではどのように実行ファイルが作られるのかを知る必要がある。エディタで書いたプログラム(ソースプログラム(source program))をコンピュータの理解できる機械語に翻訳する(コンパイル(compile))。このときできたプログラムをオブジェクト(object)という。このオブジェクトモジュールを連結させることをリンク(link)といい、連結させるプログラムをリンカ(linker)という。こうして実行可能形式ファイル(ロードモジュール(load module))ができる。ロードモジュールはファイルとしてハードディスクなどに格納されているにで主記憶装置(メモリ)に展開しなければならない。この動作をロード(load)といい、OSによって行われる。ロードされたロードモジュールは、その先頭から実行を開始される。一般に、メモリは連続して並んだバイトの列で、ロードモジュールはメモリ上に連続的にロードされる。このバイトの列には、それぞれアドレス(address)、もしくは番地と呼ばれる連続した番号がふってある。CPUは、このアドレスを目印にしてプログラムの実行を制御したり、データの読み込みや書き出しを行う。多くの場合、ロードモジュールは変数や定数などのデータを格納しているデータ部とプログラムのアルゴリズムを記述したコード部から構成されている。さて、今まで型宣言とか定義という言葉を使ってきたが、定義された変数はメモリ上のアドレスに割り当てられている。
ポインタとは
ポインタとは、データオブジェクトの配置されているメモリ上のアドレスを保持する変数であると定義できる。データオブジェクトとは変数であると思ってかまわない。そして、変数とはある値を入れておく箱のようなものである。メモリ上のアドレスとは、この箱がどこにあるかを示す情報で、このアドレスを格納する箱がポインタ(pointer)である。つまり、ポインタもアドレスという数値を入れる箱なので変数である。変数といっても、今まで学習してきた変数とは異なる点がある。ポインタ変数はそれが指し示す対象となる変数が存在しなければ、何の意味もない。
また、ポインタも変数なのでアドレスを持っている。
アドレス演算子
変数定義
int x;
としたとき、変数xが配置されるメモリはコンパイラが勝手に決めてしまう。このアドレスを知るには、アドレス演算子である&(ampersand)を用いて
&x
とすることで、求めることができる。
例題7.1 変数xの配置アドレスを表示するプログラムを作成しよう。
実行結果
解答
#include <stdio.h> void main(void) { int x; printf(“\n Address = %p\n”,&x); } |
キャッチ printf関数の書式指定%pは、ANSI規格で採択されたポインタ表示書式指定であるが、古いコンパイラではサポートされていない。 |
ポインタ変数の定義
&xで得られたアドレスを保持できるポインタ変数pを定義すると
int *p;
となる。変数pの前についているアスタリスク(*)は、ポインタという派生語を作る記号です。これは配列を作るときに出てきた角括弧[ ]と同じようにすべての方に適用できる。では、変数xのアドレスとx変数xの値をポインタ変数を使用して表示してみよう。
例題7.2 変数xの配置アドレスをポインタを用いて表示するプログラムを作成しよう。
実行結果
解答
#include <stdio.h> void main(void) { int x; int *p; x = 10; p = &x; printf(“\n Address = %p\n”,p); printf(“\n value = %d\n”,*p); } |
実行結果 Address = 0012FF7C
value = 10
これは、CPUがインテル系なのでセグメントアドレスが0012、オフセットアドレスがFF7C。
キャッチ 最後の行を見ると、ポインタ変数pにアスタリスク(*)がついている、このことにより、ポインタ変数pが指し示す内容(この場合、変数x)を間接的に参照することができる。この「*」を間接演算子とよび、定義や宣言に現れるときは、ポインタとしての型を作ることを示し、式の中でポインタ変数の前に現れるときは間接参照を示している。 |
ポインタと配列
Cではポインタと配列の間に強い関係がある。配列の添え字を使って実行できるような操作は、ポインタでできるし、ポインタを使うほうが一般に高速である。しかし、初心者にとっては分かりにくいといわれている。どうだろうか?
次の宣言
int a[10];
は、大きさが10の配列aを定義するものである。これはa[0],a[1],…a[9]という名の10個連続したオブジェクトからなるブロックと考えることができる。
a[i]と書くと、先頭からi番目の位置の配列要素を参照する。
paが整数へのポインタだとすると、
int *pa;
と宣言され、代入
pa = &a[0];
によってaの0番目の要素を指すようにpaがセットされる。つまりpaはa[0]のアドレスを保持する。これで代入文
x = *pa;
によりa[0]の内容がxにコピーされることになる。
*(pa
+ 1)はどんなことを意味するのか
paはa[0]を指し示しているので、*(pa+1)はa[1]の内容を参照する。また、pa+1はa[1]のアドレスである。
pa = &a[0]と書くと、paとaは同じ値をもつ。配列の名前はその先頭の要素の位置と同義であるから、pa = &a[0]という代入文は次のようにも書ける。
pa = a;
キャッチ scanf関数で配列を読み込むときに、よくある間違いはscanf(“%s”,&str)と書いてしまうことである。正解はscanf(“%s”,&str[0])かscanf(“%s”,str)である。 |
ポインタと関数引数(詳しくは8章を参照)
Cでは関数に対して、引数を値で渡すから、呼ばれた関数のほうで、呼び出した関数内の変数を直接変更することはできない。例えば、ある分類用ルーチンで、swapと呼ばれる関数で2つの要素の入れ替えを行うとする。次のように書いたのでは十分でないというより間違いである。何がいけないのか考えてみる。
#include <stdio.h>
void swap(int,int); /* プロトコル宣言*/
void main(void)
{
int a,b;
printf("整数を2つ入力したください\n");
scanf("%d %d",&a ,&b);
if(a < b){
swap(a,b); /* swap関数を呼び出す*/
}
printf("%d %d",a,b);
}
void swap(int x, int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
このプログラムを解析してみる。swap(a,b)は値による呼び出しであるので、swap関数が呼ばれて、変数a,bがx,yに渡される。swap関数でaとbの値の入れ替えが行われるが、呼び出したswap(a,b)のa,bには何の影響も及ぼさない。
正解は次のように書く。呼び出し側のプログラムで変更すべき値のポインタを渡すようにする必要がある。
swap(&a,&b);
演算子&は変数のアドレスを与えるから、&aはaへのポインタである。ポインタ引数を使えば、関数の中でそれを呼んだ関数の中のオブジェクトをアクセスし変更することが可能になる。次に示すように、swap自身内では引数はポインタとして宣言され、実際の被演算数はそのポインタを通して間接的にアクセスされる。
#include <stdio.h> void swap(int *,int *); void main(void) { int a,b; printf("整数を2つ入力したください\n"); scanf("%d %d",&a ,&b); if(a < b){ swap(&a,&b); } printf("%d %d",a,b); } void swap(int *px, int *py) { int temp; temp = *px; *px = *py; *py = temp; } |
練習問題7.1 選択法ソート
<配列を使った問題例>
「プログラムの説明」
1. このプログラムは、入力された5つの数値を昇順に並べ替える。
2. 並べ替えは、内部の配列dで行われる。
3. 並べ替えの順序は、dの先頭から順に最小のものから設定される。
4. 最小の値を設定する方法は、先頭の値とそれ以降の値を比べていき、先頭より小さな値を見つけたら先頭の値とその値を取り替えることである。
5. 値を取り替えるには、関数swapを用いる。関数swapの引数は取り替える配列の2つのアドレスである。
6. 先頭の最小値が設定される。次は、2番目を先頭として、それ以降の最小値を先頭に設定する。
7. 出力時の番号は、3桁の整数で表す。
次の実行例をみて、プログラムの□を埋めて完成せよ。
<実行例>
入力 出力
1バン = 30 1番 = 70 35%
2バン = 10 2番 = 50 25%
3バン = 40 3番 = 40 20%
4バン = 70 4番 = 30 15%
5バン = 50 5番 = 10 5%
計 200 100% |
<プログラム>
#include <stdio.h>
#define N_MAX 5
int d[N_MAX];
void main(void)
{
int i,j,sum;
for(i=0,sum=0;i < N_MAX;i++)
{
printf(“ ア “, i+1);
scanf(“%d”, イ);
sum+=d[i];
}
for(i=0;i < N_MAX-1;i++)
{
for(j=i+1;j < N_MAX;j++)
{
if(ウ)
swap(&d[i],&d[j]);
}
}
printf(“ソート後\n”);
for(i=0;i < N_MAX;i++)
{
printf(“エ番 = %d %2ld%\n”
,オ,カ,キ);
}
printf(“_________________________\n”);
printf(“計%3d 100%\n”,sum);
}
void swap(int *a,int *b) /* *aは&d[i]で得られたアドレスを保持するポインタ*/
{
int w;
w = *a;
*a = *b;
*b = w;
}
&d[i]はd[i]のアドレスを表し、int *aでポインタの定義をし、a = & d[i]でポインタaへ変数d[i]のアドレスを代入する。(ポインタは変数のアドレスを格納する変数)
解答 アには%3dバン= イには&d[i] ウにはd[i] < d[j] エには%3d オにはi+1 カにはd[i] キには(((long)1000*(long)d[i] + 5)/(long)sum)/10が入る。