情報基礎演習II − 第12回


1.プリプロセッサ


コンパイルの際、 C言語のソースプログラムは、 機械語に翻訳される前に一旦プリプロセッサと呼ばれるプログラムによって 処理されます。

プリプロセッサは、 ソースプログラム中から 翻訳には関係ないコメントを取り除くほか、 ソースプログラム中のプリプロセッサ制御文をもとに ソースプログラムを加工し、 それをコンパイラ本体に引き継ぎます。

プリプロセッサの制御は、 ソースプログラムの第1桁目に # を書くことによって行います。

#define SIZE 1024
プリプロセッサはソースプログラム中で上記の指令に出合うと、 ソースプログラムのその部分から後に含まれる 全ての SIZE という文字列を、 1024 に置き換えます。

マクロ

前述の例のように、 ソースプログラム中の定数などを記号定数に置き換えることができます。
#define NINSU 100

unsigned char tensu[NINSU];

float heikin(void)
{
    int i;
    float sum = 0;

    for (i = 0; i < NINSU; i++)
        sum += tensu[i];

    return sum / NINSU;
}
上の関数 heikin は、 配列 tensu に 100 人の点数データが入っているとき、 その平均点を計算するものであるとします。 ここで仮に NINSU という記号定数を使わずに 100 という定数を そのまま書いたとすると、 データの数が変わったとき3箇所の 100 を変更しなければなりません。 これをあらかじめ記号定数で書いておけば、 そういう変更のときも記号定数の定義のところだけを 変更するだけで済みます。
#define DEBUG
上記のように記号定数に割り当てる値を省略した場合、 その記号定数の内容は空になります。

記号定数の定義を取り消すには、#undef を使います。

#undef NINSU
#undef DEBUG
記号定数の名前の決め方のルールは一般の変数と同じですが、 変数と区別しやすいように全部大文字にするなどの 配慮をしたほうがいいでしょう。

■課題54■

記号定数は引数を使うこともできます。
#define SWAP(type, a, b) { type t = a; a = b; b = t; }

void func(float x[], float y[], int n)
{
    int i;

    for (i = 0; i < n; i++)
        SWAP(float, x[i], y[i]);

この SWAP は関数で定義した場合と異なり、 引数 type に引数 a, b に指定した変数の型を指定することで、 任意の型の変数の内容の交換ができます。
#define EXPR(c) ((c) * 5 + 3)

int func(int x)
{
    return EXPR(x + 1) * 5;
}
この EXPR は式の中で使うことができます。 ここで EXPR(x + 1) * 5((x + 1) * 5 + 3) * 5 に置き替わります。

ここで注意すべきことは、 引数および定義全体が ( ) でくくってある点です。 仮に、次のような定義を行ったとします。

#define EXPR(c) (c * 5 + 3)
この場合 EXPR(x + 1) * 5(x + 1 * 5 + 3) * 5 に置き替わってしまいます。 同様に次のような定義を行った場合についても考えてみます。
#define EXPR(c) (c) * 5 + 3
この場合は SQARE(x + 1) * 5(x + 1) * 5 + 3 * 5 に置き替わってしまいます。

この他、次のような場合も注意が必要です。

#define SQARE(c) ((c) * (c))
このとき、SQARE(++x)((++x) * (++x)) に置き替わるため、 変数 x の内容は2増えてしまいます。

■課題55■


外部ソースプログラムの挿入

ソースプログラム内に別のソースプログラムを埋め込むために、 #includeが使用されます。
#include <stdio.h>
この stdio.h のようなファイルもC言語のソースプログラムで、 ヘッダファイルとばれます。 ヘッダファイルには普通関数定義は書かず、 外部変数の宣言や関数プロトタイプ、マクロ定義などを書きます。 通常のCのソースプログラムと区別するために、 普通ファイル名の最後には ".h" を付けます。 stdio.h には標準入出力(standard input/output、printf/scanf など) を使うための関数プロトタイプなどが書かれています。

#include において、 埋め込むファイル名を < > ではさんだ場合、 そのファイルを システムに最初から用意されているヘッダファイルを 集めた場所から取り出します。 情報処理センターのシステム(大半の UNIX)では、これは /usr/includeというディレクトリにあります。 上の例では、 実際には /usr/include/stdio.h というファイルが 埋め込まれます。

ヘッダファイルには stdio.h のように システムに最初から用意されているものの他に、 自分でも作ることができます。 #include において、 埋め込むファイル名を " " ではさんだ場合、 そのファイルは現在のディレクトリ (もとのソースコードのあるディレクトリ)から取り出します。

#include "myheader.h"

■課題56■


条件コンパイル

次のようにして、 ソースプログラムの一部だけを有効にする(あるいは、 一部を無効にする)ことができます。
#if DEBUG
    printf("DEBUG: x=%f\n", x);
#endif
このようにすると、 #if 〜 #endif の間は 記号定数 DEBUG が真(非0)のときのみコンパイルされます。 これはソフトウェアの開発中は 動作を確かめるプログラムを埋め込んでおきたいが、 完成したときにはその部分は除きたい場合などによく使われます。

#if の条件には 変数を含まない式を使うことができます。 これにはC言語(の本文)同様、 定数同士の四則演算や 関係演算論理演算、 それから ?:演算子 などを使うことができます。

#if X > 5 && X < 10
    printf("Xは5より大きく10より小さい\n");
#else
    printf("Xは5以下あるいは10以上\n");
#endif

#if DEBUG == 1
    printf("デバッグレベル1\n")
#elif DEBUG == 2
    printf("デバッグレベル2\n")
#elif DEBUG == 3
    printf("デバッグレベル3\n")
#endif

#if 0
    この部分は一切コンパイルされません。
    従ってコメントのように使うこともできます。
    でもそう言うことはあまりしません。
#endif
#else 以降の部分は #if の条件が偽(0)のときのみ コンパイルされます。 また #elif は いくつかの条件判断を続けたいときに使います。 #elif を使わない場合は、 #if 〜 #endif は入れ子にする必要があります。
#if DEBUG == 1
        printf("デバッグレベル1\n")
#else
#  if DEBUG == 2
        printf("デバッグレベル2\n")
#  else
#    if DEBUG == 3
        printf("デバッグレベル3\n")
#    endif
#  endif
#endif
# が左端(1桁目)にあれば、 # の右に空白があっても構いません。

記号定数の内容ではなく、 記号定数が定義されているかどうかを調べたいときは defined(..)を使います。

#if defined(DEBUG)
    printf("デバッグします\n");
#endif

#if defined(SUN) || !defined(SGI)
    printf("これは SUN か、あるいは SGI でないものなら動きます\n");
#endif
なお #if defined(..)#ifdef ..#if !defined(..)#ifndef .. と書くこともできます。
#ifdef DEBUG
    printf("デバッグします\n");
#endif

/* どこかで記号定数 SIZE が定義されていなければここで定義する */
#ifndef SIZE
#  define SIZE 1024
#endif

■課題57■