マクロ

一般的にC言語処理系では実際にソースプログラムをコンパイルするまえに プリプロセッサ というプログラムを利用して事前処理として いくつかの書き換えを行なう。

たとえば、コメント文などはコンパイルのときには必要ないので、 プリプロセッサが事前にはぎ取る。もうひとつ代表的なものが # 記号で始まる行の内容を特別に解釈する働きで、たとえば

#include <stdio.h>

という行があったときに、/usr/include/stdio.h ファイルの 内容をその行(#includeのある行)の場所に取り込むといった処理 が該当する。

【体験してみよう】

実際に自分の作ったCのソースプログラムをプリプロセッサに掛けた結果を見 てみよう。プリプロセッサのコマンド名は cpp である。 以前作ったもの(たとえば aisatsu.c)をcppに処理させ、 その結果を less コマンドで見てみよう。

% cpp aisatsu.c | less

less コマンドの終了には q をタイプする。 次のページを見るのは SPC、戻るのは b

#include と書いていた行が、置き換えられていることに 注意しよう。

# 記号で始まる、プリプロセッサに司令を与える 単語のことを ディレクティブ という。

defineディレクティブ

プログラムを作っていて、将来的に変えるかもしれない定数は マクロ として定義しておくと見通しが良くなる。たとえば、 成績処理のプログラム listup.cでは、 最大処理人数として300人を想定し、以下のように配列宣言した。

  /* 1行は100バイト、氏名は50バイトあればいいだろう */
  char buffer[100], name[300][50]; /* nameは 50バイト×300人分 */
  int point[300];               /* 得点も300人分 */

しかしこれでほんとうに十分だろうか。もし 寿限無さんが入学してきたらどうしよう。

(C言語に限らず)一般的に、プログラムで扱えるデータの上限を決めたりする場合は 数値そのものを書くのではなく、定数を表す変数に置き換えておく。 Cプリプロセッサ(cpp)では、このために #define というディレクティブが利用できる。

成績処理プログラムの場合、名前を格納するバッファ長を定数化しておき、 配列宣言も書き換える。

#define	NAME_LEN	250
#define	BUFF_LEN	280

int main()
{
  char buffer[BUFF_LEN], name[300][NAME_LEN];
  int point[300];               /* 得点300人分 */
    :
    :

同様に、最大処理人数も「300」人と数値そのままで書くのをやめる。

#define	NAME_LEN	250
#define	BUFF_LEN	280
#define MAX_NINZU	300

int main()
{
  char buffer[BUFF_LEN], name[MAX_NINZU][NAME_LEN];
  int point[MAX_NINZU];     /* 得点 MAX_NINZU 人分 */
    :
    :

もう一歩進めよう。上記の定義で

氏名の最大見積り NAME_LEN 250
1行入力の最大見積り BUFF_LEN 280

は、それぞれ無関係ではない。読み込みデータ1行の中には必ず氏名が 含まれるので NAME_LEN < BUFF_LEN でなければならない。

〜〜〜〜バッファ全体(280バイト)〜〜〜〜
氏名相当分(250バイト) 得点相当分(残り)

このような場合は、

バッファ全体の長さは氏名の長さ見積りより10バイトくらい大きけりゃ いいんじゃないの?

のように考えるだろう。そのように考えると、バッファ長 BUFF_LEN のマクロ定義は以下のように変えるのが望ましい。

#define	NAME_LEN	250
#define	BUFF_LEN	(NAME_LEN+10)

マクロ定義の本体に式を使う場合は、必ず全体を括弧で括る。 もし、括弧で括らずNAME_LEN+10 とした場合を考えてみる。 実際にプログラム本体で

int big_buffer[BUFF_LEN*3];

のように利用されるとき、BUFF_LEN*3 は以下のように 展開される。

BUFF_LEN*3
  ↓
NAME_LEN+10*3
  ↓
250+10*3
  ↓
280

本来、BUFF_LEN全体の3倍、のつもりで宣言したものが 違う結果になってしまう。これを防ぐため、マクロ定義の本体の式は 全体を括弧で括る。

BUFF_LEN*3
  ↓
(NAME_LEN+10)*3
  ↓
(250+10)*3
  ↓
780

拡張を見越したプログラム

以上を考慮して、全員分の成績データを記憶して読み込むプログラムは 以下のように書き換えられる。

store2.c

#include <stdio.h>
#define	NAME_LEN	250     	/* 氏名の長さ最大見積り */
#define	BUFF_LEN	(NAME_LEN+10)	/* バッファの長さ */
#define MAX_NINZU	300     	/* 最大処理可能人数 */

int main()
{
  /* 1行はBUFF_LENバイト、氏名はNAME_LENバイトあればいいだろう */
  char buffer[BUFF_LEN], name[MAX_NINZU][NAME_LEN];
  int point[MAX_NINZU];         /* 得点も人数分確保 */
  int n=0;                      /* 今何人目? (0から) */
  int i;                        /* ループ用変数 (最初に定義すべし) */

  while (NULL != fgets(buffer, sizeof buffer, stdin)) {
    if (2 == sscanf(buffer, "%s %d", name[n], &point[n]))
      n++;                      /* nを1増やす */
    }
  }
  puts("全部読み終わったよ!");
  /* では改めて最初から表示*/
  /* この時点で n は最後の配列要素より1大きくなっている */
  for (i=0; i<n; i++) {
    printf("%d 番目: %s さんは %d 点\n", i+1, name[i], point[i]);
  }
  puts("以上");
}

ただし、プログラムのセキュリティを考えると今一歩及ばない。 sscanf のtところで読み取りフォーマットを "%s %d" としているので、名前の部分に251文字以上入れられるとname 配列はあふれてしまう。そこで、sscanfで解析に使う フォーマット文字列も、NAME_LENから自動的に修正する ように変える。これを直したものが以下のプログラムである。

store3.c

#include <stdio.h>
#define	NAME_LEN	250     	/* 氏名の長さ最大見積り */
#define	BUFF_LEN	(NAME_LEN+10)	/* バッファの長さ */
#define MAX_NINZU	300     	/* 最大処理可能人数 */

int main()
{
  /* 1行はBUFF_LENバイト、氏名はNAME_LENバイトあればいいだろう */
  char buffer[BUFF_LEN], name[MAX_NINZU][NAME_LEN];
  char sfmt[10];
  int point[MAX_NINZU];         /* 得点も人数分確保 */
  int n=0;                      /* 今何人目? (0から) */
  int i;                        /* ループ用変数 (最初に定義すべし) */

  sprintf(sfmt, "%%%ds %%d", NAME_LEN-1);
  puts(sfmt);
  while (NULL != fgets(buffer, sizeof buffer, stdin)) {
    if (2 == sscanf(buffer, sfmt, name[n], &point[n]))
      n++;                      /* nを1増やす */
  }
  puts("全部読み終わったよ!");
  /* では改めて最初から表示*/
  /* この時点で n は最後の配列要素より1大きくなっている */
  for (i=0; i<n; i++) {
    printf("%d 番目: %s さんは %d 点\n", i+1, name[i], point[i]);
  }
  puts("以上");
}

sprintf関数は、printf関数に似ているが、 書式を整えた結果を出力するのではなく、第1引数で指定した文字列 に書き込む。たとえば、

printf("%%%ds %%d", NAME_LEN-1);

とすると、

"%249s %d"

と出力される(%%の部分は%文字自身に 置き換わる)。 sscanfはこれと同じ文字列を sfmt 変数に書き込む。そのため、その下でsscanf 用のフォーマット文字列としてsfmt変数の値を利用できる。


目次