スポンサーリンク

このドキュメントの内容は、以下の通りです。

はじめに

ソフトウェア開発では、いろいろな間違えが起こります。ソフトウェア開発は、いくつものフェーズに分けることができますが、たとえば、企画段階、設計段階、コーディング、テストといった風にフェーズをわけられます。

コーディングの段階では、プログラミング言語ごとの間違いというのが起こります。言語によって、起こる間違いというのは、多少、異なっているのだと思います。

年々、新しいプログラミング言語が登場し、人気の言語が徐々に変化していっているように思います。Go, Rust, Node.js (JavaScript), Java といった言語に人気が集まっているのではないでしょうか。そうした変化の中でも、 Unix の開発のために、AT&Tベル研究所で開発されたC言語は、利用されています。

TIOBE Software が発表した人気のプログラミング言語のランキングを発表しています。2020年5月の TIOBEプログラミングコミュニティインデックスでは、C言語が一位でした。

あくまでも個人的な推測であり、データなどの根拠があるわけではありませんが、C言語が利用され続ける背景には、組み込み系で使われているのではないかと思っています。また、速度を優先しているといった背景もあるかもしれません。

古来より、C言語は、間違いのおこりやすい言語として、知られていました。セキュリティの問題となる脆弱性もC言語のメモリ管理の問題が多いと言われています。そういった背景からは、C言語は間違わないように書くのが難しい言語といえます。

長く使われてきたC言語やC++言語ですが、時代は変わりつつあるようです。 ウェブブラウザのFirefox (ファイヤーフォックス) を開発する Mozilla (モジラ) では、Rust(ラスト)というプログラミング言語の開発を支援しています。RustはC言語やC++言語の代替として開発されました。Rustは、安全性、速度、並行性の特徴があります。RustはFirefoxの開発に利用されています。マイクロソフトもRustをC言語やC++言語の代替として、試用しています。

時代は変わりつつあるかもしれませんが、まだまだ、過去の遺産として大量のC言語のソースコードが世界にはあり、それらが Rust とか、それとも別の言語に書き換えたり、新たな代替ソフトウェアが開発されるのは、少し未来のことでしょう。それでは、Cプログラミング言語で、よくある間違えトップ10について紹介します。

もともとは、海外の方が書かれたブログをもとに書いた記事でした。日本語訳が大変よくなかったことと、おそらくサンプルのコードだけでは、内容が難しかったかもしれないと思って、この記事を書いた13年後にあたる2020年にかなり説明を追記しました。13年前のままのわかりにくい部分が残っているかもしれませんが、多少、意味が伝わりやすくなったことを期待します。

1. ターミネートされていないコメント


C言語のコメントは、二通りの記述ができます。スラッシュを2つ並べた、1行コメントとスラッシュとアスタリスクで囲んだ、複数行のコメントです。
複数行のコメントは、コメントの終了を忘れた場合、次のコメントの終了までをコメントアウトしてしまいます。

例えば、以下の例では、1行目のコメントが、コメントの終端を表す「*/」(アスタリスクとスラッシュ)を忘れています。そのため、次の行のコメントの終端までがコメントとなります。

a=b; /* これはバグ
c=d; /* c=d 決して実行されない */

つまり、どういうことかというと、上記の2行のソースコードにおいて、 c=d; のコードはコメントアウトされたものとして扱われます。 a=b; は実行されますが、 c=d; は、決して実行されることはありません。コンパイルしたときに、コメントアウトとして扱われるため、実行ファイルの命令の中には、「dをcに代入する」という命令は存在しないのです。

つまり、以下のようにコメントしたのと同じことを意味しています。
a=b; /* これはバグ
c=d;    c=d 決して実行されない */

このように、C言語のコメントは、間違いが起こりやすいものだということです。 /* */ の複数行のコメントアウトは非常に便利でもありますが、もろ刃の剣ということになります。


ここでは、本当は、以下のコメントを書きたかったということです。
a=b; /* これはバグを修正してある */
c=d; /* c=d は、正しく実行される */

上記のコメントであれば、 a=b と c=d の両方が実行されます。これで、プログラムは、期待した動作をします。

ここで重要なのは、たった2文字あるかないかで、プログラムの動作は、変わってしまうということです。

このようなバグを作らない方法としては、 /* */ なコメントをやめることでしょう。 つまり、一行コメントを利用することです。

a=b; // このコメントは終端がいらない
c=d; // このコメントは終端がいらない

一行コメントであれば、コメントの閉め忘れから解放されます。

2. 思いがけない代入/思いがけないブーリアン式


これは、容易に起こりやすい間違いです。この間違いは、意図的なものか、そうでないのか、判断が難しいケースもあると思います。そうであるがゆえに、非常に厄介なものと言えます。

if(a=b) c;      /* aは常にb, しかし c は b!=0なら実行される */

このプログラムは見解に応じて、言語のバグは、代入演算子は等価演算子によって混乱 させることが非常に容易いです。または、たぶん、バグは、ブーリアン式の構成について、あまり気にしてないからではないでしょうか。
(a=b) は、ブーリアン式ではありません!(しかし、C言語は気にしていません)。

ここでの問題は、aとbが同じであることを確認したかったのかのに、 == を = と打ち間違えたのか、 b を a に代入して、b (というか a) の真偽を確認したかったのかが、わかりません。

a=b においては、bが0の場合は、aが0になり、aが0のため、cは実行されません。bが0以外、たとえば、1であれば、aが1になり、aは1のため、cが実行されます。
if(a=b) c ;

下記のコードであれば、aとbが同値であれば、cが実行される、と単純明快です。
if(a==b) c ;

しっかりとブーリアンの厳しさの不足の関わりについて、よく考えてみてください。

それでは、別の例を示します。下のコードを見て、どのような結果になるか想像できますでしょうか?私は、このようなコードを書かないため、まったく想像もできませんでした。でも、この式を書いた人の意図するところは、想像はできます。
雰囲気としては、整数という仮定で話をしますが、変数 a が 0 と5の間、つまり1,2,3,4,であれば、c を実行するというようなことだと想像はできます。しかし、C言語の仕様として、そのような結果になるでしょうか?

if( 0 < a < 5) c;      /* この "ブーリアン" は常に真だ! */

実は、上記の式は、常に真となります。なぜなら if(0<a) によって (0<a)は0か1のどちらかとなります。よって、その結果と5を比較し、それは常に真になります。C言語は本当にブーリアン式を持ちません。

上記の誤ったコードを書き直しましたので、下記に示します。
if( (0 < a) && (a < 5) c;      /* 修正版 */


#include <stdio.h>

int main(int argc, char *argv[]){
	if( 0 < argc < 5) {
		puts("OK");
	}
	return 0;
}

GNU gccコンパイラのgcc9で上記のコードをコンパイルしました。-Wall オプションを使用し、警告を出るようにしています。
if.c: In function 'main':
if.c:11:15: warning: comparison of constant '5' with boolean expression is always true [-Wbool-compare]
   11 |  if( 0 < argc < 5) {
      |               ^
if.c:11:8: warning: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses]
   11 |  if( 0 < argc < 5) {
      |      ~~^~~~~~

GCC様から非常にありがたい以下の警告を頂きました。
comparison of constant '5' with boolean expression is always true

ありがたい助言を翻訳すると、以下の意味になります。
ブーリアン式の定数5の比較は、常に真(true)です。

常に真だと何が悪いの?ということですが、常に真なら、そもそも比較する意味がないということですよね。

clang 8.0.1 では以下のエラーがでます。

if.c:11:15: warning: result of comparison of constant 5 with boolean expression is always true [-Wtautological-constant-out-of-range-compare]
        if( 0 < argc < 5) {
            ~~~~~~~~ ^ ~
1 warning generated.

clang 様からもありがたいお言葉(警告)を頂きました。 clang は -Wall を指定することもなく、警告を出してくださるので、非情によいコンパイラだと思います。GCCとまったく同じ警告が出ています。


また、以下のコードを考えてみてください。

if( a =! b) c;      /* これをコンパイルする (a = !b), 割り当てる,
		       (a != b) または (a == !b) */

上記のコードは、意図した通りなのか、タイプミスなのか、想像ができませんが、たとえば、3つのことが考えられるように思います。
a = !b と書きたかったのか、a==!bなのか、 a!=bとしたかったのか、です。
a=!bは、bの否定をaに代入します。

上記のプログラムを実際に、コンパイルしてみます。
#include <stdio.h>
int main(int argc, char *argv[])
{
	int a;
	if ( a =! argc) {
		printf("%d\n", a);
	} else {
		printf("%d\n", a);
	}
	return 0;
}

GCC 9 のコンパイラから、以下のアドバイスを頂いております。
top10-sample1.c: In function 'main':
top10-sample1.c:11:7: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
   11 |  if ( a =! argc) {
      |       ^

parentheses とは、括弧のことです。グーグル翻訳によると「真理値として使用される代入の周りの括弧を提案する」となり、ちょっと意味がわかりませんね。これは、代入を if の式のところでやるのをやめたら、ということです。


clang 8 では、以下のエラーになります。
./top10-sample1.c:11:9: warning: using the result of an assignment as a condition without parentheses [-Wparentheses]
        if ( a =! argc) {
             ~~^~~~~~~
./top10-sample1.c:11:9: note: place parentheses around the assignment to silence this warning
        if ( a =! argc) {
               ^
             (        )
./top10-sample1.c:11:9: note: use '==' to turn this assignment into an equality comparison
        if ( a =! argc) {
               ^
               ==
1 warning generated.

warning: using the result of an assignment as a condition without parentheses

グーグル翻訳では、「警告:括弧のない条件として割り当ての結果を使用しています」とおっしゃってます。

place parentheses around the assignment to silence this warning

翻訳: この警告を止めるには、割り当てを括弧で囲んでください。

use '==' to turn this assignment into an equality comparison

翻訳:'=='を使用して、この割り当てを等価比較に変換します。

一番最後の一番わかりやすい気がします。
代入ではなく、等価比較に変えるために、 ==' を使いましょう、ということです。


この間違いは、以下の2つの書き方を間違えたということです。

if (a == !argc) {
	;
}

if (a != argc) {
}

もし、最初の a=!b が意図したコードであるなら、以下のように書きましょう。
int a = !b;
if (a) {
	;
}

これで疑いは晴れました。


C言語の仕様として、代入であるか、不等号なのか、プログラムの読み手には、わからないし、プログラムの書き手は、タイプミスによって、プログラムの意味が期待したものではなくなる可能性を秘めています。 不等号についても、言語仕様がフリーダムなせいで、不思議な不等号をかけてしまう、というのが問題なようです。 C言語を書く人間が、混乱を起こさないように、if の式で代入をすることをやめたほうが良いでしょう。 また、コンパイラの警告に耳を傾けることを忘れないでください。警告は、きっとあなたとあなたのプログラムを支援してくれます。

3. 非衛生的なマクロ


マクロは、非情に便利です。マクロはコーディングを容易にしてくれます。マクロによりコーディング量を減らすことが可能です。

一方で、マクロは、間違いを起こしやすいものの1つです。間違いは、マクロがどのように展開されるのかが、正しく理解できていないときに、起こるかもしれません。

C言語の define で定義されたマクロは、C言語のコンパイラを実行したときに、展開されます。GNUのGCCコンパイラのケースでは、cpp と呼ばれる Cプリプロセッサーが include や define などのプリプロセッサディレクティブを処理して、展開を行います。コンパイラによっては、Cプリプロセッサを行うだけで終わらせることができますので、マクロがどのように展開されるか興味がある方は、試してみてください。

まず、以下のサンプルのコードを読みてください。
#define assign(a,b) a=(char)b
assign(x,y>>8)

assign のマクロは、Cプリプロセッサによって、以下のように展開されます。

x = (char)y>>8	/* おそらなく、あなたが望むものではない */

これは、おそらく、C言語における評価の順番の問題だと思います。 つまりどういうことかというと、右辺は、2つのことが同時に係れています。 1つ目は、char型へのキャストです。もう1つは、シフト演算子によるビットシフトです。 clang のコンパイラで試したところ、上記の式の左辺は、キャストが先に評価されます。キャストした結果をビットシフトし、ビットシフトした結果が x に代入されます。


簡単な検証用のソースコードを書きました。ビットシフトをカッコで囲む場合(A)とそうでない場合(B)の違いをみてください。
#include <stdio.h>

int
main()
{
	int y = 1024;
	int a=8;
	int x=(char)(y>>a); // A カッコで囲む
	printf("%d\n", x);

	int z=(char)y>>a; // B カッコで囲まない
	printf("%d\n", z);
	return 0;
}

上記のソースコードをコンパイルします。
cc shift.c

コンパイルが作成した実行ファイルを実行すると、以下の違いがでました。
$ ./a.out
4
0

このように、Bのカッコで囲わない場合、charへのキャストが先に行われていることがわかります。これは言語仕様をきちんと知っていれば、なにが起こるかわかるのかもしれませんが、評価の優先順位を細かく覚えていられる優秀なプログラマは少ないのではないでしょうか。結果、記憶があいまいであるがゆえに、曖昧さを解決するために、無駄にカッコをたくさん書いて、間違いを起こりにくくする、といったノウハウができてしまったようにも思います。

プリプロセッサを使う例も、あわせて記載しておきます。

以下のコードを a.c とします。
#define assign(a,b) a=(char)b
void func(void) {
	assign(x,y>>8);
}

cpp コマンドで、マクロを展開します。
cpp a.c

上記のコマンドを実行すると、cpp が標準出力にプリプロセスを実行した結果を出力します。
# 1 "/tmp/a.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 347 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "/tmp/a.c" 2

void func(void) {
 x=(char)y>>8;
}

このように、マクロがどのように展開されるか、確認する場合は、cpp コマンドなどを利用してください。

4. 不適切なヘッダファイル


foo.hが以下を含むという仮定です。
struct foo { BOOL a};

ファイル F1.cは、以下の通りです。
#define BOOL char
#include "foo.h"

ファイル F2.cは、以下の通りです。
#define BOOL int
#include "foo.h"

F1とF2の構造体fooの基本的な属性について一致しません。これらがお互いに会話するなら、あなたは負けます。

BOOLの宣言が別々になされているため、Cプリプロセッサが define や include の処理をしたときに、それぞれのソースコードには、foo.h が展開され、それぞれのファイルの中の BOOL がそれぞれのファイル内で展開されます。 struct foo という構造体は、メンバの型が異なる形で展開されてしまいます。

F1.c と F2.c で、メンバの型が異なる構造体をやりとりするときに、おかしなことがおきる可能性がある、ということを想像してみてください。

これの解は、BOOL の定義を struct foo が宣言されている foo.h で行うことでしょう。

5. 幻は値を返した


このプログラムは、本当にコンパイルできるのだろうか?と思われる人もいるのではないかと思います。

int foo (a)
{
	if (a) return(1);
} /* バグがある, ときどき値を返さない */


a が 0 以外のときは、1 が返ります。 もし a が 0 であれば、foo 関数はいったい何を返すのでしょうか?そのとき、スタックに積んである値がそのまま返るのでしょうか。レジスタの中にたまたま入っていた値が返るのでしょうか。CPUの実装や処理系の実装に依存するように思いますが、検討もつきません。

関数の戻り値が指定されているため、 return 文で何らかの値が返されるべきです。そのため、コンパイラによっては、警告を出します。

2007年当時のgccは、以下の警告を出力していました。
not_return.c:14: warning: control reaches end of non-void function

制御が non-void な関数の終わりに到達した、と言っています。

2020年の gcc9 を -Wall 付きで実行すると以下の警告が出ます。
control may reach end of non-void function [-Wreturn-type]

制御は、non-void な関数の終わりにたどり着いてます、といっています。 void じゃない関数なのに終わってるよ、といっています。

以下のプログラム foo.c があったとします。
int foo (int a)
{
	if(a) return 1;
}

上記の foo.c をコンパイルすると、以下の警告がでますが、オブジェクトファイル (foo.o) は作成されます。
gcc9 -Wall -c ./foo.c
./foo.c: In function 'foo':
./foo.c:4:1: warning: control reaches end of non-void function [-Wreturn-type]
    4 | }
      | ^

clang の 8.0.1 でも試してみました。おっしゃることはgccと同じようです。
$ clang -v
FreeBSD clang version 8.0.1 (tags/RELEASE_801/final 366581) (based on LLVM 8.0.1)
Target: x86_64-unknown-freebsd12.1
Thread model: posix
InstalledDir: /usr/bin
$ clang -c foo.c 
foo.c:4:1: warning: control may reach end of non-void function [-Wreturn-type]
}
^
1 warning generated.

一般的に言えば、CコンパイラやCランタイムは、なにが悪いかをあなたに伝えることができません。実際に起こることは、特定のCコンパイラに依存します。不運にも、プログラムがうまく動作しているように見えるかもしれません。

それでは、本当に何がおきるのか確認してみましょう。
#include <stdio.h>
int foo (int a)
{
	if(a) return 1;
}
int main() {
	printf("%d\n", foo(0));
	return 0;
}

実行してみると、なぞの値が返ってきました。ただ単にレジスタとかに入っていた値がそのままでているんじゃないかと思いますけど。
$ gcc9 main.c
$ ./a.out
6293960

clang だと常に0が表示されました。処理系によって、異なるのかもしれませんね。アセンブラ言語に変換して、中身をみるのが良いのだと思いますが、ここでは、そこまでやらないことにします。

fooがもしポインタを返すなら、それによって大惨事がおきることを想像してみてください!

本来であれば、このような関数の実装を期待しています。
int foo (int a)
{
	if(a) {
		return 1;
	}
	return 0; // この return 文追加しました
}

6. 予測できない構造体の構築


ここでの話は、C言語の構造体です。普段は、あまり使わないかもしれませんが、構造体には、ビットフィールドが指定できます。

このビットパッキング構造体について考えてみましょう。

struct eeh_type
{
	uint16 size:	10;	/* 10 bits */
	uint16 size:	6;	/* 6 bits */
};

C コンパイラやあなたのマシンのエンディアン次第で、
実際には、

<10ビット><6ビット>

もしくは

<6ビット><10ビット>

として実行されるかもしれません。

また、Cコンパイラ、マシンアーキテクチャ、そして不可解な優先設定次第で、 アイテムはもっともほとんど8,16,32か64ビットにそろえるかもしれません。


エンディアン(endian) という言葉に聞き覚えがあるでしょうか?ビッグエンディアン (Big endian) やリトルエンディアン(Little endian)といった言葉が使われます。

16進数で 1234ABCD と表現される 1ワードが4バイトのデータがあったとしてます。このデータをバイトごとに上位から 12 34 AB CD と並べる順序はビッグエンディアンです。逆に、下位のバイトから CD AB 34 12 の順序に並べる場合をリトルエンディアンと呼びます。 このように複数のバイトで構成されるデータの並びをバイトオーダー( byte order) といいます。

エンディアンの異なるマシン同士で、データをやりとりするときに、ことなるバイトオーダーのデータを交換してしまうと、エンディアンを考慮せずにデータを扱う場合、データが壊れてしまう可能性を秘めています。


以下の例ではどうでしょうか?

Rect foo = { 0, 1, 2, 3 };	// 最初の4つのスロットに数値を割り当てる

最初にプログラムを書いたときは、おそらく問題がないのだと思います。4つのメンバがなんであるかも、わかっていたと思います。構造体のメンバに変更があったときに、どの値をどのメンバに代入していたのか、わからなくなるかもしれません。将来構造体がかわるようなことがあるなら、このコードは将来困難を生むかもしれません。

C言語のC99の拡張では、構造体の初期化時に、メンバの名前を指定することができます。

Rect foo = { .a=0, .b=1, .c=2, .d=3 };

こうすることで、将来、メンバを増減するときに、混乱を緩和できるのだと思います。

7. 不明瞭な評価のオーダ


ここでの話は、C言語のポータリビリティの話でもあります。ポータリビリティというのは、移植性のことです。 移植性の高いコードを書くことで、さまざまなCPUやOSで利用することができます。移植性の高いコードを書くためには、なにをやってよくて、なにをやってはダメなのかを知っている必要があります。

C言語は、あいまいなところがあります。
以下のコードは、関数の呼び出しを行います。以下のコードがどのように評価されるかわかりますでしょうか?

foo(pointer->member, pointer = &buffer[0]);

上記のコードは、gccコンパイラでは動きます。ほかのコンパイラでは、動かない場合もあるでしょう。

この理由は、gccは、関数の引数を右から左に評価します。その逆に評価するコンパイラもあるようです。
もう少し具体的にどのように評価されるのか、分解して話をしてみましょう。
まず、 gcc の左から右に評価の流れです。
pointer = &buffer[0];
p = pointer;
m = pointer->member;
foo(m, p);

上記の変数 p と m の代入はとくに意味はないのですが、評価される順番ということで、ちょっと書いてみただけです。

今後は、逆に、左から右に評価する場合です。
m = pointer->member;
pointer = &buffer[0];
p = pointer;
foo(m, p);

pointer には、 buffer[0] のポインタが渡されています。ポインタが渡されたからこと、アロー演算子を用いて pointer->member にアクセスができるはずです。このコード以前に pointer が期待した値で初期化されていない限り、成り立ちません。

pointer が NULL (ヌルポインター) の場合 、このコードは、 segmentation fault で終了することになります。

なお、現代(2020)のコンパイラのGCC 9 では、ポインタを pointer に代入するコードで、 warning: operation on 'pointer' may be undefined という警告が出ます。

K&RとANSI/ISO Cの仕様書では、関数の引数の評価順番を定義していません。 K&R というのは、カーニハンとデニス・リッチーというC言語の生みの親です。

左から右、右から左、もしくは、他の何でもありえて、そして、明示されていません。 従って、同じプラットフォームのコンパイラであっても、この評価の順番を当てにしたコードは、ポータブルではありません。


簡単に行える検証をしてみたい場合には、以下のコードを試してみてください。
#include <stdio.h>
int main () {
	int = 0;
	printf ("%d %d\n", i++, i++);
	return 0;
}

右から左に評価する場合であれば、これを実行すると 1 0 と表示されます。
OS が FreeBSD 12.1-RELEASE amd64 で、GCC9 で検証しました。

8. 簡単に変更されるブロックスコープ


C言語では、よくブロックを表すブレースを省略しないようにと言われています。ブレースを書かない場合において、コードを書き加えたときに、意味が変わってしまうことがあるからです。

まず、はじめに、よくある、 if / else を使ったコードがあったとします。ここでは、ブレースが使われていません。

if( ... ) 
	foo(); 
else 
	bar();

上記のコードに、デバッグ用ステートメントを追加した結果、 下記のコードになります。


if( ... ) 
	foo(); /* このセミコロンの重要性は、誇張されることができない */
else 
	printf( "Calling bar()" );      /* おっと! elseはここで止まる */
	bar();                          /* しまった! bar は常に実行される */

類似したエラーの大きな分類があり、 セミコロンやブラケットの置き忘れによって生じます。

正しくは、このようなコードをはじめから書いておくべきでした。

if( ... ) {
	foo(); 
} else {
	bar();
}

このコードは、デバッグコードを追加すると以下のようになります。

if( ... ) {
	foo(); 
} else {
	printf( "Calling bar()" );      /* Debug OK */
	bar();
}

デバックコードの追加前後で、コードの意味は何も変わってません。これなら問題ありません。

9. 寛容なコンパイル


C言語のコンパイラは、コードがなにを意味しているのかは、気にせずに、コンパイルしてくれます。特に意味のないコードであっても、コンパイルしてくれます。そして、おそらく、そのコードは何もしません。

マクロを通した関数呼び出しのコードを変更しました。
CALLIT(functionName,(arg1,arg2,arg3));

CALLITは、ただ単なる関数呼び出しではありません。
余計なことをしたくないので、マクロだけ消し、こうなりました:
functionName,(arg1,arg2,arg3);

しまった。これでは関数を呼びださない。これはカンマ式です。

1. functionNameのアドレスを評価して、それから捨てます。
2. カンマ式(arg1,arg2,arg3)を括弧に入れ評価します。

C言語では、それがなにを意味するかについて、気にせず、コンパイルだけしかしません。

switch (a) {
int var = 1;    /* この初期化は典型的に何も起こらない */
	/* コンパイラは苦情を言わないが、確かに台無しにする */
case A: ...
case B: ...
}

注意
最近のgccは、 以下の警告が出ます。
switch.c:16: warning: unreachable code at beginning of switch statement

しかし、確信していない?これを試しなさい:

#define DEVICE_COUNT 4 
uint8 *szDevNames[DEVICE_COUNT] = {
	"SelectSet 5000",
        "SelectSet 7000"}; /* テーブルは2つのジャンクのエントリを持つ */


10. 危険な戻り値


これは、アクセスすべきではないメモリにアクセスしているのが問題です。

下記のプログラムの動作について簡単に説明します。関数 g は、 関数 f を呼び出します。 f は、ローカル変数 result の配列に、文字列を sprintf で書き込みます。result の先頭のポインタを g に返します。 g は、受け取ったポインタを printf に渡して、標準出力に出力します。

char *f() {
	char result[80];
	sprintf(result,"anything will do");
	return(result);    /* おっと! 結果は、スタックに割り当てられている */
}

int g()
{
	char *p;
	p = f();
	printf("f() returns: %s\n",p);
} 

このバグについての 不思議なことは、ときどき正しい プログラムであるように見えることです。 利用されたスタックの 特定の部分が再利用されない限りは、正しく動作しているように見えてしまいます。

f によって使われたスタック領域(result[80])が他の関数呼び出しなどによって、再利用されて書き換えられていない限り、問題は顕在化しない、ということですが、間違ったコードです。

ここでは、おそらく、2つのソリューションがあります。

1. result を static な変数にする
2. fが書き込み用のバッファを受け取って、データを返す

11. 未定義のオーダの副作用




12. 未初期化のローカル変数


C言語には、グローバル変数とローカル変数があります。ローカル変数は、宣言をしただけでは、初期化されることはありません。

実際に、このバグは非常によく知られています。この問題に遭遇したときに、より致命的な問題になるでしょう。もっともシンプルなケースについて考えてみましょう。

void foo(a)
{
	int b;
	if(b) {/* バグ! bは初期化されていない! */ }
}

そして、実際は、現代的なコンパイラは、上記と同じぐらいあからさまなケースにおいて、警告を出します。

少しコードは違いますが、以下のサンプルプログラムをコンパイルしてみます。

#include <stdio.h>
int main() {
	int b;
	if (b) {
		puts("OK");
	}
	return 0;
}

上記のコードは、 -Wall オプションを使った場合、gcc 9 は、以下の警告を出しました。

% gcc9 -Wall 12-1.c
12-1.c: In function 'main':
12-1.c:4:5: warning: 'b' is used uninitialized in this function [-Wuninitialized]
    4 |  if (b) {
      |     ^

未初期化の変数 b が使われていると警告が出ています。

コンパイラの裏をかくために、少しだけ賢くなければなりません。

void foo(int a) 
{
	BYTE *B;
	if(a) B=Malloc(a);
	if(B) { /* バグ! Bは初期化された/されていないかも */ *b=a; } 
}

以下のサンプルコードを容易しました。
#include <stdio.h>
#include <stdlib.h>
void f(int a) {
	int *p;
	if (a) { p = malloc(sizeof(int)); }
	if (p) { *p = a; } // 未初期化かもしれない
}
int main (int argc, char *argv[]) {
	f(argc-1);
	return 0;
}

以下のコマンドラインで gcc や clang でコンパイルすると、エラーは出ません。
gcc9 -Wall 12-2.c
clang  12-2.c

clang -Wall にしてみると話は変わります。

12-2.c:5:6: warning: variable 'p' is used uninitialized whenever 'if' condition is false [-Wsometimes-uninitialized]
        if (a) { p = malloc(sizeof(int)); }
            ^
12-2.c:6:6: note: uninitialized use occurs here
        if (p) { *p = a; } // 未初期化かもしれない
            ^
12-2.c:5:2: note: remove the 'if' if its condition is always true
        if (a) { p = malloc(sizeof(int)); }
        ^~~~~~~
12-2.c:4:8: note: initialize the variable 'p' to silence this warning
        int *p;
              ^
               = NULL
1 warning generated.

if 文に入らないと p が未初期化で使われるよ、と警告をしています。非常に賢いですね。未初期かでここで使われるよ、とも書いてあります。

つまり、gcc 9 の裏をかくことはできましたが、 clang 8.0.1 の裏をかくことはできませんでした。

13. 散らかったコンパイル時環境


典型的なコンパイラのコンパイル時環境は、あなたが認識していないが、 100 (もしくは 1000 )ぐらい散らかされているでしょう。 ときどき、危険な一般名を持ち、 見つけることが不可能なアクシデントに至ります。

たとえば、以下のコードがあったとします。 これは BUFFSIZE を BUFSIZ にミスタイプしたコードです。

#include <stdio.h>
#define BUFFSIZE 2048
long foo[BUFSIZ];                //スペルに注意 BUFSIZ != BUFFSIZE

エラーなくコンパイルが行われ、予想された恐怖とミステリーに陥ります。 ミスタイプしているのに、なぜ、エラーが起きずに、コンパイルされてしまうのでしょうか?

それは、BUFSIZのシンボルがstdio.hに定義されているからです。

ためしに grep コマンドで /usr/include を探してみるとよいでしょう。 私の FreeBSD には、以下のコードが見つかりました。
/usr/include/stdio.h:#define    BUFSIZ  1024            /* size of buffer used by setbuf */

このつまらない例のタイポは、エラーをみつけることよりも困難かもしれません。

名前をつけるときに、もっとユニークな名前にするといった対策ができるかもしれません。

14. underconstrained 基本的なタイプ


15. まったく安全でない配列


C言語の配列は、配列のサイズを意識してアクセスする必要があります。配列の範囲外にアクセスしたとしても、エラーにならずに、プログラムは動いてしまいます。

以下のケースは、明らかにまずいコードですが、コンパイラは何もいってくれません。

int thisIsNuts[4]; int i;
for ( i = 0; i < 10; ++i )
{
	thisIsNuts[ i ] = 0;     /* 大きくないか?  私が使える要素は
				    1-10 の 4 要素 配列,
				    そして誰も心配しない*/
}

上記の例では、ループは 0 から 10 だが、配列は、 0 から 3 の要素にしかアクセスしてはなりません。

境界を越えてアクセスしていく場合は、配列のとなりのメモリをアクセスしていきます。つまり、範囲外のメモリを読み書きすることにあるため、範囲を超えてデータを書き込んでいくと、データが破壊されていきます。

上記以外のケースで、添え字でアクセスする場合に、警告を出してくれるパターンも存在します。

以下の単純なケースにおいて、 clang であれば、警告を出してくれます。

#include <stdio.h>
int main () {
	int a[5];
	a[10] = 12354; // これはマズイ
	printf ("%d\n", a[10]);
	return 0;
}

このプログラムの配列aの配列のサイズは、5のため、添え字では 0 から 4 までしかアクセスができませんが、 a[10] つまり11番目にアクセスをしているプログラムです。

15-1.c:4:2: warning: array index 10 is past the end of the array (which contains 5 elements) [-Warray-bounds]
        a[10] = 12354;
        ^ ~~
15-1.c:3:2: note: array 'a' declared here
        int a[5];
        ^
15-1.c:5:18: warning: array index 10 is past the end of the array (which contains 5 elements) [-Warray-bounds]
        printf ("%d\n", a[10]);
                        ^ ~~
15-1.c:3:2: note: array 'a' declared here
        int a[5];
        ^
2 warnings generated.

つまり、警告しているケースとそうでないケースがあります。コンパイラとプログラマーの成長に期待いたします。

16. 8進数


17. 符号ありキャラクタ/符号なしバイト


18. とてつもなく大変な "標準ライブラリ"


19. 将来の拡張のために予約されている


もし、なにか、よくある間違いがあれば、ご連絡ください。

元ネタについて

今回の記事の元ネタは、下記のサイトでした。2020年に確認したところ、ページはないようでしたので、リンクはやめておきます。
http : // www.andromeda.com/people/ddyer/topten.html

さいごに


C言語は、言語仕様的に、間違いが起こりやすい言語の1つだということがおわかりいただけたのではないでしょうか。 どの言語にも、なんらかの間違いやすいポイントというのはあると思います。

間違わないための対策として、言語仕様をしっかり学ぶことや、間違いのおこりにくいコードの書き方を身に着けることだと思います。 勉強と訓練をして、困難に打ち勝ちましょう。

基礎的なC言語入門コンテンツを /c/ に用意しておりますので、よろしかったら、見てください。

プログラミング言語を学ぶ


  • C言語 をアマゾンで探す
  • C言語 を楽天で探す
  • C言語 をヤフーショッピングで探す



参照しているページ (サイト内): [2007-12-27-2] [2007-10-01-1]

スポンサーリンク
スポンサーリンク
 
いつもシェア、ありがとうございます!


もっと情報を探しませんか?

関連記事

最近の記事

人気のページ

スポンサーリンク
 

過去ログ

2020 : 01 02 03 04 05 06 07 08 09 10 11 12
2019 : 01 02 03 04 05 06 07 08 09 10 11 12
2018 : 01 02 03 04 05 06 07 08 09 10 11 12
2017 : 01 02 03 04 05 06 07 08 09 10 11 12
2016 : 01 02 03 04 05 06 07 08 09 10 11 12
2015 : 01 02 03 04 05 06 07 08 09 10 11 12
2014 : 01 02 03 04 05 06 07 08 09 10 11 12
2013 : 01 02 03 04 05 06 07 08 09 10 11 12
2012 : 01 02 03 04 05 06 07 08 09 10 11 12
2011 : 01 02 03 04 05 06 07 08 09 10 11 12
2010 : 01 02 03 04 05 06 07 08 09 10 11 12
2009 : 01 02 03 04 05 06 07 08 09 10 11 12
2008 : 01 02 03 04 05 06 07 08 09 10 11 12
2007 : 01 02 03 04 05 06 07 08 09 10 11 12
2006 : 01 02 03 04 05 06 07 08 09 10 11 12
2005 : 01 02 03 04 05 06 07 08 09 10 11 12
2004 : 01 02 03 04 05 06 07 08 09 10 11 12
2003 : 01 02 03 04 05 06 07 08 09 10 11 12

サイト

Vim入門

C言語入門

C++入門

JavaScript/Node.js入門

Python入門

FreeBSD入門

Ubuntu入門

セキュリティ入門

パソコン自作入門

ブログ

トップ


プライバシーポリシー