プログラミング基礎 第14回

目的

ここまでのまとめを兼ねて,講義で触れてこなかった話題を補足しましょう.

マクロ

マクロとは,プログラム中で良く使われる一連の処理や 定数などに名前を付け, プログラムを読みやすくしたり,変更の手間を軽減するための仕組みです. マクロは #define 文という擬似命令によって定義されます. 擬似命令はコンパイルに先立ってプリプロセッサによって解釈,展開される命令です. 例えば以前紹介した
#define PI 3.14159265358979323846
もマクロの一種で,以降の文脈で PI という文字列は 3.14159265358979323846 に置き換わります(引用符 " " で囲まれた場合を除く). また,引数つきのマクロを使うことで, もう少し凝った応用も可能です. 例えば,
#define MAX(A, B) A > B ? A : B
と定義しておくと
i = MAX(2, 3);
はプリプロセッサによって
i = 2 > 3 ? 2 : 3;
と展開されます. ここで
条件式 ? 式1 : 式2;
は3項演算子の ? を使った記法で, 条件式が真ならば式1偽ならば式2を評価するという 意味になります. 従って,マクロ MAX(A, B) は A と B の大小を比較し,大きい方の値を返す次の関数と 見かけ上は似た動作をします.
int max(int a, int b)
{
    int v;
    if (a > b) {
        v = a;
    } else {
        v = b;
    }
    return (v);
}
関数の呼び出しは引数の値をローカルなメモリ領域にコピーするといった処理が伴いますが, マクロではそのような処理が不要となるため, ループの中で繰り返し関数を呼び出す部分をマクロに置き換えることで 高速な実行が可能になります. しかも関数とは異なり引数の型を決めておく必要がないため, A や B の型が実数であるか整数であるかを気にせず同じように扱えるという利点もあります.

しかし,マクロの安易な利用は思いがけないトラブルを招くこともあります. 例えば

i = 2 * MAX(2, 3);
i = 2 * 2 > 3 ? 2 : 3;
と展開され,比較演算子 > よりも算術演算子 * の方が優先度が高いため, i の値は 2 となり期待した結果とは異なってしまいます. このようなトラブルを防ぐためにはマクロの定義を括弧を使って
#define MAX(A, B) ((A) > (B) ? (A) : (B))
のように定義する必要があります. しかし,このように定義した場合でも
i = MAX(a++, b++);
といった記述は
i = ((a++) > (b++) ? (a++) : (b++));
と展開されるため,関数の場合と比べてインクリメント演算子が余計に実行されてしまいます. このようにマクロの利用はその仕組みを十分に理解しておくことが大切です.

なお,#define などの擬似命令は,プリプロセッサによって行単位で処理されます. したがって,マクロの定義も原則として1行に記述する必要がありますが, 1行が非常に長くなって見にくくなってしまう場合は, 行末に次の行を連結するという意味の¥記号 (バックスラッシュ)を書いておくことで, 見かけ上行を分割することができます. 更に,このテクニックを応用すると, 複数の文を組み合わせたひとまとまりの手順を一つのマクロに 置き換えることも可能です(これを複文マクロといいます). 例えば,

#define TUS() {¥
    printf("******* *     *  ***** ¥n");¥
    printf("   *    *     * *     *¥n");¥
    printf("   *    *     * *      ¥n");¥
    printf("   *    *     *  ***** ¥n");¥
    printf("   *    *     *       *¥n");¥
    printf("   *    *     * *     *¥n");¥
    printf("   *     *****   ***** ¥n");¥
}
のようなマクロを定義しておけば,以降
    TUS();
と記述するだけでアスキーアート風に画面に大きく"TUS" という文字を表示できるようになります. なお,上の例で複文マクロの定義を中括弧{ }で囲んでいるのは,
    if (tus != 0) TUS();
のように if 文と組み合わせた場合,
    if (tus != 0) printf("******* *     *  ***** e¥n");
    printf("   *    *     * *     *¥n");
    printf("   *    *     * *      ¥n");
    printf("   *    *     *  ***** ¥n");
    printf("   *    *     *       *¥n");
    printf("   *    *     * *     *¥n");
    printf("   *     *****   ***** ¥n");
のように展開されて,最初の行だけが特別扱いされてしまうのを避けるためです.

もうひとつのマクロの応用例として条件コンパイルがあります. 条件コンパイルは擬似命令で

#ifdef マクロ
文
#endif
と書いておくと,マクロが定義されていた時のみ #if から #endif で囲まれた行を コンパイラに渡すという機能を利用したもので,例えば
#ifdef DEBUG
    fprintf(stderr, "x = %d¥n", x);
#endif
などとしておくと,予め
#define DEBUG 1
等と定義した場合のみ変数 x の値を表示させることができます. すなわち,この状態で printf デバッグを行い完全にバグが修正できたら 上の #define 文を削除するだけでデバッグ用の printf 文を即座に無効にできるというわけです. この他にも #ifndef, #else, #if, #elif といった擬似命令を組み合わせることで, 様々な条件でコンパイルを行うことが可能です. また,マクロの定義は #define 文だけではなく, コンパイラのオプションを通して行うことも可能です.
nodabls?% gcc -Wall -DDEBUG=1 foo.c

※ 最近のコンパイラではオプションによって関数をインライン展開 (マクロと同様な仕組みにより関数呼び出しのオーバヘッドを削減する手法) することが可能であるため, 必ずしもマクロに置き換えた方が高速になるとは限りません.

構造体

これまでの講義で,C言語には整数や実数,文字列といった様々な型のデータを 変数として扱えることを学びました. また,配列を使うことで, 複数の変数に共通の名前を付け,添え字を介して効率的に扱えることも示しました. しかし,配列の要素は同一の型でなくてはならないという制約があります. これに対し,異なる型の変数に共通の名前を付けてまとめて扱えるようにしたものを 構造体 (structure) と呼びます. 以下のプログラムは, 地球と火星が最接近した瞬間(つまり太陽からみて2つの惑星が同一方向に一直線上に並んだとき) から day 日後の両惑星間の距離を計算するプログラムです※1

ここでは,各惑星の 公転周期(倍精度実数), 公転半径(倍精度実数), 衛星の数(整数)という 3つのデータをまとめて構造体を構成しています.

#include <stdio.h>
#include <math.h>

#define PI 3.14159265358979323846

/* 構造体テンプレートの宣言 */
struct planet {
    double period;         /* 公転周期 (日) */
    double radius;         /* 太陽からの平均距離 (10^6 km) */
    int satellites;        /* 衛星の数 */
};

/* プロトタイプ宣言 */
double distance(struct planet, struct planet, double);

int main()
{
    double day;
    struct planet earth, mars;	/* 構造体変数の宣言 */

    /* 地球のデータ */
    earth.period = 365.26;
    earth.radius = 149.6;
    earth.satellites = 1;

    /* 火星のデータ */
    mars.period = 686.98;
    mars.radius = 227.9;
    mars.satellites = 2;

    /* 最接近時からの日数を入力 */
    scanf("%lf", &day);

    /* 地球と火星の距離を表示 */
    printf("distance between two planets = %.3f (10^6 km)\n",
           distance(earth, mars, day));
    return (0);
}

/* day 日後の2惑星間の距離を計算 */
double distance(struct planet p1, struct planet p2, double day)
{
    double dist, t, x1, y1, x2, y2;

    t = 2.0 * PI * day / p1.period;
    x1 = p1.radius * cos(t);
    y1 = p1.radius * sin(t);
    t = 2.0 * PI * day / p2.period;
    x2 = p2.radius * cos(t);
    y2 = p2.radius * sin(t);
    dist = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
    return (dist);
}
構造体を使用するためには, まずstruct という命令に続いて 構造体タグと呼ばれる名前と 構造体メンバーと呼ばれる変数を列挙したブロック を記述して構造体の雛形(テンプレート)を宣言します.
/* 構造体テンプレートの宣言 */
struct planet {
    double period;         /* 公転周期 (日) */
    double radius;         /* 太陽からの平均距離 (10^6 km) */
    int satellites;        /* 衛星の数 */
};
上の例では planet が構造体タグ, period, radius, satellites という3つの変数が 構造体メンバーに相当します. テンプレートは構造体に含まれるデータの型を定義しただけであり, 実際にデータを格納するメモリを確保するためには, 更に構造体変数の宣言が必要となります.
    struct planet earth, mars;	/* 構造体変数の宣言 */
ここでは地球と火星のデータを格納するために, 構造体タグ planet で参照される2つの構造体変数 earth と mars を宣言しています.

なお,typedef 宣言を使うと, 構造体をあたかも新しいデータ型のように扱うことができます.

/* 構造体の typedef 宣言 */
typedef struct {
    double period;         /* 公転周期 (日) */
    double radius;         /* 太陽からの平均距離 (10^6 km) */
    int satellites;        /* 衛星の数 */
} planet;

   :       :

    planet earth, mars;   /* 構造体変数の宣言 */

   :       :

double distance(planet p1, planet p2, double d)

   :       :

この場合 planet は構造体タグではなく データ型の別名を表す typedef 名 として扱われます. typedef 名は関連付けられたデータが構造体であることを知っていますので, 構造体変数を宣言する際に struct を記述する必要がありません.

構造体変数と構造体メンバーをドット記号 . で結びつけると,構造体の個々のデータにアクセスすることができます. 上の例では構造体の個々のデータに順番に値を代入していますが, これを一般の変数と同様に構造体変数の宣言時に初期化することも可能です.

struct planet earth = {365.26, 149.6, 1};   /* 地球のデータ */
struct planet mars = {686.98, 227.9, 2};    /* 火星のデータ */
typedef 宣言を行った場合の初期化も同様です.
planet earth = {365.26, 149.6, 1};          /* 地球のデータ */
planet mars = {686.98, 227.9, 2};           /* 火星のデータ */

構造体変数は,他の変数と同様に代入を行ったり, 関数の引数として利用したりすることができます※2. しかし,大規模な構造体ではメモリサイズが大きくなるため, 代入や関数呼び出しに伴うデータコピーのオーバヘッドの処理が重くなる傾向があります. このような場合には,構造体のポインタを介したデータのやり取りが有効です. 上のプログラムで関数 distance の引数を ポインタに変更した例を以下に示します.

#include <stdio.h>
#include <math.h>

#define PI 3.14159265358979323846

/* 構造体の typedef 宣言 */
typedef struct {
    double period;         /* 公転周期 (日) */
    double radius;         /* 太陽からの平均距離 (10^6 km) */
    int satellites;        /* 衛星の数 */
} planet;

/* プロトタイプ宣言 */
double distance(planet *, planet *, double);

int main()
{
    double day;
    planet earth = {365.26, 149.6, 1};          /* 地球のデータ */
    planet mars = {686.98, 227.9, 2};           /* 火星のデータ */

    /* 最接近時からの日数を入力 */
    scanf("%lf", &day);

    /* 地球と火星の距離を表示 */
    printf("distance between two planets = %.3f (10^6 km)\n",
           distance(&earth, &mars, day));
    return (0);
}

/* day 日後の2惑星間の距離を計算 */
double distance(planet *p1, planet *p2, double day)
{
    double dist, t, x1, y1, x2, y2;

    t = 2.0 * PI * day / p1->period;
    x1 = p1->radius * cos(t);
    y1 = p1->radius * sin(t);
    t = 2.0 * PI * day / p2->period;
    x2 = p2->radius * cos(t);
    y2 = p2->radius * sin(t);
    dist = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
    return (dist);
}
ここで赤色の部分に示した -> という記号は ポインターが参照する構造体のメンバーを参照する演算子で, 以下の2つは全く同じ意味となります.
    t = 2.0 * PI * day / (*p1).period;
    t = 2.0 * PI * day / p1->period;

※1 ここでは地球と火星の公転軌道が共に真円で同一平面上にあると仮定しています. 従ってこのプログラムの出力結果はあまり正確とはいえません.

※2 古いコンパイラでは構造体の代入や関数の引数での構造体の利用がサポートされていない場合があります.

メモリの動的割り当て

大量のデータを扱うときには一般に配列を使いますが, 配列を宣言するときのサイズは扱うデータの最大数に合わせておく必要があります. マクロを使って,
#define MAXSIZE 1000

   :       :

int a[MAXSIZE], b[MAXSIZE]
などとしておけば,扱うデータの数に応じてプログラムを書き換える手間を軽減できますが, プログラム実行中にデータの数が判明するような状況には対処できません※1. このような場合には,メモリを動的割り当て・解放する malloc, free 関数を使う必要があります. malloc はプログラム実行中に必要なサイズのメモリを確保する関数で, 確保したメモリの内容はポインタを介してアクセスできるようになります. また,free は不要になったメモリを解放する関数です. これらの関数のを利用するにはヘッダファイル stdlib.h をインクルードしておく必要があります. 例えば int 型のデータを 1000 個扱いたいなら
#include <stdlib.h>

   :       :

int *a, *b;

a = (int *)malloc(sizeof(int) * 1000);
b = (int *)malloc(sizeof(int) * 1000);

   :       :   /* a の k 番目の要素にアクセスしたいなら a[k] などとする */

free(b);
free(a);
などとします.ここで sizeof(データ型) は 指定したデータ型のサイズ(バイト数)を表す演算子で, 本講義で使用する計算機では sizeof(int) = 4 となります. malloc の引数は必要なメモリのバイト数を意味しますので, 扱うデータ型のサイズにデータ数を乗じた値を指定する必要があります. また,malloc の戻り値は void 型のポインタですので,単純に
a = malloc(sizeof(int) * 1000);
とおくと,代入文におけるデータ型の不一致を意味する警告エラーが出ます. これを避けるには
a = (int *)malloc(sizeof(int) * 1000);
のように malloc 関数の前にデータ型を括弧で囲んだものを指定します. これは明示的な型変換 (キャスト) と呼ばれる記法で, 直後のデータを指定したデータ型に明示的に変換するという意味になります.

また,malloc 関数で確保したメモリを使い終わったら, free 関数で解放することでメモリの再利用が可能となります. free 関数の引数には malloc 関数の戻り値と同じポインタの値を指定します. メモリの途中のアドレス(例えば &a[500])を指定しても 正しく解放されません. 何度も malloc 関数を呼び出すような場合は その都度 free 関数で解放しておかないと, 計算機のメモリーを使い果たして異常終了する場合があります※2. また,malloc と free を不規則に繰り返すと, 使用中のメモリーと解放されたメモリーの断片が混在した状態となり, メモリーの総使用量がそれほど大きくなくても 新たなメモリを確保できなくなる場合があります. 効率的なプログラミングのためには, なるべく連続したメモリの領域をまとめて確保・解放するなど, メモリーの配置を配慮した工夫が必要となります.

以下のプログラムはファイル solar.dat から太陽系の 惑星のデータを読み込み, それらを構造体 planet に格納した上で各惑星の公転周期の2乗と軌道半径の3乗の比を 表示するという例です※3. ファイルの先頭に惑星の数が記録されていますので, その値に応じて必要なサイズのメモリを動的に確保しています. つまり読み込むファイルを別途用意するだけで太陽系以外の恒星系にも対応できるようになっています.

/* kepler.c: ケプラーの第3法則を確認 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define DATAFILE "solar.dat"

/* 構造体の typedef 宣言 */
typedef struct {
    char name[10];         /* 惑星の名前 */
    double period;         /* 公転周期 (日) */
    double radius;         /* 太陽からの平均距離 (10^6 km) */
    int satellites;        /* 衛星の数 */
} planet;

int main()
{
    FILE *fp;
    int i, num;
    double k;
    planet *p;

    /* ファイルのオープン */
    if ((fp = fopen(DATAFILE, "r")) == NULL) {
        fprintf(stderr, "Can't open %s!¥n", DATAFILE);
        exit(-1);
    }

    fscanf(fp, "%d", &num);                 /* num = 惑星の数 */
    p = (planet *)malloc(sizeof(planet) * num); /* 必要なメモリを確保 */
    for (i = 0; i < num; i++) {
        /* 各惑星のデータを読み込み */
        fscanf(fp, "%s %lf %lf %d",
               p[i].name,                       /* &(p[i].name[0]) と同じ */
               &(p[i].period),
               &(p[i].radius),
               &(p[i].satellites));
    }
    fclose(fp);

    for (i = 0; i < num; i++) {
        k = pow(p[i].period, 2.0) / pow(p[i].radius, 3.0);
        printf("%-10s%12.8f\n", p[i].name, k);  /* 計算結果を表示 */
    }
    free(p);                                    /* メモリの解放 */
    return (0);
}

※1 C言語の新しい規格であるC99では,実行時に要素数を変更できる 可変長配列の機能が追加されています. 実は gcc もこの機能を先取りしています.

※2 malloc と free の誤対応などの原因により, メモリの再利用が正しく行われないことをメモリーリークといいます. メモリーリークは当該プログラムの異常終了にとどまらず, 計算機全体のパフォーマンス低下などの重大な障害を招く場合があるので 十分に注意する必要があります.

※3 ケプラーの第3法則によれば,惑星の公転周期の2乗は公転軌道の半径の3乗に比例するとされています.

課題

以下の枠内にホスト名 (nodab???) とログイン名 (j7??????) を入力し, 「課題表示」ボタンを押して下さい.
ホスト名:
ログイン名:
提出状況
戻る