内 容
ポインタ
ポインタ変数を引数とする関数
ポインタ
変数とメモリアドレス
例えば、次のようにchar型、int型、double型の変数を宣言します。この時、メインメモリには、型に応じた応じたサイズの領域が確保されます。下図のように、char型は1バイト、int型は4バイト、double型は8バイトです。
char ch;
int a;
double x;
プログラム中では各変数への値の代入や参照は宣言時に付けられた変数名を利用しますが、実際にはメモリアドレスを使ってメモリ領域への書き込み・読み出しが行われます。複数バイト領域からなるint型やdouble型の場合は、その先頭アドレスが使われます。
Fig2-1.変数のサイズとメモリ割り当て
図中のアドレスは実行時の一例で、実際に配置されるアドレスはオペレーティングシステムなどの環境によって異なります。
アドレス演算子
変数のメモリアドレスは、その変数名の前に
アドレス演算子& を記述することで求めることができます。
[アドレス演算子] &変数名
関数scanf()では、アドレス演算子&を使って、入力された値を変数ではなく、変数のアドレス値を受け取るように引数が定義されていました。これは、引数では関数にその値のコピーを渡すだけで、変数の値は更新されないからです。アドレス値を渡すことで、その変数のメモリ領域への書き込みを実現していたのです。
ポインタ型変数の宣言
ポインタ型変数はメモリアドレスを格納するための変数です。別の変数のアドレス値を格納して、そのアドレス値を使ってその変数を指し示します。ポインタ型変数を利用するためには、他の変数と同様に変数宣言をしなければなりません。ただし、ポインタ型であることを示すために変数名の前に宣言子*を付加します。
[ポインタ型変数の宣言] データ型 *ポインタ変数名
ポインタ型変数の宣言では、ポインタ型ではない変数の宣言とデータ型の指定方法が異なります。文字ならばchar型(1Byte)、整数型ならばint型(4Byte)、浮動小数点型ならばdouble型(8Byte)、と格納するデータのタイプとサイズに合わせてデータ型を指定します。ポインタ型ならばアドレスのサイズ32bit(4Byte)に合わせてint型を指定するところですが、実際にはint型とは限りません。ポインタ型変数では、指し示す変数のデータ型と同じデータ型を記述します。例えば、char型で宣言された変数を指し示すためのポインタならば、データ型にはchar型を指定します。これは
コンパイラに対してポインタが指し示す変数の型のサイズを知らせるためです。
ポインタ型変数の宣言時のデータ型は、指し示す変数の型を書く
ポインタ型の読み方
例えば、int *aと宣言したポインタaの型は、int *型「int型のポインタ」と呼びます。
ポインタ型変数の使い方
ポインタは他の変数のアドレスを持つことで、初めてポインタとして利用することができます。変数にアドレス演算子&を使ってそのメモリアドレスを取得して、ポインタに代入します。
ポインタ変数名 = &変数名
例えば、int型の変数aをポインタ型変数pで指し示すためには以下の例ように記述します。
int a;
int *p; //ポインタの宣言
a = 1000; // 変数aの値を確定
p = &a; // 変数aのアドレスをポインタpに代入
・・・
そうすると、下図のようにポインタpは変数aを指し示すことになります。
間接参照演算子
間接参照演算子*を使うとポインタが指し示す変数の実体(値)を参照することができます。ポインタを使って間接参照できる実体と変数の実体が同一になることから、間接参照演算子*を付与したポインタを変数のエイリアス(別名)として利用することができます。
[間接参照] *ポインタ変数
ポインタ型変数の宣言をするときの宣言子*とは異なります。
*ポインタ変数名は、そのポインタが指し示す変数のエイリアス(別名)
ポインタを理解するための実験
[実験1] 変数のメモリアドレスを取得する(1)
次のソースコードの実行結果を予測してメモリ配置図として描きなさい。次に実際に実行結果を確認して、変数が格納されているメモリのアドレス値を記入しなさい。完成したメモリ配置図が事前の予想と同じになったか確認しなさい。もし、イメージと違っていた場合はその理由について調べて変数とアドレスの関係について納得しなさい。
adressop1.c
#include <stdio.h>
int main(void)
{
int a,b,c;
a = 1000;
b = 2000;
c = 3000;
printf("a = %d (%p)\n", a, &a); //変数aのアドレス
printf("b = %d (%p)\n", b, &b); //変数bのアドレス
printf("c = %d (%p)\n", c, &c); //変数cのアドレス
return 0;
}
[メモリ配置図の例]
[実験2] 変数のメモリアドレスを取得する(2)
次のソースコードを実行すると、char型、int型、double型の各型のメモリアドレスとそのサイズを調べることができます。実行結果をメモリ配置図として描き、型ごとのメモリ配置について確認しなさい。もし、イメージと違っていた場合はその理由について調べて納得しなさい。変数のサイズの取得にはsizeof演算子を、またその書式指定の変換指定子には%luを使います。
adressop2.c
#include <stdio.h>
int main(void)
{
char c;
int a;
double x;
c = 'A';
a = 1000;
x = 0.124;
printf("c = %c (%p) %luByte\n",c,&c,sizeof c); //変数cのアドレスとサイズ
printf("a = %d (%p) %luByte\n",a,&a,sizeof a); //変数aのアドレスとサイズ
printf("x = %f (%p) %luByte\n",x,&x,sizeof x); //変数xのアドレスとサイズ
return 0;
}
[メモリ配置図の例]
[実験3]ポインタ変数の宣言と使い方
アドレス演算子&を使って取得したアドレスを格納するための変数がポインタです。ポインタの基本的な使い方とその書き方について確認しなさい。また、実行結果から変数とポインタのメモリ配置図を描いて実行前のイメージと同じ結果が得られたか確認しなさい。もし、イメージと違っていた場合はその理由について調べて納得しなさい。
pointer1.c
#include <stdio.h>
int main(void)
{
int a;
int *p; //ポインタ変数の宣言
a = 1000;
p = &a; //変数aのメモリアドレスをポインタ変数に代入
printf("変数aの値\t\t: %d\n", a);
printf("変数aのアドレス\t\t: %p\n", &a);
printf("ポインタpの値\t\t: %p\n", p);
printf("ポインタpのアドレス\t: %p\n", &p);
printf("ポインタpの指す値\t: %d\n", *p);
return 0;
}
プログラムの説明
[6行目] ポインタの宣言です。このポインタはその前で宣言しているint型の変数aのアドレスを格納するために使われることから、「int型変数aへのポインタ」と呼びます。型をintにしたのは、指し示す先の変数aの型がint型だからです。
[9行目] 変数aのアドレスをポインタへ代入します。変数宣言の際の*が付いていませんが、ポインタに格納されているアドレス値を扱うときには*は付けません。
[15行目] ポインタpに付けられた記号*は、間接参照演算子 と呼ばれる演算子です。間接参照演算子*を使って、ポインタが指し示す先の変数の値(この例では整数値1000)を示しています。ポインタが指し示す先の値のことを実体と呼びます。
[メモリ配置図の例]
[実験4]ポインタを使って変数の値を変更する
次のソースコードを実行して、間接参照演算子を使ってポインタが指し示す変数の値を変更できることを確認しなさい。また、変数の値の書き換え前後のメモリ配置図を描いて、ポインタが示す変数の値が間接参照によって変更できる理由について確認しなさい。
pointer2.c
#include <stdio.h>
int main(void)
{
int a;
int *p;
a = 123;
p = &a;
printf("a = %d\n",a);
printf("ポインタが指し示す値 %d\n",*p);
*p = 789;
printf("ポインタが指し示す値 %d\n",*p);
printf("a = %d\n",a);
return 0;
}
変数aに代入された値と、ポインタpが指し示している値*pは同じ値になります。そのため、*pを変数aの「別名」もしくは「エイリアス」とも言います。
[実験5]ポインタ変数の型が指し示す変数の型と同じでなければならい理由
[実験3]のプログラムpointer1.cにおいて、ポインタ変数pのデータ型を指し示す変数aの型とは異なるshort int型(サイズ2Byte)に変更した上で、コンパイル、実行して、その結果を確認しなさい。
次に、変数aのデータ型をshort int型に変更して、その実行結果を確認しなさい。
一つのアドレスを格納するためには必要な領域は32bit=4Byteです。ポインタ変数の型が、int *型でもshort int *型でも変わることはありません。ポインタ変数の型に指定されるintやshort intは指し示す先の変数の型で、アドレス計算を実現するためにコンパイラが必要とする情報です。アドレス計算については次回に詳しく見ていきます。
[実験6]ポインタに変数の値を代入しなかった場合の結果について確認する
[実験3]のプログラムにおいて、以下のように1行をコメントした上で、コンパイル後、実行しなさい。また、その結果について考察しなさい。
// p = &a;
NULLポインタ
ポインタが何も指し示していない状態にするためには、ポインタに空を示す定数
NULL を代入します。
ポインタ変数を引数とする関数
関数内で関数外の変数の値を変更する
ローカル変数を別の関数に渡してその値を変更したい場合を考えます。関数の実引数にローカル変数が設定されると、関数にはその値のコピーが渡されます。これを
値渡し(call by value) と言います。関数内でこの値を変更しても、コピーなので元のローカル変数の値が変わることはありません。一方、次のプログラム例のように、関数の仮引数をポインタ型とし、ローカル変数のメモリアドレスを実引数とすることで、関数の呼び出し側と呼び出される側でローカル変数を共有することができます。これを
参照渡し(call by reference) と言います。関数内ではそのポインタが指し示すメモリ領域の値を変更することができ、結果として元のローカル変数の値が変更されることになります。
void twice(int *x)
{
*x = x*2; //2倍
}
int main(void)
{
int a = 0;
twice(&a);
printf("a = %d", a);
return 0;
}
関数内での処理結果を複数個返したい
二つの整数を渡して値を交換する関数swap()について考えてみましょう。
次の関数swap()は交換したい変数の値を引数を使って引き渡しています。関数内では受け取った値の交換を実現していますが、関数の呼び出し側の実引数の値に影響はありません。なぜなら先の例と同様に、関数が引数で受け取った値は実引数のコピーだからです。
illegal swap function
void swap(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
}
関数外の変数の値を変更したければ、引数をポインタ型として宣言して、関数内にその変数のポインタを引き渡すことです。
swap function
void swap(int *a, int *b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
引数が変数とポインタ変数の時のそれぞれのメモリ配置図を以下に示します。
expand_less Back to TOP