●はじめに
こんにちは。
今回は、C言語で皆さんが最もつまずくとされているポインタを実際に使ったプログラムを提示していきたいと思います。
私もポインタは概念が難しく理解するのに時間がかかりましたがわかりやすく解説していきます。
「え、そもそもC言語とか初めてなんだけど。。。」
「プログラミングの基礎がわからない。」
という方向けの記事ではないので以下のサイトの基本編を学んでからこの記事を読んでください。
→一週間で身につくC言語の基本
「C言語の関数くらいまでならできるよー」
って方でポインタにつまずいている方は是非、読んでください。
また、前回はポインタの概念と簡単な使い方まで説明したので読んでいない方はこちらをご覧ください。
→【C言語】難解なポインタに挑む「忍法写し身の術!」その1
●関数の引数としてのポインタ変数
上記の記事で説明したように、ポインタ変数には、他の変数になりきれる(忍法写し身の術!ができる)という非常に面白い特徴があります。これを利用して、以下のような処理を行うことができます。
#include <stdio.h>
// 変数の値入れ替えを行う関数
void swap(int*, int*);
int main(int argc, char** argv) {
int a = 1, b = 2;
printf("a = %d b = %d\n", a, b);
swap(&a, &b);
printf("a = %d b = %d\n", a, b);
return 0;
}
// 値の入れ替え
void swap(int* num1, int* num2) {
int temp = *num1;
*num1 = *num2;
*num2 = temp;
}
実行すると以下のような結果になると思います。
a = 1 b = 2
a = 2 b = 1
このプログラムはswap関数の引数にポインタを渡しています。このように、引数にポインタを渡すことをポインタ渡しといいます。これに対し、従来のように値を引数として渡すことを値渡しといいます。
プログラムを実行すると、最初に整数型変数a、bがそれぞれ1、2という値で初期化されます。その次に、swap関数で変数a、bのアドレスを引数として呼び出すと、変数a、bの値が入れ替わっていることがわかります。
つまり、swap関数では、アドレスを与えられた2つのポインタ変数の値を入れ替えているのです。今までのように値だけを与えるタイプのような関数であれば、このような処理はできませんでしたが、引数にポインタを与えることにより、与えた変数の値を変更することができます。これが写し身の強みですよね。
また通常、変数は1つの戻り値しか返すことができませんが、このように引数をポインタ変数として渡すことにより、実質的に複数の戻り値を持つ、もしくは引数を戻り値と同じように扱うことができる関数を作ることが可能なのです。ポインタ変数の引数の数だけ実質的な戻り値が作れます。
・swap関数の処理
番号 | *num1 | *num2 | temp | 処理の概要 |
---|---|---|---|---|
① | 1 | 2 | 1 | tempに*num1を代入 |
② | 2 | 2 | 1 | *num1に*num2を代入 |
③ | 2 | 1 | 1 | *num2にtempを代入 |
swap関数にはmain関数の変数a、bのアドレスが渡され、それがポインタ変数num1及びnum2に代入されます。
いったん、変数tempに*num1(aの値に該当)を代入します(①)。
次に*num1に*num2(bの値に該当)を代入します。これにより、aの値がbと等しくなります(②)。
次に、*num2にtempを代入することにより、bにaの値が入ります(③)。これにより、最終的にa、bの中身が入れ替わるのです。
main関数内にswap関数の処理を書けば、ポインタ変数を使わずに同じ機能を実装できますが、保守性を考えるとswap関数を用いた方が良いのでポインタ変数が大変役に立っているのです。
●ポインタ変数と配列変数の関係性
上記のように、ポインタ変数が、他の変数のアドレスを指定することにより、その変数に写し身することができ、それにより1つのポインタで様々な値を設定したり取得したりすることを学びました。この特性は、配列変数に適用すると、より効果が発揮できるのです。そのことについて説明する前に、配列とポインタの関係性について学んでみましょう。
まずは、以下のプログラムを入力してみてください。
#include <stdio.h>
// #defineマクロで定義
#define SIZE 5
int main(int argc, char** argv) {
// サイズがSIZEの配列を用意する
int ar1[SIZE];
char ar2[SIZE];
int i;
int* p1 = NULL;
char* p2 = NULL;
// 値を代入
for (i = 0; i < SIZE; i++) {
ar1[i] = i;
ar2[i] = 'A' + i;
}
// ポインタにアドレスを代入
p1 = &ar1[0];
p2 = &ar2[0];
// 値を出力
for (i = 0; i < SIZE; i++) {
printf("ar1[%d]=%d *(p1+%d)=%d ",i, ar1[i], i, *(p1+i));
printf("ar2[%d]=%c *(p2+%d)=%c\n",i, ar2[i], i, *(p2+i));
}
return 0;
}
実行すると以下のような結果になると思います。
ar1[0]=0 *(p1+0)=0 ar2[0]=A *(p2+0)=A
ar1[1]=1 *(p1+1)=1 ar2[1]=B *(p2+1)=B
ar1[2]=2 *(p1+2)=2 ar2[2]=C *(p2+2)=C
ar1[3]=3 *(p1+3)=3 ar2[3]=D *(p2+3)=D
ar1[4]=4 *(p1+4)=4 ar2[4]=E *(p2+4)=E
SIZEの値を5にすることにより、int型の配列変数ar1と、char型の配列変数ar2がそれぞれ宣言されます。値の代入は、上の方のfor文の中で行なっています。ar1はこのforループの中でiの値である0〜4をar1[0]〜ar1[4]にそのまま代入します。
これに対し、char型の配列変数ar2の要素には、'A'+iという値を代入しています。'A'は、アルファベットの大文字AのASCIIコードを表し、iの値を1、2、3と値に変化させると、'B'、'C'、'D'、...という風に変化していきます。これはASCIIコードでは、A、B、C、D、...という文字のコードが連続しているためです。
次に、ポインタ変数へのアドレスの代入を見てみましょう。
p1 = &ar1[0];
p2 = &ar2[0];
p1、p2はそれぞれint型、char型のポインタ変数です。ar1、ar2は配列変数なので、&ar1[0]と&ar2[0]はそれぞれ、その配列の先頭の変数となるアドレスなのです。
このとき、p1+1は、p1の次のアドレス、つまり、配列で言うと、&ar1[1]に該当し、p2+1も同様に、&ar2[1]と等しくなるのです。p1を例にして表にすると、以下のようになります。
・ポインタ変数と配列変数の関係性
配列の要素 | 配列変数のアドレス | 該当するポインタ | ポインタ変数の値 |
---|---|---|---|
ar[0] | &ar[0] | p1 | *p1 |
ar[1] | &ar[1] | p1+1 | *(p1+1) |
ar[2] | &ar[2] | p1+2 | *(p1+2) |
ar[3] | &ar[3] | p1+3 | *(p1+3) |
ar[4] | &ar[4] | p1+4 | *(p1+4) |
例ではar1、p1を取り上げていますが、ar2、p2についても同様です。p1、p2はそれぞれint、charであることから、1つの変数のデータのサイズは違いますが、ポインタ変数に1を足すと、その型のサイズ分だけ後ろのアドレスに移行し、逆に1を引くと前に移行します。これは配列の番号1つ分だけ移動することになるのです。
このように、ポインタには、整数を足してアドレスを移動することができますが、それは、アドレス化した配列変数の要素の番号を変えるのと等しいことなのです。例えば、配列変数aとポインタ変数pの対応は、以下のようになります。
int a[5] = {1, 2, 3, 4, 5};
int *p = &a[2];
・配列aの値とポインタ変数pの関係
配列の要素 | a[0] | a[1] | a[2] | a[3] | a[4] |
---|---|---|---|---|---|
該当するポインタ変数 | p-2 | p-1 | p | p+1 | p+2 |
ポインタ変数の値 | *(p-2) | *(p-1) | *p | *(p+1) | *(p+2) |
このように、ポインタと配列変数は相性が良いのです。
ポインタに1を足すとアドレス化した配列変数の要素の番号も1増えるというのがイマイチ掴みづらいかもしれませんが非常に便利なので覚えておいてください。次回、この仕組みを有効活用していきたいと思います。
【投稿者】
エンジニアファーストの会社 株式会社CRE-CO 田渕浩之
【参考文献】
→1週間でC言語の基礎が学べる本