S-JIS[2006-02-25/2010-05-19]

C言語の基礎知識(?)

C言語の基礎知識だと思われる事柄のメモ。

Java等の【クラスが作れて例外機構があってメモリ管理が楽な】言語に慣れてしまうと C言語で大規模なプログラムを作るのは苦痛だ(苦笑)が、バイト列を加工することに関してはポインターのあるC言語の方が便利。



C言語の規格

C言語の規格も変更され、進歩している。
だいたいgccが先行し、便利なものが標準規格として取り込まれているように感じる。

K&R カーニハンとリッチーによる『プログラミング言語C』によるもの。冗談でC78と呼ばれることがあるらしい。
C89 いわゆる「ANSI-C」。
C99 C++から便利な機能が取り込まれたもの。実際、便利(笑)

単語の読み方

この単語はこう読むんだ、というのを独断で(笑) [2008-03-20]

単語 読み 備考
char キャラ charactor 「チャー」じゃ「茶ー」でしょ(爆)
int イント もしくは「イントゥ」(イントゥー(into)ではない)
malloc エムアロク memory allocate
calloc シーアロク clear & allocate [2010-05-19]
realloc リアロク re-allocate [2010-05-19]
mem   メム  
memcmp メムコンプ memory compare
memcpy メムコピー memory copy
NULL ヌル 「ナル」が正しいという説も。
printf printf プリントエフ print format?
sprintf エスプリントエフ  
std   スタンダード 昔は「エスティーディー」と呼んでいたけど。
stdio スタンダードアイオー standard I/O、すなわち標準入出力
stdin スタンダードイン 標準入力
stdout スタンダードアウト 標準出力
stderr スタンダードエラー 標準エラー
stdlib スタンダードライブラリー
スタンダードリブ
 
str   エスティーアール string (BASICのSTRやINSTRは「スター」「インスター」、VBのCStrは「シースター」って呼んでたけど)
strcmp エスティーアールコンプ string compare
strcpy エスティーアールコピー
ストゥルコピー
「ストリングコピー」ならまだしも、「ストラコピー」は…なんだか違和感がある。
strtok ストラトック
スタートオーケー(嘘)
←↑言行不一致(爆) string token

我ながら、「str」を何と呼ぶかが統一されていない(苦笑)
思うに、これは(自分の中での)知名度の問題?
何をするものだかよく知らない(知らなかった)関数(例:strtok)では、英単語のそれっぽい読みを適当に割り当てる。
strが文字列を指すと熟知している場合は元の意味を表す音にしたいところだが、「ストリング」だとなんとなく長く感じる。かと言って「ストラ」だと、別の意味がありそうな気がしてしまう。「スター」でも星って感じだし。
memoryが「メム」と略しやすい語になっているのに対し、stringが略しにくいのがいけないんだ!(爆)


NULL

無効なポインターを表すには、C言語では「0」(ゼロ)を使う。[2006-10-30]
が、プログラマーがソースを見たときに分かり易くするため、「NULL」という別名を使うことが多い。(「ヌル」と読まれることが多い

#define	NULL	(0)
あるいは
#define	NULL	((void*)0)

C言語のソース上の表現は「0」だが、コンパイルされてアセンブラになったときには、(バイト列としての)0x00000000になるとは限らない。(マシン・CPU依存。 例えば機種によっては0xffffffffになっても不思議ではない)
したがって、ポインターを含む構造体をmemset()で0クリアするだけでは本来は正しくないし、そんな初期化をしたポインターをNULLと比較するのは大間違い

自分の知る限り、アセンブラレベルで0x00000000以外の値にコンパイルされる環境(Cコンパイラー)は無いので、0クリアでも実用上の問題は無いと思うが、意味が違う。[2010-05-19]
例えば、構造体の変数を初期化するとき、以下の書き方は意味が異なる。

	typedef struct { int a; int *b; } TYPE;
	TYPE s1 = { 0 };
	TYPE s2; memset(&s2, 0, sizeof(s2));

s1の初期化の仕方は、「s1.a = 0; s1.b = 0;」と等しい。
つまり、もしNULLを0xffffffffに変換しないといけない環境であれば、s1.bには0xffffffffが入るが、s2.bは0が入る(0クリアされる)ので実行時にNULLとして扱われない。
if (s2.b == NULL)」というソースは、「s2.bの値が0xffffffffと等しいかどうか」というロジックにコンパイル・アセンブルされるはずだから。

また、C言語には無いがC++では演算子を独自定義することが出来る。
構造体の要素をクラスにして そのクラスに代入演算子を独自定義していた場合、たぶんs1の方法ではその演算子が呼ばれるが、s2の方法では呼ばれない。
memset()は、コンパイラーにとってはそういう関数を呼び出すだけのことであって、それがやっている内容・意味を考慮するわけじゃないから。


文字列

C言語では、文字列は「終端にnul文字('\0')が入っている、文字(char)の配列」で表現する。
(例えばMSX-BASICでは、文字列の内部表現には「文字数」が入っており、終端文字は無い)

したがって、例えば"abc"という文字列は'a','b','c','\0'で構成されており、3文字(strlen("abc")の戻り値は3)だが、実際に使われているのは4バイト(sizeof("abc")の値は4)。

末尾を表す'\0'のことをヌル文字(あるいはナル文字)と呼ぶ。
値としては0なので、プログラムに書く場合は「0」でも「(char)0」でも「'\0'」でもいいのだが、文字であることを明示するには'\0'という表現を使うのがいいと思う。
ただし、NULL」を使うのだけは明らかに間違い
NULLが「0」と定義されている場合は文字に使ってもコンパイルエラーにならないし期待した動作をするのだろうが、「(void*)0」とか定義されていたら困ったことになるだろう。ただ、NULLは「0」と定義する方が都合が良いらしいので、最近のコンパイラーはだいたいそうなっている様だけど。
そもそもNULLはポインターに対して使うものなので、文字に使うのはおかしい。「ヌル文字」と呼ぶので、誤解してNULLを使いたくなるのだろうか。
そういう意味で、「NULL文字」という書き方は直訳としては間違っていないのだろうが、「ヌル文字」「nul文字」と表現すべきだと思う。制御文字(コントロール文字。0x20より小さいコードや0x7f)はASCIIコード表では2文字ないし3文字のアルファベットを使った略称で表現されており、ヌル文字は「nl」もしくは「nul」なので、これを使えばはっきり区別できる。

さらに(まぎらわしいことに)、1文字も入っていない文字列("")を「空文字列」「ヌル文字列」と呼ぶ。もちろん、""は'\0'が入っている1バイトのchar配列。
「空文字列」を「空文字」と言ってしまうと(双方の違いをきちんと区別して知っている人間には)nul文字のように思えてしまうので、「文字」と「文字列」はきちんと区別しなければならない。


代入式・代入文

C言語においては、代入は演算である。[2008-06-21]
つまり=という演算子は、第1項(左辺値(変数))に第2項(右辺値(値))を代入し、その値を返す。
(例えば+という演算子は、第1項(左辺)と第2項(右辺)を足した値を返す)

なので、以下のような書き方が出来る。

	int a, b, c;
	a = b = c = 0;			/* …(1) */
	a = (b = ((c = 1) + 2)) + 4;	/* …(2) */

(1)は、まずcに0が入り、その結果は0になる。次にその結果がbに代入される。そしてその結果がaに代入される。つまり全て同じ0になる。
(2)は、まずcに1が入り、その結果は1になる。次に「1+2」が計算され、bに代入される(3になる)。そして「3+4」が計算され、aに代入される。


C言語では、式の末尾に「;(セミコロン)」を付けると文になる。

	a = 1	…代入式
	a = 1;	…代入文
	1 + 2;	…これも文。どこにも値を返していない(副作用が無い)ので、意味は無いけど…

if文の()内には、式であれば何でも使える。
その式を評価した結果が0なら偽、それ以外なら真と判断される。

なので、代入式も当然使うことが出来る。

	FILE *f;
	if (f = fopen(〜)) {
		//処理
	}

↑変数fには、fopen()の戻り値が入る。それが0(すなわちNULL)以外の場合は真と判断される。

if(a = b = c = 1)という書き方も出来るが、混乱を招くだけなので、すべきではないだろうな(苦笑)

if文の()内には、直接条件判断に使う演算以外は書かない方がいいと思う。


等値比較の書き順

値が等しいかどうかを判断するには、==演算子を使う。[2008-05-02]
C言語では値の代入が「=」で比較が「==」。他の言語では比較が「=」というものもあり、間違う可能性がある。

	if (変数 = 0) { 〜 }

この場合、変数に0が代入される。if文の条件判断に使われる値はその0になる。
C言語のif文では0以外が真/0は偽と判断されるので、この間違った代入式は常に「偽」と判断されてしまう(0以外の値であれば常に「真」と判断されてしまう)。

こういった事を回避する定石として、変数と値を逆に書くものがある。

	if (0 == 変数) { 〜 }

こうしておけば、仮に間違って「=」1つにしてしまった場合は、コンパイルエラーになる。
(定数は左辺値ではないので、値を代入することが出来ないから)
なるべくコンパイラーによってエラーを発見させるという意味で、良いアイデアだと思う。

個人的には、C言語に慣れればそんな間違いはしないので、より仕様に近い書き方をすべき(つまり変数を左に書くべき)だと思うけど。


printf

画面(標準出力)へ文字列を出力する、誰でも知っている関数。
でもフォーマット文字列には けっこう色々指定できる。ただし当然のことながら、バージョンによって出来るものと出来ないものがある。

書式 説明
%   指定を開始する。「%」自身を表示したい場合は「%%」とする。 printf("%%\n"); %
d int 整数(int)を十進数で表示する。 printf("%d\n", 123); 123
u int 整数(int)を十進数(符号なし)で表示する。[2009-05-25] printf("%u\n", 123); 123
x int 整数(int)を十六進数で表示する。 printf("%x\n", 123); 7b
ld long 整数(long)を十進数で表示する。[2009-05-25]    
lx long 整数(long)を十六進数で表示する。[2009-05-25]    
hd short 整数(short)を十進数で表示する。[2009-05-25]    
hx short 整数(short)を十六進数で表示する。[2009-05-25]    
c char 文字を表示する。 printf("%c\n", 'x'); x
s char* 文字列を表示する。 printf("%s\n", "xyz"); xyz
p void* ポインターの値を表示する(環境によって具体的な表示のされ方が異なる)。
%xを使っても似たようなものだが、ポインター値を表示するなら%pの方が移植性が高い。
printf("%p\n", &i); 0012F13C
数$   引数の番号を指定する(値の引数のうち、一番左が1)。
通常これは省略されており、その場合は引数の並び順に従って値が表示されるのは衆知の通り。
printf("%d:%d:%d\n",11,22,33);
printf("%1$d:%2$d:%3$d\n",11,22,33);
printf("%3$d:%2$d:%1$d\n",11,22,33);
printf("%3$d:%1$d:%d\n",11,22,33);
11:22:33
11:22:33
33:22:11
33:11:22
-   左詰で表示する(省略時は右詰)。 printf("[%-4d]\n", 123);
printf("[%-4s]\n", "xyz");
[123 ]
[xyz ]
0   足りない桁数を0埋めする。文字列(例えば%04s)でも!(笑) printf("%04x\n", 123); 007b
  桁数(最小フィールド幅)を指定する。
最低限確保される桁数であり、この数値以上の桁の値は全部表示される。
printf("[%4d]\n", 123);
printf("[%4s]\n", "xyz");
printf("[%4s]\n", "xyzab");
[ 123]
[ xyz]
[xyzab]
.数   精度(最大表示幅)を指定する。
この桁数より大きい場合は切り捨てられ、表示されない。
nulターミネートしていない文字列(nul文字で終わっていない固定長文字列 )を表示するのに便利。
printf("[%.4s]\n", "xyzab");
printf("[%5.4s]\n", "xyzab");
printf("[%-5.4s]\n", "xyzab");
[xyza]
[ xyza]
[xyza ]
*   桁数精度引数番号の指定を引数から取ってくる。  printf("[%*s]\n", 4, "xyz");
(printf("[%4s]\n", "xyz");と同じ)
 printf("[%0*d]\n", 4, 123);
 printf("[%*$*d]\n", 2,4, 11,22,33);
(printf("[%2$4d]\n", 11,22,33);と同じ)
[ xyz]

[0123]
[  22]
 

%dは引数の値をintとして取得する。%hdはshort、%ldはlongとなる。[2009-05-25]
printf()は可変引数なので、可変部分のそれぞれの領域の長さ(何ビットか)が書式文字列での指定と一致していないと、ずれていってバグとなる。
(C言語においては、可変引数の引数それぞれの長さ(型)を、呼ばれた側で知る方法が無い。引数は全てスタックに連続して詰まれる前提なので。
printfの場合は書式文字列の指定によって個々の長さを把握する仕様になっているので、書式の指定と引数の型が必ず合っていなければならない)

%p%xは、32ビットのUNIX環境なら出力される形に違いは無いかもしれない。しかしアドレスが64ビットだったり、Windowsでコンパイル・実行したりすると、%pと%xでは表示されるものが変わってくるはず。(アドレスの長さが違うのに%xとか使ってたら、バグるだろーなー)


標準出力へ出すのではなく、バッファへ編集する「sprintf(buf, 書式文字列, …)」という関数もある。
ただしsprintfを使う際には、結果がバッファより長くなってメモリ破壊を起こす危険(バッファオーバーラン/バッファオーバーフロー)に注意しなければならない。
「snprintf(buf, バッファサイズ, 書式文字列, …)」という関数はバッファサイズまでしか書かれないので安全。

普通はあまり気にしないけれど、printfやsprintfの戻り値の型は環境(ライブラリー)によっては違うことがあるので、使うのであれば注意が必要。


実行時に値が変わるような引数を使わないのであれば、当然以下のように書くことが出来る。[2006-04-14]

	printf("hoge\n");

ただしこれを、ユーザーが入力したものをそのまま表示すること等に使いたいなら要注意!
「%」を含んだ文字列が入っていると、そこで引数を探しに行くので、最悪ABEND(abnormal end:異常終了)することになる。

	char text[] = "%special";
	printf(text);

「%special」の先頭が「%s」と認識されて引数の文字列ポインターを使用しようとするが、それは指定されていないのでその時点でスタック(あるいはレジスター)に入っている適当な値が使われることになる。

	char text[] = "%special";
	printf("%s", text);	//訂正版

こんなセキュリティーホールのあるプログラムを作ったら、修正するのが当然でしょ!ユーザーに「%を入力するな」じゃないでしょ→某メーカー


malloc

C言語の標準で、プログラマーが使いたいメモリを動的に確保する関数。[2006-10-30]
メモリは“C言語のランタイム(実行時)ライブラリが用意したヒープと呼ばれる領域”の中に確保される。
関連する関数には以下の関数がある。「stdlib.h」で宣言されている。

関数名 読み(独断) 関数名の由来(想像) 説明 使用例
malloc エム・アロク memory allocate メモリ(領域)を確保し、その先頭アドレスを返す。
確保できなかった場合はNULLを返す。
使用後はfree()で解放する。
構造体 *addr = (構造体*)malloc(sizeof(構造体) * 個数);
if (addr == NULL) {
  return; //エラー
}
calloc シー・アロク clear & allocate メモリを確保する点ではmalloc()と同じだが、ついでに0埋め(クリア)される。 構造体 *addr = (構造体*)calloc(個数, sizeof(構造体));
if (addr == NULL) {
  return; //エラー
}
realloc リアロク re-allocate メモリの再確保を行う(確保した領域の長さを変更する)。
第一引数には以前のアドレスを指定する。これがNULLの場合は、malloc()と同じ動作になる。
返されるアドレスは以前と同じ場合もあるし異なる場合もあるが、アドレスが差す先の内容は以前と同じものが保証される。
返されたアドレスが以前と異なるものであった場合、以前の領域は自動的に解放される。
新しい領域が確保できなかった場合はNULLを返すが、その際、旧領域は解放されない。だから、realloc()の戻り値を第1引数と同じ変数に直接代入してしまうのは誤り。
構造体 *temp = (構造体*)realloc(addr, sizeof(構造体) * 個数);
if (temp == NULL) {
  return; //エラー
}
addr = temp;
free フリー free malloc()系の関数で確保した領域を解放する。
引数がNULLの場合は何も処理されない(悪影響も無い)ので、malloc()系の戻り値をそのまま渡しても平気。
free(addr);

free()を実行しなくても、プログラムの終了時にCのランタイムライブラリが自動的に解放してくれる。
が、それを期待してfree()を使わないのは ゴミプログラム(使い捨てのプログラム)では構わないが、少なくとも無限ループに近い(例えば常駐型の)アプリケーションでは、明示的に解放するようプログラマーがコーディングしなければならない。
(こういったリソースの確保・解放をきちんと制御できるのが、まともなプログラマーというものでしょう)


C言語目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま