記事一覧

第06問の答え

答え

main()の手前でサブルーチンを宣言するのが正解!
void PrintNumber(int n);
または、こう書いてもOK!
void PrintNumber(int);

解説

C言語では、手前で定義されたものしか使えません。今回のプログラムでは、main()からPrintNumber()を呼び出そうとしています。でも、この時点ではPrintNumber()が定義されていないので、コンパイルできなかったのです。

int main(void) {
  PrintNumber(123); /* ← この時点で定義されていない関数なので呼び出せない */

  ……
}

void PrintNumber(int n) { /* ← ここで定義されている */
  ……
}

次のように2つの関数の並び順を入れ替えれば、一応コンパイルできるようにはなります。

void PrintNumber(int n) {
  ……
}

int main(void) {
  PrintNumber(123);

  ……
}

だからといって、いつも並び順を気にしながらプログラムを考えるのは無理がありますね。

そこで、関数の宣言(プロトタイプ宣言)を使います。どこか別の場所で定義されている関数が、どのような形になっているのかをコンパイラに伝えるのです。

void PrintNumber(int n); /* ← プロトタイプ宣言 */

int main(void) {
  PrintNumber(123); /* ← 宣言された関数は呼び出せる */

  ……
}

こうしてPrintNumber()を宣言すれば、その定義(実体)はmain()関数のあとに書くことができます。または、別のソースファイルで定義するのでも構いません。

え、別のソースファイルって何のことですか?
それはね、大きなプログラムになると、ソースファイル1つじゃ作れないでしょう?
はい。そのときはファイルを分けると思います。
その場合でも、宣言があれば別のファイルで定義された関数を呼び出せるのよ。

C言語のこのような仕組みは、標準関数にも用いられているものです。例えばprintf()を呼び出したいときは、プログラムの冒頭で<stdio.h>をインクルードしますね。

#include <stdio.h>

<stdio.h>のなかには、標準入出力を扱う関数の宣言が収められています。printf()もその一つなので、冒頭にインクルードを書いておけば、プログラム中から呼び出せるようになるというわけです。

ここがポイント!
別の場所で定義された関数も、宣言すれば呼び出せる!

修正後のプログラム

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

void PrintNumber(int n);

int main(void) {
  PrintNumber(123);

  return EXIT_SUCCESS;
}

void PrintNumber(int n) {
  printf("number = %d\n", n);
}
実行結果
number = 123

第06問

数字を表示するプログラムを作ろうとしています。サブルーチンを1つ使っただけの単純なプログラムなのですが、コンパイルエラーになってしまい動きません。

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

int main(void) {
  PrintNumber(123);

  return EXIT_SUCCESS;
}

void PrintNumber(int n) {
  printf("number = %d\n", n);
}
期待される実行結果
number = 123
PrintNumber()がサブルーチンになっているのが分かるかしら?
はい。main()から呼び出そうとしてるんですよね。
でも、この書き方だとコンパイルできないのよ。
とくに間違っているようには見えないですけど……。
じゃあ、前の問題にもサブルーチンが出てきていたのは覚えてる?
え?もしかして、第04問のプログラムのことですか?
そうそう。あのときと何が違うのか考えてみるといいかもよ。
2つの関数の並び順に注目してみましょう。

第05問の答え

答え

これだと字下げのスペースが文字列に含まれてしまう。
  puts(
    "むかしむかしあるところに、\
    おじいさんとおばあさんがすんでいました。\
    あるひのこと、おじいさんはやまへしばかりに、\
    おばあさんはかわへせんたくにいきましたとさ。\
    めでたしめでたし。"
  );
こうやって字下げするのが正解!
  puts(
    "むかしむかしあるところに、"
    "おじいさんとおばあさんがすんでいました。"
    "あるひのこと、おじいさんはやまへしばかりに、"
    "おばあさんはかわへせんたくにいきましたとさ。"
    "めでたしめでたし。"
 );

解説

C言語では、連続する文字列リテラルは結合されます。つまり、ダブルクオート(")で囲まれたテキストは、2つ以上に分割して書いても意味が変わらないということです。

これを知っていれば、長い文字列でもきれいに書けるようになるのよ。
まぁ、そうですかねぇ……。
あら、あまり納得してない顔ね。
なんというか、メリットが少なくないですか?
たしかに。長い文字列を分けたいときなんて、そんなにないものね。
そうですよ!そこが引っかかってたんです!
それじゃあ、応用編のテクニックを教えちゃおうかしら。
やったー!

ここで、応用例を一つ紹介しましょう。以下は第04問のプログラムを、文字列リテラルが結合されることを活かして書き直したものです。

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

#define GREETING_MESSAGE(NAME) ("Hello, " NAME "!")

int main(void) {
  puts(GREETING_MESSAGE("Leo"));

  return EXIT_SUCCESS;
}
実行結果
Hello, Leo!
たしか、あいさつを表示するプログラムでしたよね。
そうね。GREETING_MESSAGE()で、文字列が結合されているのが分かるかしら?
ええと、ちょっと考えてみますね……。
GREETING_MESSAGE()の中身はこうだから……
"Hello, " NAME "!"
NAMEのところに名前が入って……
"Hello, " "Leo" "!"
3つの文字列が結合されるのか!
"Hello, Leo!"
正解!いろいろと応用がきくテクニックだということが、分かってもらえたかしら?
はい!おぼえておくと、いろいろ使えそうですね。
ここがポイント!
ダブルクオートで囲まれた文字列は、続けて書くと結合される!

修正後のプログラム

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

int main(void) {
  puts(
    "むかしむかしあるところに、"
    "おじいさんとおばあさんがすんでいました。"
    "あるひのこと、おじいさんはやまへしばかりに、"
    "おばあさんはかわへせんたくにいきましたとさ。"
    "めでたしめでたし。"
 );

  return EXIT_SUCCESS;
}
実行結果
むかしむかしあるところに、おじいさんとおばあさんがすんでいました。あるひのこと、おじいさんはやまへしばかりに、おばあさんはかわへせんたくにいきましたとさ。めでたしめでたし。

メンテナンスが最優先

プログラミングをするときに最低限考えなくてはいけないのは、クライアントの要求を満たしたり、バグがないようにしたりすることです。

このほかにも、ソフトウェアエンジニアはしばしばトレードオフ問題に頭を悩ませます。開発プロジェクトに欠かせない3つの要素を表す「QCD」という言葉を聞いたことがある人も多いでしょう。

QCDとは

  • Q:品質(Quality)
  • C:コスト(Cost)
  • D:納期(Delivery)

これらの3つは、すべて達成してこそプロジェクトの成功といえます。

ところが現実のプロジェクトでは、開発が終盤に近づいてくるとQCDに「調整」が入ることも少なくありません。

納期に間に合わせたいから人を増やすとか、ありがちですね。
でも、QCDのどれかを優先させなければならない状況というのは、すでに異常事態なのよ。

3つの目標をすべて達成するには、こうした「調整」をせずに済むようコントロールする必要があります。それには、プロジェクトや成果物の状態がいつ誰が見ても分かるように、見通しのよい状態に保っておくことが大切です。

見通しのよい状態に保つって、難しくないですか?具体的には、何をすればいいんでしょうか?
日頃から設計やコードをキレイにしたり、ドキュメントを整理したりしておくといいわね。
え?メンテナンスってことですか?
そういうことね。

例えば、以前に自分が関わっていたプロジェクトにバグレポートが届いたと想像してみましょう。現在は新しいプロジェクトで手一杯なので、ほかの誰かが修正作業を担当することになったとします。

このとき、コードがグチャグチャだったら?仕事をするうえで必要なドキュメントが不十分だったら?自分たちがメンテナンスを怠っていたせいで、ほかの人に迷惑がかかってしまうわけです。

もしかするとバグ修正の担当者にとっては、前任者に聞かなければ分からないことだらけかもしれません。その場合は、山ほどの質問を浴びせられることになるでしょう。

新しいプロジェクトで忙しいのに、過去のプロジェクトの面倒までみるのは大変ですね。
そうでしょ。だから、日頃のメンテナンスが大切なのよ。

メンテナンスに時間を割くのは、なにも将来のためだけではありません。プロジェクトや成果物の状態を整理することで、QDCに影響が出る前にプロジェクトの進行を素早くコントロールできるようになります。分かりやすく、見通しのよい状態を維持できれば、今取り組んでいる仕事もスピードアップするでしょう。

ここがポイント!
日頃のメンテナンスを優先的にやっておきましょう。その結果、プロジェクトをコントロールしやすくなります。

とはいっても、誰が見ても理解できるコードやドキュメントを作っておくことは、決して簡単ではないでしょう(実際とても高度な技術かも?)。

一方で、分かりやすく作るというのは、理解しながら作ることと似ています。まずは自分でよく理解したうえで、誰がみても分かるように工夫しましょう。そうすれば、設計力と実装力も自然と向上していきます。

なるほど〜。メンテナンスを優先させることは、ソフトウェアエンジニアとしての能力アップにつながるんですね!
そういうことね!設計力と実装力のバランス感覚も、自然に身に付いていくんじゃないかしら。

第05問

長いテキストを表示するプログラムを作りました。プログラムを見やすくするために、文字列を途中で折り返しています。でも、もう少し見やすくできる書き方があります。

どういう書き方をすれば見やすいプログラムになるでしょうか?
main.c
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  puts("むかしむかしあるところに、\
おじいさんとおばあさんがすんでいました。\
あるひのこと、おじいさんはやまへしばかりに、\
おばあさんはかわへせんたくにいきましたとさ。\
めでたしめでたし。");

  return EXIT_SUCCESS;
}
実行結果
むかしむかしあるところに、おじいさんとおばあさんがすんでいました。あるひのこと、おじいさんはやまへしばかりに、おばあさんはかわへせんたくにいきましたとさ。めでたしめでたし。
今回のプログラムは、間違っているわけじゃないんですよね。
そうね。このままでも問題はないのよ。
分かった!字下げを調節すれば見やすくなりますよね!

どうやらレオ君は、字下げ(インデント)を調節すればプログラムが見やすくなることに気付いたようです。それで、次のように手直しをしてみたのですが……。

字下げを調節してみたよ。
  puts(
    "むかしむかしあるところに、\
    おじいさんとおばあさんがすんでいました。\
    あるひのこと、おじいさんはやまへしばかりに、\
    おばあさんはかわへせんたくにいきましたとさ。\
    めでたしめでたし。"
  );
あれれ?実行結果が変わってしまった……。
むかしむかしあるところに、    おじいさんとおばあさんがすんでいました。    あるひのこと、おじいさんはやまへしばかりに、    おばあさんはかわへせんたくにいきましたとさ。    めでたしめでたし。
おしい!字下げのスペースが、そのまま出力されてしまったのね。
なるほど。ということは……。
きれいに字下げができて、実行結果も変わらない方法を考えてみましょう。

第04問の答え

答え

引数がchar *だと文字列を書き換えられてしまいそう……。
void Greet(char *pName) {
引数をconst char *にすれば安心して呼び出せる!
void Greet(const char *pName) {

解説

だいたいの場合、関数の引数をポインタにする目的は次の2つのどちらかです。

  • 大きなデータを効率的に参照するため
  • 値を書き込むため

この2つは意味が違うので、constキーワードを使って区別します。つまり、値を参照したいだけのときはconstを付けて、「このポインタはデータを受け取るためのものですよ」という意思表示をするのです。constが付いていない場合は、「このポインタが指しているデータを書き換えますよ」という意思表示になります。

こうした作法は、全体を見渡せる程度の小さなプログラムではそれほど問題にはなりません。でも、プログラムが大きくなってくると、だんだん馬鹿にできなくなってきます。関数を呼び出すときに、いつも中身まで見えるとは限らないからです。

標準関数も、使うときには中身が見えないので、やはり作法に則って作られています。例えば、文字列の長さを数えてくれるstrlen()は、こういう風になっています。

size_t strlen(const char *s);

constが付いているので、文字列を受け取るための引数だということが明確ですね。

では、文字列をコピーするときに使うstrncpy()はどうでしょうか。

char *strncpy(char *dest, const char *src, size_t n)

第1引数はコピー先なのでconstなし、第2引数はコピー元なのでconstありとなっています。constの有無が、関数の使い方を示すさりげないヒントになっていることが分かるでしょう。

こういうさりげない作法ができると、プロって感じがするわよね。
デスヨネ〜。(今度やってみよう)
ここがポイント!
データを受け取るためにポインタを引数にするときは、constを付けよう!

修正後のプログラム

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

void Greet(const char *pName) { /* ← const を追加 */
  printf("Hello, %s!\n", pName);
}

int main(void) {
  Greet("Leo");

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

第04問

あいさつを表示するプログラムを作りました。このプログラムは正常に動作しますが、あと少し改善できる部分があります。

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

void Greet(char *pName) {
  printf("Hello, %s!\n", pName);
}

int main(void) {
  Greet("Leo");

  return EXIT_SUCCESS;
}
実行結果
Hello, Leo!
あ、「Leo」って僕の名前じゃないですかぁ。
うん。レオ君にあいさつするプログラムよ。
Greet()っていう関数に名前を渡すとあいさつが表示されるんですね!
そのあたりで、ちょっとだけ手直ししておきたい箇所があるのが分かるかしら?
問題なさそうに見えますけど……。まさか僕の名前がダメとか言いませんよね?
Greet()の引数の型に注目してみましょう。

第03問の答え

答え

malloc()free()を呼び出す回数が違う!
  for (int i=0; i<strlen(text); i++) {
    pPartOfText = malloc(sizeof(text)); /* ← 繰り返し呼び出される */
    if (pPartOfText) {
      strncpy(pPartOfText, text + i, sizeof(text));
      printf("%d: %s\n", i, pPartOfText);
    }
  }

  if (pPartOfText) {
    free(pPartOfText); /* ← 1度しか呼び出されない */
    pPartOfText = 0;
  }
malloc()free()の呼び出し回数を揃えるのが正解!
  for (int i=0; i<strlen(text); i++) {
    pPartOfText = malloc(sizeof(text));
    if (pPartOfText) {
      strncpy(pPartOfText, text + i, sizeof(text));
      printf("%d: %s\n", i, pPartOfText);

      free(pPartOfText);
      pPartOfText = 0;
    }
  }

解説

これは、メモリーリークに関する出題でした。メモリーリークとは、不要になったメモリーを解放せずに放置してしまうことです。

問題のプログラムではmalloc()forループの内側にあり、全部で6回呼び出されています。これに対して、free()は高々1回しか呼ばれません。これでは、5つのメモリー領域が解放されずに残ってしまいます。

それでも、今回のプログラムを実行すると期待通りの内容が表示されました。メモリーリークは、発生したからといってすぐに致命的な問題を引き起こすわけではないのです。でも、こういう小さな見落としの積み重ねが、やがて大きな問題につながります。

だからmalloc()で確保したメモリーの数だけfree()が呼び出されるように気を付けないと危険なのよ。
そんなこと言われても、どうやって気を付けたらいいのか……。
そうね。今回のプログラムの場合は、ポインタを使い回してるところが良くないわね。

問題のプログラムでは、ループの内側でmalloc()の結果を受け取るポインタが、ループの外側で宣言されていました。1つのポインタを何度も使い回しています。

  char *pPartOfText = 0;
  for (int i=0; i<strlen(text); i++) {
    pPartOfText = malloc(sizeof(text));

ポインタの宣言もループの内側に入れてしまえば、よりシンプルで見通しの良いプログラムになります。

  for (int i=0; i<strlen(text); i++) {
    char *pPartOfText = malloc(sizeof(text));
こうすれば、ループの外側にはfree()を書けなくなるでしょう?
え?……そっか、ポインタはループの内側にあるから、コンパイルエラーになっちゃいますね。
だからmalloc()free()も、必然的にループの内側に書くことになるわけ。
なるほど、それなら呼び出し回数も同じになりますね!

なお、malloc()で確保されたメモリー領域は、プログラムが実行を終えるときにすべて解放されます。そのため、今回のような小さなプログラムでは大した問題は起こりません。でも、プログラムが複雑になってくると、メモリーリークは致命的な問題につながります。

例えば、常駐型のプログラムが毎日少しずつfree()を呼び出し忘れていたらどうなるでしょうか。動作を開始してから数週間後にメモリー不足になり、突然クラッシュしてしまうかもしれません。

ここがポイント!
変数の使い回しを避けて、プログラムをシンプルにしよう!

修正後のプログラム

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

int main(void) {
  char text[] = "Hello!";

  for (int i=0; i<strlen(text); i++) {
    char *pPartOfText = malloc(sizeof(text)); /* ← ループの内側で宣言 */
    if (pPartOfText) {
      strncpy(pPartOfText, text + i, sizeof(text));
      printf("%d: %s\n", i, pPartOfText);

      free(pPartOfText); /* ← 確保されたメモリー領域の数だけ呼び出す */
      pPartOfText = 0;
    }
  }

  return EXIT_SUCCESS;
}
実行結果
0: Hello!
1: ello!
2: llo!
3: lo!
4: o!
5: !

第03問

次のプログラムは、文字列の一部をコピーして次々に表示するものです。正常にコンパイルされ、実行すると期待通りの表示になりました。でも、問題点があります。

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

int main(void) {
  char text[] = "Hello!";

  char *pPartOfText = 0;
  for (int i=0; i<strlen(text); i++) {
    pPartOfText = malloc(sizeof(text));
    if (pPartOfText) {
      strncpy(pPartOfText, text + i, sizeof(text));
      printf("%d: %s\n", i, pPartOfText);
    }
  }

  if (pPartOfText) {
    free(pPartOfText);
    pPartOfText = 0;
  }

  return EXIT_SUCCESS;
}
実行結果
0: Hello!
1: ello!
2: llo!
3: lo!
4: o!
5: !
ふむふむ、malloc()でメモリーを確保して、strncpy()で文字列の一部をコピーしてるんですね。
で、その動作をforループで繰り返したあと……
仕上げにfree()でメモリーを解放してますね!
malloc()free()が呼び出される回数を数えてみましょう。

第02問の答え

答え

free()printf()の順番が間違い!
  if (pCopyOfText) {
    strncpy(pCopyOfText, text, sizeof(text));
    free(pCopyOfText);
  }

  printf("%s\n", pCopyOfText);
printf()free()の前に実行するのが正解!
  if (pCopyOfText) {
    strncpy(pCopyOfText, text, sizeof(text));
    printf("%s\n", pCopyOfText);

    free(pCopyOfText);
  }

解説

これは、ダングリングポインタに関する出題でした。ダングリング(dangling)は「宙ぶらりんの」という意味で、ポインタが有効なアドレスを指していない状態を表しています。

だいたいの場合、ダングリングポインタはメモリーを解放するタイミングで発生します。今回のプログラムでは、malloc()で確保したメモリーをfree()で解放していますね。このとき、ポインタが解放済みの領域を指したままになっています。

このポインタを、そのままprintf()のところで使おうとしているのが問題です。もう無効になったアドレスを指しているため、何が起こるか分かりません。たまたま期待どおりの結果が表示されるかもしれないし、おかしな値が表示されるかもしれないのです。最悪の場合は、プログラム自体がクラッシュします。

解放済みのメモリーは、うっかり使わないように気を付けないと危険ってことね。
それって、気を付けようがないんじゃ……。
大丈夫。ポインタをきちんとクリアするクセを付ければいいのよ。

free()でメモリーを解放したあと、そのメモリーを指すポインタを使い続けるのは危険です。それなら、ポインタの値をクリアしてしまいましょう。

    free(pCopyOfText);
    pCopyOfText = 0;

こうしておけば、ポインタが「宙ぶらりん」になるのは一瞬だけです。クリア済みのポインタは、うっかり使ったとしても確実にクラッシュさせることができます。

ええっ!クラッシュしないほうがいいんじゃないんですか?
それはそうよ。でも、バグがあるのにうまく動いてしまったら、なかなか問題に気付けないでしょう?
あ、もしかして……。クラッシュさせたほうがバグに気付きやすくなるってことですか?
そう!そうしたら早めに修正できるから、最終的にはクラッシュしにくいプログラムになるわね。
そういうことか〜。
ここがポイント!
メモリーを解放したら、ポインタも一緒にクリアするクセを付けよう!

修正後のプログラム

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

int main(void) {
  char text[] = "Hello!";

  char *pCopyOfText = malloc(sizeof(text));
  if (pCopyOfText) {
    strncpy(pCopyOfText, text, sizeof(text));
    printf("%s\n", pCopyOfText); /* ← この行を修正 */

    free(pCopyOfText);
    pCopyOfText = 0; /* ← ポインタをクリア */
  }

  return EXIT_SUCCESS;
}
実行結果
Hello!