C言語基礎|インライン関数と番兵法

ここまでで、関数を自分で作って呼び出せるようになってきましたね。次のステップでは、「速く(なる可能性)と読みやすさ」を意識した書き方を学びます。
今回のテーマはこの2つです。

  • インライン関数:小さな処理を関数にまとめつつ、関数呼出しの負担を減らせる可能性がある。
  • 番兵法:探索ループの終了判定を減らし、ループを軽くする工夫

インライン関数とは

インライン関数は、関数定義の前に inline を付けた関数です。コンパイラが「この関数は小さいし、呼び出すより展開したほうがよさそう」と判断すると、呼び出し箇所に関数本体を埋め込むことがあります。

書式

inline 返却値型 関数名(仮引数型並び)
{
    文;
}

例(絶対値)

inline int iabs(int x)
{
    return x < 0 ? -x : x;
}

インライン関数の特徴

観点内容
ねらい小さい処理を関数化しつつ、速くなる可能性を残す。
注意inline を付けても必ず展開されるわけではない。
使いどころ何度も呼ぶ短い関数、条件演算子などで完結する処理

番兵法とは

線形探索では、ループのたびに「終端か?」と「一致か?」のように 複数の判定が入りがちです。
番兵法では、配列の末尾に 番兵(sentinel) を置いて、

  • 「必ずどこかで key が見つかる」状態を作る。
  • ループ中の判定を v[i] == key だけにする。

という形にします。

通常探索と番兵法

方法ループ中の判定特徴
通常の線形探索終端判定 + 一致判定判定が2つになりやすい
番兵法一致判定だけループがシンプルになりやすい

サンプルプログラム

題材を「社員IDを探す」に変更し、表示メッセージも差し替えます。

プロジェクト名:chap6-17-1 ソースファイル名:chap6-17-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

// 逐次探索(番兵法):社員IDを探す

#include <stdio.h>

#define SIZE 6
#define NOT_FOUND -1

//--- 要素数nの配列vからkeyを探索(番兵法) ---//
int search_sentinel(int v[], int n, int key)
{
    int i = 0;

    v[n] = key;                 // 番兵を格納(書き込みがあるのでconstは付けない)

    while (v[i] != key) {       // 判定は1つだけ
        i++;
    }

    return i < n ? i : NOT_FOUND;
}

int main(void)
{
    int ids[SIZE + 1];
    int key;

    puts("社員IDを入力してください。");
    for (int i = 0; i < SIZE; i++) {
        printf("ids[%d]:", i);
        scanf("%d", &ids[i]);
    }

    printf("探したい社員ID:");
    scanf("%d", &key);

    int idx = search_sentinel(ids, SIZE, key);

    if (idx == NOT_FOUND)
        puts("該当するIDはありません。");
    else
        printf("見つかりました:ID %d は ids[%d] にあります。\n", key, idx);

    return 0;
}

なぜ const が外れるの?

番兵法は v[n] = key のように 配列へ書き込むので、引数を const int v[] にすると代入できずエラーになります。

const を付ける判断

関数の役割const を付ける?
読むだけ最大/最小、合計、表示付けるのが基本
書き換える反転、代入、番兵を置く付けない

演習問題

演習6-8:平均との差の最大を返す

要素数 n の int 配列 v の各要素について、平均との差(平均との差の絶対値)を計算し、
その最大値を返す関数を作成せよ。

int max_dev(const int v[], int n);

例:v が {10, 20, 30} のとき平均は 20、差は {10, 0, 10} なので返却値は 10。

解答例

プロジェクト名:chap6-17-2 ソースファイル名:chap6-17-2.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

inline int iabs(int x) { return x < 0 ? -x : x; }

int max_dev(const int v[], int n)
{
    int sum = 0;
    for (int i = 0; i < n; i++) sum += v[i];

    int avg = sum / n;               // int平均(小数は切り捨て)
    int max = iabs(v[0] - avg);

    for (int i = 1; i < n; i++) {
        int d = iabs(v[i] - avg);
        if (d > max) max = d;
    }
    return max;
}

int main(void)
{
    int a[] = {10, 20, 33, 18, 19};
    printf("平均との差の最大=%d\n", max_dev(a, 5));
    return 0;
}

解説

  • max_dev は 配列を読むだけなので const int v[] にするのが安心です。
  • inline の iabs は短い処理なので、可読性アップと最適化の可能性を両取りできます。
  • 平均を int で計算しているので、小数は切り捨てになります(学習段階ではこれでOK)。

演習6-9:左回転する

要素数 n の int 配列 v を、左に k 個回転させる関数を作成せよ。
例:{1,2,3,4,5} を k=2 左回転 → {3,4,5,1,2}

void rot_left(int v[], int n, int k);

解答例

プロジェクト名:chap6-17-3 ソースファイル名:chap6-17-3.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

void rot_left(int v[], int n, int k)
{
    if (n <= 0) return;
    k %= n;
    if (k < 0) k += n;

    while (k-- > 0) {
        int first = v[0];
        for (int i = 0; i < n - 1; i++)
            v[i] = v[i + 1];
        v[n - 1] = first;
    }
}

void print_array(const int v[], int n)
{
    printf("{ ");
    for (int i = 0; i < n; i++) printf("%d ", v[i]);
    printf("}\n");
}

int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    print_array(a, 5);
    rot_left(a, 5, 2);
    puts("左に2回転させました。");
    print_array(a, 5);
    return 0;
}

解説

  • rot_left は 配列を書き換えるので const は付けません。
  • 回転は「1回左へずらす」を k 回繰り返すだけでも実装できます(まずはシンプルに)。
  • n が 0 のときや k が大きいときのために k %= n を入れておくと安心です。

演習6-10:フィルタコピーする

要素数 n の int 配列 src から、偶数だけを dst に順に詰めて格納する関数を作成せよ。
返却値は、dst に格納した個数とする。

int copy_even(int dst[], const int src[], int n);

解答例

プロジェクト名:chap6-17-4 ソースファイル名:chap6-17-4.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

int copy_even(int dst[], const int src[], int n)
{
    int cnt = 0;
    for (int i = 0; i < n; i++) {
        if (src[i] % 2 == 0) {
            dst[cnt++] = src[i];
        }
    }
    return cnt;
}

int main(void)
{
    int src[] = {7, 4, 9, 2, 2, 5, 8};
    int dst[7];

    int m = copy_even(dst, src, 7);

    printf("偶数だけを%d個コピーしました。\n", m);
    for (int i = 0; i < m; i++)
        printf("dst[%d]=%d\n", i, dst[i]);

    return 0;
}

解説

  • src は読み取り専用なので const int src[] がぴったりです。
  • dst は書き込み先なので const は付けません。
  • 「配列を受け渡すときは、処理対象の個数 n を別に渡す」が基本形です。

演習6-11:条件を満たす添字列挙

要素数 n の int 配列 v の中から、threshold(しきい値) 以上の要素の添字を idx に格納する関数を作成せよ。
返却値は格納した添字の個数とする。

int collect_ge(const int v[], int idx[], int threshold, int n);

解答例

プロジェクト名:chap6-17-5 ソースファイル名:chap6-17-5.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

int collect_ge(const int v[], int idx[], int threshold, int n)
{
    int cnt = 0;
    for (int i = 0; i < n; i++) {
        if (v[i] >= threshold) {
            idx[cnt++] = i;
        }
    }
    return cnt;
}

int main(void)
{
    int v[] = {5, 12, 7, 20, 10};
    int idx[5];

    int c = collect_ge(v, idx, 10, 5);

    printf("条件を満たす要素は%d個ありました。\n", c);
    for (int i = 0; i < c; i++)
        printf("idx[%d]=%d (v[%d]=%d)\n", i, idx[i], idx[i], v[idx[i]]);

    return 0;
}

解説

  • 「見つけた位置を idx 配列に詰める」方式は、6-11系の王道パターンです。
  • idx のサイズは最大で n 個必要なので、呼び出し側は idx を n 以上で確保しておくのが安全です。
  • v は読むだけなので const を付けると「書き換えない」意思が明確になります。