関数の基本 編集

C言語のプログラムは全て1つ以上の「関数」(かんすう)と呼ばれる処理のまとまりから構成されます。

関数を用いて、いくつかの文をまとめることができます。「main関数」も関数であり、「printf」や「scanf」なども関数であるが、それとは別に、ユーザーがいくつかの処理をまとめて「関数」として定義できます。

C言語の「関数」は、他のプログラム言語でいう「サブルーチン」のような性質がある(値を返すことができるかが関数とサブルルーチンの大きな違いです。)。

数学でいう「関数」とは意味が違うので、混同しないでください。

Visual Studio で、デバッグ セッションの終了時にコンソールが閉じてしまう場合

Visual Studio で、デバッグ セッションの終了時にコンソールが閉じてしまう場合は、

[ツール] -> [オプション] -> [デバッグ] -> [デバッグの停止時に自動的にコンソールを閉じる]

を無効にします。

関数の定義 編集

ユーザーが自分で定義する関数について、記法の例を表します。

例 2つの整数を引数にとりその和を返却する関数
int function(int a, int b)
{
	return a + b;
}

上記コードの「function」の部分が、その関数の名前であり、プログラマーが英数字と _ なら自由に関数に名前をつけられます(ただし「if」などのキーワードや「printf」などのライブラリ関数名は使用できません)。「function」でなくても「myFunc」などでも構いません。

以下、特にことわらないかぎり、ユーザーが定義した関数を、単に「関数」と省略する。main関数やprintfなども「関数」と省略する場合がありますが、ユーザーが定義したこと以外に違いはありません。

関数を呼び出す際、関数の 呼び出し元(仮に「親」と呼ぼう) から 呼び出し先関数(仮に「子」と呼ぼう)へ情報を渡すことができ、この渡された情報のことを仮引数(かりひきすう、parameter)、渡した情報のことを実引数(じつひきすう、argument)と呼びます。

関数の定義の記述は次のようになっている。

形式
返却値のデータ型 関数名 ( 引数リスト )
{
	/*いくつかの文*/
}

関数名とはその関数を他の関数と区別するために付ける名前のことで、関数名に使えるのは半角英数、「_(下線)」である。ただし、先頭は非数字でなければならない。 多バイト文字を使用できるかは言語処理系によります。 関数は他の関数の中で呼び出すことができます。

また、引数は省略できる場合があります。

返却値のデータ型 関数名()
{
	/*いくつかの文*/
}

のように関数を記述する場合もある[1]

関数の呼び出し 編集

定義した関数を呼び出す際の記述は、たとえば次のように行う。

//例 上の例の関数を呼び出す
int myFunc(int a, int b)
{
	return a + b;
}

int main(void){
	int r;
	r = myFunc(1, 2);//rには1と2との和である3が代入されます。
}

関数の定義側で書かれた引数のことを仮引数(かりひきすう、parameter)という。

いっぽう、関数の呼び出し側で書かれた引数のことを実引数(じつひきすう、argument)という。


複数の仮引数を持つ場合、「,(コンマ)」で区切る。仮引数を持たない場合 void を書きます。

仮引数
仮引数のデータ型 仮引数名



任意個数の実引数について、実引数を持たない場合は何も書かきません。 複数の実引数を持つ場合、「,(コンマ)」で区切る。

実引数
関数名(任意個数の実引数);

また、関数の呼び出し先から呼び出し元へ情報を渡すこともでき、この情報を返却値と呼びます。 返却値のデータ型で返却値の有無や種類を決定すします。 任意個数の仮引数で仮引数の数や種類を決定すします。


この例では、myFunc関数の実引数である1,2が、myFunc関数の仮引数であるa,bにコピーされ、a+bがmyFunc関数の返却値として返され、その値がrに代入されます。

関数を呼び出すとき、 引数と戻り番地(リターンアドレス)がスタックに格納されます[2]。 また、返却値がレジスタや予め呼び出し側の関数が用意した領域に格納されます[3]

引数と返却値 編集

引数と返却値をもたない関数 編集

//例 引数と返却値をもたない関数
#include <stdio.h>

void HelloWorld()
{
	printf("Hello, World!\n");
}

int main(void)
{
	HelloWorld();
}

この例では自作のHelloWorld関数を使って文字列を表示しています。 このHelloWorld関数は引数と返却値をもたない関数である。 このプログラムでは、 まずmain関数が呼ばれ、 次にmain関数内のHelloWorld関数を呼ぶ文が実行されることでHelloWorld関数が呼ばれ、 HelloWorld関数内のprintf関数を呼ぶ文が実行されることでprintf関数が呼ばれる。 そしてprintf関数が終了してHelloWorld関数に戻り、 HelloWorld関数が終了してmain関数に戻り、 main関数が終了してプログラムが終了する。

この例では関数を自作したメリットはないが、 同じ処理がソースコードのあちこちに出てくる場合などは、 その同じ処理を自作の関数にまとめるとメリットがあります。

引数と返却値をもつ関数 編集

//例 引数と返却値をもつ関数
#include <stdio.h>

double calc(double radius)//「calc関数の定義」
{
	return 3.14*radius*radius;
}

int main(void)
{
	double radius, area;
	printf("円の半径を入力してください。:");
	scanf("%lf", &radius);
	area=calc(radius);//「calc関数の呼び出し」
	printf("円の面積は%fです。\n",area);
}

この例では、自作のcalc関数を用いて、円の半径から円の面積を計算しています。 このcalc関数は引数と返却値をもつ関数である。 「calc関数の呼び出し」のところのradiusを実引数と呼びます。 「calc関数の定義」のところのradiusを仮引数と呼びます。 このプログラムでは、 「calc関数の呼び出し」で、まずmain関数のradiusが実引数としてcalc関数に渡され、 その値がcalc関数の仮引数のradiusにコピーされ、 それから、計算結果が返却値として返され、 その値が変数areaに代入されている。


関数に複数の返却値を持たせる方法 編集

関数による通常の返却値の方法では、1つの値しか、呼び出し元関数に値を送れない

しかし、特殊な方法を使うことにより、呼び出しもとの関数に、複数の値を送ることができます。

下記に述べるように、少なくとも以下の3つの方法があります。

「サブルーチン」との違い 編集

C言語の関数のような「関数スコープの変数」のシステムは、他のプログラム言語(BASICなど)の「サブルーチン」には無いシステムです[4]。BASICのプログラム言語に、C言語でいう「ポインタ」システムが無いのは、そもそも関数スコープの変数のシステムがBASICには無いからです。

このC言語の「関数スコープの変数」システムこそが、C言語の「関数」システムの真骨頂(しんこっちょう)なのです。

構造化以前のBASICでサブルーチンを呼び出しても、ルーチン内で使用する変数に呼び出し前の変数とは別アドレスは与えられませんし、そもそもサブルーチン呼び出し時に「引数」のコピーをしませんし、そもそもサブルーチン呼び出しに「引数」は不要ですし、そもそも「引数」のような概念じたいBASICのサブルーチンの仕様には無いのです[4]

BASICでのサブルーチンの呼び出しは、単に「GOSUB 100」の形式で100行目に処理を移し、RETURNで呼び出した行の次の行(GOSUBの次の行)から処理を続行します。


static 変数とポインタの必要性 編集

このように、C言語の(ユーザーが自分で定義する)「関数」は、かなり特殊なシステムである。


// static変数の使用例(コンパイルできる)
#include <stdio.h>

void func(void) {
  int b = 0;
  static int c = 0;
  b = b + 1;
  c = c + 1;

  printf("b = %d ,c = %d \n\n", b, c);
}

int main(void) {
  func();
  func();
}
実行結果
b = 1 ,c = 1
b = 1 ,c = 2

のようになる。

このように、static宣言された変数(上の例ではc)は、その関数が終わっても、数値が保存されます。

また、初期化がされる(cに0が代入される)のは、最初に呼び出された1回だけである。

static(静的)はdynamic(動的)との対比からです。ローカル変数はスタック上に一時的に記憶領域が確保され、関数を抜ける度にスコープを抜けます。 これに対して、static 変数は、プログラムが開始され終了するまでの期間に渡って同じ領域(同じアドレス)を持ち、これはグローバル変数と同じ特徴ですが、関数内で static 宣言された変数は、関数の外から参照できません。

すべての関数ブロックの外の、コード前半部の部分(グローバル領域)で、変数宣言をすると、その変数は、すべての関数からアクセスできます。

グローバル変数には、かつての errno の様に有用な用途もありますが、プログラムのどこからも参照や変更ができるので、goto文と同程度に有害です。 C11からは、errno はマクロになりました。

変数とスコープ 編集

int g; // gはグローバルスコープの変数

int main(void)
{
	int l; // lはmain関数の関数スコープな変数
}

C言語では通常、変数をある関数ブロックの内部で宣言した際、その変数は、その関数を抜けると参照できなくなります。 ある関数でしか通用しない変数を関数スコープの変数と呼びます。 これに対して、関数外で宣言した変数をグローバルスコープな変数と呼びプログラム中どこからでも参照できます。

関数スコープな変数は、関数ブロックを抜けると参照できなくなります。 そして、再度関数を呼び出したときには、以前の同名の変数の値は引き継ぎません。

用途によっては、この性質は好ましくなくグローバルスコープの変数の様にプログラムが実行中は値が永続してほしいことがあります。

そこで、関数スコープな変数の値を永続化する「記憶域クラス指定子static」があり、変数宣言に前置します。前の値は保存されていない。

スコープ

変数(などの識別子)の通用範囲のことをスコープと呼びます。 スコープは関数の本体を示す { から } までが代表的ですが、ブロック({ から } まで)もブロックスコープでブロック内で宣言された変数は、ブロックを抜けると参照できなくなります。 また、for文の第一項で宣言された変数のスコープは(たとえ { } で囲まれていなくても)もfor文がスコープになります。

関数原型 編集

main関数から呼び出される別の関数は、main関数よりも前で、存在が宣言されていなければならない。

そのため、main関数以外の関数は、main関数よりも前で、宣言されていなければならない。

しかし、main関数から呼び出される関数は、mainよりも前で宣言さえされていれば、たとえ具体的な定義内容がmain関数のあとで書かれていても、その関数をmain関数から呼び出して使用できます。そのため、以下のように、main関数から呼び出される関数の存在の宣言だけを先に行うという記法が許されており、この記法では、呼び出し先の関数の具体的な定義内容はmain関数のあとで定義する事が許されている。

このような関数の宣言方法のことを「関数のプロトタイプ宣言」などという。

(関数のプロトタイプ宣言の例)

//例 関数原型の使用例
#include <stdio.h>

double function(double a, double b);//関数原型

int main(void)
{
	printf("%f", function(0.12, 3.45));
}

//2つの浮動小数点数の和を求めるfunction関数
//関数原型があるおかげで、
//関数の使用の後に関数の定義を書くことができます。
double function(double a, double b)
{
	return a + b;
}

関数原型(関数プロトタイプ)とは、関数の宣言である。 ある関数が使われる前に、その関数名、引数、返却値を示し、その関数が使えるようにする。 関数原型を使うと、引数の個数とデータ型がチェックされます。

関数原型を使わなくても、関数が使われる前に、その関数の定義を書くことで、その関数を使うことができます。 だが、ソースコードが大規模になるとそれも難しくなる。 また、関数原型を使わず、関数が使われる前にその関数の定義を書かなかった場合、返却値は暗黙のうちint型とされ引数のデータ型もチェックされません(C11 では、関数宣言において型指定子を必須とし、暗黙的な型指定を禁じています)。

関数原型の記述は次のようになっている。

返却値のデータ型 関数名(引数のリスト);

引数のリストの記述は次のようになっている。

引数のデータ型 引数名

引数のリストには、引数がない場合はvoidのみを書く。 複数の引数がある場合、コンマで区切る。

関数原型の引数の有効範囲は、関数宣言子の最後で終了する(関数原型有効範囲)。 [5]


static変数を使う 編集

(※ 未記述)

上述したとおり。

ポインタを使う方法 編集

ポインタを使う方法である。しかし、ポインタを使えば、2つ以上の数値を操作する事ができるので、ポインタを使って、親(呼び出し元)関数の複数の値を操作する事ができるので、それを「複数の返却値である」と解釈する事もできる

このような事を、「ポインタによる参照渡しの引数を、返却値として用いる」などと言ったりするが、要するにポインタを使って親(呼び出し元)の変数の値を操作するというだけの事であり、あまり深い意味はない。

//例 ポインタを使って関数に複数の返却値を持たせる。
#include <stdio.h>

int function(int *a, int *b)
{
	*a = 1234;
	*b = 5678;
}

int main(void)
{
	int i,j;
	function(&i, &j);
	printf("iの値は%d、jの値は%d。", i, j);
}

構造体を使う方法 編集

別の方法として、返却値のデータ型に構造体を使って、 その構造体で複数の返却値を表現する方法もある。

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

//例 構造体を使って関数に複数の返却値を持たせる。
typedef struct 
{
  int i;
  double d;
  char c;
  char str[32];
} sKouzoutai;

sKouzoutai function()
{
  sKouzoutai ret;
  ret.i = 1234;
  ret.d = 3.14;
  ret.c = 'a';
  strcpy(ret.str, "Hello. World!");
  return ret;
}

int main(void)
{
  sKouzoutai kouzoutai;
  kouzoutai = function();
  printf("kouzoutaiのメンバの値は、%d %f %c %sです。\n", kouzoutai.i, kouzoutai.d, kouzoutai.c, kouzoutai.str);
  return 0;
}

しかし、関数の返却値は、エラー判別などに利用される場合も多いので(関数によって「エラーなら返却値に 0 以外を返却する」などの仕様のある場合も多い)、よって上述の手法は、あまりオススメできません。

なお、strcpyは、文字列をコピーするための関数。strcpyを使うには、string.hのインクルードが必要である。

swap関数 編集

aとbとの値を入れ替えるswap関数を考える。下記コードは誤った例である。

//例 誤ったswap関数
#include <stdio.h>

// グロ-バル変数を使って、交換用の変数をswap外部と共有しようと意図しているが・・・
int x;
int a;
int b;

void swap(int a, int b)
{
	x = a;
	a = b;
	b = x;
}

int main()
{
	a = 1;
    b = 2;
	printf("swap前のa=%d, b=%d\n", a, b);
	swap(a, b);
	printf("swap後のa=%d, b=%d\n", a, b);
}


この例は意図通りに動作しない。結果は

swap前のa=1, b=2
swap後のa=1, b=2

と表示され、なにも交換されていない。

こう失敗する原因は、呼び出され側のswap()の引数で用いられている値を格納する変数(上記コード例の swap(int a,int b) のaおよびb )によって、グローバル変数の a ,b が隠されてしまっているからである。 たとえグローバル領域で「a」「b」と同名の変数があっても、swap関数内では同名なだけの別変数としての仮引数「a」「b」が新たに関数swap内では用意されてしまう。

この様に、外部のスコープのインスタンスがより内側のスコープで同じ識別子を持つインスタンスによって隠されてしまう事を、シャドーイング(Shadowing)と呼びます。

ユーザ定義関数内で変数の値を交換させるためには、一例として、下記のようにポインタを使ってmain関数の変数a,bを書き換える方法があります。

//例 正しいswap関数
#include <stdio.h>

void swap(int *a, int *b)
{
	int x = *a;
	*a = *b;
	*b = x;
}

int main()
{
	int a = 1, b = 2;
	printf("swap前のa=%d, b=%d\n", a, b);
	swap(&a, &b);
	printf("swap後のa=%d, b=%d\n", a, b);
}

この例は意図通りに動作する。

swap前のa=1, b=2
swap後のa=2, b=1

と表示されます。

ポインタを使うと成功する理由は、main関数の関数スコープの変数のアドレスが仮引数にコピーされたため、swap関数がmain関数の関数スコープの変数a,bにアクセスできているからである(ポインタを介したswap関数内のa,bはもはや関数スコープの変数だけでなくグローバル変数にもアクセスできる)。



ユーザ定義関数を何も書かずに全てmain関数に書く方法を使えば、ポインタを使わずとも交換できます。

標準C言語では考える必要は無いが、別のプログラム言語だと、言語によってはポインタが標準設定では使用禁止されていたり(たとえばC#がそうである)、そもそもポインタに相当する機能の無い言語も考えられる[6]。そのような場合でも、下記コードのようなアルゴリズムで、変数の交換は可能である。

//例 swap関数を使わない場合
#include <stdio.h>

int main()
{
	int a = 1, b = 2;
	printf("swap前のa=%d, b=%d\n", a, b);
	
	int x = a;
	a = b;
	b = x;
		
	printf("swap後のa=%d, b=%d\n", a, b);
}
swap前のa=1, b=2
swap後のa=2, b=1

この処理を、マクロを使って一般化すると

//例 マクロを使った場合
#include <stdio.h>
#define swap_int(a,b) do {int __temp=a;a=b;b=__temp;}while(0)
int main()
{
	int a = 1, b = 2;
	printf("swap前のa=%d, b=%d\n", a, b);
	
	swap_int(a,b);
		
	printf("swap後のa=%d, b=%d\n", a, b);
}

※ 一度も反復しないdo { ... } while(0)は、変数__temp のスコープを切るため。

swap前のa=1, b=2
swap後のa=2, b=1
新たに変数を用意せず2つの変数を入れ替える
#include <stdio.h>

int main()
{
	int a = 1, b = 2; /* a の初期値を A、b の初期値を B として... */
	printf("swap前のa=%d, b=%d\n", a, b);
	a ^= b;           /* a = A ^ B */
    b ^= a;           /* b = B ^ A ^ B ... A */
	a ^= b;           /* a = A ^ B ^ A ... B */
	printf("swap後のa=%d, b=%d\n", a, b);
}

は、排他的論理和演算子。

結果

swap前のa=1, b=2
swap後のa=2, b=1


関数の引数として配列を渡す 編集

関数の引数として配列を渡すには、配列変数名を引数とする(配列変数名は配列の先頭要素へのポインタ)。

関数の引数として配列を渡す
#include <stdio.h>

int sum(int *array, int size) {
  int s = 0;
  for (int i = 0; i < size; i++)
    s += array[i];
  return s;
}

int main(void) {
  int a[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
  int s = sum(a, sizeof a / sizeof *a);
  printf("%d\n", s);
}
実行結果
129

ブロックスコープ 編集

複合文( Compound statement )は、ブロックとも呼ばれ、宣言や文を1つの構文単位にまとめたものです。自動保存期間を持つオブジェクトの初期化子や、ブロックスコープを持つ通常の識別子の可変長配列宣言子は、宣言が実行順に到達するたびに、あたかも文のように評価されてオブジェクトに値が格納され(初期化子のないオブジェクトに不確定な値が格納されることも含む)、各宣言内では宣言子が現れる順に評価されます[7]

文字列型を引数にする関数 編集

脚註・出典など 編集

  1. ^ 引数の省略はC23で廃止予定で、voidを明示する必要があります。
  2. ^ ARMアーキテクチャの様にリンクレジスタに戻り番地を記憶するアーキテクチャもあります。
  3. ^ 引数・返却値・戻り番地をどの様に渡したり返したりするかはアーキテクチャごとに異なり、ABI(application binary interface)で定められています。
  4. ^ 4.0 4.1 Full BASIC(ISO/IEC 10279)などの構造化BASICではサブルーチンや関数のなかでローカル変数を使用できます。
  5. ^ 『JISX3010:2003』p.21「6.2.1 識別子の有効範囲」
  6. ^ 最近は Fortran ですらポインタがあるのでポインタのない言語は非常に限られ、ポインタがなくてもVariant型に類する型がある場合が多い。
  7. ^ N2596 working draft — December 11, 2020 ISO/IEC 9899:202x (E). ISO/IEC JTC1/SC22/WG14. p. 122, §6.8 Statements and blocks. http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2596.pdf. 

参考文献 編集

  • 日本工業標準調査会『JISX3010 プログラム言語C』2003年12月20日改正