記事一覧

第15問

以下は、第14問で取り上げたC言語のプログラムです。これを、C++のプログラムとして実行したいと考えました。でも、C++のコンパイラがエラーを出すので、動かすことができません。

どのように修正すれば、コンパイルエラーにならずにC++として実行できるようになるか、分かりますか?
main.cpp
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  double base;
  double height;
} Triangle;

double TriangleArea(const Triangle *triangle) {
  return triangle->base * triangle->height / 2.0;
}

int main(void) {
  Triangle *triangle = malloc(sizeof(Triangle));
  if (triangle) {
    triangle->base = 6.0;
    triangle->height = 4.0;

    printf("area = %f\n", TriangleArea(triangle));

    free(triangle);
    triangle = 0;
  }

  return EXIT_SUCCESS;
}
期待される実行結果
area = 12.000000
あれれ?「C言語実力診断クイズ」っていうタイトルはウソですかー?
それは文句かしら?
ごめんなさい。ちょっとツッコミを入れてみたかっただけです。
まぁいいわ。C++を使ったことはあったかしら?
一応あるんですけど……。C言語のプログラムってC++でも動くんじゃないんですか?
そうでもないのよ。よく似た言語ではあるんだけどね。
えー。上位互換だって聞いてたんですけど……。
malloc()のあたりに注目してみましょう。

超重要なファイルスコープ

う〜ん。こんな感じでいいのかなぁ……?
どうしたの、レオ君。なにか考えごとかしら?
あ、ユキ先輩!……実は、今書いているプログラムがある程度大きくなってきたので、ソースファイルを分割しようと思っているんですけど。
分割コンパイルね。
はい。でも、イマイチこれでちゃんと動くのか自信がなくて。……分割コンパイルをするときに、気を付けておくと良いポイントとかってあるんでしょうか?
そうねぇ……。「スコープ」をきちんと意識すると、メンテナンスがしやすいプログラムになるわよ。
スコープですか!?

C言語のスコープ(変数や関数などの有効範囲)には、以下の3つのスコープがあります。

C言語のスコープ

  • グローバルスコープ:どこからでもアクセスできる
  • ファイルスコープ:モジュール内のみアクセスできる
  • ローカルスコープ(ブロックスコープ):ブロック内のみアクセスできる

たしか、分割コンパイルをするときはグローバルスコープの変数をできるだけ少なくした方がいいんですよね?
そのとおりよ。
でも、ファイルスコープの変数っていうのは、あまりピンとこないです……。
じゃあ、まずはファイルスコープについて説明するわね。

ファイルスコープにする必要がある変数とは、「モジュール内の複数の関数からアクセスされるが、モジュール外からはアクセスされない変数」です。

例えば、次のようなC言語のプログラムがあったとします。ドキュメントファイルを扱うためのモジュールです(細かい処理は省略しています)。

document.h
#ifndef document_h
#define document_h

void OpenDocument(const char *pDocumentName);
void CloseDocument(void);

#endif /* document_h */
document.c
#include <assert.h>
#include "document.h"

typedef enum {
  Closed,
  Open
} DocumentOpenStatus;

static DocumentOpenStatus openStatus = Closed;

void OpenDocument(const char *pDocumentName) {
  assert(openStatus == Closed);

  /* ドキュメントを開く処理(省略) */

  openStatus = Open;
}

void CloseDocument(void) {
  assert(openStatus == Open);

  /* ドキュメントを閉じる処理(省略) */

  openStatus = Closed;
}

OpenDocument()では、すでにドキュメントが開いている状態での呼び出しを禁止するためにassert()を使っています。同様に、CloseDocument()ではドキュメントが開いていない状態での呼び出しを禁止しています。

assert()については別のトピックで詳しく説明します。

上記のモジュールは、main()から次のように呼びだされます。

main.c
#include <stdlib.h>
#include "document.h"

int main(void) {
  OpenDocument("document-name");
  CloseDocument();

  return EXIT_SUCCESS;
}

では本題に入りましょう。

「document.c」には、openStatusという変数がありますね。これがファイルスコープ変数です。モジュール内の複数の関数からアクセスされるため、関数の外側で定義されています。

そして、キーワードstaticを付けることによって、ほかのモジュールからは見えないようにしています。(staticを付けないとグローバルスコープ、つまりほかのモジュールからも見える変数になります。)

ここで重要なのは、この変数は「他のモジュールから見えない」という点よ。

もしopenStatusがグローバルだったら、いつその値が変更されてしまうか分かりません。ファイルスコープにすることによって、変数の値が予期せぬタイミングで変更されないようにしているのです。

また、この変数はモジュール「document.c」の支配下にあります。

ファイルスコープ変数にしておけば、変数の有効範囲はモジュール内だけになります。そのため「document.c」を修正するときには、変数がモジュール内でどのように振る舞うかにだけ注意を払えばよいのです。

これがファイルスコープを使う本質的な理由よ。

情報系の仕事をしている人なら「情報隠蔽」という言葉を聞いたことがあるでしょう。これはまさに「コーディングするときに同時に気を配る必要がある範囲をいかにして狭めるか」という問いへの答えです。

なるほど〜。変数のスコープはなるべく狭めて、情報を隠蔽するといいんですね。
そういうこと。C言語では、そのためにファイルスコープ変数を使いこなせるようになっておくのがとても重要なのよ。
ここがポイント!
モジュール分割をするときは、変数のスコープを意識しましょう。ファイルスコープ変数は、モジュール分割の設計における「情報隠蔽」の手段です。
……あれれ?じゃあ、グローバルスコープの変数は、どんなときに使うのがいいんでしょうか?
ふふふ。じゃあ次のトピックでは、グローバルスコープの変数について説明するわね。

第14問の答え

答え

malloc()が失敗するケースが考慮されていない!
  Triangle *triangle = malloc(sizeof(Triangle));
  triangle->base = 6.0;
  triangle->height = 4.0;
  ……
malloc()の戻り値は毎回チェックするのが正解!
  Triangle *triangle = malloc(sizeof(Triangle));
  if (triangle) {
    triangle->base = 6.0;
    triangle->height = 4.0;
    ……

解説

メモリーは有限なので、malloc()はメモリーの確保に失敗する場合があります。実際に失敗するかどうかはプログラムを実行してみないと分からないので、こういうエラーケースのことを「実行時エラー(ランタイムエラー)」といいます。実行時エラーは、毎回チェックするのが鉄則です。

malloc()は失敗すると0(NULL)を返すので、戻り値をif文でチェックすればよいでしょう。

  Triangle *triangle = malloc(sizeof(Triangle));
  if (triangle) {
    ……
ずるいですよ!三角形のくだり関係ないじゃないですかー。
あら、そうでもないかもしれないわよ?

実行時エラーを漏れなくチェックするというのは、なかなか面倒くさい作業です。何かほかのこと、例えば三角形の面積の計算式などに気を取られていると、つい忘れてしまうかもしれません。

それでも、こうしたチェックは必要なものです。プログラムを作るときは、いつもエラーケースのことを頭の片隅に入れておきましょう。

もしかして、「パッと見ただけで解ける人もいる」って、このことだったんですか?
そうね。いつもエラーケースに気を配っていると、「このmalloc()危ないなぁ」って分かるようになるのよ。
なるほどー。

なお、チェックが必要な箇所をなるべく減らすというのも、一つの考え方です。今回のプログラムの場合は、次のようにしてmalloc()の使用を避ける方法も考えられます。

int main(void) {
  Triangle triangle = {
    .base = 6.0,
    .height = 4.0
  };

  printf("area = %f\n", TriangleArea(&triangle));

  return EXIT_SUCCESS;
}
ここがポイント!
実行時エラーは毎回チェックしよう!(または、チェックが不要な方法で置き換えよう!)

修正後のプログラム

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

typedef struct {
  double base;
  double height;
} Triangle;

double TriangleArea(const Triangle *triangle) {
  return triangle->base * triangle->height / 2.0;
}

int main(void) {
  Triangle *triangle = malloc(sizeof(Triangle));
  if (triangle) {
    triangle->base = 6.0;
    triangle->height = 4.0;

    printf("area = %f\n", TriangleArea(triangle));

    free(triangle);
    triangle = 0;
  }

  return EXIT_SUCCESS;
}
実行結果
area = 12.000000

第14問

三角形の面積を計算して表示するプログラムを作りました。このプログラムには、まれにクラッシュを引き起こす問題が潜んでいます。

何が問題なのか分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  double base;
  double height;
} Triangle;

double TriangleArea(const Triangle *triangle) {
  return triangle->base * triangle->height / 2.0;
}

int main(void) {
  Triangle *triangle = malloc(sizeof(Triangle));
  triangle->base = 6.0;
  triangle->height = 4.0;

  printf("area = %f\n", TriangleArea(triangle));

  free(triangle);
  triangle = 0;

  return EXIT_SUCCESS;
}
期待される実行結果
area = 12.000000
この問題は、パッと見ただけで解ける人もいるんじゃないかしら。
ホントですか?
えっと、この構造体が三角形を表してて……
typedef struct {
  double base; /* 底辺の長さ */
  double height; /* 高さ */
} Triangle;
この関数で面積を求められるのか。
double TriangleArea(const Triangle *triangle) {
  return triangle->base * triangle->height / 2.0;
    /* 「底辺の長さ」×「高さ」÷ 2 */
}
で、ここで底辺が6で高さが4の場合の面積を表示してる!
  triangle->base = 6.0;
  triangle->height = 4.0;

  printf("area = %f\n", TriangleArea(triangle));
そうね。だいたいの場合は、このプログラムは期待どおりに動くのよ。
このプログラムが「まれにクラッシュ」する理由を考えてみましょう。

第13問の答え

答え

モジュール内でのみ使用する変数がグローバルになっている!
double sumOfValues = 0;
int numberOfValues = 0;
グローバルにする必要のない変数はスコープを限定したほうがベター!
static double sumOfValues = 0;
static int numberOfValues = 0;

解説

今回のプログラム(ソースファイル「average.c」)は、2つの関数をインターフェースとするモジュールでした。つまり、以下のような2つの宣言(プロトタイプ宣言)にもとづいて、ほかのモジュールから呼び出されるということです。

void AddValue(double n);
void PrintAverage(void);

「average.c」にある、これらの宣言以外のすべてのものは、モジュールの内部的な実装です。具体的にどのような実装になっているかは、呼び出す側が知る必要はありません。

標準関数が使える理由も、これと同じなのよ。
え?どういうことですか?
標準関数を呼び出すには、宣言が必要よね?
はい。使いたい関数の宣言をインクルードする必要があるってことですね。
でも、関数の中身がどうなっているかは気にしないでしょう?
たしかに!実装を知る必要がないって、そういうことなんですね。

呼び出し側が知る必要のない実装は、呼び出し側から見えないようにしておくほうがベターです。これは、設計をするうえでの基本となる、「情報隠蔽」という考え方です。

ところが、問題のプログラムでは、以下の変数のスコープがグローバルになっていました。

double sumOfValues = 0;
int numberOfValues = 0;

これら2つの変数は「average.c」のファイル内でしか使わないものですが、グローバルのままではどこからでもアクセスできてしまいます。staticを付けて、「ファイルスコープ」にしてしまいましょう。

static double sumOfValues = 0;
static int numberOfValues = 0;

こうしておけば、モジュールの外部からこれら2つの変数に直接アクセスすることはできなくなります。

外部からアクセスできなくなると、何が嬉しいんですか?
いい質問ね!それはね、あとで実装を変えたくなったときのことを考えてみて。
もっといい実装方法を思い付いたときとかですか?
そうそう。そのときに、余計なものがグローバルになっていると……
うーん、なんだか手を出しにくい感じがしますね。
ほかのモジュールがアクセスしている可能性があるからね。
なるほど!中身を改良したいだけなのに、外部に影響が出てしまうってことですね。
そういうこと!ソースファイルが2つ以上になったら気を配りたいポイントね。
はーい。分かりました!

グローバル変数やグローバル関数は、そのモジュールのインターフェースとなるものだけにしましょう。そうすれば、あとからモジュール内部の実装を改良したくなったとき、その影響を最小限に抑えられます。プログラムの規模が大きくなってモジュールの数が増えていくほど、このようにスコープに気を配ることが重要になっていきます。

ここがポイント!
変数や関数をグローバルにするのは、モジュールの外部から使うときだけにしよう!

修正後のプログラム

average.c
#include <stdio.h>

static double sumOfValues = 0;
static int numberOfValues = 0;

void AddValue(double n) {
  sumOfValues += n;
  numberOfValues += 1;
}

void PrintAverage(void) {
  if (numberOfValues > 0) {
    printf("Average = %f\n", sumOfValues / numberOfValues);
  } else {
    printf("No data.\n");
  }
}

第13問

平均値を表示するプログラムを作りました。以下のソースファイル「average.c」はその一部なのですが、改善できる部分があります。

どこが改善できるか分かりますか?
average.c
#include <stdio.h>

double sumOfValues = 0;
int numberOfValues = 0;

void AddValue(double n) {
  sumOfValues += n;
  numberOfValues += 1;
}

void PrintAverage(void) {
  if (numberOfValues > 0) {
    printf("Average = %f\n", sumOfValues / numberOfValues);
  } else {
    printf("No data.\n");
  }
}

なお、このプログラムは別のソースファイル内から(main()などから)、次のように2種類の関数を呼び出して使用することを想定したものです。

サンプルコード
  /* 数値を足していく */
  AddValue(2.0);
  AddValue(1.5);
  AddValue(5.5);
  AddValue(1.0);

  /* 最後に平均値を表示する */
  PrintAverage();
実行結果
Average = 2.500000
今回のプログラムは、複数のモジュールからできていることが分かるかしら?
えっと、「average.c」が1つのモジュールってことですよね?
そうね。その中にある2つの関数を……
別のモジュールから呼び出す仕組みですね!
そういうこと!2つの関数が、そのためのインターフェースになっているということよ。
「average.c」の実装に注目してみましょう。

第12問の答え

答え

マクロの定義に括弧が付いていないのが間違い!
#define VALUE_FOR(X) 2 * X + 1
マクロの定義では数式全体と引数を括弧で囲むのが正解!
#define VALUE_FOR(X) (2 * (X) + 1)

解説

これは、マクロの定義方法に関する出題でした。問題のプログラムでは、期待どおりにマクロが展開されません。

マクロの部分がどういう風に展開されるか分かるかしら?
はーい。やってみます!
えっと、マクロの部分というのはここだな。
VALUE_FOR(2 + 3)
これが2 * X + 1に展開されるとき、X2 + 3に置き換わるから……
2 * 2 + 3 + 1
ね、11になると思ったのに、8になっちゃったでしょ。
あ!ほんとですね。
2 + 3を括弧で囲まないと、思ったとおりの計算にならないわね。
そっか。ここに括弧が必要なんだ!
2 * (2 + 3) + 1
つまり、マクロ定義のX(X)にすればいいんですね!
おしい!

マクロの定義には、展開されるときのことを考えて括弧を付けておく必要があります。今回のプログラムでは、次のように引数に括弧を付ければ期待どおりの結果が得られることが分かりました。

#define VALUE_FOR(X) 2 * (X) + 1

でも、実はそれだけでは足りません。マクロ定義では数式全体も、このように括弧で囲む必要があります。

#define VALUE_FOR(X) (2 * (X) + 1)

なぜ全体を括弧で囲む必要があるのかは、次の例で考えれば理解できるでしょう。

  printf("Value is %d\n", VALUE_FOR(2 + 3) * 2);
この場合、結果が22になるのが正しいわね。
ですね。そうなるかどうか、確認してみます!
この部分に(2 * (X) + 1)を当てはめて展開すると……
VALUE_FOR(2 + 3) * 2
括弧が付いて、こうなる!
(2 * (2 + 3) + 1) * 2
なるほど!ちゃんと22になりますよ。
もしマクロに全体を囲む括弧がなかったら、結果が変わってしまうのが分かるかしら?
はい。うしろの1 * 2が先に計算されて、12になっちゃいますね。

このように、マクロで数式を定義するときは引数と全体を括弧で囲むのが鉄則です。括弧を付け忘れると予想外の動作になってしまうこともあるので、できる限り注意しましょう。

ちなみに、どうしてもミスをしやすいところなので、「マクロは極力使うべきではない」という考え方もあります。とくに必要性がない限り関数を使うようにするのも、厄介ごとを避ける一つの方法です。

ここがポイント!
マクロを定義するときは、引数と数式全体の両方に括弧を付けよう!

修正後のプログラム

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

#define VALUE_FOR(X) (2 * (X) + 1)

int main(void) {
  printf("Value is %d\n", VALUE_FOR(2 + 3));

  return EXIT_SUCCESS;
}
実行結果
Value is 11

第12問

ある数式を計算するための関数を作りました。これをマクロで書き換えたところ、なぜか実行結果が変わってしまいました。

なぜ動作が変わってしまったのか、理由が分かりますか?

以下は、元のプログラムです。

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

int ValueFor(int x) {

  return 2 * x + 1;
}

int main(void) {
  printf("Value is %d\n", ValueFor(2 + 3));

  return EXIT_SUCCESS;
}
実行結果
Value is 11

以下は、書き換えたプログラムです。意図に反して、実行結果が変わってしまいました。

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

#define VALUE_FOR(X) 2 * X + 1

int main(void) {
  printf("Value is %d\n", VALUE_FOR(2 + 3));

  return EXIT_SUCCESS;
}
期待とは異なる実行結果
Value is 8
数式「y=2x+1」を計算するプログラムになっているのが分かるかしら?
はい。「x=2+3」のときの「y」の値を求めてるんですね。
ということは、計算が正しければ11が表示されるはずなのだけど……
なぜか8になってるんですね。
マクロがどのように展開されるのか考えてみましょう。

命名法を考える

プログラミングをするうえで、変数や関数などの識別子に名前を付けるという作業は避けて通れません。それだけに、識別子の命名法について考えてみる価値はあるでしょう。

そもそも人間は「名前を付ける」という一種の抽象化によってものを認識します。逆に「適切な名前を付けられない」ということは、その変数や関数の意味を正確に把握していない証拠です。

そのため、識別子には意味のある名前を付けるよう心掛けましょう。たったそれだけのことでアルゴリズムが明確になり、バグも減ります。

また、識別子に名前を付ける際には、ある程度ルールを決めておくことが大切です。命名のしかたがバラバラだと、プログラムが読みづらくなってしまいます。

このとき重要なのは、一貫性があるかどうかよ。どのような命名法がもっとも優れているか、という話ではないの。
え、そうなんですか!?
チームで開発をするときはとくに、全員が同じルールで名前を付けるようにすることが大切ね。
なるほど。そうすれば意思疎通もスムーズにできそうですね。
そういうこと。ここからは、命名法について私が普段から使っている方法や考え方を紹介していくので参考にしてね。
はい。お願いします!

ルール1:大文字と小文字

まずは、大文字と小文字の扱いについて考えてみましょう。

識別子のスタイルには、大きく分けて次のような4つのパターンがあります。

  • アッパーキャメルケース:HowToNameIdentifiers
  • ローワーキャメルケース:howToNameIdentifiers
  • スネークケース:how_to_name_identifiers
  • スクリーミングスネークケース:HOW_TO_NAME_IDENTIFIERS

C言語によるソフトウェア開発が主流だった頃には、スネークケースが使われる場面が多くありました。

いろいろなプログラミング言語が使われている現在では、

  • クラスなどの型の名前にはアッパーキャメルケース
  • メソッドや変数の名前にはローワーキャメルケース

と、使い分けることも多くなっています。

というわけで、何をどのパターンで命名するかルールを決めておきましょう。

ルール2:関数やメソッドの名前

関数やメソッドには、大きく分けて次の2種類があるわよ。

  • 動作をともなう関数:fopen()malloc()など
  • 値を返すだけの関数:strlen()ceil()など

前者には、「何かをするぞ」という意味を込めて動詞を使った名前を付けるのがおすすめです。後者については、「この情報が見たい」という意味で名詞を使った名前にするとよいでしょう。

例えば、線形リストのデータを扱う関数なら、次のような感じです。

  • ノードを追加・削除する関数は動詞:AppendNode()RemoveNode()など
  • ノードを取り出す関数は名詞:FirstNode()NextNodoe()など

ルール3:変数名のプレフィックス

変数名には、「データの形式」と「データの意味」が両方とも分かるような名前が付いていると便利なときがあるわよ。

C言語やC++では変数名をローワーキャメルケースにして、最初の単語を「型を表すプレフィックス」にしておくのがおすすめです。プレフィックスには、次のようなものが考えられます。

  • n:整数値を表す
  • b:ブーリアンを表す
  • p:ポインタを表す

例えば、次のように使います。

int nValue = 123; /* ← 整数値 */
int *pValue = &nValue; /* ← ポインタ(より明確に pnValue とするのもあり) */

ちなみに、最近の(モダンな)プログラミング言語では、こういったプレフィックが必要になる場面はそれほどありません。

ルール4:ブーリアンの意味

ブーリアンには、どちらがtrueでどちらがfalseなのかがはっきり分かる名前を付けましょう。

例えば、エラーの有無を表す変数がbResultだと、エラーがあったときにtrueなのか、なかったときにtrueなのか分かりません。こういう場合は、bSuccessbErrorのような、判定のしかたで迷わない名前を付けます。

ブーリアンを返す関数についても、同じようなことがいえます。例えば、通信の接続状態を返す関数に名前を付けるとしたら、ConnectionStatus()とするよりIsConnected()としたほうが、どちらがtrueなのか明確ですね。

ルール5:よく使う省略形

いろいろな名前に繰り返し現れる単語については、省略形(短縮形)を決めておくと便利です。

省略形には、次のようなものが考えられます。

  • Cnt:Countの略(個数を表す)
  • Idx:Indexの略(項目番号や添字を表す)
  • Pos:Positionの略(位置を表す)
  • Len:Lengthの略(長さを表す)

ちなみに、Numberを略してNumとするのは、おすすめしません。「Number of(〜の個数)」なのか、それとも「Index of(〜の項目番号)」なのかが、一目で分からない場合が多いためです。

もちろん、省略形を使わないルールにするのもありです。

ルール6:よく使う対義語

対になる単語をリストアップしておくと、名前を付けるときに迷うことが少なくなります。

例えば、「Start」の反対は「End」なのか「Stop」なのかなど、基本とするルールを決めておきましょう。

  • StartStop
  • BeginEnd
  • CreateDestroy
  • LHSRHS(「左側(Left Hand Side)」と「右側(Right Hand Side)」のこと)

ルール7:名前の長さ

変数や関数に名前を付けるときは、長さも意識しましょう。

長い名前を付けた変数や関数は、プログラム中でよく目立ちます。目立たせたい(重要な)ものほど、長くてしっかりとした名前を付けるようにしましょう。

これとは反対に、変数にあえて短い名前を付けると良いこともあります。例えば、ループカウンタにはiという名前が使われることが多いですね。これは、一時的な変数だということを表すのに適した名前だといえます。


なるほど〜。名前の付け方一つで、プログラムの意味や意図をうまく表せるんですね。
そういうこと。どういうふうに名前を付けると伝わりやすいかを、常に意識しておくと良いわね。
ここがポイント!
識別子には、意味のある名前を付けましょう。伝わりやすさを意識しながら、全員が同じルールを採用することが大切です。一貫性のある命名法は、スムーズな意思疎通やバグの削減にもつながります。

チームで開発をするときはもちろん、ひとりで開発をするときもこれらの命名法を意識しておくのがおすすめです。作ってから時間が経ったとしても、どんなプログラムにしたかを思い出しやすくなるでしょう。

第11問の答え

答え

値が同じで意味が異なる数値がややこしい!
  for (int i=0; i<10; i++) {
    if (values[i] % 10 == 0) {
      ……
意味が異なるものには別々の名前を付けるのがベター!
  for (int i=0; i<NUMBER_OF_VALUES; i++) {
    if (values[i] % BASE_VALUE == 0) {
      ……

解説

これは、マジックナンバーに関する出題でした。マジックナンバーとは、プログラム中に突然現れる数字のことです。

あ、できるとかっこいいやつですか!
いえいえ、マジックナンバーは「悪役」なのよ。
なんと!

マジックナンバーは、プログラムを書いた本人にしか意図が分からない数字です。ほかの人からは、なんだか分からないけれどプログラムは正しく動いているように見えます。「まるで魔法だね!」という皮肉が込められた言い方なので、決してかっこいいものではありません。

今回のプログラムには、2種類の「10」が書かれていました。

  • 1つ目は、配列の要素数を表す「10」
  • 2つ目は、10の倍数かどうかを判定するための「10」

今回のような小さなプログラムならある程度は解読できますが、もっと大きなプログラムでマジックナンバーがたくさん出てくると困ったことになります。例えば、配列の要素数を「15」に変更したくなったときのことを考えてみましょう。どの「10」を「15」にして、どの「10」をそのままにしておくべきなのか、簡単には分かりません。

つまり、どの数字がどういう意味なのかが分かりづらいのが問題なんですね。
そういうことよ。
えっと、数字の意味が分かるようにするには……

プログラム中で数字を使うときは、それぞれの意味に応じて名前を付けましょう。そのためには、マクロを使うのが簡単です。

今回のプログラムに出てきた2種類の「10」には、例えば次のような名前を付ければいいですね。

#define NUMBER_OF_VALUES 10
#define BASE_VALUE 10

そして、直接「10」としていたところを、対応するマクロに置き換えます。

  int values[NUMBER_OF_VALUES] = { 5, 3, 14, -6, 20, 372, 1000, 18 };

  for (int i=0; i<NUMBER_OF_VALUES; i++) {
    if (values[i] % BASE_VALUE == 0) {
      printf("values[%d] is a multiple of %d.\n", i, BASE_VALUE);
    }
  }

これで、配列の要素数を変更しやすくなりました。また、「10の倍数か」という判定を「8の倍数」に変えたくなったときも、簡単に対応できそうですね。

ここがポイント!
意味のある数字には名前を付けておこう!

修正後のプログラム

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

#define NUMBER_OF_VALUES 10
#define BASE_VALUE 10

int main(void) {
  int values[NUMBER_OF_VALUES] = { 5, 3, 14, -6, 20, 372, 1000, 18 };

  for (int i=0; i<NUMBER_OF_VALUES; i++) {
    if (values[i] % BASE_VALUE == 0) {
      printf("values[%d] is a multiple of %d.\n", i, BASE_VALUE);
    }
  }

  return EXIT_SUCCESS;
}
実行結果
values[4] is a multiple of 10.
values[6] is a multiple of 10.
values[8] is a multiple of 10.
values[9] is a multiple of 10.