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のアドレスを受け取り, xaのアドレスが代入される.
  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