C言語編 第33章 ポインタB(引数や戻り値への利用)

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

大きなデータを渡す

ここからしばらく、ポインタを引数や戻り値に使うことについて説明していきます。 まずは、大きなデータの受け渡しを目的とする利用について見ていきましょう。

関数に引数を渡すことや、戻り値を返すことも、当然ながら、プログラムを実行するときに処理時間を必要としています。 普通の代入操作と同様、引数や戻り値の受け渡しはコピー操作に他なりませんから、基本的にはデータ量が大きい程、処理時間を多く消費します

C言語において、大きなデータといえば、配列や構造体が思い浮かびます。 しかしこれまでに何度も書いているように、配列はそのまま関数に受け渡しすることはできません

int array[1000];
func( array );

もしこのように func関数を呼び出したとすれば、引数の型は int型配列ではなく、int*型なのです。 式の中で配列名だけが現れたとき、自動的にポインタに変換される訳です。

一方、構造体はそのまま引数や戻り値にできます

struct Student_tag s;
func( s );

しかし、大抵の構造体はそれなりのデータ量を持っています。 複数のメンバを含んでいれば当然です。 もし、非常に大きい構造体であれば、引数や戻り値に直接、構造体を利用するのは避け、 代わりに構造体を指すポインタを使うべきです

struct Student_tag s;
func( &s );

func関数の中では、->演算子を使ってメンバにアクセスできます。

#include <stdio.h>
#include <string.h>

#define STUDENT_NAME_LEN 32         /* 生徒の名前データの最大長 */

/* 生徒のデータ */
typedef struct {
	char  name[STUDENT_NAME_LEN];   /* 名前 */
	int   grade;                    /* 学年 */
	int   class;                    /* 所属クラス */
	int   score;                    /* 得点 */
} Student;


void printStudentData(Student* student);

int main(void)
{
	Student student;

	strcpy( student.name, "Saitou Takashi" );
	student.grade = 2;
	student.class = 3;
	student.score = 80;

	printStudentData( &student );
	
	return 0;
}

/*
	生徒のデータを出力する。
	引数:
		student: 出力するデータを集めた構造体変数。
*/
void printStudentData(Student* student)
{
	printf( "name: %s\n", student->name );
	printf( "grade: %d\n", student->grade );
	printf( "class: %d\n", student->class );
	printf( "score: %d\n", student->score );
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

配列を渡したい

配列を関数に渡すことは出来ないという話ですが、まず、やるとしたらどうなるのか見ておきましょう。

#include <stdio.h>

#define ARRAY_SIZE 5

void printArray(int array[ARRAY_SIZE]);

int main(void)
{
	int array[ARRAY_SIZE] = { 0, 1, 2, 3, 4 };

	printArray( array );
	
	return 0;
}

/*
	配列の各要素を出力する。
*/
void printArray(int array[ARRAY_SIZE])
{
	int i;

	for( i = 0; i < ARRAY_SIZE; ++i ){
		printf( "%d ", array[i] );
	}
	printf( "\n" );
}

実行結果:

0 1 2 3 4

「あれ? ちゃんと動くじゃないか」と思うでしょうが、動くには動くのです。 ただ、配列を渡せてはおらず、いつものようにポインタに変換されてしまっています。

ここで、少し改造して確かめておきましょう。 配列をそのまま渡しているのだとすれば、printArray関数の中で、array の要素を書き換えても、呼び出し側には影響を与えないはずです。

#include <stdio.h>

#define ARRAY_SIZE 5

void printArray(int array[ARRAY_SIZE]);

int main(void)
{
	int array[ARRAY_SIZE] = { 0, 1, 2, 3, 4 };

	printArray( array );
	printf( "%d\n", array[3] );  /* 配列のコピーを渡したなら 3、アドレスを渡したなら 999 になるはず */
	
	return 0;
}

/*
	配列の各要素を出力する。
*/
void printArray(int array[ARRAY_SIZE])
{
	int i;

	for( i = 0; i < ARRAY_SIZE; ++i ){
		printf( "%d ", array[i] );
	}
	printf( "\n" );
	
	array[3] = 999;
}

実行結果:

0 1 2 3 4
999

実行結果にあるように、printArray関数の中で代入した 999 が、呼び出し元の方の array に影響を与えていることが分かります。 これは、配列のコピーを渡しているのではなく、アドレスを渡しているからです。
もっと直接的に確かめることもできるでしょう。

#include <stdio.h>

#define ARRAY_SIZE 5

void printArray(int array[ARRAY_SIZE]);

int main(void)
{
	int array[ARRAY_SIZE] = { 0, 1, 2, 3, 4 };

	printArray( array );
	printf( "%p\n", array );
	
	return 0;
}

/*
	配列の各要素を出力する。
*/
void printArray(int array[ARRAY_SIZE])
{
	int i;

	for( i = 0; i < ARRAY_SIZE; ++i ){
		printf( "%d ", array[i] );
	}
	printf( "\n" );
	
	printf( "%p\n", array );
}

実行結果:

0 1 2 3 4
002EFE3C
002EFE3C

printArray で出力した array のアドレスと、呼び出し元の array のアドレスが一致していることが分かりますね。 これが答え。関数へは、配列のコピーではなくて、アドレスが渡されています


結局、配列を関数にそのまま渡すことはできないので、

void func(int array[]);
void func(int* array);

仮引数では、上のように [] を使って配列形式で記述しても、* を使ってポインタ形式で記述しても同じ結果になります
なお、[] の中の要素数は省略しても構いません。 結局はポインタに変換されてしまうため、要素数を書いても実は無意味です。

今回のサンプルプログラムでは、ARRAY_SIZEマクロに配列の要素数を定義してありますが、これを使わない場合には、 関数に要素数もセットにして渡す必要があります。


なお、少し裏ワザ的な方法ですが、配列をそのまま関数に渡す手段も無い訳ではありません。 構造体なら、引数にそのまま使えるのですから、配列を構造体で包んであげればいいのです。

struct MyStruct {
    int array[1000];
};

struct MyStruct s;
func( s );

これなら一応可能です。

関数に値を書き換えてもらう

ここでは、2つの引数を渡して、値を交換する関数を作ってみましょう。 第28章の練習問題Bでは、これをマクロで定義しましたが、今回は関数です。

#include <stdio.h>

void swap(int* a, int* b);

int main(void)
{
	int num1 = 10;
	int num2 = 20;

	swap( &num1, &num2 );
	printf( "%d %d\n", num1, num2 );
	
	return 0;
}

/*
	2つの int型変数の値を交換する。
*/
void swap(int* a, int* b)
{
	int work = *a;
	*a = *b;
	*b = work;
}

実行結果:

20 10

swap関数は、引数に、2つの int型のポインタを持ちます。 実引数に、int型変数のアドレスを渡せば、それぞれの変数の値が交換されます。

swap関数は、呼び出し元が持っている変数の値を、関数の中で書き換えている訳です。 これもポインタの利用法の1つです。

戻り値が複数欲しいとき

C言語の関数では、戻り値は 0個(void)か、1個のいずれかです。 しかし、どうしても2個以上の情報を返したい場面はよくあります。

例えば、次のような表があるとします。

12345
678910
1112131415
1617181920
2122232425

このとき、1〜25 の値を指定し、その数値がある行と列の番号を返すような関数を作るとしましょう。

#include <stdio.h>

void getColAndRow(int num, int* col, int* row);

int main(void)
{
	int col, row;

	getColAndRow( 17, &col, &row );
	printf( "%d %d\n", col, row );

	getColAndRow( 21, &col, &row );
	printf( "%d %d\n", col, row );
	
	return 0;
}

/*
	表の中の num のある位置の行番号と列番号を返す。
	引数:
		num:	表の中にある番号。1〜25
		col:	行番号を受け取るアドレス。0基準
		row:	列番号を受け取るアドレス。0基準
*/
void getColAndRow(int num, int* col, int* row)
{
	*col = (num - 1) % 5;
	*row = (num - 1) / 5;
}

実行結果:

1 3
0 4

行番号と列番号を 1つの関数で返そうとしても、戻り値だけでは 2つの情報を返せません。 そこで、ポインタを利用して、引数経由で結果を返してもらっています。
この手法は言うなれば、関数に結果を入れる入れ物だけを渡して、「結果はここに入れておいてくれ」という感覚です。

ところで、getColAndRow関数は色々と制約を持っています。 引数num は 1〜25 でなければならず、col と row も確実に有効なアドレスを渡さないといけません。 もし、

getColAndRow( 30, NULL, NULL );

こんな実引数を指定しようものなら、実行時エラーになってしまうでしょう。 こういう制約は、プログラムコードそのものを使ってチェックしてやるべきです。
例えば、assertマクロを使って停止させるのも手です。

void getColAndRow(int num, int* col, int* row)
{
	assert( 1 <= num && num <= 25 );
	assert( col != NULL );
	assert( row != NULL );

	*col = (num - 1) % 5;
	*row = (num - 1) / 5;
}

必ず守られなければならない前提条件のようなものは、assert でチェックするようなクセを付けておくと、大きなプログラムを書く際に助けになります。 もちろん、エラーであるという情報を返すという選択肢もありますが、その場合は、呼び出し側でのチェックが必要になります。
次のように作るのも手段の1つです。

int getColAndRow(int num, int* col, int* row)
{
	/* num が適切な範囲にあるか */
	if( num < 1 || 25 < num ){
		return 0;
	}

	if( col != NULL ){
		*col = (num - 1) % 5;
	}
	if( row != NULL ){
		*row = (num - 1) / 5;
	}
	return 1;
}

少し脱線気味ですが、要するに、手段は幾つもあるのだから色々考えましょうという話でした。


ちなみに、関数から 2つ以上の情報を返す方法は他にも考えられます。

  1. ポインタを渡して、結果を格納してもらう。
  2. 構造体を定義し、関数内で構造体変数を宣言、そこに結果を入れて、普通に戻り値で返す。
  3. グローバル変数を用意しておき、そこに結果を格納する。

構造体を作る方法は、実際にそうすることもまれにありますが、ポインタを使う方がずっと楽です。 戻り値のためだけに構造体を定義するのは、少々面倒なのです。

グローバル変数を経由させる方法は、はっきりいって最低の部類に入る方法なので、絶対に避けましょう。

ローカル変数を返すとき

いきなりですが、次のプログラムは正しく動作するでしょうか?

#include <stdio.h>
#include <assert.h>

char* getString(int max);

int main(void)
{
	char* str;
	
	str = getString( 5 );
	puts( str );  /* 受け取った文字列を出力 */
	
	return 0;
}

/*
	標準入力から max以下の文字数の文字列を受け取る。
	引数:
		max:	最大文字数。1〜255
	戻り値:
		受け取った文字列。
*/
char* getString(int max)
{
	char str[256];

	assert( 1 <= max && max <= 255 );

	printf( "%d文字以内の文字列を入力して下さい。\n", max );
	fgets( str, max+1, stdin );		/* 改行文字のことを考えて +1 */
	
	return str;
}

実行結果:

5文字以内の文字列を入力して下さい。
abcdefg
abcde

「実行結果」を書いてあるぐらいだから、正しく動作するんだろうと考えるかも知れませんが、これは保証はありません。 このプログラムは、非常によくありがちなミスが混ざっているのです。

問題なのは、getString関数の戻り値にあります。 return文が返している str は、getString関数内で宣言されたローカル変数ですから、 getString関数から抜け出してしまった後は、もうメモリ上に残っている保証はないのです

最近のコンパイラであれば、このプログラムはコンパイル時に警告ぐらいは報告してくれるでしょうが、 それもコンパイラ次第ですし、警告であれば実行できてしまいます。
こういうエラーレベルの警告もあるので、警告は無視してはいけません。

今回のような、ローカル変数のアドレスを返す間違いへの対応策としては、

  1. そもそもやめる。
  2. 静的ローカル変数に変更する。
  3. グローバル変数に結果を格納するように変更する。
  4. グローバル変数に結果を格納して、そのアドレスを返すように変更する。
  5. 動的変数を使う(第34章で説明)

といったものがあります。 例によって、グローバル変数を使う方法はやめましょう。トラブルを増やすだけです。

静的ローカル変数(第22章)を使う方法は、うまく動いてくれますが、 この方法には問題もあります。

例えば、関数が 2回呼び出されると、1回目のときの結果が格納されている静的ローカル変数を上書きしてしまいます。 これは、関数を使う側が注意を払わないといけなくなり、あまり好ましい作りとは言えないでしょう。

標準関数の中にも、strtok関数(⇒リファレンス)のように、 内部にある静的変数のアドレスを返すものもありますが、やはりよく問題に上がります。 特に、マルチスレッドという高度なプログラミングスタイルを使う場合には注意が必要になってきます。

動的変数を使う方法は、うまく動いてくれますが、これは次章で改めて説明します。

「そもそもやめる」という選択肢も有力です。 この場合、結果を受け取る場所を関数外で用意して、そのアドレスを引数に渡します。 これは結局、「引数・戻り値とポインタB(戻り値が複数欲しいとき)」で見た手法と同じことです。


練習問題

問題@ 関数から情報を返す方法として、グローバル変数を経由させる方法がよくない理由を答えて下さい。

問題A strcpy関数(⇒リファレンス)を自作して下さい。

問題B strcmp関数(⇒リファレンス)を自作して下さい。


解答ページはこちら

参考リンク

更新履歴

'2009/11/26 新規作成。



前の章へ

次の章へ

C言語編のトップページへ

Programming Place Plus のトップページへ