記事一覧

第23問の答え

答え

文字列リテラルを指すポインタの配列なので、こう書くのが正解!
  const char *names[] = {
    "Leo",
    "Yuki"
  };

解説

これは、「ポインタの配列」についての問題でした。一つ一つの要素がポインタなので、それぞれが別々のデータを指すことができます。

あれ?今回のプログラムは「名前の一覧」を格納してるから、文字列の配列になるんじゃないんですか?
意味合いとしては、そのとおりよ。でも、文字列の長さはみんな違うから、実装上は……
……あ、そっか。分かったかも!
いいわね。それじゃ続きをどうぞ。
はい。ポインタなら長さが同じになるから、文字列を指すポインタを配列にしてるんですね!
そういうこと!

配列は、同じサイズの要素を隙間なく並べたデータ構造です。例えば、次のような整数の配列があったとしましょう。

  int numbers[] = {
    123,
    456
  };

ここで、123456はどちらもint型の値なので、サイズも同じです。そのため、1つの配列にまとめることができています。このことは、次のように各要素を間接的に書き直してみれば明確になるでしょう。

  int number0 = 123;
  int number1 = 456;

  int numbers[] = {
    number0,
    number1
  };

さて、問題の穴埋め部分は次のような配列でした。

  /* ここに入るコードは? */ = {
    "Leo",
    "Yuki"
  };

一見すると「文字列の配列」のように見えますが、これが実際には「文字列を指すポインタの配列」なのです。このことが明確に分かるよう、各要素を間接的に書き直してみましょう。

  const char *name0 = "Leo";
  const char *name1 = "Yuki";

  /* ここに入るコードは? */ = {
    name0,
    name1
  };

"Leo""Yuki"は、文字列リテラルです(第05問でも出てきましたね)。実行時に書き換えられない文字列なので、これらを指すポインタの型はconst char *となります。

したがって、穴埋めの答えは次のようになります。

  const char *names[] = {
    "Leo",
    "Yuki"
  };
なるほど、これが「ポインタの配列」だというのは分かったんですけど……
質問がありそうね。
はい。問題のプログラムには「ポインタのポインタ」が出てきてましたよね。これって?
ああ、それはね……

問題のプログラムには、「ポインタの配列」を「ポインタのポインタ」として受け取る関数が出てきていました。

void GreetTo(int nameIndex, const char **ppNames) { /* ← ポインタのポインタとして受け取る */
  ……
}

int main(void) {
  const char *names[] = {
    ……
  };

  GreetTo(0, names); /* ← ポインタの配列を渡す */
  ……
}

C言語では、配列を引数にして関数を呼び出すと「配列の先頭要素のアドレス」が引き渡されます。この例では要素の型が「文字列を指すポインタ」なので、受け取る側は「文字列を指すポインタのポインタ」になるのです。

この動作は変数に代入するときも同じです。

  const char *names[] = {
    "Leo",
    "Yuki"
  };

  const char **ppNames = names; /* ← ポインタの配列を、ポインタのポインタに代入 */
  printf("Hello, %s!\n", ppNames[0]);
  printf("Hello, %s!\n", ppNames[1]);

少しややこしく感じるでしょうか?その場合は、整数の配列に戻って考えてみるとイメージがわいてくるかもしれません。

  int numbers[] = {
    123,
    456
  };

  int *pNumbers = numbers; /* ← 整数の配列を、整数のポインタに代入 */
  printf("Number = %d\n", pNumbers[0]);
  printf("Number = %d\n", pNumbers[1]);
なるほど〜。最初は難しく感じましたけど、こうやってシンプルに考えればなんとかなりそうです!
でしょ?まぁハッキリ言えば、const char *names[]っていう文法がややこしいだけなのよ。
あ、やっぱり。これ分かりづらいですよね。
もっと分かりやすくしたかったら、typedefを使うのも一つの方法ね。
例えば、文字列リテラルをこうやっておいて……
typedef const char *StringLiteral;
その配列をこんな感じにすれば分かりやすい!
  StringLiteral names[] = {
    "Leo",
    "Yuki"
  };
わ、ほんとだ分かりやすい!
でもね、「ポインタの配列」はよく出てくるデータ構造だから、文法に慣れておくことも大切よ。
はーい!
ここがポイント!
一見ややこしい「ポインタの配列」も、シンプルに考えればイメージしやすくなる!

ちなみに、main()関数でコマンドライン引数を受け取るときに使うargvも、今回の配列とよく似たデータ構造です。

int main(int argc, char *argv[]) {
  ……
}
コマンドライン引数は、第10問でも出てきたわね。
はい。でも、char *argv[]だと、引数なのに「ポインタのポインタ」になってないですけど……
この場合、char **argvって書くのと同じになるわ。
えええ?これと同じってこと?
int main(int argc, char **argv) {
  ……
}
これはね、関数の引数を書くときだけの特別ルールなのよ。
うわぁ、また混乱しそう。
ふふふ。でもね、char *argv[]と書けば「このポインタは配列を指すよ」っていう意図が伝わりやすくなるでしょ。
そっか、そのための特別ルールなんですね。

修正後のプログラム

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

void GreetTo(int nameIndex, const char **ppNames) {
  printf("Hello, %s!\n", ppNames[nameIndex]);
}

int main(void) {
  const char *names[] = {
    "Leo",
    "Yuki"
  };

  GreetTo(0, names);
  GreetTo(1, names);

  return EXIT_SUCCESS;
}
実行結果
Hello, Leo!
Hello, Yuki!

第23問

以下は、名前の一覧の中から1つずつ指定して、あいさつを表示するプログラムです。ただし、コメントの部分に入れるべきコードが隠れています。

どんなコードが入るか分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>

void GreetTo(int nameIndex, const char **ppNames) {
  printf("Hello, %s!\n", ppNames[nameIndex]);
}

int main(void) {
  /* ここに入るコードは? */ = {
    "Leo",
    "Yuki"
  };

  GreetTo(0, names);
  GreetTo(1, names);

  return EXIT_SUCCESS;
}
実行結果
Hello, Leo!
Hello, Yuki!
おお〜、穴埋め問題ですね!
たまにはこういうのもいいでしょ?
はい。ちょっと楽しそうです!
えっと、これがあいさつを表示する関数かな。……ん?
void GreetTo(int nameIndex, const char **ppNames) {
  printf("Hello, %s!\n", ppNames[nameIndex]);
}
あのー、**っていうのは、タイプミスか何かですかね?
ああ、それはポインタのポインタよ。
うわぁ、何ですかそれ?
うーん、ちょっと難しかったかな。
はい、あんまりイメージがわかないです。
それじゃあ、似ているけどもう少し簡単なプログラムで練習しましょ。
main.c
#include <stdio.h>
#include <stdlib.h>

void PrintNumber(int numberIndex, int *pNumbers) {
  printf("Number = %d\n", pNumbers[numberIndex]);
}

int main(void) {
  int numbers[] = {
    123,
    456
  };

  PrintNumber(0, numbers);
  PrintNumber(1, numbers);

  return EXIT_SUCCESS;
}
実行結果
Number = 123
Number = 456
あ、これなら分かりそうですよ!
整数の配列があって……
  int numbers[] = {
    123,
    456
  };
この関数にインデックスと配列を渡して、要素を1つ表示してる!
void PrintNumber(int numberIndex, int *pNumbers) {
  printf("Number = %d\n", pNumbers[numberIndex]);
}
ここまでは大丈夫そうね。それじゃあ、問題のプログラムに戻ってみてね。
はーい!
「名前の一覧」を表すにはどんな型が必要か考えてみましょう。

論理型を使いこなす

メンテナンスしやすいプログラムにするために、変数の「スコープ」を意識するのがいいって説明してもらいましたけど、ほかにも変数の扱いで意識しておくといいことってあるんでしょうか?
そうねぇ……。エラー処理では積極的に論理型を使うようにすることかしら。
論理型って、ブーリアンのことですよね!?
そう。論理型は、意外に使いどころが多い型なのよ。
えー、自分ではあんまり使ったことないかもしれないです。詳しく教えてください!

成功・失敗を返却する

まずは、関数の戻り値に論理型を使うケースについて考えてみましょう。

例えば、以下のような正の整数を返す関数があったとします。

int GetPositiveValue(void);

ただし、この関数はエラーになる場合があります。

もし、エラー時には0以下の値を返すという仕様だったとしたら、呼び出し側は次のようになるでしょう。

  int nValue = GetPositiveValue();
  if (nValue > 0) {
    /* 通常の処理 */
  } else {
    /* エラー処理 */
  }

このようなコーディングをするためには、この関数の仕様をしっかりと頭に入れておかなければなりません。

でも、うっかり忘れてしまうこともあるかもしれませんね。

問題なのは、関数のプロトタイプを見ただけではエラーのことを思い出すのが難しいという点よ。

そこで、関数を次のように改良するとどうでしょうか。

bool GetPositiveValue(int *pValue);

こうすれば、関数を使おうとするたびにbool型の戻り値に気付き、エラー処理のことを思い出せるようになりますね。

ちなみに呼び出し側は次のような感じです。

  int nValue = 0;
  if (GetPositiveValue(&nValue)) {
    /* 通常の処理 */
  } else {
    /* エラー処理 */
  }
なるほど。関数の戻り値を論理型にしておけば、それをそのままエラーの判定に使えるんですね!
そういうこと。直感的に関数の仕様が理解できるから分かりやすいし、エラー処理が漏れてしまうことも防げるのよね。

ブロック内の処理の成功・失敗を覚えておく

処理の成功・失敗を一時的に覚えておきたいときもboolが便利よ。

例えば、次のような場合です。

  bool bSuccess = true;

  for (・・・) {
    /* 何かの処理をして・・・ */

    if (・・・) {
      /* ここで失敗した! */
      bSuccess = false;
      break;
    }
  }

  if (bSuccess) {
    /* 成功時の処理 */
  } else {
    /* 失敗時の処理 */
  }

この方法は手前の処理の成功・失敗によって、次の処理を振り分ける場合に有効です。

そっかぁ。今までも同じようなプログラムは書いていたはずなんですけど、boolを使ったほうがうっかりミスを防げそうですね。
そうなのよ。だから使えそうな場面では、積極的にboolを使うのがおすすめよ。

C言語でブーリアンを使うには?

ちなみに。

C言語でboolを使うには、<stdbool.h>をインクルードする必要があるわよ。
#include <stdbool.h>

ただし、古めのC言語はboolをサポートしていません。

どうしても古い環境でブーリアンを使いたいときは、自分で定義してしまいましょう。

例えば、こう。

#ifndef __cplusplus
  typedef int bool;
  #define true  1
  #define false 0
#endif

または、こんな風にすればいいですね。

#ifndef __cplusplus
  typedef enum {
    false = 0,
    true  = 1
  } bool;
#endif

なお、C++の場合は最初からboolに対応しています。


boolを使うとエラー判定がシンプルに書けるし、あとからプログラムを見直したときも何をやっているかが一目瞭然になりますね。
そう。分かりやすくて必要な処理に集中しやすいプログラムになるから、実装漏れやバグの防止にもつながるわね。
ふむふむ。これからは、論理型の変数を使いこなせるように意識してみます!
ここがポイント!
処理の成功・失敗を判定するケースでは、積極的に論理型の変数を使うようにしましょう。関数の仕様を直感的に理解でき、エラー処理をうっかり忘れるというミスも防げます。

第22問の答え

答え

ローカル変数のアドレスをreturnで返してしまっている!
const char *ToUpperText(const char *pText) {
  ……
  return UpperText; /* ← ローカル変数のアドレス */
}
ローカル変数のアドレスを返さないようにするのが正解!
void ConvertToUpper(char *pText) {
  ……
}

解説

これは、変数の寿命に関する問題でした。ローカルスコープ(関数の内側など)で宣言された変数には「寿命」があり、ずっと有効なわけではないのです。

問題のプログラムをコンパイルしてみたんですけど……
うん。どうだった?
それが、警告が出ちゃうんですよ。
でしょ。その警告が、今回のプログラムを直すためのヒントなのよ。
え?コンパイラの警告って、デバッグのヒントになるんですか?
もちろん!警告をちゃんと読めばね。
なるほど。じゃあ、読んでみますね。
えっと……警告の内容はこれか。
Address of stack memory associated with local variable 'UpperText' returned
ざっと訳すと、「ローカル変数UpperTextにあたるスタックメモリーのアドレスが返却された」ですかね。
そうそう。最近のコンパイラは警告も優秀よねぇ。昔はもっと大変だったんだから……
え、ユキ先輩っていったい何年前からプログラミングやってるんですか?
なぁに?よく聞こえなかったわ。
あ。なんかスミマセン。(気を取り直して……)「スタック」ってなんですか?
スタックというのは、一時的なデータが格納されるメモリー領域のことよ。

今回コンパイラが出した警告は、関数ToUpperText()return文に対するものでした。ここで、スタックのアドレスが返却されてしまっているという内容です。

const char *ToUpperText(const char *pText) {
  size_t length = strlen(pText);
  char UpperText[length + 1]; /* ← この配列はローカル変数なのでスタックに確保される */

  for (int i=0; i<length; i++) {
    UpperText[i] = toupper(pText[i]);
  }
  UpperText[length] = '\0';

  return UpperText; /* ← ここでスタックのアドレスを返してしまった! */
}

関数の内側にあるローカル変数は、その関数が呼び出されている間しか使わない一時的なデータなので、スタックに確保されます。そして、関数の実行が完了するときに解放されます。ローカル変数は、役目を終えると自動的に消滅するということです。

今回のプログラムでは、returnでローカル変数の配列を返そうとしていました。でも、実際に返却されるのは配列全体ではなく、そのアドレスです。関数の呼び出し元からこのアドレスにアクセスしても、もう配列は存在しません。

なるほど。いくらアドレスを覚えていても、消滅してしまったデータにはアクセスできないわけですね。
そういうこと。
でも、「消滅する」といっても、本当にすぐメモリーからデータが消えちゃうんですか?
わざわざメモリーをクリアするわけではないから、returnした直後なら残ってるかもしれないのだけど……
ふむふむ。
解放されたメモリーは次の関数呼び出しで真っ先に再利用されるから、実質的にはすぐに消えちゃうわね。
そうなんですね。じゃあ、自動的に消滅しないようにするには……malloc()を使えばできますよね!
うん。それも一つの正解ね。でも、別の方法も考えてみるといいかもよ。

malloc()は、スタックではなく「ヒープ」と呼ばれる領域からメモリーを確保します。そのため、ローカル変数のように自動的にデータが消滅することはありません。今回のプログラムを修正するには、ヒープを使うのがもっとも直接的な方法だといえるでしょう。

でも、ヒープを使うと、気を配らなければならない点が増えてしまいます。

  • メモリーの確保に失敗する(malloc()が0を返す)ケースに対処しなければならない
  • メモリーを解放する(free()を呼び出す)タイミングについて考えなければならない

そこで今回は、別の方法を考えてみましょう。関数の仕様を変更するのです。

以下は、問題のプログラムから、関数ToUpperText()がどのように使われているのかが分かる部分だけを抜き出したものです。

const char *ToUpperText(const char *pText) {
  ……
}

int main(void) {
  char text[] = "The C Programming Language";
  puts(text);
  puts(ToUpperText(text));
  ……
}

この関数を、既存の文字列を上書きする仕様に変更したらどうなるでしょうか。

void ConvertToUpper(char *pText) {
  ……
}

int main(void) {
  char text[] = "The C Programming Language";
  puts(text);
  ConvertToUpper(text); /* 元のテキストを上書きして大文字に変換 */
  puts(text);
  ……
}
新しい文字列をreturnで返すのをやめて、引数の文字列そのものを変換する仕様にしたのが分かるかしら?
はい。関数の仕様が変わったから、呼び出し側にも少し変更が入ってるんですね。
そうそう。
えっと、引数からconstがなくなってますけど、これはたしか……
それは第04問でやったわね。
そっか。思い出しました!
それじゃあ、おまけの問題。仕様を変更した関数の中身がどうなるか考えて、修正後のプログラムを仕上げてみてね。
はーい、やってみます!
ここがポイント!
ローカル変数には「寿命」があって、自動的に消滅する!

修正後のプログラム

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

void ConvertToUpper(char *pText) {
  size_t length = strlen(pText);

  for (int i=0; i<length; i++) {
    pText[i] = toupper(pText[i]);
  }
}

int main(void) {
  char text[] = "The C Programming Language";
  puts(text);
  ConvertToUpper(text);
  puts(text);

  return EXIT_SUCCESS;
}
実行結果
The C Programming Language
THE C PROGRAMMING LANGUAGE

第22問

テキストを大文字に変換するプログラムを作りました。コンパイルはできたのですが、実行してみると期待どおりの結果になりません。

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

const char *ToUpperText(const char *pText) {
  size_t length = strlen(pText);
  char UpperText[length + 1];

  for (int i=0; i<length; i++) {
    UpperText[i] = toupper(pText[i]);
  }
  UpperText[length] = '\0';

  return UpperText;
}

int main(void) {
  char text[] = "The C Programming Language";
  puts(text);
  puts(ToUpperText(text));

  return EXIT_SUCCESS;
}
期待される実行結果
The C Programming Language
THE C PROGRAMMING LANGUAGE
このプログラムを実行すると、表示がおかしくなるってことですか?
そうね。最悪の場合は、クラッシュしてしまうかも。
じゃあ、クラッシュしないプログラムに直せばいいんですね。
そういうことよ。
えっと、怪しいのはこの関数かな……
const char *ToUpperText(const char *pText) {
  ……
}
関数名がToUpperText()だから、これがテキストを大文字に変換する関数ですね。
そうそう。引数で受け取った文字列を、大文字の文字列に変換して返す仕様になってるわね。
関数の中身は……
  for (int i=0; i<length; i++) {
    UpperText[i] = toupper(pText[i]);
  }
  UpperText[length] = '\0';
ふむふむ。forループで1文字ずつ大文字に変換して、最後に'\0'を付けてるんですね。
'\0'は、第17問でも出てきた「ヌル文字」ね。
ですね!……あれ?このループはとくに問題なさそうですよ。
ふふふ。それじゃあ、問題があるのはどの部分かしらね。
実際にコンパイルしてみれば分かるかも?

第21問の答え

答え

typedefや大文字・小文字の使い方がバラバラだと読みづらい!
typedef enum {
  rock,
  Paper,
  Scissors
} Hand;

enum result {
  Won,
  Lost,
  draw
};
書き方を統一したほうが読みやすい!
typedef enum {
  Rock,
  Paper,
  Scissors
} Hand;

typedef enum {
  Won,
  Lost,
  Draw
} Result;

解説

今回は、プログラムを読みやすくするための練習問題でした。手直しできる部分がいくつかあるので、順番に説明していきます。

あのー、質問いいですか?
いいよ〜!
えっと、どうしてプログラムは読みやすいほうがいいんですか?
あらら、そこが疑問だったの?
いや、読みやすいほうがいいとは思ってるんですよ?でも、理由をちゃんと説明できないというか……
そういうことね。プログラミングって、けっこう頭を使う作業でしょう?
はい。デバッグとかで悩むことも多いです。
だとしたら少しでも読みやすいほうが、肝心な部分に集中しやすいと思わない?
たしかに、そのほうが考えもまとまりそうですね。
だから多少時間をかけてでも、読みやすいプログラムにするのはいいことなのよ。
やっぱり、そうですよねー!
府に落ちたみたいね。それじゃあ、問題のプログラムを手直ししていきましょうか。

プログラムには、「こういう書き方にしないとダメ」という決まりがあるわけではありません。でも、読みやすさのためには、ある程度の一貫性が大切です。例えば変数や関数などの名前の付け方(命名法)を統一するのは、すぐにでも実践できることの一つでしょう。

問題のプログラム中にあった次のenumは、名前の付け方が統一されていませんでした。

typedef enum {
  rock, /* ← ここだけ小文字で始まっている */
  Paper,
  Scissors
} Hand;
ここではenumを大文字で始まる名前に統一!
typedef enum {
  Rock,
  Paper,
  Scissors
} Hand;

次のenumはどうでしょう。大文字・小文字の使い方がバラバラなうえに、typedefもありません。

enum result { /* ← typedef がない */
  Won,
  Lost,
  draw /* ← 小文字 */
};
enumにはtypedefを付けるルールで統一するのがおすすめ!
typedef enum {
  Won,
  Lost,
  Draw
} Result;

structの書き方も統一しましょう。次の部分では、メンバーになっている変数名の付け方がバラバラですね。

struct game {
  Hand myHand; /* ← 途中に大文字を入れる形式 */
  Hand your_hand; /* ← アンダースコアでつなげる形式 */
};
メンバー名の書き方を揃えて、structにもtypedefを付けるルールで統一!
typedef struct {
  Hand myHand;
  Hand yourHand;
} Game;

これで、もう一つのstructとも書き方が揃いました。

typedef struct {
  Hand wins;
  Hand loses;
} Rule;

では、関数の部分はどうでしょうか。問題のプログラムには、main()のほかに次の2つの関数がありますね。

enum result result_of(struct game game) {
  ……
}

void Play(struct game game) {
  ……
}

enumstructの書き方を統一したので、ここは次のようになります。

Result result_of(Game game) { /* ← typedef のおかげで短くなった! */
  ……
}

void Play(Game game) { /* ← こちらも! */
  ……
}

typedefのおかげで記述量が減ってスッキリしましたが、まだ関数名の付け方がバラバラですね。

ここではアンダースコア(_)を使わず、大文字で始まる書き方に統一!
Result ResultOf(Game game) {
  ……
}

void Play(Game game) {
  ……
}

統一感が出て、かなり読みやすくなったのではないでしょうか。

あとは、enumstructの名前が変わった部分を全体に反映させていけば、修正完了です。

ここがポイント!
プログラムは読みやすく書いたほうが、考えもまとまりやすくなる!
ちなみに……「switch文にdefaultが足りない」って思わなかった?
あ、そこ少しだけ気になってました。
ここにdefaultがないのはいいのかなぁ。
void Play(Game game) {
  switch (ResultOf(game)) {
    case Won:
      puts("The winner is me!");
      break;
    case Lost:
      puts("The winner is you!");
      break;
    case Draw:
      puts("It's a draw.");
      break;
  }
}
今回の出題内容とは趣旨が違うのだけど、switch文にはdefaultを付けておくとさらにいい感じね。
このdefaultには到達しないはずだから、assert(false)を入れておくのがベター!
#include <stdbool.h>
#include <assert.h>

void Play(Game game) {
  switch (ResultOf(game)) {
    case Won:
      puts("The winner is me!");
      break;
    case Lost:
      puts("The winner is you!");
      break;
    case Draw:
      puts("It's a draw.");
      break;
    default:
      assert(false);
  }
}

こういう風にassertを入れておくと、将来ジャンケンの判定結果が増えた場合に備えられます。例えば、「連勝」や「反則負け」が増えるかもしれませんね。するとdefaultに到達してassert(false)でプログラムが停止するので、caseを追加しなければならないことが早期に分かるというわけです。

修正後のプログラム

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

typedef enum {
  Rock,
  Paper,
  Scissors
} Hand;

typedef enum {
  Won,
  Lost,
  Draw
} Result;

typedef struct {
  Hand myHand;
  Hand yourHand;
} Game;

typedef struct {
  Hand wins;
  Hand loses;
} Rule;

Result ResultOf(Game game) {
  Rule rules[] = {
    [Rock] = { .wins = Scissors, .loses = Paper },
    [Paper] = { .wins = Rock, .loses = Scissors },
    [Scissors] = { .wins = Paper, .loses = Rock }
  };

  Rule myRule = rules[game.myHand];
  if (myRule.wins == game.yourHand) { return Won; }
  if (myRule.loses == game.yourHand) { return Lost; }
  return Draw;
}

void Play(Game game) {
  switch (ResultOf(game)) {
    case Won:
      puts("The winner is me!");
      break;
    case Lost:
      puts("The winner is you!");
      break;
    case Draw:
      puts("It's a draw.");
      break;
    default:
      assert(false);
  }
}

int main(void) {
  Play((Game) { .myHand = Rock, .yourHand = Scissors });
  Play((Game) { .myHand = Scissors, .yourHand = Rock });
  Play((Game) { .myHand = Paper, .yourHand = Paper });

  return EXIT_SUCCESS;
}
実行結果
The winner is me!
The winner is you!
It's a draw.

第21問

ジャンケンの結果を判定するプログラムを作りました。期待どおりに動作したのですが、悩みながら作ったせいか、どことなく読みづらいソースファイルになってしまいました。

どうすれば読みやすくなるか、分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>

typedef enum {
  rock, /* グー */
  Paper, /* パー */
  Scissors /* チョキ */
} Hand;

enum result {
  Won, /* 勝ち */
  Lost, /* 負け */
  draw /* 引き分け */
};

struct game {
  Hand myHand; /* 自分の手 */
  Hand your_hand; /* 相手の手 */
};

typedef struct {
  Hand wins; /* 自分が勝つときの、相手の手 */
  Hand loses; /* 自分が負けるときの、相手の手 */
} Rule;

enum result result_of(struct game game) {
  Rule rules[] = {
    [rock] = { .wins = Scissors, .loses = Paper },
    [Paper] = { .wins = rock, .loses = Scissors },
    [Scissors] = { .wins = Paper, .loses = rock }
  };

  Rule myRule = rules[game.myHand];
  if (myRule.wins == game.your_hand) { return Won; }
  if (myRule.loses == game.your_hand) { return Lost; }
  return draw;
}

void Play(struct game game) {
  switch (result_of(game)) {
    case Won:
      puts("The winner is me!");
      break;
    case Lost:
      puts("The winner is you!");
      break;
    case draw:
      puts("It's a draw.");
      break;
  }
}

int main(void) {
  Play((struct game) { .myHand = rock, .your_hand = Scissors });
  Play((struct game) { .myHand = Scissors, .your_hand = rock });
  Play((struct game) { .myHand = Paper, .your_hand = Paper });

  return EXIT_SUCCESS;
}
実行結果
The winner is me!
The winner is you!
It's a draw.
いつもより長めのソースファイルだけど、大丈夫かしら?
はい、たぶん……。
よしよし。まず、列挙型(enum)が2つあるわね。
列挙型は……あった!この2つか。
typedef enum {
  rock, /* グー */
  Paper, /* パー */
  Scissors /* チョキ */
} Hand;

enum result {
  Won, /* 勝ち */
  Lost, /* 負け */
  draw /* 引き分け */
};
1つ目はジャンケンの手が「グー」「チョキ」「パー」のどれかを、2つ目は結果が「勝ち」「負け」「引き分け」のどれかを表すものですね。
うんうん。じゃあ、構造体(struct)のほうはどう?
……これだな。
struct game {
  Hand myHand; /* 自分の手 */
  Hand your_hand; /* 相手の手 */
};
gameっていう名前で、自分と相手の手が格納されるみたいだから、ジャンケン1回分のデータを表してるんですかね。
そうね。あと、もう一つ構造体があるわね。
……うん、あるある。
typedef struct {
  Hand wins; /* 自分が勝つときの、相手の手 */
  Hand loses; /* 自分が負けるときの、相手の手 */
} Rule;
これですね。Ruleってことは、勝ち負けのルールみたいなものが入るとか……?
そうそう。result_of()の中で使われてるわね。
……あ、これか。
  Rule rules[] = {
    [rock] = { .wins = Scissors, .loses = Paper },
    [Paper] = { .wins = rock, .loses = Scissors },
    [Scissors] = { .wins = Paper, .loses = rock }
  };
  ……
この配列ですね。……あ、なるほど。「グー」は「チョキ」に勝つ、でも「パー」には負けるとかのルールが入ってますね。
そういうこと!少し長いプログラムだけれど、だいたい理解できたかしらね。
はい。あと、なんだか書き方がバラバラになってるのも分かったかも!
そうよね。そこをキレイに直す練習をしてみようっていうのが今回の問題よ。
やってみます!
直せる場所は1カ所だけではありません!

変数の寿命

グローバルスコープファイルスコープときたら、次はローカルスコープね。
はい。……でも、ローカルスコープの変数を使うのに、コツみたいなものってあるんでしょうか?
もちろん、あるわよ。
そうなんですね。教えてください!

ローカルスコープは、ブロックスコープとも言います。「ブロック」とは、中括弧で囲まれた部分のことです。

ブロックを使うとローカル変数の寿命を制御できますが、このことを知らない人も意外に多いようです。

次のプログラムを見てみましょう。

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

int main() {
  int n1 = 123;
  int n2 = 456;

  {
    int temp = n1;
    n1 = n2;
    n2 = temp;
  }

  printf("n1 = %d\n", n1);
  printf("n2 = %d\n", n2);

  return EXIT_SUCCESS;
}
実行結果
n1 = 456
n2 = 123

2つの変数の値を入れ替えるだけの単純なプログラムです。

ここでは、変数tempに注目してね。
突然あらわれた中括弧で囲まれた内側で宣言されていますね!

この例のように、ブロックの内側で宣言されたローカル変数は、そのブロックの内側だけで有効です。ブロックの外側では使えません。そして、その変数は制御がブロックを抜けるときに消滅します。

つまりこういうこと

  • ローカル変数は、ブロックの内側で確保される
  • ブロックの終わりで、確保されていたローカル変数は解放される

もうひとつ、別の例をあげましょう。

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

int main() {
  int nSum = 0;

  for (int i=0; i<10; i++) {
    int temp = i * i;
    nSum += temp;
  }

  printf("SUM = %d\n", nSum);

  return EXIT_SUCCESS;
}
実行結果
SUM = 285

ifwhileなどの制御構文とともに現れる中括弧で囲まれた部分もブロックです。上記はfor文の例ですが、中括弧の内側にある変数tempは、ブロックを抜けるときに消滅します。同じ名前の変数がループの回数(ここでは10回)だけ、生まれては消えているというわけです。

一般的に、一つ一つの変数の寿命(生まれてから消えるまで)を短くするほうが理解しやすいプログラムになります。アルゴリズムを考えるときに、一度に気を配らなければならない範囲を狭められるためです。

上の例のtempのように、一時的にしか使わない変数は、ブロックで囲んでしまうと分かりやすいでしょ?
たしかに、スコープが限られていると、プログラムが理解しやすくなりますね!
そういうこと。
ここがポイント!
一時的にしか使わないローカル変数は、ブロックで囲んで寿命を制御しましょう。一度に気を配らなければならない範囲を狭めることで、理解しやすいプログラムになります。

第20問の答え

答え

関数名SearchUpper()から連想される「大文字を検索する」という動作が実際と違う!
int SearchUpper(const char *pText) {
  ……
「大文字が含まれるか」を調べる関数なので、それが分かる名前(命名法)にするのがベター!
int ContainsUpper(const char *pText) {
  ……
戻り値をboolにすると、さらに分かりやすい!
bool ContainsUpper(const char *pText) {
  ……

解説

もしも、次のような関数があったとしたら、どう思うでしょうか?

int Add(int n1, int n2) {
  return n1 - n2;
}
これは……さすがにダメですよ!
そうね。どうダメなのか説明できる?
はい。関数名のAdd()は「足す」という意味なのに、実際には引き算になってます。
そのとおり!名前と動作がズレているのは困るわね。

この例は、今回のプログラムの問題点をストレートに示しています。関数名から連想される動作と、実際の動作との間に食い違いがあるのです。

とくに困るのは、名前と動作のどちらが間違っているのか分からなくなってしまったときです。プログラムを作った直後なら正しく直せますが、時間が経って「あのときは何をしたかったんだっけ?」と忘れてしまうこともあるでしょう。できる限り最初から、実態に合う名前を付けておくことが大切です。

では、今回のプログラムを確認してみましょう。

int SearchUpper(const char *pText) {
  const char *p = pText;
  while (*p != '\0') {
    if (isupper(*p)) {

      return 1;
    }
    p += 1;
  }

  return 0;
}

関数名SearchUpper()から連想されるのは、「大文字を検索する」という動作です。戻り値がintなので、「最初に見つけた大文字の文字コードが返ってくるのかな」と想像する人が少なくないでしょう。

ところが、実際の動作は次ようなものでした。

  • 文字列中に大文字が1つでもあれば、1を返す
  • 文字列中に大文字が1つもなければ、0を返す

「大文字を検索する」というよりは、「大文字が含まれるか」を調べる動作になっていますね。さて、間違っているのは関数名でしょうか?それとも動作のほうでしょうか?

今回作ったのは、「文字列に大文字が含まれているかどうか確認するプログラム」でした。そして、実行結果は期待どおりでした。ということは、直すべきなのは関数名のほうですね。

例えば、次のような関数名に変更すれば、動作との食い違いをなくせるでしょう。

int ContainsUpper(const char *pText) {
  ……
}
たしかに……さっきまでのモヤモヤは晴れたような気がします。
でしょう?これって意外に重要なとこよ。
そうなんですか?
そうなのよ。今回みたいに微妙にズレた名前がたくさん出てくるプログラムがあったら、どうなると思う?
うえぇ……それは勘弁してほしいです!
ね?だから全部を完璧にできなくても、できる限り正しい名前を付けることが大切なの。
なるほど。「できる限り」だったらやれそうです!

ちなみに、今回のプログラムでは戻り値をboolにするのもおすすめです。boolはC++で標準的に使えるようになった型ですが、実はC言語でも<stdbool.h>をインクルードすれば使えます。

#include <stdbool.h>

bool ContainsUpper(const char *pText) {
  ……
}

関数名がContainsUpper()で戻り値がboolなら、「大文字が含まれていた場合にtrueを返すんだな」と想像しやすくなりますね。

ここがポイント!
関数には、実際の動作を正しく想像できそうな名前を付けよう!

修正後のプログラム

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

bool ContainsUpper(const char *pText) {
  const char *p = pText;
  while (*p != '\0') {
    if (isupper(*p)) {

      return true;
    }
    p += 1;
  }

  return false;
}

int main(void) {
  const char *pName = "Leo";
  if (ContainsUpper(pName)) {
    printf("%s contains uppercase.\n", pName);
  }

  return EXIT_SUCCESS;
}
実行結果
Leo contains uppercase.

第20問

文字列に大文字が含まれているかどうか確認するプログラムを作りました。実行結果は期待どおりになったのですが、ソースファイルの中身がイマイチです。

どこが手直しできそうか、分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int SearchUpper(const char *pText) {
  const char *p = pText;
  while (*p != '\0') {
    if (isupper(*p)) {

      return 1;
    }
    p += 1;
  }

  return 0;
}

int main(void) {
  const char *pName = "Leo";
  if (SearchUpper(pName)) {
    printf("%s contains uppercase.\n", pName);
  }

  return EXIT_SUCCESS;
}
実行結果
Leo contains uppercase.
これは、僕の名前("Leo")には大文字が含まれているっていう実行結果ですね。
そうね。じゃあ、ソースファイルの中身を解読してみましょうか。
はい。SearchUpper()っていうのは、大文字を検索する関数ですかね。
うんうん、それから?
えっと……大文字があったら1を返して、なかったら0を返すようになってます。
正解よ!
やったー。
それじゃあ、今の説明の中で、ちょっとモヤモヤする部分がなかったかしら?
そうですね……。正直「なんか変だぞ」とは思ったんですけど。この気持ちは一体……
関数の名前に注目してみましょう。