授業で説明しないCの機能



時間の都合上、C言語の機能の一部については説明を省略しています。 それらの機能は習得難易度が高かったり使用できる状況が限られていたりしますが、 特定の状況では非常に便利な場合があります。 余力のある人は以下の説明を読んで、勉強してみてください。 2年生以上になって知識が増えてから、 あらためて読むのもよいかと思います。


voidポインタ

授業で扱ったように、 ポインタ型変数は int などの型を指定して宣言し、 その型の変数だけを指すことができます。

int m[10];
char str[] = "hello";
int *ip;
char *cp;
ip = m;   // ip = &m[0]; でも同じ
cp = str;
ip ++;  // m[1]を指す
cp ++;  // str[1]を指す

ポインタ変数のアドレス演算 (++など) は指す型の大きさ単位で計算されるため、 違う型のデータを指しているとおかしな結果になってしまいます。 たとえばこのコード例で cp=m; などと代入できてしまうと、 cp ++; としたときにアドレスが(char 型の大きさの) 1 だけ加算され、(おそらく4バイトの)整数の途中を指してしまうという、 おかしな結果になってしまいます。 このため、ポインタ変数で宣言時と異なる型のデータを指すような代入を行うと、 型が異なると警告されます。

しかし、複数の型を取りうる値を一つのポインタで指したいことがあります。 このようなときは、ポインタ型 void * を用います。 たとえば、次のような例を考えます。

#include <stdio.h>
 
typedef enum {tt_report, tt_arbeit} todo_type_t;
 
typedef struct {
    char *subject;
    char *title;
} report_t;
typedef struct {
    char *place;
    int st, en;
} arbeit_t;
 
report_t report1 = {"中級プログラミング", "第一回課題"};
report_t report2 = {"計算機アーキテクチャII", "メモリ階層"};
arbeit_t arbeit1 = {"Makayan", 18, 23};
arbeit_t arbeit2 = {"中華1000番", 17, 21};
void print_todo(int month, int day, todo_type_t type, ??? *todo){
    printf("%2d/%2d ", month, day);
    switch(type){
    case tt_report:
        printf("%sレポート「%s」\n", todo->subject, todo->title);
        break;
    case tt_arbeit:
        printf("%sでバイト(%d-%d時)\n", todo->place, todo->st, arbeit->en);
        break;
    }
}
int main(void){
    print_todo(10, 7, tt_report, &report1);
    print_todo(10, 9, tt_arbeit, &arbeit1);
    print_todo(10, 16, tt_arbeit, &arbeit2);
    print_todo(11, 10, tt_report, &report2);
    return 0;
}

レポートやアルバイトといった予定が report1arbeit1 といった変数に入っていて、これを表示関数 print_todo を呼んで次々に表示させたいのですが、 レポートとアルバイトはそれぞれ異なる構造体型になっています。 関数の引数は型を一つに決める必要がありますが、 print_todo 側の仮引数 todoreport_t*, arbeit_t* のどちらにしても、もう一方を受け取れなくなってしまいます。解決策として

なども考えられますが、前者は両型共通の日付表示のコードが重複しますし (日付表示も別関数に分けてもよいが面倒)、 後者は不要なメンバまで持つことになるため、 メモリが無駄になります (共用体を使う手もありますが)。 このような場合は todovoid * にすると、どちらの構造体ポインタ型も受け取ることができます。

void print_todo(int month, int day, todo_type_t type, void* todo){
    report_t *report;
    arbeit_t *arbeit;
    printf("%2d/%2d ", month, day);
    switch(type){
    case tt_report:
        report = todo;
        printf("%sレポート「%s」\n", report->subject, report->title);
        break;
    case tt_arbeit:
        arbeit = todo;
        printf("%sでバイト(%d-%d時)\n", arbeit->place, arbeit->st, arbeit->en);
        break;
    }
}

実行結果

10/ 7 中級プログラミングレポート「第一回課題」
10/ 9 Makayanでバイト(18-23時)
10/16 中華1000番でバイト(17-21時)
11/10 計算機アーキテクチャIIレポート「メモリ階層」

ただし、以下の点に注意する必要があります。

実用的には、 voidポインタは上記例のように単独で使ってもあまり利点がありません。 様々な型のデータを同じ関数に渡せたとしても、 実際にデータにアクセスする際には個々の型で場合分けして代入/キャストする必要があります。 このため、コードがあまりコンパクトにならないうえ、 新しい型のデータを導入する度にその関数を拡張することになります。 関数ポインタと合わせて使うことで、 多様なデータ型を扱うコードを美しく書くことができます。


関数ポインタ

授業で扱った「ポインタ」が他の変数などデータの入っている領域を指すのに対し、 関数ポインタは名前の通り、関数を指すポインタです (実際には、関数のアドレスが入っています)。 通常、関数を呼び出すときは関数名を明記して書きますが、 関数ポインタを使うと以下のように間接的に呼び出すことができ、 呼び出す関数を変更することができます。

#include <stdio.h>
 
int add(int m, int n);
int sub(int m, int n);
 
int main(void){
    int(*f)(int, int);  // int型引数2個をとりint型を返す関数へのポインタf

    f = add;            // ポインタfが関数addを指す
    printf("%d\n", f(10, 20));
    f = sub;            // ポインタfが関数subを指す
    printf("%d\n", f(10, 20));
    return 0;
}
int add(int m, int n){
    return m+n;
}
int sub(int m, int n){
    return m-n;
}

実行結果

30
-10

この例だとまったくありがたみがありませんが(素直に addsub を呼べばよい)、 ポインタを使って間接的にデータを示すことで処理するデータを差し替えられたように、 関数ポインタを使うと、呼び出す関数を実行時に差し替えることができます。 具体的には、以下のような使い道があります。

なお、関数ポインタptrは、以下の形式で宣言します。

返り値の型(*ptr)(引数の型の並び);

例1: 引数・返り値なし
void(*p)(void);
例1: 文字列(文字型ポインタ)2個を引数とし、文字列(文字型ポインタ)を返す
char *(*p)(char *, char *);

関数ポインタに関数(のアドレス)を代入するには、右辺に関数名を書きます。 代入後、関数ポインタの指す関数を呼び出すには、 通常の関数呼び出しと同様にして、 関数名の部分に関数ポインタを書きます。 関数ポインタも変数の一種なので、 別の関数の引数にしたり、 構造体のメンバにしたりできます。

関数内の処理の一部を差し替える

データを決まった順序で並べ替えるソートは実用上もよく使われますし、 2年生の「上級プログラミングI」でも実際にソートプログラムを書く演習があります。 しかし、アルゴリズムの説明や演習でよく使われる「多数の整数を昇順/降順に並べ替える」 関数は、実プログラムではあまり役に立ちません。 表計算ソフトなどでのデータ処理を思い浮かべてもらうとわかりやすいと思いますが、 実用的なデータ処理では複数の値を組にしたものを、 1個の「データ」とすることが多いためです。

typedef struct {
    char *name;  // 氏名
    int score;   // 得点
} student_t;
int main(void){
#include <stdio.h>
    student_t students[1000];
    read_data(students); // 各学生データをファイルから読み込む
    sort_data(students); // クイックソートアルゴリズムでデータを成績順に並べ替える
}

int 型配列の内容をクイックソートアルゴリズムで並べ替える関数を作ってあったとしても、 その関数は sort_data としては使えません。この場合、大小判定はメンバ score を比較しなければならず、
if (students[i] < students[j])
ではなく
if (students[i].score < students[j].score)
と書く必要があります。 このため、 sort_data は 「student_t 型をソートする関数」として、新たに作成する必要があります。 さらに、似たようなプログラムでも構造体中の「得点」が数学と英語に分かれていれば、 両者を足してから比較する必要があるでしょうし、場合によって 「合計点が同じ場合は数学の得点が高い方を上位とする」といったルールがあれば、 そのような判定も書く必要があります。 また、一つのプログラム内で 「総合得点でソート」 「数学の得点でソート」 「英語の得点でソート」 など複数のソート方法が必要になる場合もあります。 こういったソート毎に毎回クイックソート関数を書くのは大変です。

しかしこうした関数の中でも、 クイックソートのアルゴリズムを実現している大部分のコードは同じであり、 大小判定を行うコードだけが異なっています。 したがって、以下のようにできれば一つの関数で様々なデータをソートできます (???となっている箇所は後で説明します)。

void qsort_generic(??? data[], int num, int type){ // なんでもソート
    ....
    if (qsort_generic_compare(type, data[i], data[j]) < 0)){
    ....
}
int qsort_generic_compare(int type, ??? x, ??? y){
    if (type == TYPE_INT){
        if (x < y) return -1;
        if (x > y) return 1;
        return 0;
    }
    if (type == TYPE_STUDENT){
        if (x.score < y.score) return -1;
        if (x.score > y.score) return 1;
        return 0;
    }
    ....
}

qsort_genericにはクイックソートのアルゴリズムが書かれていますが、 二つのデータ値を比較する必要がある箇所で、 == などを使わずに比較関数 qsort_generic_compare を呼び出します。 qsort_generic_compare では、今回扱っているデータの型に応じて適切な比較を行い、 結果を返します。 これにより、 qsort_generic_compare の方は任意のデータをソートできるようになります。

しかし、上記のコードは問題点が二つあります。

Cの標準ライブラリには、これらの問題点を解決した汎用のクイックソート関数 qsort が用意されています。プロトタイプ宣言は以下の通りです。
void qsort(void *base, size_t n, size_t size, int(*cmp)(void *, void *))
baseはデータ列(配列など)の先頭アドレスであり、 nはデータ数です。 上記の qsort_generic と異なり、データの種類を指定する代わりに、データ1要素の大きさを指定する size と、比較関数への関数ポインタ cmpが追加されています。

問題点の一つ目は、 voidポインタを用いることで解決しています。 ただし、void *型である base はデータ1要素の大きさがわからないため、 base[i] のように添え字で要素指定ができません。 このため、一要素の大きさを size で渡し、内部で base + i * size のような計算を行っています。 また、上記コードでは書いていませんが、 要素の入れ替えなどを行うときに代入が必要です。 baseの指す先の型がわからないと普通の代入文は書けませんので、 かわりに size を利用して、「対象要素のアドレスから大きさ分のバイト数をコピー」しています。

問題点の二つ目は、 「渡したデータに対応した比較関数」をプログラマが書き、 関数ポインタで渡すことで解決しています。 最初のプログラムを、ライブラリ関数 qsort を使うように直したコードを以下に示します。

typedef struct {
    char *name;  // 氏名
    int score;   // 得点
} student_t;
 
int cmp_student(void *s1, void *s2){
    if (((student_t*)s1)->score < ((student_t*)s2)->score) return -1;
    if (((student_t*)s1)->score > ((student_t*)s2)->score) return 1;
    return 0;
}
 
int main(void){
    student_t students[1000];
    read_data(students); // 各学生データをファイルから読み込む
    qsort(students, 1000, sizeof(student_t), cmp_student);
}

このように、ソートしたいデータの比較関数を書くだけで、 任意の形式のデータをソートできます。 自分で作成する関数についても、 qsort と同様の構造をとることで、 データ毎に異なる部分を切り出して差し替えできるようにして、 共通部分のコードを再利用できます。

データの種類毎に異なる関数を呼び出す

たとえば、シューティングゲームで複数の種類の敵を出すことを考えます。 とりあえず、敵は以下の3種類があり、 複数種類が(pawn8機+knight2機など)任意の組み合わせで登場できるものとします。

typedef enum {
    en_pawn,     // ゆっくり前進するだけの雑魚
    en_knight,   // 変則的な動きをする
    en_rook,     // 高速につっこんでくる
} enemy_type_t;

それぞれの敵個体は、上記のいずれの種類であるか、現在位置、体力などの情報を持ちます。 これらの情報のうち、同種の敵で共通のもの(体力初期値や表示色)と、 個体毎に異なるもの(現在位置や現在の体力)で、異なる構造体型に分けます。 前者は敵の種類毎に一つずつ必要なので配列変数で、 後者はゲームの進行につれ、不規則に出現(生成)/破壊(解放)を繰り返すので、 リスト構造で管理します。

typedef struct { // 敵の種類毎の情報
    int color;     // 表示色(RGBで16進数2x3=6桁)
    int life_max;  // 登場時の体力初期値
} enemy_stat_t;
typedef struct enemy_s{ // 敵の個体毎の情報
    enemy_type_t type; // この個体の種類
    int x, y;          // 現在位置
    int life;          // 現在の体力
    struct enemy_s *next;  // 敵リスト上の次の敵
} enemy_t;
enemy_stat_t enemy_stat_table[] = {
    {0x00ff00, 1},
    {0xff0000, 8},
    {0xffff00, 4},
};
enemy_t *enemy_list;

敵の種類別情報は配列 enemy_stat_table に格納され、 enemy_type_t型の値をインデックスとしてアクセスできます。 また、ポインタ enemy_list からリスト構造をたどることで、 現在生きているそれぞれの敵個体の情報にアクセスできます。 後者の初期化やリストノードの生成・破棄については、 ここでは本題ではないので省略します。

すべての(生存している)敵を動かす関数は、以下のようになります。

void move_enemy_list(void){
    enemy_t *enemy = enemy_list;
    while (enemy != NULL){
        switch (enemy->type){
        case en_pawn:
            // ゆっくり前進する処理
            break;
        case en_knight:
            // 変則的な動きの処理
            break;
        case en_rook:
            // 高速につっこんでくる処理
            break;
        default:
            puts("Unknown enemy type.");
            exit(1);
        }
        enemy = enemy->next;
    }
}

この書き方だと、敵の種類を増やす毎に caseを増やしていく必要があり、 関数 move_enemy_listがどんどん長くなります。 また、 すべての敵を描く関数draw_enemy_list なども同様の書き方になるため、 プログラム全体の見通しが悪くなり、 draw_enemy_listの方だけ追加し忘れた、 といったミスも起こりやすくなります。 そこで、次のように「移動する処理」を関数として分離し、 敵の種類毎にどの関数を呼び出すかを、関数ポインタを使ってデータ化します。

typedef struct { // 敵の種類毎の情報
    int color;     // 表示色(RGBで16進数2x3=6桁)
    int life_max;  // 登場時の体力初期値
    void(*move_func)(enemy_t *enemy);
} enemy_stat_t;

void move_pawn(enemy_t *enemy){
    // ゆっくり前進する処理
}
void move_knight(enemy_t *enemy){
    // 変則的な動きの処理
}
void move_rook(enemy_t *enemy){
    // 高速につっこんでくる処理
}
enemy_stat_t enemy_stat_table[] = {
    {0x00ff00, 1, move_pawn},
    {0xff0000, 8, move_knight},
    {0xffff00, 4, move_rook},
};

このように、敵の種類毎の「移動関数」を enemy_stat_table に格納しておくことにより、 すべての敵を移動する関数は次のようにシンプルなコードになります。

void move_enemy_list(void){
    enemy_t *enemy = enemy_list;
    while (enemy != NULL){
        enemy_stat_table[enemy->type].move_func(enemy);
        enemy = enemy->next;
    }
}

これぐらいの短い例ではあまりメリットを感じられませんが、 新しい敵を追加する際に

だけでよくなります。 void move_enemy_list のような既存の関数を変更しなくてよいため、 一度作ったコードに対し変更時にバグを仕込んでしまう、 というリスクを避けられます。 また、敵の情報を保持する enemy_stat_table 上では、「移動関数」のような処理も「体力初期値」のような値と同様に扱えるため、 敵の種類が増えても敵情報全体が把握しやすくなります。

typedef enum {
    en_pawn,         // ゆっくり前進するだけの雑魚
    en_knight,       // 変則的な動きをする
    en_rook,         // 高速につっこんでくる
    en_tough_rook, // 体力の高いrook 
    en_nt_pawn,  // knightの動きをするpawn
    
} enemy_type_t;
typedef struct { // 敵の種類毎の情報
    int color;     // 表示色(RGBで16進数2x3=6桁)
    int life_max;  // 登場時の体力初期値
    void(*init_func)(enemy_t *enemy);// 生成時に呼ばれる。必要ない場合はNULLにしておく
    void(*move_func)(enemy_t *enemy);
    void(*draw_func)(enemy_t *enemy);
} enemy_stat_t;
enemy_stat_t enemy_stat_table[] = {
    {0x00ff00, 1, NULL, move_pawn, draw_pawn},
    {0xff0000, 8, NULL, move_knight, draw_knight},
    {0xffff00, 4, init_rook, move_rook, draw_rook}, // 生成時に専用エフェクトを発生
    {0xffffff, 12, NULL, move_rook, draw_rook},
    {0x00ffff, 1, NULL, move_knight, draw_pawn}, // 見た目は色違いのpawn
    
};

可変引数リスト

通常、授業や教科書で最初に関数の使い方・作り方が出てくるときには、 「Cの関数は引数の個数とそれぞれの型が決まっている」 と説明しています。

#include <stdio.h>
#include <math.h>
double max(double a, double b){
    if (a>b)
        return a;
    else
        return b;
}
int main(void){
    double x, y, d1, d2;
    read_angles(&d1, &d2);
    x = cos(max(d1, d2));
    y = sin(max(d1, d2));
    printf("(%f, %f)\n", x, y);
}

標準ライブラリ関数 cos, sin は、 double 型の引数1つ(角度)を受け取り、 cos/sin値を double 型で返します。 この情報は include で取り込んだ math.h に関数プロトタイプ宣言として書かれているため、 万が一ユーザが間違って異なる個数や型の引数を渡しても、 エラーとして検出してくれます。

一方、ユーザが自作した関数 max は、 最大値を返す関数です。 上記のように引数の個数と型を決めないといけないため、 ここでは2個の実数 ( double 型) を引数にしています。 したがって、整数型の変数について最大値を知りたい、 実数型だが3つ以上の変数について最大値を知りたいと行った場合は、 似たような変数を別途作成する必要があります。 後者の場合は、配列を引数にする方法がありますが、 調査対象となる値をすべて同じ配列に格納する必要があるため、 状況によっては不便です。

double max(int n, double a[]){
    // a[0]〜a[n-1]のうち値が最大のものを見つけて返す
}
int main(void){
    double d[4], l, x, y, z;
    ....
    // x, y, zの最大値をlに格納したい
    // x, y, zを直接maxに渡せないので、いったん配列dにコピーしないといけない
    d[0] = x;
    d[1] = y;
    d[2] = z;
    l = max(3, d);
}

しかしよく考えると、授業や教科書で最初のうちに習う printf 関数は、引数の個数と型が決まっていません。

    char c;
    int x, y, old_x, old_y;
    ....
    printf("hello, world\n");
    printf("%c moves from(%d, %d) to (%d %d)\n", unit_chr, old_x, old_y, x, y);

上の例で考えると、最初の printf 関数は引数に文字列リテラルを渡していますが、 次の printf 関数では文字列リテラルに続いて文字型1個と整数型4個を渡しています。 つまり関数プロトタイプ宣言で書くと、 それぞれ次のようになります (通常無視しますが、 printfint 型を返します)。

    int printf(char *s);
    int printf(char *s, char c, int a, int b, int c, int d);

少し面倒ですが、 Cでもこのように個数や型が異なる引数を受け付ける関数を作ることができます。 以下、やり方を説明します。

まず、関数定義の際の引数は、必須の引数を通常通り型付きで並べた後に、 宣言 ... を書きます。たとえば printf では、 最初に文字列(を指す文字型ポインタ)一つが必須であり、 その後には文字列内の %c, %d などに応じた型の引数を並べることができます。 この場合は、以下のように定義されます。

    int printf(char *s, ...);

... の部分は、この通りピリオド3個を並べます。 これでこの関数は、先頭に文字列1個を渡せば、 あとは好きな型の引数を好きなだけ渡すことができます (※当然ですが、 この関数が内部で処理できない型は、 渡しても正常に動作しません。 printf 関数であれば、構造体型とかは不可です)。

しかし上の宣言では、第2引数以降は関数側の仮引数がないため、 関数内で読み書きが記述できません。 これらの引数(以下、無名引数と呼びます) に通常の名前を振ることはできない(個数不明だから)ので、 少し回りくどい方法で順番にアクセスしていきます。

まず、ヘッダファイル stdarg.h を取り込みます。 これにより、以下の型やマクロが使えるようになります。

可変引数をとる関数内では、 va_list 型の変数を一つ定義します。 この変数(ここではapとする)は、 無名引数をどこまで参照したかを表すポインタです。 実際の参照は、以下の手順で行います。

  1. va_startによりapを初期化する。
    このとき、マクロの第2引数には、...の直前の (つまり名前のある最後の)仮引数名を渡します。 これにより、 ap は 最初の無名引数を指すようになります。
  2. va_arg(ap, type)により、 apが現在指している引数を参照し、 その後apが次の引数を指すようにする。
    以下の点に注意してください。
    • va_arg を呼ぶ度にapは次の引数へと進んでしまうので、 返り値を変数に代入するか、 値を使いたい式内にva_argを埋め込む必要があります。
    • 最後の引数を判別する手段はないので、 なんらかの方法で無名引数の個数を渡し、 その回数だけループするように書く必要があります。 名前付きの引数で個数を渡す方法もありますし、 printf 関数では、書式文字列中に % が出現する毎にva_argを呼び出しています。
    • 無名引数は型が不明なので、 正しい型をva_argに渡す必要があります。 上の max の例ではすべて double を渡せばよいので簡単ですが、 printf のように型が混在する・呼び出し毎に変わる場合は、 やはりなんらかの方法で各無名引数の型を関数に教える必要があります。 printf の場合は、「%cに対応した無名引数ならchar型」 などと書式文字列から型を決めることができます。
  3. 無名引数の参照が終わったら、 va_end を呼び出して、後始末をします。

以下にコード例を示します。 関数 max は上のコードと同様に任意個数の実数値の最大値を返す関数ですが、 実数値の並びを第2引数に配列で渡す代わりに、 第2引数以降に引数として並べることができます。

#include <stdio.h>
#include <stdarg.h>

double max(int n, ...);

int main(void){
    double max_val;
    max_val = max(4, 1.4, 5.1, -2.0, 4.6);
    printf("max=%f\n", max_val);
    return 0;
}

double max(int num, ...){
    va_list ap;
    int i;
    double max_val = 0, val;

    va_start(ap, num);
    for (i = 0 ; i < num ; i++){
        val = va_arg(ap, double);
        printf("current val=%f\n", val);
        if (max_val < val)
            max_val = val;
    }
    va_end(ap);
    return max_val;
}

注意点として、以下のような呼び出し方をすると誤動作します。

    max_val = max(4, 1.4, 5.1, -2, 4.6);
引数の中に整数値-2が混ざっています。 普通の関数ならば、仮引数の型に自動的に変換されますが、 この maxでは 無名引数の型が不明なので、 -2int型のまま引き渡されます。 max内の va_arg(ap, double) は 「double型に変換してくれる」 わけではなく 「double型だと思って参照する」 だけなので、このように int型の値にアクセスしてしまうと値が化けてしまいます。

ビット演算

〈工事中〉


extern宣言

〈工事中〉


分割コンパイル

〈工事中〉


引数付きマクロ

〈工事中〉



最終更新: 2023年 4月 13日 木曜日 18:00:46 JST

御意見、御感想は ohno@arch.info.mie-u.ac.jp まで