関数の基本概念
C言語における関数は、数学の関数と類似した概念を持ちます。特定の入力(引数)を受け取り、定義された処理を実行して結果(戻り値)を返す独立したモジュールです。例えば、入力値を二乗して1を足す処理を関数として実装すると以下のようになります。
#include <stdio.h>
int calculate(int val) {
return val * val + 1;
}
int main(void) {
int num = 4;
printf("計算結果: %d\n", calculate(num));
return 0;
}
標準ライブラリ関数の活用
C言語には、文字列操作や入出力など、頻繁に使用される機能が標準ライブラリ関数として予め用意されています。printf や scanf、strlen などがその代表例です。これらはC言語の規格によって仕様が定義されており、各コンパイラ(GCCやClangなど)が具体的な実装を提供しています。
strcpy 関数の利用
strcpy は文字列をコピーするための関数です。コピー元(ソース)からコピー先(デスティネーション)へ、終端文字 \0 を含めてデータを転送します。利用するには <string.h> をインクルードする必要があります。
#include <stdio.h>
#include <string.h>
int main(void) {
char src[] = "Hello, C Language!";
char dest[50] = "------------------";
strcpy(dest, src);
printf("コピー後: %s\n", dest);
return 0;
}
ユーザー定義関数の作成
開発者が独自のロジックを実装するために作成する関数をユーザー定義関数と呼びます。戻り値の型、関数名、パラメータ、および処理本体で構成されます。
2つの数値から最大値を取得する
#include <stdio.h>
int get_maximum(int val1, int val2) {
return (val1 >= val2) ? val1 : val2;
}
int main(void) {
int x = 15, y = 28;
printf("最大値は %d です。\n", get_maximum(x, y));
return 0;
}
2つの変数の値を交換する
C言語では値渡し(Call by Value)が基本となるため、関数内で引数の値を変更しても呼び出し元の変数には影響しません。元の変数を変更するには、ポインタを用いてメモリアドレスを渡す必要があります。
#include <stdio.h>
void swap_values(int *ptr1, int *ptr2) {
int temp = *ptr1;
*ptr1 = *ptr2;
*ptr2 = temp;
}
int main(void) {
int a = 10, b = 20;
printf("交換前: a=%d, b=%d\n", a, b);
swap_values(&a, &b);
printf("交換後: a=%d, b=%d\n", a, b);
return 0;
}
アルゴリズムの実装演習
素数判定アルゴリズム
素数は1と自分自身以外に約数を持たない2以上の整数です。処理の効率化のため、判定する約数の範囲をその数の平方根までに限定します。
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
bool check_prime(int target) {
if (target <= 1) return false;
int limit = (int)sqrt(target);
for (int divisor = 2; divisor <= limit; divisor++) {
if (target % divisor == 0) {
return false;
}
}
return true;
}
int main(void) {
int number = 29;
if (check_prime(number)) {
printf("%d は素数です。\n", number);
} else {
printf("%d は素数ではありません。\n", number);
}
return 0;
}
うるう年の判定
西暦が4で割り切れ、かつ100で割り切れない年、あるいは400で割り切れる年はうるう年となります。
#include <stdio.h>
#include <stdbool.h>
bool is_leap(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int main(void) {
int y = 2024;
printf("%d年はうるう年ですか? %s\n", y, is_leap(y) ? "はい" : "いいえ");
return 0;
}
ソート済み配列の二分探索
二分探索は、整列されたデータから高速に目的の値を見つけるアルゴリズムです。探索範囲を半分に絞り込みながら進めます。中央のインデックスを計算する際、整数のオーバーフローを防ぐ計算式を用いるのが安全です。
#include <stdio.h>
int search_sorted_array(const int *data, int length, int target) {
int low = 0;
int high = length - 1;
while (low <= high) {
int middle = low + (high - low) / 2;
if (data[middle] == target) {
return middle;
} else if (data[middle] < target) {
low = middle + 1;
} else {
high = middle - 1;
}
}
return -1;
}
int main(void) {
int dataset[] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
int size = sizeof(dataset) / sizeof(dataset[0]);
int search_val = 23;
int index = search_sorted_array(dataset, size, search_val);
if (index != -1) {
printf("値 %d はインデックス %d に存在します。\n", search_val, index);
} else {
printf("値 %d は見つかりませんでした。\n", search_val);
}
return 0;
}
関数のネストと呼び出しチェーン
ある関数の内部で別の関数を呼び出すことをネスト呼び出しと呼びます。また、関数の戻り値を直接別の関数の引数として利用する手法をチェーン呼び出しと言います。
#include <stdio.h>
#include <string.h>
void display_message(void) {
printf("ネストされた呼び出し\n");
}
void repeat_message(int times) {
for (int i = 0; i < times; i++) {
display_message();
}
}
int main(void) {
repeat_message(3);
printf("文字列の長さ: %zu\n", strlen("Chained Call"));
return 0;
}
printfの戻り値を利用したチェーン
printf 関数は、出力した文字数を戻り値として返します。これを利用すると、以下のような特異な出力が可能です。
#include <stdio.h>
int main(void) {
printf("%d", printf("%d", printf("%d", 987)));
return 0;
}
上記コードでは、まず最も内側の printf("%d", 987) が実行され、コンソールに 987 (3文字) が出力され、戻り値として 3 を返します。次に printf("%d", 3) が実行され 3 (1文字) を出力し、戻り値 1 を返します。最後に printf("%d", 1) が実行され 1 を出力します。結果としてコンソールには 98731 と表示されます。
関数の宣言と定義
C言語では、関数を呼び出す前にコンパイラがその存在(シグネチャ)を認識している必要があります。関数の実装(定義)が呼び出し箇所より後にある場合、事前にプロトタイプ宣言を行う必要があります。一般的に、宣言はヘッダーファイル(.h)に、定義はソースファイル(.c)に記述し、モジュール性を高めます。
#include <stdio.h>
int multiply(int x, int y);
int main(void) {
int result = multiply(6, 7);
printf("積: %d\n", result);
return 0;
}
int multiply(int x, int y) {
return x * y;
}
再帰関数の設計
関数が自分自身を呼び出す処理を再帰呼び出し(再帰関数)と呼びます。再帰処理を実装する際は、必ず「再帰を終了させる条件(ベースケース)」と「問題の規模を縮小させながら再帰呼び出しを行う条件」の2つを満たす必要があります。
整数の各桁を順に出力する
数値を10で割る操作と、10で割った余りを求める操作を組み合わせ、再帰的に桁を分解して出力します。
#include <stdio.h>
void print_digits(unsigned int num) {
if (num > 9) {
print_digits(num / 10);
}
printf("%u ", num % 10);
}
int main(void) {
print_digits(7531);
printf("\n");
return 0;
}
ポインタを用いた文字列長の計算
反復処理(ループ)を用いて、終端文字 \0 に到達するまでの文字数をカウントします。
#include <stdio.h>
size_t count_characters(const char *str) {
size_t len = 0;
while (str[len] != '\0') {
len++;
}
return len;
}
int main(void) {
const char *text = "Pointer";
printf("長さ: %zu\n", count_characters(text));
return 0;
}
再帰による文字列長の計算
ループ用のカウンタ変数を作成せず、再帰関数の戻り値を累積させることで文字列の長さを算出します。
#include <stdio.h>
size_t recursive_strlen(const char *str) {
if (*str == '\0') {
return 0;
}
return 1 + recursive_strlen(str + 1);
}
int main(void) {
const char *word = "Recursion";
printf("再帰による長さ: %zu\n", recursive_strlen(word));
return 0;
}