記事一覧

第19問の答え

答え

問題のヘッダーファイルは2回インクルードするとエラーになってしまう!
#include "triangle.h"
……
#include "triangle.h"
ヘッダーファイル内に「ガード」を入れるのが正解!
#ifndef triangle_h
#define triangle_h

……

#endif /* triangle_h */

解説

これは、「2重インクルード」に関する出題でした。問題の「triangle.h」は、うっかり次のように2回インクルードするとエラーになってしまいます。

#include <stdio.h>
#include <stdlib.h>
#include "triangle.h"
#include "triangle.h"

int main(void) {
    ……

「triangle.h」の中には次のようなtypedefがあり、これが重複してしまうためです。

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

このような問題は、標準関数のヘッダーファイルでは発生しません。例えば、次のように「stdio.h」を2回以上インクルードしてもエラーにはならないのです。

#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include "triangle.h"

int main(void) {
    ……

新しくヘッダーファイルを作ったら、何回でもインクルードできるようにしておきましょう。それには次のようにします。

triangle.h
#ifndef triangle_h /* 2回目以降はマクロが定義済みなので #endif まで読み飛ばす */
#define triangle_h /* 1回目ならマクロを定義 */

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

double TriangleArea(const Triangle *triangle);

#endif /* 2回目以降はここまで読み飛ばされる */

問題の「triangle.h」に、全体を囲むような記述が追加されたのが分かるでしょうか。これは、1回目のインクルード時にだけ、「自分はすでにインクルードされた」という意味のマクロを定義するものです。ほかのヘッダーファイルとかぶらないように、ファイル名に合わせたマクロ名になっています。

2回目以降のインクルードでは、すでにマクロが定義済みなので全体がスキップされます。これならtypedefの部分も1度しか読まれないため、エラーは発生しません。

このテクニックを覚えておけば、2重インクルードを「ガード」できるのよ。
まぁ、そうですよね。そういうテクニックがあるのは分かりましたけど……
あらら。納得いかないところがあったかしら?
えっと、2重にインクルードしなければいいだけじゃないんですか?
あぁ、そこが疑問だったのね。

2重インクルードを「ガード」するテクニックは、プログラムが大きくなるほど重要になっていきます。例えば、問題のプログラムに「三角柱の体積」を求めるモジュールを追加することを考えてみましょう。このモジュールのヘッダーファイルからは、おそらく「triangle.h」をインクルードする必要があるでしょう。

さらに「三角錐の体積」を求めるモジュールも追加したらどうなるでしょうか。こちらのヘッダーファイルからも、「triangle.h」がインクルードされますね。

つまり、こういう状況になります。

  • 「三角柱」は「triangle.h」をインクルードする
  • 「三角錐」は「triangle.h」をインクルードする

この状況で「三角柱」と「三角錐」のヘッダーファイルを両方ともインクルードしたら、「triangle.h」は間接的に2回インクルードされることになります。これが、2重インクルードの「ガード」が重要な理由です。

なるほど!2重インクルードでエラーになっちゃうと、どうしても困るときがあるってことですね。
そういうこと。やり方も決まってるし、簡単でしょ?
簡単ですねー!
ここがポイント!
ヘッダーファイルは、何回インクルードされても大丈夫なように「ガード」しておこう!

修正後のプログラム

triangle.h
#ifndef triangle_h
#define triangle_h

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

double TriangleArea(const Triangle *triangle);

#endif /* triangle_h */
triangle.c
#include "triangle.h"

double TriangleArea(const Triangle *triangle) {
  return triangle->base * triangle->height / 2.0;
}
main.c
#include <stdio.h>
#include <stdlib.h>
#include "triangle.h"

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

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

  return EXIT_SUCCESS;
}
実行結果
area = 12.000000

第19問

第14問で取り上げた「三角形の面積を計算して表示するプログラム」から、「三角形の面積を計算する」部分だけを別のモジュールに切り分けました。以下はそのヘッダーファイルですが、C言語としてぜひ入れておきたい要素が足りません。

このヘッダーファイルに何が足りないか分かりますか?
triangle.h
typedef struct {
  double base;
  double height;
} Triangle;

double TriangleArea(const Triangle *triangle);

なお、このモジュールは別のソースファイル内から、次のように使用することを想定しています。

main.c
#include <stdio.h>
#include <stdlib.h>
#include "triangle.h" /* ヘッダーファイルをインクルード */

int main(void) {
  Triangle triangle = { /* 「三角形」の構造体を使用 */
    .base = 6.0,
    .height = 4.0
  };

  printf("area = %f\n", TriangleArea(&triangle)); /* 面積を計算 */

  return EXIT_SUCCESS;
}
実行結果
area = 12.000000
このプログラムが、2つのモジュールでできているのが分かるかしら?
はい。「triangle」と「main」ですね。
そうね。上には「triangle」のヘッダーファイルしか掲載しなかったけれど、ほんとは実装ファイルもあるのよ。
ヘッダーが「triangle.h」だから、実装は「triangle.c」ですかね。
そういうこと!ちなみに、中身はこんな感じ。
triangle.c
#include "triangle.h"

double TriangleArea(const Triangle *triangle) {
  return triangle->base * triangle->height / 2.0;
}
それで、今回のプログラムは間違ってはいないんですよね。
間違いとまでは言えないけれど、ぜひ入れておいてほしい記述がないの。
ヘッダーファイルに何かを足せばいいんですね。ええと……
ヘッダーファイルは、いつでも、どこからでもインクルードできれば便利ですね。

第18問の答え

答え

構造体のサイズを、メンバーのサイズの合計だと考えているのが間違い!
    AlphabetInfo *info = malloc(5);
こう書いても結果は同じなので、やっぱり間違い!
    AlphabetInfo *info = malloc(sizeof(uint8_t) + sizeof(uint32_t));
構造体のサイズは全体で表すのが正解!
    AlphabetInfo *info = malloc(sizeof(AlphabetInfo));

解説

これは、「アラインメント」に関する出題でした。アラインメントとは、メモリー上にデータを置くときに都合がいい「区切り」のことです。

例えばuint32_tのサイズは4バイトですね。多くのコンピューターでは、4バイトのデータは4の倍数のアドレスに配置されます。そうすることによって、データを高速で読み書きできるようになっているのです。

構造体のメンバーについても、話は同じです。今回の構造体は、こういう形をしていました。

typedef struct {
  uint8_t letter;
  uint32_t order;
} AlphabetInfo;

uint8_tのサイズは1バイトですね。その直後に4バイトのuint32_tを置くのは都合が悪いので、メモリー上ではletterのあとは3バイトとばしてorderが配置されます。ちなみに、このような3バイト分のメモリーは、間を埋めているだけなので「パディング」と呼ばれます。

そういうわけで、構造体のサイズは、すべてのメンバーのサイズを合計した値と一致するとは限りません。この現象は、次のようにすれば確かめられるでしょう。

サンプルコード
  printf("total size  = %zu\n", sizeof(uint8_t) + sizeof(uint32_t));
  printf("struct size = %zu\n", sizeof(AlphabetInfo));
実行結果
total size  = 5
struct size = 8
うわぁ、ほんとに3バイト増えてますよ!
でしょ。つまりね、構造体のサイズを知りたかったら、素直にsizeof(構造体名)とすればいいのよ。
そっか。そう言われると単純な答えですね。
じゃあ、構造体のメンバーの並び順を入れ替えたらどうなると思う?
えっ?
並び順を入れ替えるとuint8_t、つまり1バイトのメンバーがうしろにくるから……
typedef struct {
  uint32_t order;
  uint8_t letter;
} AlphabetInfo;
パディングは必要ないんじゃないかな。確かめてみよう!
  printf("total size  = %zu\n", sizeof(uint32_t) + sizeof(uint8_t));
  printf("struct size = %zu\n", sizeof(AlphabetInfo));
実行結果
total size  = 5
struct size = 8
ええっ!入れ替えてもサイズが変わらないじゃないですか!
そうなのよ。構造体の最後に、やっぱり3バイトのパディングが入っているわね。なんでか分かる?
うーん、メモリーを無駄遣いしているようにしか思えないんですけど……
これはね、配列にしたときのことまで考えられているからなの。
構造体の配列ってことですか?
そう。配列って、要素が隙間なく並べられるでしょう?だから、もし最後にパディングがなかったら……
あ、もしかして……。すぐ隣の要素のアラインメントがずれちゃうってことですか?
そういうこと!
なるほどー!うまく考えられてるんですね!
ここがポイント!
メモリー上のデータは、サイズによって都合のいい「区切り」で配置されていることを覚えておこう!

修正後のプログラム

main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>

typedef struct {
  uint8_t letter;
  uint32_t order;
} AlphabetInfo;

AlphabetInfo *mallocAlphabetInfo(uint8_t letter) {
  const char *pAll = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  const char *pFound = strchr(pAll, toupper(letter));
  if (pFound) {
    AlphabetInfo *info = malloc(sizeof(AlphabetInfo));
    if (info) {
      info->letter = letter;
      info->order = 1 + (uint32_t)(pFound - pAll);

      return info;
    }
  }

  return 0;
}

int main(void) {
  AlphabetInfo *info = mallocAlphabetInfo('x');
  if (info) {
    printf("'%c' is the %dth letter.\n", info->letter, info->order);

    free(info);
    info = 0;
  }

  return EXIT_SUCCESS;
}
実行結果
'x' is the 24th letter.

第18問

アルファベット1文字と、それがABC順で何番目なのかを構造体に格納するプログラムを作りました。でも、このプログラムには重大な誤りがあります。

どこが間違っているのか分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>

typedef struct {
  uint8_t letter; /* 文字 */
  uint32_t order; /* ABC順で何番目か */
} AlphabetInfo;

AlphabetInfo *mallocAlphabetInfo(uint8_t letter) {
  const char *pAll = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  const char *pFound = strchr(pAll, toupper(letter)); /* toupper() で大文字にしてから検索 */
  if (pFound) { /* strchr() は見つけた文字のアドレスを返す */
    AlphabetInfo *info = malloc(5);
    if (info) {
      info->letter = letter;
      info->order = 1 + (uint32_t)(pFound - pAll); /* Aが「1番目」なので1を足す */

      return info;
    }
  }

  return 0;
}

int main(void) {
  AlphabetInfo *info = mallocAlphabetInfo('x');
  if (info) {
    printf("'%c' is the %dth letter.\n", info->letter, info->order);

    free(info);
    info = 0;
  }

  return EXIT_SUCCESS;
}
期待される実行結果
'x' is the 24th letter.
これは、アレですか?場合によってクラッシュする系の……
そうそう。クラッシュしないプログラムに直す問題よ。
やっぱり!ちょっと怪しいなと思った箇所があるんですよ。
いいわね!どこらへんが怪しいのかしら?
はい。えっと……プログラム中にあるmallocAlphabetInfo()っていうのが、構造体を作る関数だと思うんですけど。
そうね。「受け取った文字がアルファベットだったら、メモリー上に構造体を作って返す」という動作になってるわね。
ですよね!その中にあるmalloc(5)のところが、なんだか怪しい気がするんですよ……
それじゃあ、そのへんを考えてみましょうか。
はーい!
malloc(5)が間違っている理由を考えてみましょう。

グローバルスコープは要らない?

このトピックで扱うのは、グローバルスコープの「変数」よ。
グローバルスコープの「関数」は、要らないはずがないですもんね。
そういうこと!

グローバルスコープの変数は、「他のモジュールからもアクセスできるようにしたい場合」に使います。

例えば次のプログラムを見てみましょう。1つ前のトピックとよく似たC言語のプログラムです。

document.h
#ifndef document_h
#define document_h

typedef enum {
  Closed,
  Open
} DocumentOpenStatus;

extern DocumentOpenStatus openStatus;

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

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

DocumentOpenStatus openStatus = Closed;

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

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

  openStatus = Open;
}

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

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

  openStatus = Closed;
}

1つ前のトピックのプログラムから変わったのは、ドキュメントが開いているかどうかを示す変数openStatusがグローバルになったことです。

「document.c」からstaticを削除して、ヘッダーファイル「document.h」にextern宣言を追加してあります。それにともなって、変数の型(enum)の宣言を「document.c」から「document.h」に移動しました。

これで何ができるようになるかは、main()を見れば分かるでしょう。

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

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

  if (openStatus == Open) {
    CloseDocument();
  }

  return EXIT_SUCCESS;
}

グローバルになった変数openStatusmain()からアクセスして、ドキュメントが開いているかどうかを判定できるようになったのです。

……あれ?ちょっと待ってください!そもそも変数をファイルスコープにしていたのには、理由がありましたよね。
ふふふ。正解!変数の値が予期せぬタイミングで変更されないように、ファイルスコープにしていたのよね。

グローバルにしてしまったら、変数の値が勝手に変更されないことを誰が保証してくれるのでしょう?もちろん、「この変数は更新しない」というルールを守ってコーディングできれば問題ありませんが、それはなかなか難しいことです。

プログラムの規模が大きくなるにつれグローバル変数の数が増えれば、ややこしいルールもどんどん増えていくでしょう。そして、いつか手におえないほど複雑になってしまうかもしれません。(実際に、そのようなプログラムで苦労したことがある人もいるのでは?)

つまり、ファイルスコープだった変数をグローバルに変更するのは、「情報隠蔽」が破られてしまうことを意味するのよ。
じゃあ、この方法はあまり良いやり方ではないんですね……
そういうこと。

ここからは、「情報隠蔽」を保ったまま(同時に気を配らなければならない範囲を狭くしたまま)、やりたいことができる方法を考えてみましょう。グローバル変数を使わずに、ドキュメントが開いているかどうかを判定できるようにするのです。

次の改良版プログラムを見てください。

document.h
#ifndef document_h
#define document_h

typedef enum {
  Closed,
  Open
} DocumentOpenStatus;

DocumentOpenStatus GetOpenStatus(void);

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

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

static DocumentOpenStatus openStatus = Closed;

DocumentOpenStatus GetOpenStatus(void) {
  return openStatus;
}

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

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

  openStatus = Open;
}

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

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

  openStatus = Closed;
}

変数openStatusは、staticを付けてファイルスコープに戻しました。その代わり、変数の値を返す関数GetOpenStatus()が追加されたのが分かるでしょうか。

main()がどうなるのかも、見てみましょう。

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

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

  if (GetOpenStatus() == Open) {
    CloseDocument();
  }

  return EXIT_SUCCESS;
}

追加されたGetOpenStatus()により、ドキュメントが開いた状態かどうかを判定できています。一方、もうopenStatusに直接アクセスすることはできません。

グローバル関数を介してあげることで、ファイルスコープ変数の「情報隠蔽」を保っているんですね!
そう。このようなアプローチをとれば、グローバル変数はほとんど必要なくなるわね。

今回の例のように、「ファイルスコープ変数の値を取り出す関数」を用意する方法には、もうひとつ重要な効果があります。それは、「インターフェースと実装の分離」です。

GetOpenStatus()関数は、インターフェースとして公開されています。でも、その内部でopenStatusにアクセスしていることまでは公開していません。つまり、実装を隠しています。

そのため、インターフェースを変えることなく、あとから実装だけを変更できるのです!

例えば、ドキュメントが開いているかどうかをファイルポインタで判定するように変えたくなるかもしれません。そのために変数を差し替えても、モジュールの外からみたGetOpenStatus()の振る舞いは変えずに済むでしょう。

static FILE *documentFile = 0;

DocumentOpenStatus GetOpenStatus(void) {
  return (documentFile)? Open : Closed;
}

もしグローバル変数を使っていたら、こうした変更は簡単ではなくなります。

なるほど。グローバル変数は、極力使わない方がメンテナンスも楽になりそうですね!
そういうこと。一方で、変数に直接アクセスせず関数を介す場合は、関数呼び出しのオーバーヘッドがかかるってことも一応覚えておいて。

関数呼び出しのオーバーヘッドは、ほとんどの場合は気にする必要がないほど小さなものです。それでも気になるようなら、inline関数を使うようにしましょう。ただし、コンパイラがinline関数をサポートしていないケースもあります。

どうしても高速化が必要な場合は、涙をのんでグローバル変数にすることもあるわ。
あははは。グローバル変数は、どうしても使わなければならない理由があるときだけ、ですね。
ここがポイント!
情報隠蔽の観点から、グローバルスコープの変数は極力使用しないようにしましょう。代わりに、インターフェースだけ公開して実装との分離を図れば、メンテナンス性も向上します。

第17問の答え

答え

これだと文字列の終端に'\0'が付かない場合がある!
  char truncatedText[length];
  strncpy(truncatedText, text, length);
文字列が確実に'\0'で終わるようにするのが正解!
  char truncatedText[length + 1];
  truncatedText[length] = '\0';
  strncpy(truncatedText, text, length);

解説

C言語では、文字列の終端に'\0'という特殊な文字を付ける決まりになっています。そのため、文字列を格納するには、表現したい長さ(文字数)よりも1バイト分だけ大きいメモリー領域が必要です。

'\0'は、いわゆる「ヌル文字」というやつね。
はい。そのへんは僕もC言語の入門書で勉強しましたよ。

次のようにして「文字列の長さ」と「文字列のサイズ」を表示してみれば、'\0'が付いている様子を理解できるでしょう。

サンプルコード
  char text[] = "Hello!";

  printf("Length of text = %zu\n", strlen(text));
  printf("Size of text = %zu\n", sizeof(text));
実行結果
Length of text = 6
Size of text = 7

問題のプログラムでは、次のようにして文字列を切り詰めようとしていました。

  char truncatedText[length];
  strncpy(truncatedText, text, length);

でも、これでは切り詰められた文字列に'\0'が付くとは限りません。strncpy()の仕様が次のようになっているためです。

  • 指定された長さの分だけ文字列をコピーするが、終端の'\0'は対象外
  • ただし、コピー元の文字数が足りないときは、残りを'\0'で埋める

つまり、文字列"Hello!"は6文字なので、終端に'\0'が付くのは長さ(length)に7以上を指定したときだけになってしまっているのです。場合によって'\0'の有無が変わるところが、少しややこしいですね。

このことを理解するために、実験としてプログラムに手を加えてみましょう。コピー先の配列の後方に、目印として1文字'X'を入れてみます。

サンプルコード
  char truncatedText[length + 2];
  truncatedText[length] = 'X';
  truncatedText[length + 1] = '\0';
  strncpy(truncatedText, text, length);
実行結果
length = 4: HellX
length = 5: HelloX
length = 6: Hello!X
length = 7: Hello!
length = 8: Hello!

strncpy()'\0'を付けてくれるかどうかは、長さによることが分かるでしょう。コピーする長さ(length)がコピー元の文字数(6文字)以下のときは'\0'が付かないので、文字列の終端を見つけられず後方の'X'まで表示されてしまっています。

今は実験として'X'の次に'\0'も入れてあるので安全ですが、問題のプログラムでは'\0'が見つかる保証はありませんでした。これは、配列のサイズを踏み越えて文字列が続いていると判断してしまうかもしれない不安定な状態です。その結果、プログラムは予期できない動作をし、場合によってはクラッシュする恐れもあったのです。

ここまで分かれば、あとは確実に'\0'を付けるにはどうしたらいいかを考えるだけです。方法は1つとは限りませんが、例えば次のようにすればいいですね。

  char truncatedText[length + 1];
  truncatedText[length] = '\0';
  strncpy(truncatedText, text, length);
ふぅ。今回の問題はけっこう難しかったですね。
そうね。難しく感じるのは、2つの要素が絡み合っているからじゃないかしら。
2つの要素……。'\0'strncpy()のことですか?
そういうこと!ごちゃ混ぜにしないで、それぞれを理解することが大切よ。
なるほどー。

今回の出題は、2つの要素が絡み合うものでした。

  • 文字列の終端には'\0'が必要なこと
  • strncpy()の仕様がややこしいこと

それぞれの要素について、きちんと理解しておくようにしましょう。そうすれば、今後似たような問題に出会ったとしても落ち着いて対処できますね。

ここがポイント!
文字列の操作では終端の'\0'を意識しよう!

修正後のプログラム

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

void PrintTruncated(const char *text, size_t length) {
  char truncatedText[length + 1];
  truncatedText[length] = '\0';
  strncpy(truncatedText, text, length);

  printf("length = %zu: %s\n", length, truncatedText);
}

int main(void) {
  for (int i=4; i<=8; i++) {
    PrintTruncated("Hello!", i);
  }

  return EXIT_SUCCESS;
}
実行結果
length = 4: Hell
length = 5: Hello
length = 6: Hello!
length = 7: Hello!
length = 8: Hello!

第17問

テキストを指定の文字数で切り詰めて表示するプログラムを作りました。このプログラムは正常に動いているように見えることもありますが、実際には重大な誤りを含んでいます。場合によっては、クラッシュしてしまうかもしれません。

どこが間違っているのか分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void PrintTruncated(const char *text, size_t length) {
  char truncatedText[length];
  strncpy(truncatedText, text, length);

  printf("length = %zu: %s\n", length, truncatedText);
}

int main(void) {
  for (int i=4; i<=8; i++) {
    PrintTruncated("Hello!", i);
  }

  return EXIT_SUCCESS;
}
期待される実行結果
length = 4: Hell
length = 5: Hello
length = 6: Hello!
length = 7: Hello!
length = 8: Hello!
切り詰められた文字列をstrncpy()で作っているのが分かるかしら?
はい。そこは分かったんですけど……。
質問がありそうね?ただいま受け付け中よ。
それじゃ、遠慮なく。配列の長さを変数で指定してるところがありますよね?
truncatedTextの長さが固定値じゃなくてlengthになってるけど……。
  char truncatedText[length];
配列の長さって、固定値じゃなくてもいいんですか?
いいのよ。これは「可変長配列」というやつね。
へぇー!そんな機能があるんですね。
じゃあ、それをふまえて何が問題か考えてみてね。
はーい!
strncpy()の仕様を確認してみましょう。

第16問の答え

答え

環境によってはunsigned intのビット幅が足りないかもしれない!
unsigned int MakeColor(unsigned char nRed, unsigned char nGreen, unsigned char nBlue) {
  ……
こうやってビット幅を明記するほうがベター!
uint32_t MakeColor(uint8_t nRed, uint8_t nGreen, uint8_t nBlue) {
  ……

解説

プログラミングでは、変数のビット幅(その変数が何ビット分の値を格納できるか)が重要になる場面があります。問題のプログラムが期待どおりに動作するには、以下の前提条件が必要なことが分かるでしょうか。

  • 三原色のそれぞれが8ビット
  • カラーコードが24ビット以上(8ビットの値3つを合成するため)

三原色のほうはunsigned charなので、まず問題ありません。カラーコードのほうも、unsigned intはだいたいの環境では32ビットなので、問題になることは少ないでしょう。ところが、intunsigned intが16ビットの環境も存在します。そのような環境では、このプログラムだと正しい計算ができません。

え?intunsigned intのサイズって決まってないんですか?
そうよ。
決まっていれば簡単なのに……。なんでそうなってないんです?
それは、扱いやすいサイズが環境ごとに違うからね。

intunsigned intは、その環境でもっとも扱いやすいビット幅をもちます。これは「高速に計算できる」ことを意味するので、可能ならintunsigned intを使うのが有利です。

でも、アルゴリズムが一定のビット幅を前提とするときは、intunsigned intだとうまく動作しない環境が出てきてしまいます。そのような場合のために、C言語にはint32_tuint32_tのようなビット幅が固定の型が用意されています。これらは<stdint.h>をインクルードすれば使えます。

問題のプログラムでは、一定のビット幅が必要な箇所にunsigned intunsigned charが使われていました。

unsigned int MakeColor(unsigned char nRed, unsigned char nGreen, unsigned char nBlue) {
  unsigned int r = (unsigned int)nRed << 16;
  unsigned int g = (unsigned int)nGreen << 8;
  unsigned int b = (unsigned int)nBlue;

  return r | g | b;
}

これは、uint32_tuint8_tを使って次のように書き直すことができます。

uint32_t MakeColor(uint8_t nRed, uint8_t nGreen, uint8_t nBlue) {
  uint32_t r = (uint32_t)nRed << 16;
  uint32_t g = (uint32_t)nGreen << 8;
  uint32_t b = (uint32_t)nBlue;

  return r | g | b;
}

ただ型の名前を置き換えただけですね。

実のところ、intunsigned intが32ビットの環境では、上記のように書き直しても動作は変わりません。uint32_tuint8_tは、以下のような意味をもつ「別名」にすぎないためです。

typedef unsigned char uint8_t;
typedef unsigned int uint32_t;

一方、同じプログラムをintunsigned intが16ビットの環境でコンパイルすると、以下のようにuint32_tの意味が変わるということです。

typedef unsigned char uint8_t;
typedef unsigned long uint32_t;
ここがポイント!
ビット幅が意味をもつプログラムを作るときは、ビット幅が固定の型を使おう!

修正後のプログラム

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

uint32_t MakeColor(uint8_t nRed, uint8_t nGreen, uint8_t nBlue) {
  uint32_t r = (uint32_t)nRed << 16;
  uint32_t g = (uint32_t)nGreen << 8;
  uint32_t b = (uint32_t)nBlue;

  return r | g | b;
}

int main(void) {
  printf("red = #%06x\n", MakeColor(255, 0, 0));
  printf("blue = #%06x\n", MakeColor(0, 0, 255));
  printf("magenta = #%06x\n", MakeColor(255, 0, 255));

  return EXIT_SUCCESS;
}
実行結果
red = #ff0000
blue = #0000ff
magenta = #ff00ff

第16問

カラーコードを表示するプログラムを作りました。期待どおりに動作しましたが、一部の環境では同じ表示にならないかもしれません。

どのように修正すれば環境への依存性を低くできるか、分かりますか?
main.c
#include <stdlib.h>
#include <stdio.h>

unsigned int MakeColor(unsigned char nRed, unsigned char nGreen, unsigned char nBlue) {
  unsigned int r = (unsigned int)nRed << 16;
  unsigned int g = (unsigned int)nGreen << 8;
  unsigned int b = (unsigned int)nBlue;

  return r | g | b;
}

int main(void) {
  printf("red = #%06x\n", MakeColor(255, 0, 0));
  printf("blue = #%06x\n", MakeColor(0, 0, 255));
  printf("magenta = #%06x\n", MakeColor(255, 0, 255));

  return EXIT_SUCCESS;
}
期待される実行結果
red = #ff0000
blue = #0000ff
magenta = #ff00ff
カラーコードっていうのは分かるかしら?
はい。HTMLとかCSSで使うやつですよね。「黄色」は「#ffff00」とかの。
そうそう。そのカラーコードを「赤・緑・青」の三原色から計算するプログラムね。
三原色はそれぞれ0〜255の範囲で指定するから、unsigned charなんですね。
そういうこと。
で、計算した結果がunsigned intになる、と。
だいたいの場合はこれで問題ないのだけれど。
一部の環境ではちゃんと動かないんですね。でも、なんで……。
期待どおりの計算が成立するための前提条件を考えてみましょう。

第15問の答え

答え

C++では、mallocの戻り値はそのままだと使えない!
  Triangle *triangle = malloc(sizeof(Triangle));
こうやってキャストするのが正解!
  Triangle *triangle = static_cast<Triangle *>(malloc(sizeof(Triangle)));
または、C言語方式のキャストでもOK!
  Triangle *triangle = (Triangle *)malloc(sizeof(Triangle));

解説

まず、C++はC言語の上位互換ではないということを知っておきましょう。C言語のプログラムは大部分がC++としても正しいプログラムといえますが、例外もあるのです。

今回の問題は、void *の扱いがC言語とC++とで異なることに起因しています。malloc()の戻り値はvoid *なので、そのままC++のプログラムとしてコンパイルすることはできません。

C言語では、こうなります。

  • 任意の型のアドレスは、void *型のポインタに代入できる
  • void *型のアドレスは、任意の型のポインタに代入できる

これに対して、C++ではこうです。

  • 任意の型のアドレスは、void *型のポインタに代入できる(C言語と同じ)
  • void *型のアドレスは、void *型以外のポインタには代入できない(C言語と違う)

これは、危険な(タイプセーフではない)代入を抑制するためのルールです。とはいえ、コンパイラは開発者を信頼することになっているので、明示的にキャストすればコンパイルエラーは発生しなくなります。

C++のキャストには種類がありますが、ここではstatic_castを使うのが適切です。

  Triangle *triangle = static_cast<Triangle *>(malloc(sizeof(Triangle)));
static_castはC++の文法よ。
キャストはC言語にもありますよね?
そうね。C言語と同じ方式のキャストを使ってもOKよ。

C++では、次のようにC言語方式のキャストも使用可能です。この書き方なら、同じソースファイルをC言語とC++どちらのコンパイラでコンパイルしてもエラーになりません。

  Triangle *triangle = (Triangle *)malloc(sizeof(Triangle));

今はC言語を使っているけれど、いずれはC++に移行するつもりだということもあるでしょう。その場合は、あらかじめ(C言語方式で)キャストしておけば、移行がスムーズになります。

なお、C言語とC++のソースファイルが混在したままで開発を進めることも可能です。それぞれのコンパイラでコンパイルしたものを、リンクする方法があるためです。既存のソースファイルはあくまで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 = static_cast<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