Subsections

第6章 関数 その2 --実引数としてアドレスを用いる--

6.1 本日の課題

  1. 以下のプログラムで, main関数の中で宣言された通常の変数 aと配列 b の要素 b[1] について, 関数func_A, func_B, func_C の呼出の前後での値を答えよ. このことから, a と b[1] では振る舞いが異なることが分かる. a, b[1]のそれぞれについて, その理由を答えよ.
    #include <stdio.h>
    #include <stdlib.h>
    
    void func_A(int x)
    {
    	x = 10;
    }
    
    void func_B(int y[])
    {
    	y[1] = 20;
    }
    
    void func_C(int z)
    {
    	z = 30;
    }
    
    /*
      scanf は引数に & を付けますが, そのような変数を受けとるときは,
      *w のように * を付けます. 呼出側, 受け側ともにアドレスの
      受け渡しを指定しています.
     */
    void func_D(int *w)
    {
    	*w = 40;
    }
    
    int main(void)
    {
    	int a;
    	int b[10];
    
    	a = 1;
    	printf("a=%d\n", a);
    	func_A(a); // 変数の値を渡す.
    	printf("a=%d\n\n", a);
    
    	b[1] = 2;
    	printf("b[1]=%d\n", b[1]);
    	func_B(b); // 配列の名前 (=アドレス)を渡す.
    	printf("b[1]=%d\n\n", b[1]);
    
    	b[1] = 2;
    	printf("b[1]=%d\n", b[1]);
    	func_C(b[1]); // 変数 b[1] の値を渡す.
    	printf("b[1]=%d\n\n", b[1]);
    
    
    	// func_A と比較すると, 
    	// func_D は, 引数に & を付けアドレスに変換している点が異なる.
    	a = 1;
    	printf("a=%d\n", a);
    	func_D( &a ); // 変数のアドレスを渡す.
    	printf("a=%d\n\n", a); // 関数呼び出しを行った後は, 値が変わる.
    	
    	return EXIT_SUCCESS;
    }
    

  2. 以下のプログラムの中から幾つかを作成する. 教科書などを写しただけでは身につかないので, 自ら実現したい機能を考え, それを実現するオリジナルのプログラムを作成すること.

  3. 講義終了時に, 課題1の解答,課題2で作成したプログラム(.cが付く ファイル)をメールで提出する. 本文に, 学籍番号, 氏名を明記すること.

6.2 学習のポイント

値とアドレス
関数の引数として値を渡す場合とアドレスを渡す場合を理解する. [6.3]を参照のこと.

配列の引数
1次元配列(p.130)および多次元配列(p.138)の受け渡しを理解する. ここでは, 関数に配列を渡すとき, 配列のアドレスが関数に渡されていることに注意すること.


6.3 通常の変数の値とアドレス

関数の実引数として, 通常の変数や定数の値を用いることもできるし, それらのアドレスを用いることもできる.

値
関数を呼び出すとき, 変数や定数の値を引数として関数に渡す. 関数宣言部の仮引数の変数に代入されて用いられるので, 呼出側の変数には変化がない.
アドレス
関数を呼び出すとき, 変数または定数の場所(アドレス)を渡す. そのアドレスが, 仮引数に代入される. そのアドレス上の変数の値を用いたり, 変更したりすることができる. 変更を行うと, 呼出側の変数の値も同時に変わる. 詳細は, ポインタ(第10章)で学ぶ.

6.3.1 ポインター

詳細は第10章「ポインタ」ので学習することとして, アドレスやそれを操作するためのポインター変数を理解しておくとよい. 教科書の「配列」の章を, 天下り的でなく, 見通しよく理解できる。

変数を用いる場合には, メモリー(記憶域)上に必要な領域を確保し, そこに変数の値を保存する. 「値」は,そこに保存されている内容であり, 「アドレス」は, 保存されている場所である. 「ポインタ変数」はアドレスを格納するための変数である. 表1に通常の変数とポインター変数を並べて示す.


Table 1: 通常の変数とポインター変数
  変数 ポインタ−変数
宣言 int a; int *p;
値 a *p;
アドレス &a p


さらに, 配列まで含めたときの変数とアドレスの関係を2に示す.


Table 2: 変数とアドレス
  変数 ポインタ変数 配列としてのポインタ変数 配列要素 配列
変数 int a; int *a; int *a; int a[10]; int a[10];
アドレス &a a &a[5] &a[5] a
  int b; のとき int b; int b; int b;のとき int *b; のとき
    int c; int c[10];    
    a = &c;のとき a = c; のとき    
代入 a = 3; *a = 3; a[5]=3; a[5] = 3;  
参照 b = a; b = *a; b = a[5]; b=a[5]; b = a;


6.3.2 アドレスを仮引数とする関数呼び出し

アドレスを仮引数とする関数呼び出しでは, 呼び出し側の変数の値が変わる. 従って, 変数の遮蔽が行えないことに注意する必要がある.

6.3.2.1 通常の変数

ポインタの章を参照のこと.

以下に, アドレスを仮引数とする関数呼び出しの例を示す.

// ポインタ変数を引数とする関数

#include <stdio.h>

void func(double *x)
{
	 printf("(2) &x = %p\n", &x); // x のアドレス.
	 printf("(2) sizeof(x) = %u\n", sizeof(x)); // x のアドレス.
	 printf("(2)  x = %p\n",  x); // xの値 (ここでは a のアドレス)
	 printf("(2) *x = %f\n", *x); // *x の値.

	 *x = 20.0;

	 printf("(2) *x = %f\n", *x); // *x の値.
}

int main(void)
{
	 double a = 3.0;

	 printf("(1) a = %f\n",  a); // a の値
	 printf("(1) &a = %p\n", &a); // a のアドレス
	 printf("(1) sizeof(a) = %u\n", sizeof(a)); // a のサイズ

	 func(&a); // funcの引数のxに aのアドレス&a が代入される

	 printf("(3) a = %f\n",  a);

	 return 0;
}

  1. main関数内で, 通常の変数 int a;を宣言している. この変数の値はaアドレスは&aで得られる. printfの書式には, アドレスを出力するときは %pを用いる.
  2. func(&a);により関数funcを呼び出している. ここで, 変数名aの前に&を付けることにより, 関数funcに変数aのアドレスを渡している.
  3. 関数funcは, 仮引数としてint *xを受け取っている. ここでxは, アドレスを受け取るポインタ変数である. ここでは, main関数中の変数aのアドレスを受け取り, x に aのアドレスが代入される.
  4. ポインタ変数xの前に*を付けることで, xが指すアドレスに格納されている値を参照したり, 代入したりすることができる. *xが通常の変数に相当する. ここでは, xは, main関数中のaを指しているので aに代入が起き, main関数中のaの内容が書き変わる.

6.3.2.2 配列

大きな配列を操作する場合に, 全ての配列要素の値を関数に渡し, 全ての配列要素の値を返すのは効率が悪い. この場合, 配列の実体は一度だけ用意し, それを操作するのがよい.

次に, 配列を引数とする関数を示す. 配列名を引数とする場合, 配列のアドレスを関数に渡すことになる.

#include <stdio.h>

#define NUMBER 5

/* 配列の各要素に100を加える関数 */
void func(double a[NUMBER])
{
	 int i;

	 for(i=0; i<NUMBER; i++)
		  a[i] += 100.0;
}

int main(void)
{
	 int i;
	 double x[NUMBER] = {0.0}; // 全ての要素を 0.0 に初期化.

	 for(i=0; i<NUMBER; i++)
		  printf("x[%d] = %g\n", i, x[i]);

	 func(x);

	 for(i=0; i<NUMBER; i++)
		  printf("x[%d] = %g\n", i, x[i]);
	 
	 return 0;
}

  1. main関数で, double x[NUMBER]; を宣言し, func(x);のように関数funcを呼び出している. この時xは, 配列x[]の先頭のアドレスを表している. したがって, 関数funcに配列x[]の先頭のアドレスを渡していることになる. 関数funcに配列の全ての要素の値を渡しているわけではない.

  2. 関数funcは, void func(double a[NUMBER])と宣言されている. ここで, 仮引数double a[NUMBER]のaはポインター変数でアドレスを受け取り, a に main 関数中の配列 int x[NUMBER]の先頭のアドレスが代入される. ここで, double a[NUMBER]という double型で NUMBER個の配列の値を保持するための領域は, 新たに確保されないことに注意する必要がある.
  3. 関数func内では, a[i]に代入を行っている. aはアドレスを保持するポインタ変数で, 添字演算子[]を付けることで, aが指すアドレスから i進んだアドレスの値を意味する. これにより, 値の変更や参照を行う. ここでは, 配列x[]の値が変更される.


6.4 注意: 仮引数が const int mat[2][3]のときの関数呼出し (List6-17)

List6-17のconstの使い方は, C言語の規格上は誤りである.

List6-17はgcc でコンパイルすると, 以下のメッセージが表示される.

List6-17org.c: In function ‘main’:
List6-17org.c:19: warning: passing argument 1 of ‘mat_add’ from incompatible pointer type
List6-17org.c:19: warning: passing argument 2 of ‘mat_add’ from incompatible pointer type
これは, 19行目でmat_add 関数を呼び出す際の実引数maおよび mbと, 3行目の関数定義で使われている仮引数ma およびmbの型と異なることが原因である.

(c)1999-2013 Tetsuya Makimura