4.ポインタ
C言語を学ぶ時、実に多くのひとが「ポインタ」のところでつまづきます。確かに分かりづらい点もありますが、ポインタを使いこなせるとCプログラミングの幅がぐっと広がることも確かです。
4.1 ポインタとアドレス
4.1.1 ポインタとは
まず、「ポインタはアドレスを記憶するための変数である」ということを頭に入れておいてください。引き出しの例えでいえば、「引き出し番号を記憶しておくための変数」、ということになります。
ポインタ変数を宣言する時には、それが「どのデータ型の変数のアドレスを記憶するためのものか」ということを指定する必要があります。それにはデータ型の後にアスタリスク(*)をつけて、次のように宣言します。
int *iptr; /* int型(の変数のアドレスを記憶するため)のポインタ */
char *cptr; /* char型(の変数のアドレスを記憶するため)のポインタ */
double *dptr; /* double型(の変数のアドレスを記憶するため)のポインタ */
このように宣言すると、iptr, cptr, dptrはそれぞれint型、char型、double型の変数のアドレスを記憶するためのポインタになります。
4.1.2 ポインタへの代入、ポインタの参照
「ポインタはアドレスを記憶するための変数」ですので、値の代入式は、例えば次のようになります。
<例4−1>
#include <stdio.h>
int main(void) {
int
n=123;
int
*iptr; /*
int型のポインタiptrを宣言 */
iptr
= &n; /*
変数nのアドレスを代入 */
printf("value
of n : %d\n", n); /* nの値を表示 */
printf("address
of n : %p\n", &n); /* nのアドレスを表示 */
printf("value
of iptr : %#x\n", iptr); /*
iptrの値を16進数表示 */
printf("value
of *iptr: %d\n", *iptr); /*
iptrが指している変数の値を表示 */
return
0;
}
(実行結果の例)
value of n : 123
address of n : 0xbffffaa0
value of iptr : 0xbffffaa0
value of *iptr: 123
[解説] ポインタ変数のサイズ
この例では、int型変数nのアドレス(&n)を、ポインタ変数iptrに代入しています。この例にあるように、ポインタの前に*をつける(*iptr)と、「そのポインタに記憶してるアドレスにある(ポインタが"指している")変数の値」が参照できます。値を参照するだけでなく、代入することもできます(*iptr=...)。
引き出しの例えに当てはめれば、変数iptrのために確保された引き出しには、別のある引き出しの番号をいれることができ、*iptrと書くことで、iptrに入っている番号の引き出しの中身にアクセスできる、ということになります。
【課題4−1】(プログラムを提出)
以下のプログラムは、初期化された変数xの値をそのまま表示するものである。このxの値を、xに直接代入することなしに2倍の値に変更する。
(x=...という文は使わずに、xを指すポインタを使ってxに値を代入する。)
#include <stdio.h>
int main(void) {
double
x = 123.45;
printf("x
= %6.2f\n", x);
return
0;
}
[解説] ポインタの宣言と初期化
《プログラム例》
4.2 配列とポインタ
実際には、ポインタは配列を指すのに用いるケースがほとんどです。ポインタと配列はある意味で等価であり、配列を使って書いてあるプログラムはポインタを使って書き換えることができ、その逆も真です。
一般に、配列で書くと直感的にとらえやすいプログラムになるのに対して、ポインタを用いると、やや直感的に理解しづらい反面、プログラムの自由度が増す、という利点があります。「ポインタはあるアドレスを記憶している」ということをいつも念頭において考えれば、C言語のポインタは、ややこしいようでも実はうまくできているということが次第に分かるでしょう。
<例4−2>
#include <stdio.h>
int main(void) {
int
data[]={1, 2, 3, 4, 5}; /* int型の配列dataを宣言し初期化 */
int
*ip; /*
int型のポインタipを宣言 */
ip
= data; /* 配列dataの先頭アドレスを代入 */
printf("value
of ip = %p\n", ip); /* ipの値を表示 */
printf("value
of *ip = %d\n", *ip); /*
ipが指している変数の値を表示 */
return
0;
}
(実行例)
value of ip = 0xbffffa80
value of *ip = 1
[解説] 文字列とポインタ
上の例では、int型の配列dataの先頭アドレスをint型のポインタ変数ipに代入しています。<例4−1>でもみたように、ipと書けばアドレスの値、*ipと書けばそのアドレスにある変数の値が見られます。この場合*ipは配列dataの先頭の要素の値である1になります。
さらにこの例で、同じポインタを使って他の配列要素にアクセスするにはどうすればよいでしょうか?答えは簡単で、ポインタに1を足せば次の要素、2を足せばその次の要素を指すようになります。下の例では、ポインタを使って配列の各要素にアクセスするための2つの方法を示しています。
<例4−3>
#include <stdio.h>
int main(void) {
int
data[]={1, 2, 3}; /* int型の配列dataを宣言し初期化 */
int
*ip1, *ip2; /*
int型のポインタip1, ip2を宣言 */
ip1
= ip2 = data; /*
ip1, ip2ともに配列dataの先頭アドレスを代入 */
/*
ip1の値は変えずに配列dataの各要素のアクセス */
printf("accessing
elements by ip1\n");
printf("%d\n",
*ip1);
printf("%d\n",
*(ip1+1));
printf("%d\n",
*(ip1+2));
/*
ip2の値を変えながら配列dataの各要素のアクセス */
printf("accessing
elements by ip2\n");
printf("%d\n",
*ip2);
ip2++;
printf("%d\n",
*ip2);
ip2++;
printf("%d\n",
*ip2);
return
0;
}
[解説] ポインタの演算
この例で、始めの方法は、ポインタip1自身(が保持しているアドレス)の値は変えることなく各要素にアクセスしています。後の方法は、ポインタip2自体の値を+1ずつ増やす(インクリメントする)ことをしています。
ポインタ変数に対する演算は、他の変数とは違った意味を持つので注意が必要です。<例4−3>の[解説]をみて、確認してください。
【課題4−2】(プログラムを提出)
【課題3−1】の課題(配列要素の順序反転)を、ポインタを用いたプログラムにする。(配列の各要素にポインタを使ってアクセスする。)
《プログラム例》
4.3 二次元配列とポインタ
二元配列は「配列の配列」です。上述のように一次元配列はポインタで扱えますので、二次元配列は「ポインタの配列」や「ポインタへのポインタ」として扱うことができます。
例として、char型の配列の配列を考えます。char型の配列は「文字列」ですので、文字列の配列、ということになります。
<例4−4>
#include <stdio.h>
int main(void) {
char
str[3][5] = {"abcd", "ABCD", "1234"}; /* 文字列の配列strを宣言し、初期化 */
int
i;
/*
各文字列を表示 */
for
(i = 0; i < 3; i++) {
printf("address
of %s : %p\n", str[i], str[i]);
}
return
0;
}
(実行例)
address of abcd : 0xbffffa90
address of ABCD : 0xbffffa95
address of 1234 : 0xbffffa9a
同じことを「ポインタ配列」で書くことができます。
<例4−5>
#include <stdio.h>
int main(void) {
char
*str[3] = {"abcd", "ABCD", "1234"}; /* char型のポインタの配列strを宣言し、初期化 */
int
i, j;
/*
各文字列の先頭アドレスを表示 */
for
(i = 0; i < 3; i++) {
printf("address
of %s : %p\n", str[i], str[i]);
}
/*
各文字列の中身を表示 */
for
(i = 0; i < 3; i++) {
for
(j = 0; j < 4; j++) {
printf("%c
", str[i][j]);
}
printf("\n");
}
return
0;
}
(実行例)
address of abcd : 0x2020
address of ABCD : 0x2028
address of 1234 : 0x2030
a b c d
A B C D
1 2 3 4
[解説] 二次元配列とポインタ配列の違い
上の例の後半のように、ポインタ配列として宣言したstrを二次元配列として使って、2つの添字で各文字要素にアクセスすることも可能です。
二次元配列よりも、ポインタ配列として定義しておいたほうが、一般に柔軟性があります。例えば行単位で要素を入れ替える(並べ替える)ことも簡単にできます。<例4−5>のstrであれば、例えばstr[0]とstr[1]の値を入れ替えれば"abcd"と"ABCD"が入れ替わります。同じことを<例4−4>でやろうとすると、文字を一つ一つ入れ替えないとならなくなります。
【課題4−3】(プログラムと実行結果を提出)
以下のプログラムは、二次元配列monthに格納された複数の文字列について、各文字列の文字数を表示するものである。これを、monthをポインタ配列としたプログラムに書き換える。
#include <stdio.h>
#include <string.h>
#define N 12
#define M 10
int main(void) {
/*
文字列の配列monthを宣言し、月の名前で初期化 */
char
month[N][M]={"January", "February", "March",
"April",
"May",
"June", "July", "August",
"September",
"October", "November", "December"};
int
i;
/*
各文字列の長さを表示 */
for
(i = 0; i < N; i++) {
printf("length
of \"%s\": %d\n", month[i], strlen(month[i]));
}
return
0;
}
【余力がある人は...】
各文字列を文字列が短い順に(文字数が同じ場合は辞書順に)並び替えるプログラムにする。
[解説] 文字列処理関数
[解説] 並べ替え(ソート)のアルゴリズム
《プログラム例》
文字列の長さを得るにはstrlen関数を、辞書順の比較するにはstrcmp関数を使います。詳しくは上の[解説]を参照して下さい。
「ポインタへのポインタ」を使うには、「メモリ領域の動的確保」が必要になってきますので、ここでは取り上げません。