関数の必要性と利点
プログラムが複雑化し、コード量が増加するにつれて、すべての処理をmain関数内で実装すると、保守性や可読性が著しく低下します。また、同じ処理を複数回書く必要がある場合、コードが冗長になるだけでなく、修正や拡張も困難になります。
こうした問題を解決するには、よく使う処理を関数として独立させ、必要に応じて呼び出すモジュール化プログラミングの考え方が有効です。この方法は、部品化された関数を組み合わせるだけであらゆる機能を実装できるため、効率的な開発と保守が可能になります。
関数の定義方法
C言語では、関数を使用する前に必ず定義または宣言する必要があります。関数の定義には次の要素が含まれます:
- 関数名の指定
- 戻り値の型の指定
- 引数の型と名前の指定(引数がない場合はvoid)
- 関数の処理内容(関数本体)
引数なし関数の定義
int getZero(void) {
return 0;
}
引数あり関数の定義
int maxVal(int a, int b) {
return (a > b) ? a : b;
}
空関数の定義
void dummyFunc() {
// 何もしない
}
関数の呼び出しとデータの受け渡し
関数の呼び出しには次の3つの形式があります:
- 単独の関数呼び出し
- 式の中での関数呼び出し
- 他の関数への引数としての関数呼び出し
引数の受け渡し方法
関数呼び出しでは、実引数から仮引数への値のコピーが行われます。この方法は値渡しと呼ばれ、仮引数の変更は実引数には影響しません。
関数の戻り値
関数はreturn文を使って呼び出し元に値を返します。戻り値の型は関数の戻り値型と一致させる必要があります。void型の関数は戻り値を持ちません。
関数プロトタイプ宣言
関数を使用する前にそのプロトタイプを宣言することで、コンパイラに型チェックをさせることができます。
int factorial(int n); // 階乗計算関数の宣言
関数のネスト呼び出し
C言語では関数の定義自体は入れ子にできませんが、関数呼び出しは入れ子にできます。ある関数内で他の関数を呼び出し、さらにその関数内で別の関数を呼び出すことが可能です。
再帰関数の利用
関数が自身を直接または間接的に呼び出すことを再帰呼び出しと呼びます。
int factorial(int n) {
if (n < 0) return -1; // エラーハンドリング
if (n == 0) return 1;
return n * factorial(n - 1);
}
配列を関数に渡す
配列を関数に渡す場合、配列名を引数にすると配列の先頭アドレスが渡されます。
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
多次元配列の渡し方
多次元配列を関数に渡す場合、第2次元以降のサイズは宣言時に指定する必要があります。
void processMatrix(int matrix[][3], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
変数のスコープと生存期間
ローカル変数
関数内部やブロック内で宣言された変数はローカル変数であり、その関数やブロック内でのみ有効です。
グローバル変数
関数の外で宣言された変数はグローバル変数であり、そのファイル内のどの関数からもアクセス可能です。
変数の保存期間
自動変数(auto)
通常のローカル変数は関数呼び出し時に作成され、関数終了時に破棄されます。
静的変数(static)
staticキーワードで宣言されたローカル変数は、関数呼び出しの間でも値を保持します。
void counter() {
static int count = 0;
count++;
printf("Count: %d\n", count);
}
レジスタ変数(register)
registerキーワードで宣言された変数は、可能であればCPUレジスタに格納されるようコンパイラに指示します。
外部変数の制御
staticキーワードでグローバル変数を宣言すると、その変数はそのファイル内でのみアクセス可能になります。
内部関数と外部関数
staticで宣言された関数は内部関数となり、同じファイル内でのみ利用可能です。
static void helperFunction() {
// この関数はこのファイル内でのみ利用可能
}