[←1つ前] [→1つ後] [↑質問一覧] [↑記事一覧] [ホームページ]
プログラムを実際に書いてみたが、コンパイルに成功しないとか、コンパイルで きても動かないとか、動いても期待したものと違うとか、止まらないとか、とに かくいろいろトラブルが発生するものです。原因はあまりにも多く、かつケース バイケースなので、とても網羅しきれるものではありませんが、今回は典型的な ものをまとめてみました。
また、これとは別に、特に最近ネット等で目についた質問を紹介します。
よくある間違いとしては、次のようなものがあります。
typoといいます。単なる打ち間違いです。これにより、他のあらゆる失敗を誘発 することになります。変数名が間違ったものになったり、キーワードでない単語 になってしまったり、括弧の数が合わなくなったりする結果となります。変数名 は、簡単に見分けることができるように工夫して付けるべきです。小文字のlと数 字の1や、大文字のOと数字の0は見間違うことがあります。明白に区別できない場 合は避けた方がいいでしょう。
インデントが正しくできていればあまり間違わないはずですが、
if
、
for
、
while
に続く文をブロックにしなかったりすると、うっかりミスすることがあります。
例えば、
while
ループの処理を書く時に、まず、
while (..) {
}
と書いてから、その中の処理を書くという習慣を身に付ければ、この種の括
弧の不整合を減らすことができます。
このケースなら、殆どの場合はエラーメッセージを見れば分かるはずです。 特に、ラベルの後に必要な空文を忘れることは、多少C言語に慣れてもあるも のです。
これらのミスを取り除けば、殆どのエラーメッセージは表示されなくなるでし ょう。
打ち間違った結果が、たまたま変な症状の原因になっているかもしれません。
ix
のつもりで
iy
とタイプしてしまって、その名前の変数が偶然ある、ということもよくあります。
default
を
defualt
と間違ったりすると、ラベルになってしまってエラーにはならないために、予期
した処理が行われない結果になります。この場合も、警告を出すモードにしてお
けば、ラベルが定義されているが一度も使われていないというメッセージが出る
はずです。
メモリに関するバグは、一見正常に動いてしまって、しかも着実に内部を破壊 するといことがあり、複雑な症状を発生します。多くのミスはポインタの誤った 使い方が原因です。これに関しては後述の項目を参照してください。
この種のあらゆるケアレスミスを除去してもふるまいが変である場合もありま す。これは、そもそもプログラムのアルゴリズムが要求仕様に合っていないので す。ロジックミスという言い方をすることがあります。単純な場合なら、部分的 な修正で間に合うこともありますが、場合によっては、プログラムの設計から見 直す必要があります。
main()
を実行する前に異常が発生してしまい、わけがわからない事があります。まず考
えられるのは、正常なコードがリンクされていない場合です。クロス開発環境の
場合、ターゲットと異なる環境のスタートアップルーチンを間違ってリンクして
いるかもしれません。
リンクが問題なければ、非常に大きな配列をローカル変数として用意しようと
していないか確かめてみましょう。自動変数はスタック上に領域を獲得しますが、
スタックのサイズが固定のシステムは結構あります。いきなりスタッフがあふれ
てしまったら、その後何が起きるかは神のみぞ知る、です。大きな配列は
malloc
で領域を確保するか、あるいはstatic変数にすることによってスタックを消費し
ないようにした方が無難です。
(参考: c.l.c FAQ 16.3)
main
の宣言が間違っていないかどうか確かめましょう。
main
は、
int
型の関数であり、プロトタイプ宣言しない約束になっています。
C言語にはクリーンアップ関数を指定する機能が用意されています。
atexit()
関数で変なことをしていないかどうか、確かめてください。
破壊してはならないメモリを破壊していたということも、よくあります。初期
化されていないポインタを使ってメモリ内容を変更したり、ファイルを処理しよ
うとしたり、
free
しようとしたり、
fclose
しようとした場合にありそうなことです。
malloc
の戻り値は、メモリが確保できない時に
NULL
となります。これをチェックしないでそのまま使うと、予期せぬトラブルの原因
となります。
(参考: c.l.c FAQ 16.4)
malloc
や
free
を呼び出すと暴走する。
malloc
や
free
は、ライブラリ関数の中で空きメモリの管理を行っています。何かの拍子にこの
管理情報が壊れてしまうと、その後に
malloc
や
free
を呼び出した時に暴走する場合があります。暴走しなくても、かなりまずい状況
になっていると考えるべきでしょう。
一旦
free
した領域をまだ確保したままだと勘違いして使ってしまうことがあります。その
ような領域に対して書き込みを行うと、本来使われていないはずのメモリ内容を
変更してしまうことになります。たまたま、それが
malloc
の管理に使われている情報とぶつかると、
malloc
や
free
を呼んだ途端に暴走する原因になる可能性があります。また、その領域がどこか
を指すポインタとして使われていたら、予期しない個所を書き換えてしまい、ど
んどん被害が拡大するかもしれません。こうなると暴走した時点では何が根本的
な原因なのか分からなくなっているでしょう。
また、もう一つよくある失敗は、
malloc
で確保したサイズを超えてメモリに書き込んでしまうというものです。特に、文
字列の最後に
'¥0'
が付くことを忘れて
strlen(s)
の大きさでメモリを確保してしまうというミスがありがちです。
この種のバグは、それを呼び出す時点ではなく、かなり離れた別の箇所に原因 が潜んでいることがありますから、一箇所に注目せずに、全体を見渡して変な所 を探す必要があります。
(参考: c.l.c FAQ 7.19)
しかし、その関数自体に問題がないのなら、何か変なことが別の箇所で起きて いるのです。ありがちな原因としては、初期化されていないポインタを使ったア クセスで、その関数が使おうとした領域を破壊してしまったような場合がありま す。または、MS-DOSのようにメモリ保護が行われていないシステムなら、プログ ラム実行中に関数の実行コードを破壊してしまって、そこを呼び出した途端に破 綻することもあります。
あるいは、たまたまそこでスタックがあふれてしまった、という場合もありま す。スタックサイズを大きくするオプションがあれば、指定して試してみてくだ さい。大きな配列を使っている場合には要注意です。
printf
に指示したフォーマットと与える引数の型が一致しなかったりすると、このよう
な状況になることがあります。
このメッセージはメモリ管理がきちんと行われている処理系上で表示されます。 MS-DOSで同じプログラムを動作させても、一見正常に見えるかもしれません。し かし、実はとんでもない箇所のメモリを変更しているということがあります。こ うなると、思わぬ所で全く無関係のプログラムが暴走したりすることもあり、始 末に負えません。ポインタの管理は特に気を付ける必要があると考えてください。
類似の状況で“Segmantation fault”や“Bus error”というメッセージが表 示されることもあります。
(参考: c.l.c FAQ 16.8)
コンパイラは、ソースプログラムをコンパイルして機械語に翻訳した後、ソー スプログラムに含まれていない関数をライブラリファイルから探そうとします。 処理系によっては、この作業はコンパイラとは別のリンカというプログラムによ って行われます。この時に、いくつかのライブラリファイルは特に指定しなくて も検索されますが、数学関数は標準ライブラリとは別のライブラリとしてまとめ れらている処理系があります。この場合はデフォルトのままではリンクできませ ん。
数学関数は、歴史的に、
mathlib
という名前のファイルになっている場合が多く、この場合はリンクするライブラ
リを指定するオプションとして-lm (処理系によっては-lmathなど、処理系付属の
ドキュメントで確認すること)のように指定すればよいことが多いようです。
(参考: c.l.c FAQ 14.3)
をインクルードするのを忘れていませんか。数学関数は
int
でない型のものが多数あるので、
をインクルードしてから使わないと、リンク時に戻り値が
int
だと勝手に解釈されてしまうため、値を正しく受け取れないことがあります。
昔はC言語に関する本はK&Rなど数冊しかなく、パソコンでC言語を使うには十万 円以上払ってコンパイラを買わなければならないし、パソコンはもっと高いとい う時代でしたから、C言語を自宅で勉強するというのは大変でした。私の場合はK &Rを読んで勉強するという所から入りました。実際にコンパイルできるようにな ったのは、かなり後です。
諸説ありますが、私見としては、プログラミング言語を身につける近道は、プ ログラムを実際に書くことだと思います。そして、素質があるかないかという点 が重要です。プログラミングに向いている人は独習してでも身に付けることがで きるし、向いていない人はセミナーを受講しても身に付かないでしょう。
また、最近の環境は非常によくなりました。C言語の入門書もたくさん出版され ています。コンパイラの種類も増えました。パソコンは高性能になって、自宅で フリーのUNIXを使うことができるようになり、費用もあまりかからなくなりまし た。このような環境を整えてからC言語を実際に使えば、独習に成功する確率は高 いと思います。さらに、今はネットに誰でも気軽に参加できる時代です。NIFTY- ServeならFCというフォーラムで質問を書けば、誰かが答えてくれます。厳密にい えばこれは独習ではありませんが、分からないことがあれば質問できる環境もあ るということかが重要です。
FILE *fp;
のように定義された
fp
のことです。ただし、これは日本語訳では「ファイル・ポインタ」となっていま
す。
ところが、UNIXの世界では、ファイルポインタという言葉を別の意味で使うこ
とがあります。つまり、現在ファイルのどこを参照しているか(例えば、ファイル
の先頭から何バイト目か)という位置情報のことをファイルポインタと呼ぶ場合が
あります。具体的には、
ftell()
が戻してくる値のことを意味します。
これらの表現が紛らわしいので、JIS Cではファイルポインタという表現をわざ と使わないようになっています。
(参考 NIFTY-Serve C言語フォーラム(GO FC) 7番会議室, "Q&A.beginner // 初心者の素朴な質問")
if ((*p++ == 'A') && (*p == 'Z')) 文
」という書き方は正しいのか。
「ビット単位の2項演算子と異なり、
&&
演算子は左から右への評価を保証する。すなわち、第1オペランドの評価の直後を
副作用完了点とする。」
(JIS X 3010 6.3.13)
従って、式
(*p++ == 'A') && (*p == 'Z')は、
(*p++ = 'A')
の評価直後に副作用完了点があり、文法に合致した記述です。
if
の条件判断箇所に書いても何の問題もありません。
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
が含まれているプログラムは、全体の構造そのものを見直した方が建設的です。
あるいは、既に問題なく動いているのなら、そのまま使う方が安全です。