初級C言語Q&A(10)

初出: C MAGAZINE 1996年3月号
Updated: 1996-03-12

[1つ前] [1つ後] [質問一覧] [記事一覧] [ホームページ]


動かないプログラム

プログラムを実際に書いてみたが、コンパイルに成功しないとか、コンパイルで きても動かないとか、動いても期待したものと違うとか、止まらないとか、とに かくいろいろトラブルが発生するものです。原因はあまりにも多く、かつケース バイケースなので、とても網羅しきれるものではありませんが、今回は典型的な ものをまとめてみました。

 また、これとは別に、特に最近ネット等で目についた質問を紹介します。


よくあるトラブル


Q 【プログラムがコンパイルできない】

 プログラムをコンパイルするとエラーメッセージが山のように表示された。

 要するにプログラムに文法上おかしな箇所があるわけです。エラーメッセージ は、一つの間違いから連鎖的に発生することがあります。見かけの数より修正す べき箇所はずっと少ないはずですから、エラーメッセージの多さにめげないでも 大丈夫です。

 よくある間違いとしては、次のようなものがあります。

これらのミスを取り除けば、殆どのエラーメッセージは表示されなくなるでし ょう。


Q 【プログラムのふるまいが変】

 プログラムをコンパイルすることは成功した。しかし、思った通りに動いてく れない。

 まず、コンパイル時に警告をできるだけ多く出すようなオプションを指定して みましょう。関数の型が定義と利用時に一致していないとか、使っているつもり の変数を使っていないとか、初期化しないままで不定の値をいきなり使っている、 等の失敗はこれで発見することができます。

 打ち間違った結果が、たまたま変な症状の原因になっているかもしれません。 ix のつもりで iy とタイプしてしまって、その名前の変数が偶然ある、ということもよくあります。

  defaultdefualt と間違ったりすると、ラベルになってしまってエラーにはならないために、予期 した処理が行われない結果になります。この場合も、警告を出すモードにしてお けば、ラベルが定義されているが一度も使われていないというメッセージが出る はずです。

 メモリに関するバグは、一見正常に動いてしまって、しかも着実に内部を破壊 するといことがあり、複雑な症状を発生します。多くのミスはポインタの誤った 使い方が原因です。これに関しては後述の項目を参照してください。

 この種のあらゆるケアレスミスを除去してもふるまいが変である場合もありま す。これは、そもそもプログラムのアルゴリズムが要求仕様に合っていないので す。ロジックミスという言い方をすることがあります。単純な場合なら、部分的 な修正で間に合うこともありますが、場合によっては、プログラムの設計から見 直す必要があります。


Q 【プログラムが走らない】

 プログラムを実行しようとすると、何もしないうちに暴走してしまう。原因は 何だろうか。

 デバッガを使ってプログラムを追跡しても、 main() を実行する前に異常が発生してしまい、わけがわからない事があります。まず考 えられるのは、正常なコードがリンクされていない場合です。クロス開発環境の 場合、ターゲットと異なる環境のスタートアップルーチンを間違ってリンクして いるかもしれません。

 リンクが問題なければ、非常に大きな配列をローカル変数として用意しようと していないか確かめてみましょう。自動変数はスタック上に領域を獲得しますが、 スタックのサイズが固定のシステムは結構あります。いきなりスタッフがあふれ てしまったら、その後何が起きるかは神のみぞ知る、です。大きな配列は malloc で領域を確保するか、あるいはstatic変数にすることによってスタックを消費し ないようにした方が無難です。

(参考: c.l.c FAQ 16.3)


Q 【プログラムが走って戻ってこない】

 プログラムを実行することができる。しかし、終了する時に暴走する。

 まず、 main の宣言が間違っていないかどうか確かめましょう。 main は、 int 型の関数であり、プロトタイプ宣言しない約束になっています。

 C言語にはクリーンアップ関数を指定する機能が用意されています。 atexit() 関数で変なことをしていないかどうか、確かめてください。

 破壊してはならないメモリを破壊していたということも、よくあります。初期 化されていないポインタを使ってメモリ内容を変更したり、ファイルを処理しよ うとしたり、 free しようとしたり、 fclose しようとした場合にありそうなことです。

 malloc の戻り値は、メモリが確保できない時に NULL となります。これをチェックしないでそのまま使うと、予期せぬトラブルの原因 となります。

(参考: c.l.c FAQ 16.4)


Q 【malloc】

mallocfree を呼び出すと暴走する。

  mallocfree は、ライブラリ関数の中で空きメモリの管理を行っています。何かの拍子にこの 管理情報が壊れてしまうと、その後に mallocfree を呼び出した時に暴走する場合があります。暴走しなくても、かなりまずい状況 になっていると考えるべきでしょう。

 一旦 free した領域をまだ確保したままだと勘違いして使ってしまうことがあります。その ような領域に対して書き込みを行うと、本来使われていないはずのメモリ内容を 変更してしまうことになります。たまたま、それが malloc の管理に使われている情報とぶつかると、 mallocfree を呼んだ途端に暴走する原因になる可能性があります。また、その領域がどこか を指すポインタとして使われていたら、予期しない個所を書き換えてしまい、ど んどん被害が拡大するかもしれません。こうなると暴走した時点では何が根本的 な原因なのか分からなくなっているでしょう。

 また、もう一つよくある失敗は、 malloc で確保したサイズを超えてメモリに書き込んでしまうというものです。特に、文 字列の最後に '¥0' が付くことを忘れて strlen(s) の大きさでメモリを確保してしまうというミスがありがちです。

 この種のバグは、それを呼び出す時点ではなく、かなり離れた別の箇所に原因 が潜んでいることがありますから、一箇所に注目せずに、全体を見渡して変な所 を探す必要があります。

(参考: c.l.c FAQ 7.19)


Q 【コードが壊れている】

 ある関数を呼び出した途端に暴走する。原因が分からない。

 もちろん、これだけでは分かりません。

 しかし、その関数自体に問題がないのなら、何か変なことが別の箇所で起きて いるのです。ありがちな原因としては、初期化されていないポインタを使ったア クセスで、その関数が使おうとした領域を破壊してしまったような場合がありま す。または、MS-DOSのようにメモリ保護が行われていないシステムなら、プログ ラム実行中に関数の実行コードを破壊してしまって、そこを呼び出した途端に破 綻することもあります。

 あるいは、たまたまそこでスタックがあふれてしまった、という場合もありま す。スタックサイズを大きくするオプションがあれば、指定して試してみてくだ さい。大きな配列を使っている場合には要注意です。


Q 【Segmentation violation】

 "Segmentation violation"というメッセージが出てプログラムが異 常終了してしまう。原因は何だろうか。

 簡単にいえば、使ってはならないメモリを使おうとした場合にこのようなメッ セージが出ます。具体的には、ポインタの使い方を誤ったとか、 printf に指示したフォーマットと与える引数の型が一致しなかったりすると、このよう な状況になることがあります。

 このメッセージはメモリ管理がきちんと行われている処理系上で表示されます。 MS-DOSで同じプログラムを動作させても、一見正常に見えるかもしれません。し かし、実はとんでもない箇所のメモリを変更しているということがあります。こ うなると、思わぬ所で全く無関係のプログラムが暴走したりすることもあり、始 末に負えません。ポインタの管理は特に気を付ける必要があると考えてください。

類似の状況で“Segmantation fault”や“Bus error”というメッセージが表 示されることもあります。

(参考: c.l.c FAQ 16.8)


Q 【数学関数の未定義エラー】

sqrt関数を使ったプログラムを書いたが、リンク時にシンボルが定義されてい ないというエラーになってしまう。

 リンクするライブラリのオプションを指定する必要があると思われます。

 コンパイラは、ソースプログラムをコンパイルして機械語に翻訳した後、ソー スプログラムに含まれていない関数をライブラリファイルから探そうとします。 処理系によっては、この作業はコンパイラとは別のリンカというプログラムによ って行われます。この時に、いくつかのライブラリファイルは特に指定しなくて も検索されますが、数学関数は標準ライブラリとは別のライブラリとしてまとめ れらている処理系があります。この場合はデフォルトのままではリンクできませ ん。

 数学関数は、歴史的に、 mathlib という名前のファイルになっている場合が多く、この場合はリンクするライブラ リを指定するオプションとして-lm (処理系によっては-lmathなど、処理系付属の ドキュメントで確認すること)のように指定すればよいことが多いようです。

(参考: c.l.c FAQ 14.3)


Q 【リンクの順序】

リンク時に-lmathを指定して、数学関数もライブラリから検索するようにした はずなのに、シンボルが未定義であるというエラーメッセージが出てしまう。

 リンクがどのような順序で行われるかは処理系に依存しますが、たいていの処 理系は最初に指定したライブラリから順番に検索します。相互に他の関数を呼び 出すようなライブラリが複数あった場合には、これらを指定する順序によってリ ンクに失敗することがあります。リンク時に指定しているライブラリの順番を変 えてみてください。

Q 【関数値がおかしい】

 数学関数を使ったプログラムをコンパイルし、リンクすることもできた。実行 もできたが、何か値がおかしいような気がする。

 プログラムに をインクルードするのを忘れていませんか。数学関数は int でない型のものが多数あるので、 をインクルードしてから使わないと、リンク時に戻り値が int だと勝手に解釈されてしまうため、値を正しく受け取れないことがあります。

Q 【処理系の欠陥】

 どう考えてもこのコードが間違っているわけがない。しかしプログラムは思っ た通りに動かない。

 あまりあって欲しくないのですが、OSそのもののバグが原因であることもあり ます。または、コンパイラのバグも皆無というわけではありません。もっとも、 このような根本的な欠陥があると、大勢がそれに気付くので、すぐに修正される ことが多いはずです。OSやコンパイラに付属のドキュメントファイルに制限事項 が書かれているかもしれません。

最近のネットでの話題


Q 【独学】

C言語を独学で身に付けることはできるか。

 その人次第です。なお、独学で身に付けた人は大勢います。

 昔はC言語に関する本はK&Rなど数冊しかなく、パソコンでC言語を使うには十万 円以上払ってコンパイラを買わなければならないし、パソコンはもっと高いとい う時代でしたから、C言語を自宅で勉強するというのは大変でした。私の場合はK &Rを読んで勉強するという所から入りました。実際にコンパイルできるようにな ったのは、かなり後です。

 諸説ありますが、私見としては、プログラミング言語を身につける近道は、プ ログラムを実際に書くことだと思います。そして、素質があるかないかという点 が重要です。プログラミングに向いている人は独習してでも身に付けることがで きるし、向いていない人はセミナーを受講しても身に付かないでしょう。

 また、最近の環境は非常によくなりました。C言語の入門書もたくさん出版され ています。コンパイラの種類も増えました。パソコンは高性能になって、自宅で フリーのUNIXを使うことができるようになり、費用もあまりかからなくなりまし た。このような環境を整えてからC言語を実際に使えば、独習に成功する確率は高 いと思います。さらに、今はネットに誰でも気軽に参加できる時代です。NIFTY- ServeならFCというフォーラムで質問を書けば、誰かが答えてくれます。厳密にい えばこれは独習ではありませんが、分からないことがあれば質問できる環境もあ るということかが重要です。


Q 【ファイルポインタ】

 ファイルポインタとは何か。

K&Rによれば、FILE構造体のオブジェクトを指すポインタのことをfile pointerと言います。すなわち、 FILE *fp; のように定義された fp のことです。ただし、これは日本語訳では「ファイル・ポインタ」となっていま す。

 ところが、UNIXの世界では、ファイルポインタという言葉を別の意味で使うこ とがあります。つまり、現在ファイルのどこを参照しているか(例えば、ファイル の先頭から何バイト目か)という位置情報のことをファイルポインタと呼ぶ場合が あります。具体的には、 ftell() が戻してくる値のことを意味します。

 これらの表現が紛らわしいので、JIS Cではファイルポインタという表現をわざ と使わないようになっています。

(参考 NIFTY-Serve C言語フォーラム(GO FC) 7番会議室, "Q&A.beginner // 初心者の素朴な質問")


Q 【副作用完了点 】

if ((*p++ == 'A') && (*p == 'Z')) 文 」という書き方は正しいのか。

JIS Cでは、論理AND演算子に対して、次のように定めています。

「ビット単位の2項演算子と異なり、 && 演算子は左から右への評価を保証する。すなわち、第1オペランドの評価の直後を 副作用完了点とする。」
(JIS X 3010 6.3.13)

 従って、式

    (*p++ == 'A') && (*p == 'Z')
 は、 (*p++ = 'A') の評価直後に副作用完了点があり、文法に合致した記述です。 if の条件判断箇所に書いても何の問題もありません。

Q 【gotoの除去】

 既にあるプログラムから goto を取り除きたい。どうすればよいか。

 基本は、等価になる他の表現に置き換えるということになります。
    if (x)
        goto label;
    /* 処理 */
label:
 この場合は、次のように書き換えることができます。
    if (!x) {
        /* 処理 */
    }
 次の場合はどうでしょうか。
    if (x)
        goto label1;
    /* 処理1 */

    if (y)
        goto label2;
    /* 処理2 */

label1:
    /* 処理3 */
label2:
 同様にして goto label1 だけを取り除くと、このようになります。
    if (!x) {
        /* 処理1 */
        if (y)
            goto label2;
        /* 処理2 */
    }

    /* 処理3 */
label2:
 今度は goto label2 を取り除くのが簡単ではありません。処理3は y が真の時には実行されないことに注目して、条件判断を2回行うと、一応 goto が除去できたかのように見えます。
    if (!x) {
        /* 処理1 */
        if (!y) {
            /* 処理2 */
        }
    }

    if (!y) {
        /* 処理3 */
    }
 しかし、これは必ずしも正しくありません。 y の値は、処理1や処理2を実行した時点で変化するかもしれないからです。処理1、 処理2が y に影響を与えないのであれば、この変数を導入する必要はありません。そこで、 処理3の実行判断時に do_3 という変数を導入します。さらに、この変数は全体を実行する前に1に初期化して おきます。
    do_3 = 1;
    if (!x) {
        /* 処理1 */
        do_3 = !y;
        if (do_3) {
            /* 処理2 */
        }
    }

    if (do_3) {
        /* 処理3 */
    }
 これで goto を除去することができました。

 さらに、ループ構造を持つような goto になってくると、話は複雑怪奇になりま すが、やれば必ずできることは分かっています。しかし、そこまでして goto を除去しても何のメリットもありません。 goto はC言語で許されている正しい命令です。わざわざ取り除く必要が本当にあるの かどうか考え直してみましょう。

 簡単に取り除けないほど複雑な goto が含まれているプログラムは、全体の構造そのものを見直した方が建設的です。 あるいは、既に問題なく動いているのなら、そのまま使う方が安全です。


※ c.l.c FAQ : comp.lang.c FAQ list
URL: http://www.eskimo.com/‾scs/C-faq.top.html
(C) 1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。