おさえておきたいプログラミングの基本
C言語の難所と言われるアドレスとポインタについての解説です。
基本編で、変数についてとりあげました。実は、変数はコンピュータのメモリ中にあるため、アドレスが存在します。 例えば、aという変数があると、&aとすることにより、変数のアドレスを取得することができます。 これにより、変数の値がメモリ中のどこに存在するのかを知ることができます。
listex2-1:main.c#include <stdio.h>
int main(int argc,char** argv){
int a = 100; // int型の変数
double b = 123.4; // double型の変数
float c = 123.4f; // float型の変数(数値の後ろにfつける)
char d = 'a'; // char型の変数
printf("aの値は%d、大きさは%dbyte、アドレスは0x%x\n",a,sizeof(int),&a);
printf("bの値は%f、大きさは%dbyte、アドレスは0x%x\n",b,sizeof(double),&b);
printf("cの値は%f、大きさは%dbyte、アドレスは0x%x\n",c,sizeof(float),&c);
printf("dの値は%c、大きさは%dbyte、アドレスは0x%x\n",d,sizeof(char),&d);
return 0;
}
実行結果(実行する度に変化する)
このプログラムの中に出てくるsizeof(サイズオブ)演算子は、変数や型のメモリのサイズを取得する演算子です。()の中に変数や、型を入れれば、そのサイズをバイト単位で取得することが可能です。
sizeof演算子の使用方法このプログラムを実行すれば、a,b,c,dの4つの変数の、メモリ空間上で占めるメモリの大きさをバイト数で、そしてそのアドレスを取得できます。 a,b,c,dは、それぞれ違う型の変数ですが、アドレスは整数型として取得されます。printf()関数の中で%x書式で表示すれば、その結果が16進数として表示されます。
実行結果から、各変数には、固有のアドレスとサイズがあることがわかります。(図2-1)
図2-1.変数のアドレス(各変数は、メモリ空間の中に適切な大きさで割り振られている)実行環境によって、変数のアドレスは変わるので、実行結果の数値はこの値と一致しないと思われます。しかし、どの変数にもアドレスがあり、型に依存した固有のサイズがあるという事実は変わりません。
このように、変数には値のほかに、その値を格納するアドレスがあることがわかりました。つまり、変数には値とアドレス、二つの側面があるのです。通常の変数は値を入れることを前提としています。
しかし、C言語には、アドレスを入れることを前提とした変数が存在します。それをポインタ変数もしくは、単にポインタと言います。では、そのポインタ変数を利用するにはどうしたらよいのでしょうか。ポインタ変数は例えば以下のように定義をします。
整数型ポインタ変数pの定義(intの場合)このように、先頭に*をつけると、その変数がポインタ変数であることを示すことができます。ではそもそも、ポインタ変数と普通の変数はどう違うのでしょうか。その違いをまとめておきましょう。(表2-1)
表2-1.通常の変数とポインタ変数の比較(intの場合)通常の変数 | ポインタ変数 | 解説 | |
---|---|---|---|
宣言 | int a; | int* p: | ポインタ変数は、変数の先頭に*をつける |
値 | a | *p | ポインタ変数で値を示すには、先頭に*をつける必要がある。 |
アドレス | &a | p | 通常の変数は、値を入れるが前提だが、ポインタ変数はアドレスを入れる |
この表から判る通り、ポインタ変数は通常の変数と違い、アドレスを入れることを前提とした変数です。したがって、ここにはなんらかのほかの変数などのアドレスを入れることを前提とします。
では、このポインタはどのように利用すればよいのでしょうか?まずはその例として、以下のプログラムを実行してみてください。
listex2-2:main.c#include <stdio.h>
void show(int,int,int);
int main(int argc,char** argv){
int a = 100; // 整数型変数a
int b = 200; // 整数型変数b
int *p = NULL; // 整数型のポインタ変数p
p = &a; // pにaのアドレスを代入
show(a,b,*p);
*p = 300; // *pに値を代入
show(a,b,*p);
p = &b; // pにbのアドレスを代入
show(a,b,*p);
*p = 400; // *pに値を代入
show(a,b,*p);
}
void show(int n1,int n2,int n3){
printf("a = %d b = %d *p = %d\n",n1,n2,n3);
}
実行結果
まず、ポインタ変数pにNULL(ヌル)を入れて初期化しています。NULLは、C言語で標準的に用いられる定数で、数値でいえば0を意味しますが、通常ポインタ変数はNULLで初期化するという習慣になっていますので、覚えておきましょう。
ポインタ変数をNULLで初期化続いてプログラムの流れに戻りましょう。見て判るとおり、このプログラムには、a,b,pという3種類のint型の変数が使われています。ただ、a,bが通常の値をとる変数であるのに対し、pはポインタ変数です。 そのため、このプログラムの代入処理の流れをまとめると、以下のようになります。(表2-2)
表2-2.a,bとpの変化番号 | 処理 | 処理内容 | 意味 | a | b | *p |
---|---|---|---|---|---|---|
① | p = &a; | pに、aのアドレスを代入 | *pはaと同じものになる | 100 | 200 | 100 |
② | *p = 300; | *pに300を代入 | *pはaに等しいので、aが変わる | 300 | 200 | 300 |
③ | p = &b; | pに、bのアドレスを代入 | *pはbと同じものになる | 300 | 200 | 200 |
④ | *p = 400; | *pに400を代入 | *pはbに等しいので、bが変わる | 300 | 400 | 400 |
まず、①で、aのアドレスを取得することにより、pは、aとして振舞うことが可能になります。そのため、値である*pは、aと同じ値をとります。次に、②で、*pに値を入れると、pには実体は無く、aをさしていることから、その値はaに反映されます。
同様に、③で、pにaのアドレスを代入すると、今度は、pはbとして振舞うことが可能になります。値である*pは、今度はbと同じ値をとっています。更に、④で、*pに値を入れると、今度はbの値が変わります。これは②の時と同様で、*pが実際にはbの値となるからです。(図2-2)
図2-2.ポインタのイメージ(ポインタは、アドレスを変更することにより、他の変数になれる)このように、ポインタ変数は、初期状態でそれ自身は値を持ちませんが、変数のアドレスを与えることにより、あたかもその変数であるかのごとく振舞うことが出来るのです。したがって、このサンプルのように、変数名はpしかないのにもかかわらず、aやbといった、ほかの変数として振舞うことができるのです。
なお、ポインタに変数のアドレスを設定するときは、原則的に同じ型のポインタ変数でするようにしましょう。例えば、「int a」であれば、「int *p」、 「double d」であれば、「double *pd」といったように、対応する型をあわせるようにしましょう。型が違っても、コンパイルエラーにはなりませんが、実行時に致命的なエラーになる可能性があります。
変数のアドレスを入れるポインタと、変数の型は一致させるようにする。前述のように、ポインタには、他の変数になりきれるという非常に面白い特徴があります。これを利用して、以下のような面白い処理を行うことが出来ます。
listex2-3:main.c#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;
}
実行結果
関数swap()では、アドレスを与えられた二つの変数の値を入れ替えています。通常、今までのように値だけを与えるタイプの今までのような関数 であれば、このような処理は出来ませんでしたが、引数にポインタを与えることにより、アドレスを与えた変数の値を変更することが出来ます。
また通常、変数は一つの戻り値しか返すことが出来ませんが、このように引数をポインタとして渡すことにより、実質的に複数の戻り値をもつ、もしくは 引数を戻りちと同じように扱うことが出来る関数を作ることが可能なのです。(図2-3)
図2-3.swap関数で行われる処理(num1,num2はa,bのアドレスを受け取り、それぞれの変数のように振舞う)通常、関数の引数の中身が変わることはありませんが、ポインタを使うとアドレスを渡すことになるので、引数として与えた変数の値を変えることもできるのです。
さて、すでに述べたとおり、ポインタ変数は、NULLで初期化する必要があります。しかし、気をつけてほしいのは、初期化したまま何らかの変数のアドレスを設定しなかった 場合、コンパイルエラーは出ませんが、実行時エラーが出てプログラムは止まってしまいます。以下のサンプルを実行してみてください。
listex2-4:main.c#include <stdio.h>
int main(int argc, char** argv){
// ポインタをNULLで初期化。
int *p = NULL;
// アドレスを指定しないまま値を代入
*p = 1;
return 0;
}
コンパイルは通りますが、これを実行すると、プログラムが異常終了します。NULLに限らず、ポインタ変数は、該当するアドレスに値が存在しない場合は、このような エラーになることがよくありますので、注意が必要です。
練習問題 : 問題2.
一週間でわかるC言語・C++言語がオンライン講座になりました!動画と音声によってさらにわかりやすくなりました!! 1講座で2つの言語を学ぶことができる上に、練習問題の回答もダウンロードできます。
Read →本講座が「1週間でC言語の基礎が学べる本」として書籍化されました!サイトの内容プラスアルファでより学習しやすくなっています!Impressより発売中です!!
Read →