一週間で身につくC言語の基本

おさえておきたいプログラミングの基本

【応用編:2日目】 アドレスとポインタ

C言語の難所と言われるアドレスとポインタについての解説です。

2-1.変数のアドレス

(1) 変数にはアドレスがある

基本編で、変数についてとりあげました。実は、変数はコンピュータのメモリ中にあるため、アドレスが存在します。 例えば、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;
}
実行結果(実行する度に変化する)
aの値は100、大きさは4byte、アドレスは0xd7fc94 bの値は123.400000、大きさは8byte、アドレスは0xd7fc84 cの値は123.400002、大きさは4byte、アドレスは0xd7fc78 dの値はa、大きさは1byte、アドレスは0xd7fc6f

(2) sizeof演算子

このプログラムの中に出てくるsizeof(サイズオブ)演算子は、変数や型のメモリのサイズを取得する演算子です。()の中に変数や、型を入れれば、そのサイズをバイト単位で取得することが可能です。

sizeof演算子の使用方法
sizeof(int)   ← int型のサイズを取得 sizeof(a)   ← 変数aのサイズを取得

このプログラムを実行すれば、a,b,c,dの4つの変数の、メモリ空間上で占めるメモリの大きさをバイト数で、そしてそのアドレスを取得できます。 a,b,c,dは、それぞれ違う型の変数ですが、アドレスは整数型として取得されます。printf()関数の中で%x書式で表示すれば、その結果が16進数として表示されます。

実行結果から、各変数には、固有のアドレスとサイズがあることがわかります。(図2-1)

図2-1.変数のアドレス(各変数は、メモリ空間の中に適切な大きさで割り振られている)
変数のアドレス

実行環境によって、変数のアドレスは変わるので、実行結果の数値はこの値と一致しないと思われます。しかし、どの変数にもアドレスがあり、型に依存した固有のサイズがあるという事実は変わりません。

2-2.ポインタ

(1) ポインタとは

このように、変数には値のほかに、その値を格納するアドレスがあることがわかりました。つまり、変数には値とアドレス、二つの側面があるのです。通常の変数は値を入れることを前提としています。

しかし、C言語には、アドレスを入れることを前提とした変数が存在します。それをポインタ変数もしくは、単にポインタと言います。では、そのポインタ変数を利用するにはどうしたらよいのでしょうか。ポインタ変数は例えば以下のように定義をします。

整数型ポインタ変数pの定義(intの場合)
int *p;

このように、先頭に*をつけると、その変数がポインタ変数であることを示すことができます。ではそもそも、ポインタ変数と普通の変数はどう違うのでしょうか。その違いをまとめておきましょう。(表2-1)

表2-1.通常の変数とポインタ変数の比較(intの場合)
通常の変数ポインタ変数解説
宣言int a;int* p:ポインタ変数は、変数の先頭に*をつける
a*pポインタ変数で値を示すには、先頭に*をつける必要がある。
アドレス&ap通常の変数は、値を入れるが前提だが、ポインタ変数はアドレスを入れる

この表から判る通り、ポインタ変数は通常の変数と違い、アドレスを入れることを前提とした変数です。したがって、ここにはなんらかのほかの変数などのアドレスを入れることを前提とします。

(2) ポインタを使ったサンプル

では、このポインタはどのように利用すればよいのでしょうか?まずはその例として、以下のプログラムを実行してみてください。

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);
}
実行結果
a = 100 b = 200 *p = 100 a = 300 b = 200 *p = 300 a = 300 b = 200 *p = 200 a = 300 b = 400 *p = 400

(3) NULLによる初期化

まず、ポインタ変数pにNULL(ヌル)を入れて初期化しています。NULLは、C言語で標準的に用いられる定数で、数値でいえば0を意味しますが、通常ポインタ変数はNULLで初期化するという習慣になっていますので、覚えておきましょう。

ポインタ変数をNULLで初期化
int* p = NULL;

(4) ポインタに変数のアドレスを代入

続いてプログラムの流れに戻りましょう。見て判るとおり、このプログラムには、a,b,pという3種類のint型の変数が使われています。ただ、a,bが通常の値をとる変数であるのに対し、pはポインタ変数です。 そのため、このプログラムの代入処理の流れをまとめると、以下のようになります。(表2-2)

表2-2.a,bとpの変化
番号処理処理内容意味ab*p
p = &a;pに、aのアドレスを代入*pはaと同じものになる100200100
*p = 300;*pに300を代入*pはaに等しいので、aが変わる300200300
p = &b;pに、bのアドレスを代入*pはbと同じものになる300200200
*p = 400;*pに400を代入*pはbに等しいので、bが変わる300400400

まず、①で、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といった、ほかの変数として振舞うことができるのです。

(5) ポインタの注意点

なお、ポインタに変数のアドレスを設定するときは、原則的に同じ型のポインタ変数でするようにしましょう。例えば、「int a」であれば、「int *p」、 「double d」であれば、「double *pd」といったように、対応する型をあわせるようにしましょう。型が違っても、コンパイルエラーにはなりませんが、実行時に致命的なエラーになる可能性があります。

変数のアドレスを入れるポインタと、変数の型は一致させるようにする。
int a → int* p; OK char *p; NG double d → double* pd; OK int *dp; NG

2-3.ポインタと関数

(1) 引数としてポインタ変数をとる関数

前述のように、ポインタには、他の変数になりきれるという非常に面白い特徴があります。これを利用して、以下のような面白い処理を行うことが出来ます。

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;
}
実行結果
a = 1 b = 2 a = 2 b = 1

(2) swap関数の処理の流れ

関数swap()では、アドレスを与えられた二つの変数の値を入れ替えています。通常、今までのように値だけを与えるタイプの今までのような関数 であれば、このような処理は出来ませんでしたが、引数にポインタを与えることにより、アドレスを与えた変数の値を変更することが出来ます。

また通常、変数は一つの戻り値しか返すことが出来ませんが、このように引数をポインタとして渡すことにより、実質的に複数の戻り値をもつ、もしくは 引数を戻りちと同じように扱うことが出来る関数を作ることが可能なのです。(図2-3)

図2-3.swap関数で行われる処理(num1,num2はa,bのアドレスを受け取り、それぞれの変数のように振舞う)
swap関数で行われる処理

通常、関数の引数の中身が変わることはありませんが、ポインタを使うとアドレスを渡すことになるので、引数として与えた変数の値を変えることもできるのです。

2-4.NULLポインタへのアクセス

(1) ポインタ利用時に気をつけること

さて、すでに述べたとおり、ポインタ変数は、NULLで初期化する必要があります。しかし、気をつけてほしいのは、初期化したまま何らかの変数のアドレスを設定しなかった 場合、コンパイルエラーは出ませんが、実行時エラーが出てプログラムは止まってしまいます。以下のサンプルを実行してみてください。

listex2-4:main.c
#include <stdio.h>

int main(int argc, char** argv){
    //  ポインタをNULLで初期化。
	int *p = NULL;
	//	アドレスを指定しないまま値を代入
	*p = 1;
    return 0;
}

コンパイルは通りますが、これを実行すると、プログラムが異常終了します。NULLに限らず、ポインタ変数は、該当するアドレスに値が存在しない場合は、このような エラーになることがよくありますので、注意が必要です。

練習問題 : 問題2.

一週間で学べるコースの一覧
Udemy
...
2024/10/01

Udemyでも学びましょう!

一週間でわかるC言語・C++言語がオンライン講座になりました!動画音声によってさらにわかりやすくなりました!! 1講座で2つの言語を学ぶことができる上に、練習問題の回答もダウンロードできます。

Read →
Impress一週間シリーズ
1週間でC言語の基礎が学べる本
2024/10/01

書籍化された一週間シリーズ

本講座が「1週間でC言語の基礎が学べる本」として書籍化されました!サイトの内容プラスアルファでより学習しやすくなっています!Impressより発売中です!!

Read →
Impress一週間シリーズ
...
2024/10/01

書籍化された一週間シリーズ

一週間シリーズは書籍化されています。こちらもどうぞ!

Read →
プログラマーなら欲しいグッズ
プログラミンググッズ

プログラミンググッズ

快適なプログラミング環境を構築したい人々にぜひとも揃えてほしいグッズです。

Read →
制作・管理
シフトシステム株式会社

シフトシステム株式会社

このサイトはシフトシステム株式会社によって制作・管理がなされています。

Read →