関数

導入

関数は、繰り返し実行できるコードの一部です。さまざまなパラメーターを受け入れ、対応する操作を完了できます。以下の例は関数です。

int plus_one(int n) {
  n + 1 を返します。
}

上記のコードは関数 plus_one() を宣言しています。

関数宣言の文法には以下の注意点があります。

(1) 戻り値の型。関数を宣言するときは、最初に戻り値の型を指定する必要があります。上記の例は int です。これは、関数 plus_one() が整数を返すことを意味します。

(2) パラメータ。関数名の後の括弧内で、パラメーターの型とパラメーター名を宣言する必要があります。plus_one(int n) は、この関数が整数パラメーター n を持つことを意味します。

(3) 関数本体。関数本体は中括弧の内側に記述する必要があり、その後 (つまり中括弧の外側) にセミコロンを追加する必要はありません。中括弧の開始位置は関数名と同じ行であっても、別の行であっても構いません。本書では同じ行の書き方を使用します。

(4) return ステートメント。 return ステートメントは関数の戻り値を返します。プログラムがこの行に到達すると、関数本体から飛び出して関数呼び出しを終了します。関数が値を返さない場合は、「return」ステートメントを省略するか、「return;」と記述することができます。

関数を呼び出すときは、以下のように関数名の後に括弧を追加し、括弧内に実際のパラメータを入れるだけです。

int a = plus_one(13);
// a は 14 に等しい

関数を呼び出すとき、パラメーターの数は定義内のパラメーターの数と一致している必要があります。パラメーターが多すぎたり少なすぎたりすると、エラーが発生します。

int plus_one(int n) {
  n + 1 を返します。
}

plus_one(2, 2); // エラーを報告する
plus_one(); // エラーを報告する

上記の例では、関数 plus_one() は 1 つのパラメータのみを受け入れることができ、パラメータが 2 つ渡された場合、またはパラメータが渡されなかった場合は、エラーが報告されます。

関数は使用する前に宣言する必要があります。宣言しないとエラーが報告されます。つまり、この関数は plus_one() を使用する前に宣言する必要があります。以下のように書くとコンパイル時にエラーが報告されます。

int a = plus_one(13);

int plus_one(int n) {
  n + 1 を返します。
}

上記の例では、plus_one() を呼び出した後にこの関数が宣言されている場合、コンパイルでエラーが報告されます。

C 言語標準では、関数はソース コード ファイルの最上位でのみ宣言でき、他の関数内で宣言できないと規定されています。

値を返さない関数の場合は、「void」キーワードを使用して戻り値のタイプを示します。パラメーターのない関数の場合、宣言時にパラメーターの型を示すために void キーワードを使用する必要があります。

void myFunc(void) {
  // ...
}

上記の myFunc() 関数には戻り値がなく、呼び出し時にパラメータも必要ありません。

関数はそれ自体を呼び出すことができます。これを再帰と呼びます。以下はフィボナッチ数列の例です。

unsigned long Fibonacci(unsigned n) {
  if (n > 2)
    フィボナッチ(n - 1) + フィボナッチ(n - 2) を返します。
  それ以外
    1を返します。
}

上記の例では、関数 Fibonacci() がそれ自体を呼び出します。これにより、アルゴリズムが大幅に簡素化されます。

主要()

C 言語では、main() がプログラムのエントリ関数であると規定されています。つまり、すべてのプログラムには main() 関数が含まれなければなりません。プログラムの実行は常にこの関数から開始されます。この関数がないとプログラムを開始できません。他の機能は、それを通じてプログラムに導入されます。

main() は他の関数と同様に、次のように戻り値の型とパラメータの型を指定する必要があります。

int main(void) {
  printf("Hello World\n");
  0を返します。
}

上記の例では、最後の return 0; は、関数が終了して 0 を返すことを示しています。

C 言語の規則によれば、戻り値「0」は関数が正常に実行されたことを意味します。他のゼロ以外の整数が返された場合は、操作が失敗し、コードに問題があることを意味します。システムは、main() の戻り値をプログラム全体の戻り値として使用して、プログラムが正常に実行されたかどうかを判断します。

通常の状況では、main() で行 return 0 が省略された場合、コンパイラは自動的にそれを追加します。つまり、main() のデフォルトの戻り値は 0 です。したがって、次のように書いても効果は全く同じです。

int main(void) {
  printf("Hello World\n");
}

C 言語はデフォルトで戻り値を main() 関数にのみ追加し、他の関数には追加しないため、統一されたコーディング スタイルを形成するために常に return ステートメントを保持することをお勧めします。

パラメータの値渡し参照

関数のパラメータが変数の場合、呼び出されると、変数自体ではなく、変数の値のコピーが渡されます。

void インクリメント(int a) {
  a++;
}

int i = 10;
インクリメント(i);

printf("%d\n", i);

上記の例では、increment(i) を呼び出した後、変数 i 自体は変更されず、依然として 10 と等しくなります。関数に渡されるのは「i」自体ではなく「i」のコピーであるため、コピーの変更は元の変数には影響しません。これを「値による参照」といいます。

したがって、パラメーター変数が変更された場合は、それを戻り値として渡すのが最善です。

int インクリメント(int a) {
  a++;
  を返します。
}

int i = 10;
i = インクリメント(i);

printf("%d\n", i);

もう一度下の例を見てください。 Swap() 関数は 2 つの変数の値を交換するために使用されます。値による参照のため、次の記述方法は有効になりません。

void Swap(int x, int y) {
  内部温度;
  温度 = x;
  x = y;
  y = 温度;
}

int a = 1;
int b = 2;
スワップ(a, b); // 無効です

渡される変数は元の変数 ab のコピーであるため、上記の記述方法は変数値を交換する効果はありません。関数が内部でどのように操作されても、元の変数には影響しません。

変数そのものを渡したい場合、方法は 1 つだけあり、それは変数のアドレスを渡すことです。

void Swap(int* x, int* y) {
  内部温度;
  温度 = *x;
  *x = *y;
  *y = 温度;
}

int a = 1;
int b = 2;
スワップ(&a, &b);

上記の例では、変数 xy のアドレスを渡すことで、関数内でアドレスを直接操作することができ、それによって 2 つの変数の値を交換できます。

パラメータの受け渡しとは関係ありませんが、関数は内部変数へのポインタを返すべきではないことに注意してください。

int* f(void) {
  int i;
  // ...
  戻る&i;
}

上の例では、関数は内部変数 i のポインタを返しますが、これは誤りです。関数が終了すると内部変数は消滅するため、このとき内部変数 i を指すメモリアドレスは無効となるため、このアドレスを再度使用することは非常に危険です。

関数ポインタ

関数自体はメモリ内のコードの一部であり、C 言語ではポインターを介して関数を取得できます。

void print(int a) {
  printf("%d\n", a);
}

void (*print_ptr)(int) = &print;

上の例では、変数 print_ptr は関数ポインタであり、関数 print() のアドレスを指します。関数 print() のアドレスは &print で取得できます。 (*print_ptr) は括弧内に記述する必要があることに注意してください。そうしないと、関数パラメータ (int)* よりも優先され、式全体が void* print_ptr(int) になります。

関数ポインターを使用すると、それを通じて関数を呼び出すこともできます。

(*print_ptr)(10);
// と同等
印刷(10);

特別なのは、C 言語でも関数名自体が関数コードへのポインタであると規定されており、関数アドレスは関数名を通じて取得できることです。つまり、print&print は同じものです。

if (print == &print) // true

したがって、上記のコードの print_ptrprint と同等です。

void (*print_ptr)(int) = &print;
// または
void (*print_ptr)(int) = 印刷;

if (print_ptr == print) // true

したがって、どの関数でも、その関数を呼び出す方法は 5 つあります。

// 書き方その1
プリント(10)

//書き方2
(*印刷)(10)

//書き方3
(&print)(10)

// 書き方4
(*print_ptr)(10)

// 書き方5
プリント_ptr(10)

簡潔かつ読みやすくするために、通常の状況では関数名の前に「*」と「&」は追加されません。

この機能の応用例の 1 つは、関数のパラメーターまたは戻り値も関数である場合、関数プロトタイプを次のように記述できることです。

int compute(int (*myfunc)(int), int, int);

上の例は、関数 compute() の最初のパラメータも関数であることを明確に示しています。

関数プロトタイプ

前述したように、関数は最初に宣言してから使用する必要があります。プログラムは常に main() 関数を最初に実行するため、他のすべての関数は main() 関数の前に宣言する必要があります。

void func1(void) {
}

void func2(void) {
}

int main(void) {
  func1();
  func2();
  0を返します。
}

上記のコードでは、main() 関数を最後に宣言する必要があります。そうしないと、コンパイル中に警告が生成され、func1() または func2() の宣言が見つかりません。

ただし、main() はプログラム全体の入り口であり、メインロジックなので、先頭に置いたほうがよいでしょう。一方、多くの関数を含むプログラムの場合、各関数の順序が正しいことを確認するのが非常に面倒になることがあります。

C 言語が提供する解決策は、関数のプロトタイプがプログラムの先頭に与えられている限り、その関数を最初に使用し、後で宣言できるということです。いわゆる関数プロトタイプは、各関数の戻り値の型とパラメーターの型を事前にコンパイラーに伝えます。他の情報は必要ありません。また、特定の関数の実装を後で追加することもできます。

int 2 回 (int);

int main(int num) {
  2 回(数値)を返します。
}

int 2 回 (int num) {
  2 * 数値を返します。
}

上記の例では、関数 twice() の実装は main() の後に配置されていますが、関数のプロトタイプはコード ヘッダーの最初に指定されているため、正しくコンパイルできます。関数のプロトタイプが事前に与えられている限り、関数の特定の実装がどこに配置されるかは問題ではありません。

関数プロトタイプにはパラメーター名を含めることもできますが、これはコンパイラーにとっては冗長ですが、コードを読むときに関数の意図を理解するのに役立ちます。

int 2 回 (int);

// と同等
int 2 回 (int num);

上記の例では、関数 twice のパラメータ名 num は、プロトタイプに出現するかどうかに関係なく受け入れられます。

関数プロトタイプはセミコロンで終わる必要があることに注意してください。

一般に、各ソース コード ファイルのヘッダーには、現在のスクリプトで使用されるすべての関数のプロトタイプが示されます。

出口()

exit() 関数はプログラム全体を終了するために使用されます。この関数が実行されると、プログラムは直ちに終了します。この関数のプロトタイプはヘッダー ファイル stdlib.h で定義されます。

exit() はプログラム外に値を返すことができ、パラメータはプログラムの戻り値となります。一般に、パラメータとして 2 つの定数が使用されます。「EXIT_SUCCESS」 (0 に相当) はプログラムが正常に実行されたことを示し、「EXIT_FAILURE」 (1 に相当) はプログラムが異常終了したことを示します。これら 2 つの定数は stdlib.h でも定義されています。

//プログラムは正常に実行されます
// exit(0) と同等。
終了(EXIT_SUCCESS);

//プログラムが異常終了する
// exit(1) と同等。
終了(EXIT_FAILURE);

main() 関数内での exit() は、return ステートメントを使用することと同じです。他の関数は exit() を使用しますが、これはプログラム全体の実行を終了し、他の効果はありません。

C 言語には、「atexit()」関数も用意されています。この関数は、「exit()」が実行されるときに実行される追加関数を登録するために使用され、プログラムを終了するときに何らかの仕上げ作業を行うために使用されます。この関数のプロトタイプもヘッダー ファイル stdlib.h で定義されています。

int atexit(void (*func)(void));

atexit() のパラメータは関数ポインタです。そのパラメータ関数 (以下の例では「print」) はパラメータを受け入れられず、戻り値を持つこともできないことに注意してください。

void print(void) {
  printf("何かが間違っています!\n");
}

atexit(print);
終了(EXIT_FAILURE);

上記の例では、exit() が実行されると、プログラムを終了する前に、atexit() によって登録された print() 関数が自動的に呼び出されます。

関数指定子

C 言語には、関数の使用法をより明確にするためにいくつかの関数指定子が用意されています。

外部指定子

マルチファイル プロジェクトの場合、ソース コード ファイルは他のファイルで宣言された関数を使用します。このとき、現在のファイルでは外部関数のプロトタイプを指定する必要があり、関数の定義が他のファイルからのものであることを示すために extern が使用されます。

extern int foo(int arg1, char arg2);

int main(void) {
  int a = foo(2, 3);
  // ...
  0を返します。
}

上の例では、関数 foo() は別のファイルで定義されており、extern は現在のファイルに関数の定義が含まれていないことをコンパイラに伝えます。

ただし、関数プロトタイプはデフォルトでは extern なので、ここで extern を追加しなくても効果は同じです。

静的指定子

デフォルトでは、関数が呼び出されるたびに、関数の内部変数が再初期化され、前回の実行時の値は保持されません。 static 指定子はこの動作を変更できます。

static は関数内で変数を宣言するときに使用され、変数を一度初期化するだけでよく、呼び出されるたびに初期化する必要がないことを示します。つまり、呼び出し間でその値は変化しません。

#include <stdio.h>

ボイドカウンター(ボイド) {
  static int count = 1 // 初期化は 1 回のみ;
  printf("%d\n", count);
  カウント++;
}

int main(void) {
  カウンタ(); // 1
  カウンタ(); // 2
  カウンタ(); // 3
  カウンタ(); // 4
}

上記の例では、関数 counter() の内部変数 countstatic 指定子で変更されており、この変数は 1 回だけ初期化され、呼び出されるたびに前の値が使用されることを示しています。その結果、増加効果が得られます。

「static」で変更された変数を初期化する場合、変数ではなく定数値のみを割り当てることができることに注意してください。

int i = 3;
static int j = i; // エラー

上の例では、「j」は静的変数であり、初期化中に別の変数「i」に割り当てることはできません。

さらに、ブロック スコープでは、「static」で宣言された変数のデフォルト値は「0」です。

静的 int foo;
// と同等
静的 int foo = 0;

static を使用すると、関数自体を変更できます。

static int Twice(int num) {
  int 結果 = 数値 * 2;
  戻り値(結果);
}

上記の例では、「static」キーワードは、関数が現在のファイルでのみ使用できることを示しています。このキーワードがなければ、他のファイルでもこの​​関数を使用できます (関数プロトタイプを宣言することによって)。

static をパラメータ内で使用してパラメータ配列を変更することもできます。

int sum_array(int a[static 3], int n) {
  // ...
}

上記の例では、「static」はプログラムの動作に影響を与えません。配列の長さが少なくとも 3 であることをコンパイラーに伝えるためにのみ使用され、場合によってはプログラムの速度が向上します。さらに、多次元配列のパラメータの場合、「static」は最初の次元の記述にのみ使用できることに注意してください。

const 指定子

関数パラメータ内の const 指定子は、関数内でパラメータ変数を変更してはならないことを示します。

void f(int* p) {
  // ...
}

上記の例では、関数 f() のパラメータはポインタ p ですが、それが指す値 *p が関数内で変更され、関数の外部に影響を与える可能性があります。

この状況を回避するには、関数を宣言するときにポインタ パラメータの前に const 指定子を追加して、パラメータが指す値を関数内で変更できないことをコンパイラに伝えることができます。

void f(const int* p) {
  *p = 0; // この行はエラーを報告します
}

上記の例では、関数を宣言するときに、ポインタ p が指す値を変更できないことを const で指定しているため、*p = 0 はエラーを報告します。

ただし、上記の書き方では、p が指す値の変更に限定されており、p 自体のアドレスを変更することも可能です。

void f(const int* p) {
  int x = 13;
  p = &x; // 変更を許可する
}

上記の例では、p 自体は変更できますが、const*p を制限するだけで変更できません。

pの変更を制限したい場合は、pの前にconstを置くことができます。

void f(int* const p) {
  int x = 13;
  p = &x; // この行はエラーを報告します
}

p*p の変更を同時に制限したい場合は、 2 つの const を使用する必要があります。

void f(const int* const p) {
  // ...
}

可変パラメータ

一部の関数のパラメータの数は不確実です。関数を宣言するとき、省略記号 ... を使用してパラメータの可変数を示すことができます。

int printf(const char* 形式, ...);

上記の例は、printf() 関数のプロトタイプです。最初のパラメーターを除いて、他のパラメーターの数は可変であり、フォーマット文字列内のプレースホルダーの数に関連します。このとき、「...」を使用して可変数のパラメータを表すことができます。

... 記号をパラメータ シーケンスの最後に配置する必要があることに注意してください。そうしないと、エラーが報告されます。

ヘッダー ファイル stdarg.h は、変数パラメーターを操作できるいくつかのマクロを定義します。

(1) va_list: 可変パラメータオブジェクトを定義するために使用されるデータ型。可変引数パラメーターを操作する場合は、最初に使用する必要があります。

(2) va_start: 可変パラメータオブジェクトを初期化するために使用される関数。これは 2 つのパラメーターを受け入れます。最初のパラメーターは可変パラメーター オブジェクトで、2 番目のパラメーターは元の関数の可変パラメーターの前のパラメーターであり、可変パラメーターを配置するために使用されます。

(3) va_arg: 現在の変数パラメータを取り出すために使用される関数で、各呼び出しの後、内部ポインタは次の変数パラメータを指します。 2 つのパラメーターを受け入れます。1 つ目は可変個引数オブジェクト、2 つ目は現在の可変個引数パラメーターの型です。

(4) va_end: 可変パラメータオブジェクトをクリーンアップするために使用される関数。

以下に例を示します。

double Average(int i, ...) {
  二重合計 = 0;
  va_list ap;
  va_start(ap, i);
  for (int j = 1; j <= i; ++j) {
    合計 += va_arg(ap, double);
  }
  va_end(ap);
  合計 / i を返します。
}

上記の例では、「va_list ap」は「ap」を可変パラメータオブジェクトとして定義し、「va_start(ap, i)」はパラメータ「i」以降のパラメータを「ap」に入れ、「va_arg(ap, double)」はap からパラメータを 1 つずつ取り出して double 型に指定するには、va_end(ap) を使って可変パラメータオブジェクトをクリーンアップします。


作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0