C言語の入門書を読む:『やさしいC 第2版』(その2)

id:eel3:20081221:1229869888 の続き。

Lesson 8 関数

書いてあったこと。

  • 関数って何?
  • 関数の定義の仕方、呼び出し方
  • 引数を使う
  • 戻り値について
  • 関数マクロ
  • 変数のスコープ、記憶寿命
  • プロトタイプ宣言、分割コンパイル

大体問題なし。

8.5 関数の利用

関数マクロの注意点として、コードのインライン展開によるサイズ肥大と型チェックの有無ぐらいしか触れていない。少しフォローが必要。
『プログラミング作法』P.38〜P.39に出ている、マクロでバグる例のメモ。

#define isupper(c) ((c) >= 'A' && (c) <= 'Z')
    ...
while (isupper(c = getchar()))
    ...
#define square(x) (x) * (x)
    ...
1 / square(x)

もっともコードが展開されることについては説明されているので、フォロー自体は苦労しないと思う。

8.6 変数とスコープ

グローバル変数とローカル変数の名前が重なった場合(P.254)の説明には少しだけフォローが必要。要するに「バグの元になりやすいから、極力名前が重ならないようにしましょう」ということ。

8.8 関数の宣言

多重インクルードのガードについては書かれてないので、いつかフォローする必要がある。この段階でフォローするのはタイミング的に微妙だけど。

その他

関数内のstaticな変数についてはそれなりに触れられているけど、staticな外部変数やstaticな関数については申し訳程度にちょこっと書かれているだけ。人によってはスルーしてしまいそう。

この段階で教えて効果があるかどうかとなると微妙だけど、実務では必須。フォローが必要。

Lesson 9 ポインタ

書いてあったこと。

  • 変数とアドレス
  • ポインタについて、ポインタの使い方(コードの記述方法)
  • 引数と値渡し
  • ポインタによる参照渡し

だいたい問題なしだけど……。

9.2 ポインタ

ポインタを宣言しただけで初期化も代入もしていない場合の説明(P.285)で、

ここには、「pA = &a;」という文がありません。つまり、ポインタpAにはどのアドレスも格納されていません。ポインタが何もさし示していない状態になっているのです。これでは*pAと記述しても、意味のある値を得ることができません。

これはちょっとマズイ。特に「ポインタが何もさし示していない状態になっている」という所が。何もさし示していないのはヌルポインタのはず。
この直後の段落に、

このように、どこをさし示しているかわからないポインタを使うと、プログラムの実行時に思いもかけないエラーが生じてしまうことがあります。

と正しく書いてあるだけに、実に惜しい。

9.3 引数とポインタ

よく考えたら、「基本型の変数へのポインタ」の実用的な使い方って、参照渡し引数が一番多いのかも。

constポインタの説明があるのは評価できるけど、基本型へのポインタしか出てきてないこの段階ではまだ早いのかも。構造体や配列を参照するようになると重宝するんだけど。

Lesson 10 配列・ポインタの応用

書いてあったこと。

  • 配列とポインタ
  • 関数の引数に配列を渡す
  • 文字列とポインタ
  • 文字列操作用の標準ライブラリ関数(有名どころ)
  • 関数ポインタ

前半がちょっと微妙なような、そうでないような……。いや、結構よく書けていると思うけど。後半は大体問題なし。

10.1 配列とポインタの関係

配列とポインタは取り扱い方に類似点が多いこともあり、内容としては間違っていないけど、何となくもやっとした説明になっているような気がする。

例えばP.308の、

これまで説明したように、配列名は配列の先頭要素のアドレスを格納しているポインタと同じであると考えられます。

という記述は間違ってない。実際に、式の中に現れた配列名は、以下の例外を除けば、配列の最初の要素を指すポインタに変わってしまう。

  1. sizeofの引数のとき
  2. アドレス演算子&の引数のとき
  3. char/wchar_t型の配列を文字列リテラルで初期値するとき

ただ、この例外について明確に触れられていない。

配列名であらわされるポインタには、
ほかの変数のアドレスを代入することができない

とは書いてあるけど。ついでに言うと、配列名はポインタと違ってインクリメント/デクリメントできないことも書いてない。

もっとも、初心者にそこまで一気に教えるのが良いかどうかとなると、判断が難しい。

あと細かいところで、ポインタ演算の例なのに、ポインタじゃなくて配列名に対して演算しているサンプルコードというのは、正直微妙だと思う。

10.2 引数と配列

関数の仮引数として「int ary」と書くのと「int *ary」と書くのは、どちらも同じ。関数の引数として配列名をつっこむとき、その配列名は配列の最初の要素を指すポインタに成り下がるので、仮引数側もポインタじゃないとダメ。

つまり仮引数として「int ary」と書いても、関数内部ではaryは配列じゃなくてポインタとして扱うことになる。

ちなみにこの項の記述は全て蛇足。*1

10.3 文字列の操作

strcatって世間一般ではよく使われてるのだろうか? strcmp、strcpy、strlenあたりは納得できるけど。*2

10.5 関数ポインタ

情報処理試験にでてくる「ノイマン型コンピュータ」とか「逐次制御方式」とか「命令サイクル」が分かっていると、(書き方は別として)関数ポインタについて感覚的に納得できるかもしれない。*3でもプログラミング初心者にそれは厳しいのか。

関数ポインタの活用法としては、関数ポインタのテーブルのみ説明している。順当な所かも。

実際には関数ポインタはコールバック関数とかで使う。*4C++STLアルゴリズムなんかもコールバックが多い(STLの場合は関数オブジェクトを使うけど)。でもそれを説明するにはまだ早い。

その他

ヌルポインタとNULLマクロがでてこなかった! う〜ん……。

あとポインタの使い道としては、

  • 関数の引数の参照渡し
  • 関数ポインタのテーブル

ぐらいしか明示的には触れられてなかったけど、実際には他にも色々ある。時期を見て、覚えていってもらうことになるはず。

ということで『CプログラミングFAQ』の質問4.1(P.60)より引用。

質問: ポインタは実際のところ何の役に立つのか。

回答: さまざまな使い道がある。例えば、

  • 動的に割り付けた配列(質問6.14と6.16参照)
  • 複数ある類似の変数への汎用なアクセス
  • 関数引数の(擬似)参照渡し(質問4.8と20.1も参照)
  • あらゆる種類のデータ構造を動的に割り付ける、とくに木構造やリンク付きリスト
  • 配列の中を動いて回る(例えば、文字列を解析するとき)
  • 配列や構造体の効率のよい参照渡しの“コピー”に、特に関数引数の場合

(このリストはすべての用例を尽くしたわけではない。)
 質問6.8も参照のこと。

Lesson 11 いろいろな型

書いてあったこと。

  • 構造体って何?
  • 構造体の(文法的な面での)使い方
  • typedef
  • ビットフィールド
  • 関数の引数と構造体
  • 共用体、列挙型

何かちょっと全体的に急ぎ足だった気がする。特に共用体と列挙型の説明はそれぞれ3〜4ページ。構造体ほど使用頻度は多くない気がしないでもないけど、でも偶に使うことがある。具体的な使用例(どんな時に使うか)を交えた解説があると嬉しかったんだけど。

構造体関係の説明については問題なし。やっぱり急ぎ気味のような感じがしないでもないけど。

Lesson 12 ファイルの入出力

書いてあったこと。

  • printf、scanfの(説明してなかった)使い方
  • それ以外の代表的な入出力関数(getchar、gets、putchar、puts)
  • テキストファイルの入出力
  • バイナリファイルの入出力、ランダムアクセス
  • コマンドライン引数の取得
  • 条件コンパイル、組み込みマクロ

この章も急ぎ足ぎみ。

12.2 いろいろな入出力関数

getsがでてくるのはNG。随分前からバッファオーバーフローがらみの危険性が指摘されていることで有名だし。もっとも代替案の「fgets + stdin」は、この時点ではstdinについて説明されていないのでアレだけど。

12.4 バイナリファイルとランダムアクセス

単純に変数をそのままfwriteでファイルに書き出したり、freadで読み出したりするのは、移植性からみるとNGなんだけど……。エンディアンとか基本型の大きさとか、構造体のパディングとかがマシンによって違ってくるので、データ自体の移植性がなくなってしまう。

どうしてもバイナリでデータをファイル保存したいなら、ちゃんとファイルに保存するデータのフォーマットを設計するべき。もしくはバイナリは諦めて、テキストデータに変換して入出力するとか。

このあたり、簡単にでもフォローの1文をいれてくれてたらよかったんだけどなあ。

総評

平易な書き方ながら、結構詳しく丁寧に解説している。本の厚さにめげなければ、プログラミング初心者でも大丈夫だと思う。*5
細かい点では微妙なところもあるけど、全体的には問題なしと判断しても支障は無いと思う。

*1:と言うか『やさしいC』には特に書いてなかった。

*2:最近ではstrcpyは使わないのかも。私の場合、コピー元データの大きさとコピー先のバッファサイズを確認してからstrcpyでコピーすることならあるけど。

*3:「命令だってメモリ上にロードされるんだから、アドレスが分かればアクセスできそうだよね」というか。

*4:処理の共通部分をフレーム化した関数を用意して、差分を別関数として個別に用意しておいて、フレーム関数を実行する時に差分の関数のアドレスを引数としてつっこむとか。

*5:当然ながら、実際にプログラムを作る段階では別の本が必要になるけど