C言語における高度なポインタ活用術:ポインタ配列、関数ポインタ、関数ポインタ配列、コールバック関数の実践

ポインタ配列とは

通常の配列が整数や文字などのデータをまとめて格納するのに対し、ポインタ配列はメモリアドレスを要素として保持する特殊な配列です。各要素が別の変数や配列の先頭アドレスを指し示すことで、間接的に複数のデータ群を管理できます。

例えば、複数の文字列を効率的に扱う場合、ポインタ配列は非常に有効です。各文字列の先頭アドレスを配列に格納することで、二次元的なデータ構造を簡潔に表現可能です。

#include <stdio.h>

int main() {
    int val1 = 10, val2 = 20, val3 = 30;
    int* ptr_list[3] = {&val1, &val2, &val3};
    
    for (int i = 0; i < 3; i++) {
        printf("値%d: %d\n", i+1, *ptr_list[i]);
    }
    return 0;
}

関数ポインタの基礎

C言語では関数もメモリ上に配置され、独自のアドレスを持ちます。この関数のアドレスを格納できる変数が関数ポインタです。関数ポインタを使うことで、実行時に呼び出す関数を動的に選択できるため、柔軟性の高いプログラム設計が可能になります。

関数ポインタの宣言は戻り値型 (*ポインタ名)(引数リスト)という形式を取ります。重要なのは、*ポインタ名を括弧で囲むことで、コンパイラに「これは関数ポインタである」と認識させる点です。

#include <stdio.h>

int multiply(int a, int b) {
    return a * b;
}

int main() {
    // 通常の関数呼び出し
    int result_normal = multiply(5, 4);
    
    // 関数ポインタの宣言と初期化
    int (*operation)(int, int) = multiply;
    
    // 関数ポインタ経由の呼び出し
    int result_via_ptr = operation(7, 8);
    
    printf("通常呼び出し: %d, ポインタ経由: %d\n", 
           result_normal, result_via_ptr);
    return 0;
}

typedefによる型定義

関数ポインタの宣言文は複雑になりがちです。typedefを使用することで、独自の型名を作成し、コードの可読性を向上させられます。

#include <stdio.h>

// 関数ポインタ型の別名定義
typedef int(*ArithmeticFunc)(int, int);

int divide(int x, int y) {
    return y != 0 ? x / y : 0;
}

int main() {
    ArithmeticFunc calc = divide;
    int answer = calc(20, 5);
    printf("計算結果: %d\n", answer);
    return 0;
}

関数ポインタ配列と転送表

関数ポインタを配列としてまとめたものが関数ポインタ配列です。この仕組みを活用した転送表(ジャンプテーブル)は、switch文の代わりに使用でき、コードの重複を大幅に削減します。

電卓プログラムでの応用

四則演算を行う電卓を例に、通常の実装と転送表を用いた実装を比較します。

#include <stdio.h>

typedef enum {
    OP_ADD = 1,
    OP_SUB,
    OP_MUL,
    OP_DIV,
    OP_QUIT = 0
} OperationType;

int add_func(int x, int y) { return x + y; }
int sub_func(int x, int y) { return x - y; }
int mul_func(int x, int y) { return x * y; }
int div_func(int x, int y) { return x / y; }

void display_menu() {
    printf("\n=== 電卓システム ===\n");
    printf("1: 加算  2: 減算\n");
    printf("3: 乗算  4: 除算\n");
    printf("0: 終了\n");
    printf("操作を選択: ");
}

int main() {
    // 関数ポインタ配列による転送表定義
    int (*calc_operations[])(int, int) = {
        add_func, sub_func, mul_func, div_func
    };
    
    OperationType choice;
    int num1, num2;
    
    while (1) {
        display_menu();
        scanf("%d", (int*)&choice);
        
        if (choice == OP_QUIT) break;
        if (choice < OP_ADD || choice > OP_DIV) {
            printf("無効な操作です\n");
            continue;
        }
        
        printf("2つの整数を入力: ");
        scanf("%d %d", &num1, &num2);
        
        int result = calc_operations[choice - 1](num1, num2);
        printf("結果: %d\n", result);
    }
    
    return 0;
}

コールバック関数の仕組み

コールバック関数は、関数ポインタを別の関数の引数として渡し、渡された関数の内部から呼び出される関数です。このパターンは、汎用的な処理の中でユーザー固有の処理を挿入する必要がある場合に威力を発揮します。

C標準ライブラリのqsort関数が代表的な例です。ソート対象のデータ型が事前に分からないため、比較処理をユーザーが提供するコールバック関数として実装します。

#include <stdio.h>
#include <stdlib.h>

int compare_ints(const void* ptr1, const void* ptr2) {
    int val1 = *(const int*)ptr1;
    int val2 = *(const int*)ptr2;
    return (val1 > val2) - (val1 < val2);
}

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

int main() {
    int dataset[] = {64, 34, 25, 12, 22, 11, 90};
    size_t data_size = sizeof(dataset) / sizeof(dataset[0]);
    
    printf("ソート前: ");
    print_array(dataset, data_size);
    
    qsort(dataset, data_size, sizeof(int), compare_ints);
    
    printf("ソート後: ");
    print_array(dataset, data_size);
    
    return 0;
}

バブルソートによるqsortの模倣実装

コールバック関数の動作を理解するため、qsortと同様のインターフェースを持つバブルソートを実装します。要素の交換処理も汎用的に行えるよう、サイズに依存しない実装とします。

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

typedef struct {
    char username[32];
    int age;
} Person;

// 汎用的なバイト単位交換関数
void swap_memory(void* p1, void* p2, size_t size) {
    unsigned char* bp1 = (unsigned char*)p1;
    unsigned char* bp2 = (unsigned char*)p2;
    for (size_t i = 0; i < size; i++) {
        unsigned char temp = bp1[i];
        bp1[i] = bp2[i];
        bp2[i] = temp;
    }
}

// コールバックを利用したバブルソート
void generic_bubble_sort(void* base, size_t count, size_t elem_size,
                         int (*compare)(const void*, const void*)) {
    unsigned char* arr = (unsigned char*)base;
    int swapped;
    
    for (size_t i = 0; i < count - 1; i++) {
        swapped = 0;
        for (size_t j = 0; j < count - i - 1; j++) {
            void* current = arr + j * elem_size;
            void* next = arr + (j + 1) * elem_size;
            
            if (compare(current, next) > 0) {
                swap_memory(current, next, elem_size);
                swapped = 1;
            }
        }
        if (!swapped) break;
    }
}

// Person構造体の年齢比較関数
int person_age_comparator(const void* a, const void* b) {
    const Person* p1 = (const Person*)a;
    const Person* p2 = (const Person*)b;
    return p1->age - p2->age;
}

int main() {
    Person members[] = {
        {"takashi", 28},
        {"akira", 22},
        {"yuki", 35}
    };
    size_t member_count = sizeof(members) / sizeof(members[0]);
    
    printf("ソート前:\n");
    for (size_t i = 0; i < member_count; i++) {
        printf("%s: %d歳\n", members[i].username, members[i].age);
    }
    
    generic_bubble_sort(members, member_count, sizeof(Person), 
                       person_age_comparator);
    
    printf("\n年齢順ソート後:\n");
    for (size_t i = 0; i < member_count; i++) {
        printf("%s: %d歳\n", members[i].username, members[i].age);
    }
    
    return 0;
}

タグ: C言語 関数ポインタ コールバック関数 qsort 転送表

8月8日 16:35 投稿