Newsgroups: fj.archives.answers,fj.lang.c From: kitano@crd.yokogawa.co.jp (Kinichi - Kinchan - Kitano) Subject: comp.lang.c Answers to Frequently Asked Questions (FAQ List) in Japanese[3/4] Sender: news@leia.pa.yokogawa.co.jp (Leia news server) Message-ID: Supersedes: Date: Mon, 14 Jul 1997 17:30:51 GMT Reply-To: kitano@crd.yokogawa.co.jp Organization: Yokogawa Electric Corporation, Tokyo, Japan. Followup-To: fj.lang.c Lines: 1728 Archive-name: c-faq-j/part3 Last-modified: 14 July. 1997 ========================= C FAQ 日本語訳[3/4] ========================= 11章 ANSI/ISO規格C 11.1: 「ANSI C 規格」とは何を意味するのか。 A: 1983年に、アメリカ国内標準規格協会(ANSI)はC言語の標準化を目指 し委員会X3J11を発足させた。何度かの広範囲にわたる公開レビュー を含む長い困難な過程の後に、委員会の作業は、アメリカ国内標準規 格X3.159として1989年12月14日に批准され、1990年の春に出版された。 ANSI Cの大部分は世の中の慣習を規格化したもので、そのほかはC++ からの(一番有名なのは関数プロトタイプである)アイデアの拝借と、 多国語文字列への対応(酷評されている三連文字(trigraph)を含む)で ある。ANSI C規格は、Cの実行時のライブラリについても規定して いる。 もっと最近の話では、規格は国際標準ISO/IEC 9899:1990として採択 された(訳注:日本ではISO 9899を翻訳したものがJIS X 3010:1993と して発行されている)。そしてこのISOの規格がそれより前に存在した X3.159に取ってかわった。これはアメリカ合衆国でも同じである。章 番号の振り方が違っている(手短に言えばISOの5章から7章がだいたい 旧ANSIの2章から4章に対応する)。ISOの規格であるから、技術正誤表 (Technical Corrigenda)や規範補遺(Normative Addenda)を発行して 絶えず改訂しなければならない。 1994年に、技術正誤表1が発行されおよそ40ヶ所で規格が修正された (訳注: 日本では『X3101:96 プログラミング言語C(追補1)』として 1996年に発行された)。たいていは些細な修正もしくはわかりにくい ところの書き直しである。もっと最近の話では、規範補遺1でおよそ 50ページの新しい題材が追加された。ほとんどは国際化のための新し いライブラリ関数についてである。国際化のための新しいライブラリ 関数の技術正誤表の作成が進行中で、2番目の正誤表は1995年後半に 発行が予定されている。さらに、ANSIもISOも自分たちの規格の定期 的な見直しを命じている。この手続きは1995年に始まり、まったく改 定された規格を産み出すだろう("C9X"と愛称がつけられている。これ は1999年までには完成するだろうと考えられているところからつけら れている)。 出版物としての規格は、「Rationale(論理的根拠)」を含んでいる。 Rationaleは、規格の決定にまつわる多くの事柄について説明し、こ のFAQで取り上げているいくつかのことがらを含む規格の数多くの微 妙な点について説明している(RationaleそのものはANSI Standard X3.159-1989ANSI Standard X3.159-1989の一部ではない。ただし資料 として含まれている。ISO規格には含まれていない)。 11.2: どこから規格を手に入れることができるか A: アメリカ合衆国国内では、以下のところから手に入れることができる。 American National Standards Institute 11 W. 42nd St., 13th floor New York, NY 10036 USA (+1) 212 642 4900 あるいは Global Engineering Documents 2805 McGaw Avenue Irvine, CA 92714 USA (+1) 714 261 1455 (800) 854 7179 (合州国内およびカナダ) その他の国では、国の規格を取り仕切る団体かジュネーブのISOに問 い合わせてみること。(訳注: 日本では日本規格協会が発売している。 JISはTEL 03-3583-8002、ISOはTEL 03-3583-8003 より入手可能であ る。) ISO Sales Case Postale 56 CH-1211 Geneve 20 Switzerland (URL http://www.iso.chやニュースグループcomp.std.internatのFAQ であるStandards.Faqも参照のこと). 現時点では価格は、ANSIから購入するときは$130で、Globalから購入 する場合は$400.50である。Rationaleを含むオリジナルのX3.159も、 ANSIからは$205.00で、Globalからは$162.50で入手することができる。 ANSIは、その運営資金を規格の印刷物の販売から得ている。よって規 格の電子的なコピーは入手不可である。 合衆国国内では元のANSI X3.159(Rationaleを含む)を「FIPS PUB 160」 という名前で以下から入手可能かもしれない。 National Technical Information Service (NTIS) U.S. Department of Commerce Springfield, VA 22161 703 487 4650 誤解を招くような題の『注釈付きANSI C規格(Annotated ANSI C Standard)』という本がHerbert Schildtの注釈付きで Osborne/McGraw-HillからISBN 0-07-881952-0として出版されている。 これはISO 9899の、数ページを除いてほとんど全文を含んでいる。合 衆国国内では$40程度で手に入る。この本と公式の規格書の値段の差 は注釈の価値を反映していると考えられている。この本は誤りや抜け が多くて閉口する。規格そのもののほうも何ページか欠落している。 ネット上で多くの人が註釈部はまるっきり無視することを勧めている。 Clive Featherによる註釈の批評("註釈付きの註釈")は http://www.lysator.liu.se/c/schildt.htmlから手にいれることがで きる。 Rationaleのテキスト(規格全体ではない)はftp.uu.netのディレクト リdoc/standards/ansi/X3.159-1989からanonymous ftp(質問18.16参 照)可能である。http://www.lysator.liu.se/c/rat/title.htmlから も入手することができる。RationaleはSilicon Pressから出版もされ ている。ISBNは0-929306-07-4である。 See also question 11.2a below. 11.2a: 規格の改訂の進み具合に関する情報はどこから得られるか。 A: http://www.lysator.liu.se/c/index.htmlやhttp://www.dmk.com/か ら少しは得られる。 11.3: 私が使っているANSI Cコンパイラに以下のようなコードを渡すと、デー タの型が違うと文句を付ける。なぜか。 extern int func(float); int func(x) float x; {… A: 新しい書き方であるプロトタイプ宣言のextern int func(float);と、 古い書き方の定義のint func(x) float x;を混ぜて使っているからで ある。両方の書き方を混ぜて使ってもたいていの場合は問題ないが (質問11.4参照)、この場合は問題がある。 旧来のCは(ANSI Cもプロトタイプがないときや、引数が可変個数のと きはそうだが。質問15.2参照)関数に渡すときにー部の引数を"広げる "。floatはdoubleに格上げされ、charやshort intはintに格上げされ る。(古い書き方の関数定義では、値は呼ばれた側の関数で変数が狭 い型で宣言されているときには自動的に対応する"狭い"型に逆変換さ れた。) 新しい書き方であるプロトタイプ宣言を関数定義の中で首尾一貫して 使うか、 int func(float x) { … } 新しい書き方の関数プロトタイプ宣言を、古い書き方の定義に合うよ うに変更することで問題は解決する。 extern int func(double); この場合パラメータのアドレスを使う気がない限り、古い書き方の 宣言ではdoubleを使ったほうがわかりやすい。 "狭い"(char、short int、float)型は、関数引数や戻り値としては避 けたほうが安全であろう。 質問1.25も参照のこと。 References: K&R1 Sec. A7.1 p. 186; K&R2 Sec. A7.3.2 p. 202; ANSI Sec. 3.3.2.2, Sec. 3.5.4.3; ISO Sec. 6.3.2.2, Sec. 6.5.4.3; Rationale Sec. 3.3.2.2, Sec. 3.5.4.3; H&S Sec. 9.2 pp. 265-7, Sec. 9.4 pp. 272-3. 11.4: 古い書き方と新しい書き方の関数の構文を混ぜて使ってもいいのか。 A: 注意さえすれば(質問11.3を特に参照のこと)混在することは、まった く合法である。しかし古い書き方の構文は時代遅れと考えられており、 その公式サポートはある日止まってしまうかもしれない。 References: ANSI Sec. 3.7.1, Sec. 3.9.5; ISO Sec. 6.7.1, Sec. 6.9.5; H&S Sec. 9.2.2 pp. 265-7, Sec. 9.2.5 pp. 269-70. 11.5: なぜ宣言 extern f(struct x {int s;} *p); で意味不明の警告メッセージ「構造体xはプロトタイプのスコープ内 で導入された」が出るのか。 A: Cのブロックのスコープの一般規則の気まぐれにより、プロトタイプ 内で初めて宣言された構造体は(名前が出てくるだけでも)、同じソー スファイル内で宣言されている他の構造体と互換性を持つことができ ない(名前はプロトタイプの終わりでスコープから出てしまう)。 この問題を解決するには、プロトタイプの前に無意味にみえる宣言 struct x; を付ける。これによりファイル全体にわたるスコープで、構造体xの (不完全な) 定義を置く場所が確保される。これで後に続くstruct xを 使った宣言は少なくとも同じstruct xを指していることが確かになる。 References: ANSI Sec. 3.1.2.1, Sec. 3.1.2.6, Sec. 3.5.2.3; ISO Sec. 6.1.2.1, Sec. 6.1.2.6, Sec. 6.5.2.3. 11.8: なぜconstの値を、初期化指定子(initializer)や配列の大きさに使え ないか理解できない。 const int n = 5; int a[n]; A: const修飾子は実際は"読み込み専用"を意味する。constと修飾された オブジェクトは(普通は)代入不可能な実行時のオブジェクトである。 したがってconstと修飾されたオブジェクトは、定数式という用語の 全体の意味を考えると定数式とは呼べない(Cはこの面ではC++と違う)。 コンパイル時の真の定数が必要なら、プリプロセッサの#defineを使 う(列挙体が使える場面もある)。 References: ANSI Sec. 3.4; ISO Sec. 6.4; H&S Secs. 7.11.2,7.11.3 pp. 226-7. 11.9: char const *pとchar * const pの違いは。 A: char const *pは文字定数へのポインタで(文字を変更すること はできない)、char * const pは文字(の変数)へのポインタ定 数(ポインタを変更することはできない)である。 一番内側から"ひっくり返して"読む。質問1.21も参照のこと。 References: ANSI Sec. 3.5.4.1 examples; ISO Sec. 6.5.4.1; Rationale Sec. 3.5.4.1; H&S Sec. 4.4.4 p. 81. 11.10: なぜconst char **を引数として取ると関数プロトタイプに書かれた 関数に、char **を渡すことができないのか。 A: (どんな型Tに対しても)const Tへのポインタを想定しているところに Tへのポインタを使うことは可能である。しかしこの(これ以外は不可 能である)規則、つまり代わりに使うことのできるポインタ型に少し の不一致を許している規則は、再帰的に適用されるわけではなく、単 に一番上の階層で適用されるだけである。 ポインタが一段である場合を除いて、型が一致しないポインタを代入 する(または引数として渡す)ときは、明示的にキャスト(この場合は const char **)しなければならない。 References: ANSI Sec. 3.1.2.6, Sec. 3.3.16.1, Sec. 3.5.3; ISO Sec. 6.1.2.6, Sec. 6.3.16.1, Sec. 6.5.3; H&S Sec. 7.9.1 pp. 221- 2. 11.12: main()をvoidとして宣言して「mainの戻り値がない」という目障りな メッセージを消すことができるか。 A: できない。main()はintを戻り値とし、(適切な型の)0個か2個の引数 を持つと定義しなければならない。exit()を呼んでもまだ警告が出る のであれば、冗長であるがreturn文を挿入するしかない(あるいは使 える環境にいるのであれば「ここには届かないよ:NOTREACHED」命令 を使う)。 関数をvoidと宣言することは警告が出なくなったり警告の順が変わっ たりするだけではない。関数の異なる呼び出し/戻りの仕組みを使う ことになる可能性がある。これが呼び手(main()の場合、Cの実行時の スタートアップコード)が期待しているものと違うものになる可能性 がある。 (このmainに関する議論は"ホスト付き(hosted)"な環境にだけ適用可 能である。"フリースタンディング(一人立ち:freestanding)"な処理 系にはどれも適用されない。こういう処理系にはそもそもmain()がな いかもしれない。しかしながら"フリースタンディング"な処理系はま れで、そうした処理系を使っているなら、使っている人はおそらく承 知しているだろう。この違いについて聞いたことがないなら、ホスト 付きの処理系を使っている。だからこの質問で出てきた規則が当ては まる。) References: ANSI Sec. 2.1.2.2.1, Sec. F.5.1; ISO Sec. 5.1.2.2.1, Sec. G.5.1; H&S Sec. 20.1 p. 416; CT&P Sec. 3.10 pp. 50-51. 11.13: main関数の3番目の引数envpは。 A: これは(よく見かけるけれど)標準でない拡張である。標準で用意され ているgetenv()関数が提供すること以上に環境の情報が必要ならば、 グローバル変数environを使うほうがまだましな手段だろう(これも同 じように標準外だが)。 References: ANSI Sec. F.5.1; ISO Sec. G.5.1; H&S Sec. 20.1 pp. 416-7. 11.14: void main()と宣言してうまくいかないわけがないと思う。なぜなら main()から戻る代わりに、exit()を呼んでいるから。だいたい今使っ ているオペレーティングシステムはプログラムのexit値/戻り値を無 視する。 A: main()から戻ってくるかどうかは関係ないし、そのステータスを見る かどうかも関係ない。問題はmain()の宣言がおかしいと、呼び出し側 (実行時のスタートアップのコード)がmain()を正しく呼び出すことす らできないかもしれないことにある(呼出しの規約にあるかもしれな い不統一による。質問11.12参照)。君が使っているオペレーティング システムは終了時のステータスを無視して、void main()でもうまく 動くかもしれない。しかし、このやりかたは移植性が低いし、正しく もない。 11.15: 僕がいつも使っている『ほんとおの馬鹿向けのC』には、いつもvoid main()と書いてる。 A: たぶんその本の著者は自分も対象読者の一人に数えているのだろう。 不思議なことに、例題のコードでvoid main()と書いてる本は多い。 そういう本は間違っている。 11.16: exit(status)の値は、main()からの戻り値statusと本当に等しいのか。 A: 等しいときもあるし違うこともある。規格は等しいといってる。ただ し、規格に従っていないいくつかの古いシステムの上では何かしら問 題がある。また、mainにローカルなデータが、後片付けで必要な場合 はうまく動くことは期待できない。質問16.4を参照。(最後に、 main()を再帰的に起動する場合は上の2つは明らかに同じものではな い。) References: K&R2 Sec. 7.6 pp. 163-4; ANSI Sec. 2.1.2.2.3; ISO Sec. 5.1.2.2.3. 11.17: ANSIの"文字列を作り出す"プリプロセッサの演算子#を使って、メッ セージの中にシンボル定数を挿入しようとしている。けれど、#はマ クロの値ではなくマクロの名前を文字列にしてしまう。 A: マクロを展開して文字列を作り出したいときは以下のような2段の手 続きを踏まなければならない。 #define Str(x) #x #define Xstr(x) Str(x) #define OP plus char *opname = Xstr(OP); これでopnameが「OP」ではなく「plus」に設定される。 同じ様な回避手段が、トークンを連結する演算子##を使って、2つの (名前ではなく)マクロの値を連結するときに必要になる。 References: ANSI Sec. 3.8.3.2, Sec. 3.8.3.5 example; ISO Sec. 6.8.3.2, Sec. 6.8.3.5. 11.18: 「警告:マクロ展開が文字列リテラル内で発生」とは何を意味してい るのか。 A: ANSI規格成立より前のコンパイラ/プリプロセッサは以下のマクロ 定義を、 #define TRACE(var, fmt) printf("TRACE: var = fmt\n", var) 以下の様に使ったら、 TRACE(i, %d); 以下のように展開するものがある。 printf("TRACE: i = %d\n", i); つまり、マクロの引数は文字列リテラルや文字定数の中でも展開され た。 マクロの展開はK&Rでも標準Cでもこういう風には定義されていない。 マクロの引数を文字列に変換したいときは、新しく導入された#とい うプリプロセッサの演算子を文字列リテラルの連結(これもANSI C で導入された機能)とともに使う。 #define TRACE(var, fmt) \ printf("TRACE: " #var " = " #fmt "\n", var) 上の質問11.17も参照のこと。 References: H&S Sec. 3.3.8 p. 51. 11.19: #ifdefで消したコードで、奇妙な構文エラーが発生した。 A: ANSI Cでは#if、#ifdef、#ifndefによってコンパイルから"消される" テキストも「コンパイル前処理のトークンとして有効なものである」 としている。このことは終端のないコメントや引用符(短縮した単語 の中のアポストロフィは文字定数の始まりにみえるので要注意である) があってはならないし、また引用符で囲まれた部分に改行があっては ならないことを意味している。よって自然言語のコメントや擬似コー ドは「公式の」コメントの区切り記号である/*と*/の間に書かなけれ ばならない。(ただし質問20.20を参照のこと。質問10.25も。) References: ANSI Sec. 2.1.1.2, Sec. 3.1; ISO Sec. 5.1.1.2, Sec. 6.1; H&S Sec. 3.2 p. 40. 11.20: #pragmaとは何物で、何の役に立つのか。 A: #pragmaは、うまく定義された唯一の"非常口"を与える。#pragmaは、 実装に固有の制御や機能の拡張を行うのに使うことができる。たとえ ばソースコードの表示方法の制御や、警告メッセージの抑制(古い lintの/* NOTREACHED */というコメントのように)に使える。 References: ANSI Sec. 3.8.6; ISO Sec. 6.8.6; H&S Sec. 3.7 p. 61. 11.21: #pragma onceは何を意味しているのか。ヘッダファイルの中にいく つか書いてあった。 A: いくつかのプリプロセッサで、ヘッダファイルの「べき等」を作るの を(2度以上読み込まれても問題が発生しないようにするのを)容易に するための拡張として用意されている。質問10.7で説明した#ifndef を使った技と同等であるが移植性が低い。 11.22: char a[3] = "abc";は正しいのか。何を意味するのか。 A: ANSI Cでは文法的に正しい(たぶんANSI C成立以前のシステムのいく つかでも正しく動くだろう)。ただし本当に稀な状況でだけ役にたつ。 これは大きさが3の配列を宣言し、中身を'a', 'b', 'c'で初期化する。 通常の終端記号の'\0'は付かない。 よってこの配列はC言語の意味で の文字列ではなく、strcpyやprintf %sなどでは使えない。 たいていの場合、配列を初期化するときは、初期化指定子の個数はコ ンパイラに数えさせるべきである。(上の初期化指定子"abc"の場合は もちろん計算結果は4になる)。 References: ANSI Sec. 3.5.7; ISO Sec. 6.5.7; H&S Sec. 4.6.4 p. 98. 11.24: なぜvoid *ポインタを相手に算術演算をすることができないのか。 A: コンパイラにはポインタが指す先のオブジェクトの大きさがわからな い。算術演算を行う前には、ポインタをchar *または操作しようとし ているデータ型のポインタにキャストする(質問4.5を見てからにする こと)。 References: ANSI Sec. 3.1.2.5, Sec. 3.3.6; ISO Sec. 6.1.2.5, Sec. 6.3.6; H&S Sec. 7.6.2 p. 204. 11.25: memcpy()とmemmove()の違いは。 A: memmove()が、コピー元とコピー先に重なりがあったときもうまく扱 うことを保証しているのに対し、memcpy()はそんな保証はしていない。 ひょっとしたらmemcpy()はそれを利用して効率のいい実装方法を取っ ているかもしれない。疑しいときはmemmove()を使うほうが安全であ る。 References: K&R2 Sec. B3 p. 250; ANSI Sec. 4.11.2.1, Sec. 4.11.2.2; ISO Sec. 7.11.2.1, Sec. 7.11.2.2; Rationale Sec. 4.11.2; H&S Sec. 14.3 pp. 341-2; PCS Sec. 11 pp. 165-6. 11.26: malloc(0)は、どういう動作をすべきなのか。ヌルポインタを返す のか、0バイトの領域を指すポインタを返すのか。 A: ANSI/ISO規格にはどちらでもかまわないと書いてある。動作は処理系依 存(質問11.33参照)である。 References: ANSI Sec. 4.10.3; ISO Sec. 7.10.3; PCS Sec. 16.1 p. 386. 11.27: なぜANSI規格は、外部識別子の7文字目以降に意味があることや大文 字小文字の違いを保証しないのか。 A: 問題は古いリンカにある。古いリンカはANSI規格の管理下にないし、 古いリンカが載ったシステムを使ってCコンパイラを開発している人 の管理下にもない。識別子は最初の6文字しか有効でないと制限して いるのであって、識別子の長さを6文字に制限するということではな い。この制限はうんざりさせられるが耐えられないものでもない。こ の制限は、規格の中で「すたれつつある」と記述されている。将来の 改定で制限は緩和されそうである。 制限を持つ現状のリンカへの譲歩は、一部の人がどんなに反対しよ うと、行わなければならない(Rationaleはこの制限を残しておかなけ ればならないことを「一番辛いこと」と書いている)。もし賛成でき ないなら、または制限のあるリンカを使ったコンパイラが外部識別 子にもっと多くの文字数を有効に扱っているとプログラマに見せる トリックを思い付いたら、X3.159のRationale(質問11.1を参照)の 3.1.2項を読むこと。この章はいくつかの方法を議論し、なぜそうい う方法を強制することができなかったか説明している。 References: ANSI Sec. 3.1.2, Sec. 3.9.1; ISO Sec. 6.1.2, Sec. 6.9.1; Rationale Sec. 3.1.2; H&S Sec. 2.5 pp. 22-3. 11.29: 私の使っているコンパイラは、考えられる限りの一番単純なテストプ ログラムに対しても、ありとあらゆる文法エラーではねつける。 A: きっと君が使っているのはANSI規格が決まる前に作られたコンパイラ で、関数プロトタイプの類をはねつけるからである。 質問1.31, 10.9, 11.30, 16.2aも参照のこと。 11.30: ANSIコンパイラを使っているのに、ANSI/ISO規格のライブラリルー チンで未定義となるものがあるのはなぜか。 A: ANSIの構文は受け付けるけれどANSI互換のヘッダファイルやランタイ ムライブラリが導入されていないコンパイラは珍しくない。(実際に はこのような状況は、ベンダーが提供しているのではないgccのよう なコンパイラを使うときにはよくあることである。) 質問11.29, 13.25, 13.26も参照のこと。 11.31: 誰か古い書き方のCプログラムをANCI Cに変換するプログラムや、そ の反対を行うツール、また自動的に関数プロトタイプを生成する ツールを持っていないか。 A: protoizeとunprotizeというのが、関数プロトタイプから古い書き方 の関数定義や宣言へ、あるいはその逆を実行するツールである(これ らのツールは、"古い"書き方のCとANSI C間の完璧な変換ツールでは ない)。FSFのGNU Cコンパイラの正式な配布の一部である。質問18.3 も参照のこと。 プログラムunproto(サイトftp.win.tue.nlのファイル /pub/unix/unproto5.shar.Z)は、プリプロセッサとコンパイラの間 に入って、ANSI Cを古いCに(コンパイル時に)変換するフィルターで ある。 GNU GhostScriptパッケージにはansi2knrという、ちょっとしたプロ グラムが付いている。 ANSI Cで書いたプログラムを古いCに変換する前に、このような変換 は安全に行うことも自動的に行うこともできないことに注意すること。 ANSI CはK&Rにない新しい機能を導入し、より複雑になっている。プ ロトタイプ付きの関数の呼び出しには特に注意すること。たぶん明示 的なキャストが必要となるだろう。質問11.3と11.29も参照のこと。 関数プロトタイプの自動生成ソフトはいくつか存在する。多くはlint に手を入れたものになっている。CPROTOというプログラムが1992年3 月にcomp.sources.miscにポストされた。他にもcextractというツー ルが存在する。コンパイラのベンダーの多くは単純なユーティリティ を用意している。質問18.16も参照のこと。(古いコード用にプロトタ イプを生成するときは"狭い"引数に注意すること。質問11.3も参照の こと。) 最後に一言。大量の古いコードをANSI Cに変換する必要が本当にある のか。古い書き方の関数の構文はそのまま使える。あわてて変換する とバグを持ち込みやすい。(質問11.3を参照。) 11.32: なぜANSI準拠が売り物のCコンパイラ轟天が、このコードをはねつけ るのか。私はこのコードはANSIに規定された通りだと考える。なぜな らgccなら受け付けるからである。 A: たいていのコンパイラは規格外の拡張機能をいくつか用意している。 gccはその最右翼である。はねられたコードが、そのような拡張機能 を使っていない自信があるか。言語の性質を調べるのに特定のコンパ イラだけで実験することはよい考えではない。すなわち、規格が拡張 を容認しているかもしれないし、実験に使ったコンパイラの誤りかも しれない。質問11.35も参照のこと。 11.33: 処理系定義の(implementation-defined)動作、未規定の (unspecified)動作、未定義の(undefined)動作を区別することを世の 中では重視するようだ。違いは? A: 簡潔に説明する。処理系定義の動作とは、どう振る舞うかを実装が選 択して、その動作を文書にすることを意味する。未規定の動作とは、 どう振る舞うかを実装が選択しなければならないが文書にする必要は ないことを意味する。未定義とは、本当にどんなことがおこっても不 思議ではないことを意味する。どの場合にも規格は、必要条件を課し ていない。最初の2つの動作について規格は時々ありそうな動作をい くつか提案している(そのなかから選択しなければならないかもしれ ない)。 規格は未定義の動作に直面したときのコンパイラの動作について何も 必要条件を設定していないことに注意すること。だからコンパイラは 本当に何をしてもいいことになる。プログラムの中に未定義の動作が あっても何とかやっていけると考えるのは大変危険である。比較的簡 単な例が質問3.2に出てくる。 移植性のあるコードを書きたいのなら上の3つの違いを無視すること ができる。これら3つの動作のどれにも頼らないコードを書こうと思 うだろうから。 質問3.9と11.34も参照のこと。 References: ANSI Sec. 1.6; ISO Sec. 3.10, Sec. 3.16, Sec. 3.17; Rationale Sec. 1.6. 11.34: ANSI規格には多くの論点が未定義のまま放置してあることを考えると ぞっとする。規格の仕事はこれらの問題を標準化することではなかっ たのか。 A: C言語は昔からその一部はコンパイラやハードウエアがどう実装され てどう振る舞ってもいいようになっている。こういうふうにわざとはっ きりさせないことで、コンパイラはありそうもないような状況にまで 適切に定義された動作を保証することのためにすべてのプログラムに 余計なコードの重荷を背負わせることなく、よくある場合についてずっ と効率のいいコードを生成することができる。よって規格はそれまで に存在した習慣を単に明文化したにすぎない。 プログラミング言語の規格は言語の使用者とコンパイラの実装者の間 の契約と考えればいい。その契約の一部はコンパイラ実装者が提供を 約束した機能で、使い手は用意されているものだと思いこんでいい。 しかしながら、別の部分はユーザーが従うと約束した決まりで、実装 者は守ってもらえると思いこんでいい。両方が自分の約束を守るかぎ り、プログラムは頑張れば動く可能性がある。どちらかでも公約を破 れば、どんな部分についても確実に動くとは保証できない。 質問11.35も参照のこと。 References: Rationale Sec. 1.1. 11.35: i = i++の動作が未定義だとうるさくいう人がいるけれどANSI準 拠のコンパイラで試して私が思うとおりの結果を得た。 A: 未定義の動作に出くわしたらコンパイラは好きなように振る舞う(実 装が定義した動作、あるいは未規定の振る舞いに出くわしたときにも ある程度好きなように振る舞う)。その中にはあなたが期待した結果 も含む。こんなことに頼るのは馬鹿げている。質問11.32と11.33と 11.34を参照のこと。 12章 標準入出力(stdio)ライブラリ 12.1: なぜ以下のコードはうまく動かないのか。 char c; while((c = getchar()) != EOF) ... A: getchar()の戻り値を格納する変数はintでなければならない。 getchar()は、文字型のあらゆる値を返すだけでなくEOFも返す。 getchar()の戻り値をcharで渡すと、普通の文字が返ってきたのにEOF と誤解されたり、EOFが他の値に変えられて(特にデータ型charが符号 なしの場合)、いつになってもEOFが出てこないかもしれない。 References: K&R1 Sec. 1.5 p. 14; K&R2 Sec. 1.5.1 p. 16; ANSI Sec. 3.1.2.5, Sec. 4.9.1, Sec. 4.9.7.5; ISO Sec. 6.1.2.5, Sec. 7.9.1, Sec. 7.9.7.5; H&S Sec. 5.1.3 p. 116, Sec. 15.1, Sec. 15.6; CT&P Sec. 5.1 p. 70; PCS Sec. 11 p. 157. 12.2: なぜ以下のコードは最後の行を2回コピーするのか。 while(!feof(infp)) { fgets(buf, MAXLINE, infp); fputs(buf, outfp); } A: Cでは、EOFは入力ルーチンが読もうとしてファイルの終わり (End-Of-File)にたどり着いた後であることを示しているだけである (言い換えればC言語の入出力はPascalの入出力とは異なる)。たいて いは入力関数(この場合はfgets)の戻り値をチェックすればよい。 feof()を使う必要が全くない場合が多い。 References: K&R2 Sec. 7.6 p. 164; ANSI Sec. 4.9.3, Sec. 4.9.7.1, Sec. 4.9.10.2; ISO Sec. 7.9.3, Sec. 7.9.7.1, Sec. 7.9.10.2; H&S Sec. 15.14 p. 382. 12.4: 私のプログラムのプロンプトと中間出力が、画面上にあらわれないこ とがある。特に、パイプを通して出力を他のプログラムに渡したとき に、この問題が起こる。 A: 出力が見えて欲しいところでは、必ず明示的にfflush(stdout)を使っ て出力をはきだすこと。ほっておいてもいくつかの仕組みが、 fflush()を"適切なときに"実行してくれる。しかし、これはstdoutが 端末のときにしか適応されないことが多い(質問12.24も参考のこと)。 References: ANSI Sec. 4.9.5.2; ISO Sec. 7.9.5.2. 12.5: RETURNキーが押されるのを待つことなく、一度に一文字ずつ読むこと ができるか。 A: 質問19.1を参照のこと。 12.6: どうすれば、printfのフォーマット文字列を使って'%'を出力できる のか。\%を試したけれど、うまくいかなかった。 A: %%というふうに、%を二回続けて書く。 \%ではうまくいかない。バックスラッシュ\はコンパイラのエスケー プシーケンスで、printfのエスケープシーケンスは%である。 質問19.17も参照のこと。 References: K&R1 Sec. 7.3 p. 147; K&R2 Sec. 7.2 p. 154; ANSI Sec. 4.9.6.1; ISO Sec. 7.9.6.1. 12.9: %lfをprintf()で使うのは間違っていると教えてくれた人がいた。 scanf()が%lfを使うのに、どうしてprintf()は%fを使うのか。 A: printf()の%f指定子はfloatの引数にもdoubleの引数にも作用する。 "省略時の引数の格上げ"により(この規則はprintfのような可変個数 の引数を取る関数に、その関数のプロトタイプがスコープ内でも外で も適応される)、データ型floatの変数はdoubleに格上げされる。よっ てprintf()が見るのはdoubleの変数だけである。質問12.13, 15.2も 参照のこと。 References: K&R1 Sec. 7.3 pp. 145-47, Sec. 7.4 pp. 147-50; K&R2 Sec. 7.2 pp. 153-44, Sec. 7.4 pp. 157-59; ANSI Sec. 4.9.6.1, Sec. 4.9.6.2; ISO Sec. 7.9.6.1, Sec. 7.9.6.2; H&S Sec. 15.8 pp. 357-64, Sec. 15.11 pp. 366-78; CT&P Sec. A.1 pp. 121-33. 12.10: printf()でフィールドの幅を可変に実装するのはどうすればいいのか。 つまり、%8dと書く替わりに、実行時に幅を指定したい。 A: printf("%*d", width, n)が君の望むことをやってくれる。質問12.15 も参照のこと。 References: K&R1 Sec. 7.3; K&R2 Sec. 7.2; ANSI Sec. 4.9.6.1; ISO Sec. 7.9.6.1; H&S Sec. 15.11.6; CT&P Sec. A.1. 12.11: 千単位でコンマで切って数を表示するのはどうすればよいか。通貨に 合わせた書式で表示するのは。 A: にあるルーチンでこれらの操作に対応するものも出始めた。 しかし上のどちらの仕事についても標準のルーチンというのは存在し ない。(printf()が、その土地の慣習に従うことといえば小数点の表 示に使う文字を変更することだけである。) References: ANSI Sec. 4.4; ISO Sec. 7.4; H&S Sec. 11.6 pp. 301-4. 12.12: なぜコードscanf("%d", i);がうまく動かないのか。 A: scanf()は、値を書き込む変数へのポインタを必要とする。 scanf("%d", &i);と書かなければならない。 12.13: なぜ以下のコードは動かないのか。 double d; scanf("%f", &d); A: printf()と違ってscanf()ではdoubleには%lfを、floatには%fを使う。 質問12.9も参考のこと。 12.15: scanf()の書式文字列で可変の幅を指定するのはどうやればよいか。 A: できない。scanf()の書式文字列に*を書くと、代入を抑 制することとなる。ANSIの文字列作成(stringizing)や文字列連結の 機能を使って、ほとんど同じことができる。scanf()の書式文字列を その場で作るのもいい。 12.17: scanf()を使ってキーボードから読み取ると、もう1行余計に打ち込 むまで、ハングするようだ。 A: 知らなくて驚くだろうけど、scanfの書式文字列で\nは改行を意味す るのではなく、空白が続くかぎり読んでは捨てることを意味している。 質問12.20も参照のこと。 References: K&R2 Sec. B1.3 pp. 245-6; ANSI Sec. 4.9.6.2; ISO Sec. 7.9.6.2; H&S Sec. 15.8 pp. 357-64. 12.18: scanfの%dを使って数字を読んで、文字列をgets()で読もうとしてい る。けれどコンパイラはgets()の呼び出しを飛ばしているようだ。 A: scanfの%dはおしりの改行は食べてくれない。もし入力の数のすぐ後 ろに改行が来たら、その改行はgets()の入力としての条件を満足する。 一般に、scanf()の呼び出しと、gets()(その他の入力ルーチンも)の 呼び出しを混ぜて使ってはいけない。scanf()の改行の扱いが妙なの で必ずやっかいなことになる。scanf()ですべて読み込むか、scanf() を使わないかのどちらかである。 質問12.20, 12.23も参照のこと。 References: ANSI Sec. 4.9.6.2; ISO Sec. 7.9.6.2; H&S Sec. 15.8 pp. 357-64. 12.19: scanf()を使うのときに、こっちの望んだ数値をユーザーが入力した ことを確かめるのに、scanf()の戻り値を調べるほうが安全だと考え た。けれど、たまに無限ループに入ってしまうようだ。 A: scanf()が数字を変換しようとするときには、数字でない文字に出く わすと変換を終了し、その上その文字を入力ストリームに放置す る。したがって他の手段を取らないと、数字でない入力が思いがけな いところで出てくるとscanf()が何度も"ジャム"ってしまう。scanf() は問題となる文字を通り過ぎて、その後ろにある正しいデータに行く ことはない。もしユーザーがscanf()の数値のフォーマットである%d や%fなどに対して'x'のような文字を入力したとすると、単にプロン プトをまた出して同じscanf()を呼び出すようなコードは、即座に同 じ'x'に出くわすことになる。 質問12.20も参照のこと。 References: ANSI Sec. 4.9.6.2; ISO Sec. 7.9.6.2; H&S Sec. 15.8 pp. 357-64. 12.20: どうして誰もがscanf()を使わないほうがいいというのか。代わりに 何を使えばいいのか。 A: scanf()には数多くの問題がある。質問12.17, 12.18, 12.19を参照の こと。さらにscanf()の%sフォーマットはgets()が抱えているのと(質 問12.23を参照)同じ問題を抱えている。つまり受け取りに使うバッファ があふれる心配がないと保証するのは難しい。 もっと一般的な話として、scanf()はかなりかっちりした構造を持っ た、書式にのっとった入力を対象に設計されている(実際scanfという 名前は「scan formatted(書式を持ったものをスキャンする)というと ころから来ている」)。注意していれば、処理が成功したかどうかわ かる。しかし、どこで失敗したかは大体のところしか分からないし、 どういう風にとか何故かはぜんぜんわからない。scanf()を使った場 合エラーからのまっとうな復帰はほとんど不可能である。たいていは 1行丸々(fgets()かなにかで)読み込んで、sscanf()かその他の技を使っ て解釈するほうがずっと簡単である。(strtol()とかstrtok()とか atoi()といったルーチンも役に立つことが多い。質問13.6も参照のこ と。)どうしてもsscanf()を使わなければならなければ、戻り値を調 べてこちらが期待した数だけ項目が見つかったことを確認するのを忘 れてはいけない。さらに、%sを使うときは、バッファをあふれさせな いように注意すること。 References: K&R2 Sec. 7.4 p. 159. 12.21: sprinftの呼び出しで、書く先のバッファがどれくらいの大きさが必 要かどうすればわかるか。sprinftの呼び出しで書く先のバッファを、 どうすればあふれさせずにすむか。 A: この2つの鋭い質問には立派な解答は(まだ)ない。ないということは、 これまでの標準入出力ライブラリの最大の欠陥をたぶん表わしている。 書式文字列が既知で結構単純なときは、毎回特別な方法でバッファの 大きさを予想することができる。書式が1つか2つの%sから出来ている ときは、固定の文字の部分は自分で数えて(または代わりにsizeofに 数えさせて)、挿入される文字列の分はstrlen()を呼んで数えさせ、 結果を加える。%dがどれくらいの大きさを占めるかは、以下の大きさ を越えることはない。 ((sizeof(int) * CHAR_BIT + 2) / 3 + 1) /* +1 for '-' */ (CHAR_BITはに定義されている。)ただし上の計算は少し控 え目過ぎるかもしれない。(上のコードは8進数表現の数を表わすのに 必要な文字の数を計算するものである。10進数表現なら必ず同じかよ り小さい領域で済む。) 書式文字列がもっと複雑だったり実行時までわからない場合は、どれ くらい大きなバッファが必要か予想することはsprintf()を改めて実 装するのと同じくらい困難となり、それだけ間違いを犯しやすくなる (したがって勧めない)。最後の手段として時々提案されるのは fprintf()を使って同じテキストをビットバケツや一時ファイルに書 き込んで、fprintf()の戻り値やファイルの大きさを見ることである (ただし質問19.12も参照のこと。書き込みエラーについても注意する こと)。 バッファがそんなに大きくないかもしれない場合は、バッファがあふ れないとかメモリの別の部分を上書きしないと保証されない限り、 sprintf()を呼びたくないだろう。いくつかの標準入出力ライブラリ (GNUや4.4BSDの)は名前から機能がわかるsnprintf()関数を用意して いる。これは以下のように使う。 snprintf(buf, bufsize, "You typed \"%s\"", answer); ANSI/ISO Cの将来の改訂がこの機能を取り入れることを願うばかりで ある。 References: Rationale Sec. 4.9.7.2; H&S Sec. 15.7 p. 356. 12.23: なぜgets()を使うなと誰もが言うのか。 A: fgetsと違ってgetsには、これから読もうとしているバッファの大き さを伝えることができないので、バッファがあふれてしまうことを防 ぐことができない。一般的な規則として、いつもfgets()を使う。質 問7.1の、gets()の代わりにfgets()を使う方法を説明したコードを参 考のこと。 12.24: なぜprintf()を呼ぶと変数errnoにENOTTYが設定されるのか。 A: 標準入出力パッケージの多くの実装はstdoutの先が端末のときは動作 を少し変える。こういう実装では、出力先を判断するためにstdoutの 先が端末でないときは必ず失敗する操作(これがENOTTYを設定する)を 行う。よって出力の操作が完全に成功するにもかかわらずerrno の値はENOTTYとなる。(プログラムがerrnoの中身を調べることは、エ ラーが報告されたときだけ意味があることに注意。) References: ANSI Sec. 4.1.3, Sec. 4.9.10.3; ISO Sec. 7.1.4, Sec. 7.9.10.3; CT&P Sec. 5.4 p. 73; PCS Sec. 14 p. 254. 12.25: fgetpos/fsetposとftell/fseekの違いは。fgetpos()やfsetpos()は何 の役に立つのか。 A: ftell()とfseek()はファイル内でのオフセット(位置)を表すのにlong intを使う。したがって、オフセットは20億(2**31-1)までに限定され てしまう。一方、新しい関数であるfgetpos()とfsetpos()は特別な typedefであるfpos_tをオフセットを表すのに使う。fpos_tを適切に 選べばオフセットの大きさを好きなように選ぶことができる。質問 1.4も参照のこと。 References: K&R2 Sec. B1.6 p. 248; ANSI Sec. 4.9.1, Secs. 4.9.9.1,4.9.9.3; ISO Sec. 7.9.1, Secs. 7.9.9.1,7.9.9.3; H&S Sec. 15.5 p. 252. 12.26: ユーザーが先打ちした入力が、次に入力待ちするところで読まれるこ とがないように、処理されていない入力を捨てることができるか。 fflush(stdin)は効くか。 A: fflush()は、出力ストリームにしか定義されていない。fflushの "flush(押し流す)"は、バッファリングされた文字の(捨てるのではな く)書き込みを達成することを意味している。読まれてない入力を捨 てることは、入力ストリームをfflushするということと同じ意味では ない。 標準入出力の入力バッファの読んでいない入力を捨てる一般的な方法 はないし、そんなことをするだけでは充分でない。読まれていない文 字は、オペレーティングシステムレベルのバッファにもため込まれて いる可能性がある。\nが出るまで文字を読んでは捨てて、それから cursesのflushinp()やシステム固有の技を使うことで済ますことがで きるかもしれない。質問19.1と19.2も参照のこと。 12.30: fopenのモード「r+」を使ってファイルを開いて、次にある文字列を 読んで、最後に修正した文字列を書き戻すことでファイルを更新しよ うとしている。でもうまくいかない。 A: 書く前にfseek()を必ず呼ぶこと。上書きしようとしている文字列の 先頭にシークして戻すことが必要であるし、読み書き両方可能の「+」 モードでデータを読むときと書くときの間にはfseek()かfflush()が 必要である。もともとあった文字と同じ個数しか上書きできないこと、 テキストモードで上書きするとファイルがその点で切り捨てられてし まうかもしれないことを忘れてはいけない。質問19.14も参照のこと。 References: ANSI Sec. 4.9.5.3; ISO Sec. 7.9.5.3. 12.33: プログラムの中からstdinやstdoutの先をファイルにリダイレクトす るのはどうすればよいか。 A: freopen()を使う(ただし以下の質問12.34を参照のこと)。 References: ANSI Sec. 4.9.5.4; ISO Sec. 7.9.5.4; H&S Sec. 15.2. 12.34: freopen()を使った後で、元のstdout(あるいはstdin)に戻すことがで きるか。 A: よい方法は存在しない。もし行ったり来たりしたいのであれば、最高 の方法はfreopen()なんて使わないことである。自分で明示的に定義 した出力(あるいは入力)ストリーム変数を使うこと。そうすれば思う ままに出力(または入力)を割り付けて、しかも元のstdout(あるいは stdin)を壊さなくてすむ。 12.38: バイナリデータのファイルを、ちゃんと読むにはどうすればよいか。 ときどき0x0aとかx0dといった値が壊れてしまうのを目撃する。デー タに0x1aが含まれていると本来より早くEOFに出くわすようだ。 A: バイナリデータを読み込むときには、fopen()を呼ぶ際に、テキス トファイルの変換が実行されないよう、"rb"モードを指定すること が必要である。同じように、バイナリファイルを書き込むときは、 "wb"を使わなければならない。 テキストとバイナリはファイルをオープンするときに区別される。 いったんファイルを開いたら、ファイルにどちらの入出力コールをして いるかは関係ない。質問20.5も参照のこと。 References: ANSI Sec. 4.9.5.3; ISO Sec. 7.9.5.3; H&S Sec. 15.2.1 p. 348. 13章 ライブラリ関数 13.1: どうすれば数を文字列に変換することができるか(atoiの反対)。関数 itoaというのは存在するのか。 A: なにも考えずにsprintf()を使え。(「sprintfはやりすぎだ。実行時 間とコード領域を無駄遣いする。」という声は無視すること。とにか く動くんだから。) 質問7.5の解答内の例を参照のこと。質問12.21も 参照のこと。 すぐわかるようにsprintf()を使えば(%ldや%fを使って)longや浮動小 数点数も文字列に変換することができる。 References: K&R1 Sec. 3.6 p. 60; K&R2 Sec. 3.6 p. 64. 13.2: なぜstrncpy()はコピー先の文字列に、終端文字の'\0'を付けないこ とがあるのか。 A: もともとはstrncpy()は、今となっては時代遅れになってしまったデー タ構造、すなわち固定長で必ずしも\0で終わるとは限らない"文字列" を扱うために設計された(strncpy()の関連する奇妙なところは、指定 された長さになるまで\0を埋めることである)。確かにこの"文字列" 以外を扱うときは少し間抜けである。しばしば目的の文字列に、自分 で' \0'を追加しなければならない。この問題はstrncpy()の代わりに strncat()を使うことで回避することができる。もしコピー先の文字 列が最初は空なら、strncat()が、strncpy()がたぶんやってくれると 思っていたことをやってくれる。他にはsprintf(dest, "%.*s", n, source)が考えられる。 (文字列ではなく)任意のバイトをコピーするときには、普通は memcpy()のほうがstrncpy()よりも適切なルーチンである。 13.5: どうしてtoupper()の中には大文字を与えると変な動きをするものが あるのか。どうしてコードによっては、toupper()を呼ぶ前に islower()を呼んでいるのか。 A: 古い版のtoupper()やtolower()は変換が必要でない引数(数字や句読 点や既に文字が望みのcaseに(toupper()の場合は大文字に、 tolower()の場合は小文字に)なっているときは)には正しく動かない 場合があった。ANSI/ISO 規格のCでは、これらの関数はすべての文字 に対してきちんと動くことが保証されている。 References: ANSI Sec. 4.3.2; ISO Sec. 7.3.2; H&S Sec. 12.9 pp. 320-1; PCS p. 182. 13.6: 文字列を空白文字列によって区切られたフィールドに分けるのはどう やればよいか。これをmain()の引数のargcとargvのように分けるには どうすればよいか。 A: この種の"字句単位に切る(tokenizing)"ルーチンで標準で使えるのは strtok()だけである。ただし使うにはコツがいるし、やりたいことを 全部やってくれるとは限らない(例えば引用符の問題)。 References: K&R2 Sec. B3 p. 250; ANSI Sec. 4.11.5.8; ISO Sec. 7.11.5.8; H&S Sec. 13.7 pp. 333-4; PCS p. 178. 13.7: 正規表現とかワイルドカードを使った比較をするコードが必要となっ た。 A: まず、古典的な正規表現(Unixのedやgrepといったユーティリティで 使われている)とファイル名のワイルドカード(たいていのオペレーティ ングシステムで使われている)の違いが分かっているかどうか確認す ること。 正規表現による比較をするパッケージは数多く用意されている。たい ていのパッケージは一組の関数からなっている。正規表現を"コンパ イル"する関数と、その結果を"実行"する関数である(結果と文字列の 比較をする)。とかといった名前のヘッダファ イルや、regcmp()/regex()とかregcomp()/regexec()とか re_comp()/re_exec()といった名前の関数の組を探すこと(これらの組 は独立の正規表現ライブラリに存在するかもしれない)。自由に配布 していい人気のある正規表現パッケージとしてHenry Spencerの書い たものがある。これはcs.toronto.eduのpub/regexp.shar.Zや、その 他のアーカイブから入手可能である。GNUプロジェクトはrxという名 前のパッケージを用意している。質問18.16も参照のこと。 ファイル名のワイルドカードによる一致(globbingとも呼ばれる)はシ ステムによってやりかたが異なる。Unixでは、ワイルドカードはプロ セスが起動される前にシェルによって自動的に展開されるので、プロ グラムが心配しなければならない場合は滅多にない。MS-DOSのコンパ イラの多くは、特別なオブジェクトファイルを用意していて、プログ ラムとリンクするとargvを作るときにワイルドカードを展開してくれ る。いくつかのシステム(MS-DOSやVMSを含む)はワイルドカードで指 定されたファイルの一覧を表示したりファイルを開いたりするサービ スを用意している。コンパイラ/リンカについてきた資料をよく読む こと。質問19.20と20.3も参照のこと。 13.8: 文字列の配列をqsort()でソートするのに、strcmp()を比較用の関数と して使用しているが、うまくいかない。 A: "文字列の配列"とはたぶん"charへのポインタからなる配列"を意味し ていると思う。qsortの比較用の関数の引数は、ソートの対象へのポ インタである。ここではcharへのポインタへのポインタであるしかし ながら、strcmpはcharへの単なるポインタを引数とする。したがって strcmp()を直接使うことはできない。以下のような間に入る比較の関 数を書く。 /* ポインタを通して比較する */ int pstrcmp(const void *p1, const void *p2) { return strcmp(*(char * const *)p1, *(char * const *)p2); } 比較に使う関数の引数は"汎用のポインタ"const void *で表わされる。 引数は実際の型(char **)に再び変換されて、間接参照される。その 結果char *の引数となってstrcmp()に渡される。(ANSI以前のコンパ イラで使うときは、ポインタ引数をvoid *ではなくchar *と宣言して、 constを削る。) (K&R 2 Sec. 5.11 pp. 119-20の議論を読むときは用心すること。そ こでは標準ライブラリのqsortを議論しているわけではない。) References: ANSI Sec. 4.10.5.2; ISO Sec. 7.10.5.2; H&S Sec. 20.5 p. 419. 13.9: qsort()を使って構造体の配列をソートしようとしている。私が書いた 比較ルーチンは、構造体へのポインタを引数として取る。けれどコ ンパイラは、私の関数がqsort()の引数としては間違ったデータ型だ と文句を付ける。関数へのポインタをどのようにキャストすれば警 告を消し去ることができるのか。 A: 型の変換を比較関数内で行わなければならない。比較関数は上の Q12.2で議論したように"汎用のポインタ" (const void *)を引数と して取ると宣言されていなければならない。コードは以下のようになる だろう。 int mystructcmp(const void *p1, const void *p2) { const struct mystruct *sp1 = p1; const struct mystruct *sp2 = p2; /* これからsp1->whateverとsp2-> ...を比較する */ (汎用のポインタから構造体mystructを指すポインタへの変換が、初 期化sp1 = p1 と sp2 = p2で行われる。p1とp2がvoidのポインタであ るから、コンパイラが暗黙のうちに変換を行う。ANSI以前のコンパイ ラを使うときは明示的なキャストとchar *のポインタを使うことが必 要となる。質問7.7も参照のこと。) 一方、構造体へのポインタをソートするのであれば質問13.8のように 間接参照が必要である。 sp1 = *(struct mystruct **)p1 ) 一般に、"コンパイラを黙らせる"だけのためにキャストをつけるのは いい考えではない。コンパイラの警告は、たいてい何かを伝えようと してるわけで、自分がなにをやってるかよく理解していないのに、無 視したり黙らせたりするとひどいめにあう。質問4.9も参照のこと。 References: ANSI Sec. 4.10.5.2; ISO Sec. 7.10.5.2; H&S Sec. 20.5 p. 419. 13.10: リンク付きリストをどうやってソートすればよいか。 A: リストを作っている間ずっとリストの要素の順が乱れないようにして おくほうが(あるいは木(tree)を使うほうが)、後でソートするよりも やさしいこともある。挿入ソート(insertion sort)やマージソート (merge sort)のようなアルゴリズムはリンク付きリストといっしょに 使うのに向いている。標準のライブラリ関数を使いたいなら、ポイン タの配列を一時的に確保して、配列内の各ポインタをそれぞれリスト の各ノードを指すようにする。qsort()を呼んで、最後にソート済み の配列に基づいてリストのポインタを再構築する References: Knuth Sec. 5.2.1 pp. 80-102, Sec. 5.2.4 pp. 159-168; Sedgewick Sec. 8 pp. 98-100, Sec. 12 pp. 163-175. 13.11: メモリに収まりきらない量のデータをどうやってソートすればよい か。 A: 「外部ソート(external sort)」を使えばよい。これは例のKnuthの第 3巻で読むことができる。基本的な概念はデータを塊にしてソートし て(一度にメモリに納まるほどの大きさで)、ソート済みの各塊を一時 的にファイルに書き込んで、ファイルを1つにまとめる。オペレーティ ングシステムが汎用のソートユーティリティを提供してるかもしれな い、提供されていたらプログラムの中から呼び出してみればいい。質 問19.27と19.30も参照のこと。 References: Knuth Sec. 5.4 pp. 247-378; Sedgewick Sec. 13 pp. 177-187. 13.12: Cのプログラムで時刻を得るのはどうすればよいか。 A: 関数time()とctime()かlocaltime()、またはこれらのすべてを使う (これらの関数は昔から存在するしANSI規格の中にも存在する)。以下 に簡単な見本を載せておく。 #include #include main() { time_t now; time(&now); printf("It's %.24s.\n", ctime(&now)); return 0; } References: K&R2 Sec. B10 pp. 255-7; ANSI Sec. 4.12; ISO Sec. 7.12; H&S Sec. 18. 13.13: ライブラリ関数localtime()はtime_tを構造体tmに変換することと、 ctime()はtime_tを印刷可能な文字列に変換するということは知って いる。さて構造体tmや文字列からtime_tへの逆変換はどうやって 行なえばよいか。 A: ANSI Cは、構造体tmをtime_tに変換するライブラリ関数mktime()を 用意している。 文字列をtime_tに変換するのは、もう少し骨が折れる。なぜなら解析 しなければならない日付や時間の書式の種類が広範囲にわたるからで ある。strptime()という関数を用意しているシステムもある。これは 基本的にはstrftime()の逆の動作をする。他に人気のあるルーチンと してはpartime(RCSのパッケージとともに配布されている)や getdate() (その他のいくつかの関数と共にCニュースのパッケージで 配布されている)がある。質問18.16を参照のこと。 References: K&R2 Sec. B10 p. 256; ANSI Sec. 4.12.2.3; ISO Sec. 7.12.2.3; H&S Sec. 18.4 pp. 401-2. 13.14: ある日からN日後が何月何日かをどうやって計算するのか。2つの日付 の差はどうやって計算するのか。 A: ANSI/ISO規格のC言語は、mktime()とdifftime()という関数を、上の 要求に答えるために用意している。mktime()は正規化されていない日 付を引数に取る。データを設定済みの構造体tmを用意して、tm_field フィールドに日付を足したり引いたりしてからmktime()を呼んで年・ 月・日のフィールドを正規化(ついでにtime_t型の値に変換)すること は難しくない。difftime()は二つのtime_tの値の差を秒で計算する。 引き算に使う日付のtime_tを計算するのにmktime()を使うことができ る。 これらの解はtime_tで表現できるに日付が収まるときにだけ正しく動 くことを保証されている。tm_mdayフィールドはintで、32,736より大 きな日付のオフセットはオーバーフローする可能性がある。夏時間と の切替時には、その土地の1日は24時間ではないことにも注意するこ と(したがって86400で割り切れると思い込まないこと)。 両方の質問の別の取り組みかたとしては"ユリアス日(Julian day)"数 を使う方法がある。ユリアス日ルーチンの実装はSimtel/Oaklandアー カイブ(質問18.16参照)からJULCAL10.ZIPという名前で入手可能であ る。参考文献に挙げた"Data conversions"という記事からも得ること ができる。 質問13.13, 20.31, 20.32も参照のこと。 References: K&R2 Sec. B10 p. 256; ANSI Secs. 4.12.2.2,4.12.2.3; ISO Secs. 7.12.2.2,7.12.2.3; H&S Secs. 18.4,18.5 pp. 401-2; David Burki, "Date Conversions". 13.15: 乱数発生器が欲しい。 A: 標準のCライブラリにrand()というのが存在する。君が使っているシ ステムのrand()の実装は完璧でないかもしれないが、よりよい関数を 書くのは容易でないことも事実である。 自分で乱数発生器を実装するはめに陥ったとしても数多くの文献が出 まわっている。Referencesを参照のこと。ネットの上にもたくさんの パッケージが流れている。r250とかRANLIBとかFSULTRAといった名前 のパッケージを探すこと(質問18.16参照)。 References: K&R2 Sec. 2.7 p. 46, Sec. 7.8.7 p. 168; ANSI Sec. 4.10.2.1; ISO Sec. 7.10.2.1; H&S Sec. 17.7 p. 393; PCS Sec. 11 p. 172; Knuth Vol. 2 Chap. 3 pp. 1-177; Park and Miller, "Random Number Generators: Good Ones are hard to Find". 13.16: ある範囲の整数からなる乱数はどうやったら生成することができるか。 A: すぐに思い付く、 rand() % N /* タコ */ (これは0からN-1までの数を返そうとする)は乱数の質が低い。なぜな ら乱数発生器の多くで下位のビットは悲しいほどランダムでない(質 問13.18を参照のこと)。よりよい方法は以下のようなものである。 (int)((double)rand() / ((double)RAND_MAX + 1) * N) 浮動小数を使うことが気になるのなら、以下の方法を試せばよい。 rand() / (RAND_MAX / N + 1) どちらの方法もRAND_MAX(ANSIはで定義している)の値を知っ ていることが当然必要である。またどちらもNがRAND_MAXにくらべて 十分小さいことを仮定している。 (ところで、RAND_MAXはCライブラリのrand()関数が返す値の範囲を 表わす定数である。RAND_MAXを他の値に変更することはできないし、 rand()に別の範囲の数を返すように指定することもできない。) 0から1の間の小数を返す乱数発生器を元にするなら、単にその乱数発 生器の出力にNを掛けると0からN-1の範囲の乱数発生器が得られる。 References: K&R2 Sec. 7.8.7 p. 168; PCS Sec. 11 p. 172. 13.17: 私のプログラムを走らせるたび、いつも関数rand()から同じ乱数列が 返ってくる。 A: srand()を使って、擬似乱数発生器に本当にランダムな初期値を与え ればよい。よくある乱数の種は、ユーザーがキーを押してからの経過 時間や時刻である(ただしキーが押された時間というのは移植性の高 い方法では求めにくい。質問19.37を参照のこと)。(プログラム中で srand()を2回以上呼んで役に立つことは滅多にない。"本当にランダ ムな"数を得ようと思って、rand()を呼ぶ前に毎回srand()を呼ぶよう な真似はぜったいにしてはいけない。) References: K&R2 Sec. 7.8.7 p. 168; ANSI Sec. 4.10.2.2; ISO Sec. 7.10.2.2; H&S Sec. 17.7 p. 393. 13.18: 真偽値からなる乱数が欲しいのでrand() % 2を使ったところ、結果は 0と1が交互に現れるだけだった。 A: 出来の悪い擬似乱数発生器では(いくつかのシステムに乗っているも のは不幸なことにそうである)下位のビットはあまりランダムではな い。上位のビットを使うこと。質問13.16を参照のこと。 13.20: 正規分布つまりガウス分布の乱数を生成するのはどうすればよいか。 A: 以下のBoxとMullerの方法はKnuthご推薦である。 #include #include double gaussrand() { static double V1, V2, S; static int phase = 0; double X; if(phase == 0) { do { double U1 = (double)rand() / RAND_MAX; double U2 = (double)rand() / RAND_MAX; V1 = 2 * U1 - 1; V2 = 2 * U2 - 1; S = V1 * V1 + V2 * V2; } while(S >= 1 || S == 0); X = V1 * sqrt(-2 * log(S) / S); } else X = V2 * sqrt(-2 * log(S) / S); phase = 1 - phase; return X; } この他の方法については、このFAQの拡張版(質問20.40参照)を参照の こと。 References: Knuth Sec. 3.4.1 p. 117; Box and Muller, "A Note on the Generation of Random Normal Deviates"; Press et al., _Numerical Recipes in C_ Sec. 7.2 pp. 288-290. 13.24: 古いプログラムを移植しようとしている。なぜ「未定義の外部シンボ ル」というエラーが出るのか。 A: これから(左側に)挙げるルーチンはそれぞれ時代遅れである。代わり に右側のルーチンを使え。 index? strchrを使え。 rindex? strrchrを使え。 bcopy? 1番目の引数と2番目の引数を入れ替えて memmoveを使え(質問11.25も参照のこと)。 bcmp? memcmpを使え。 bzero? 2番目の引数を0にしてmemsetを使え。 逆に、右側の列の関数が載っていない古いシステムを使っているのな ら、右側の関数を左側の関数を使って実装、あるいは代用することが できるかもしれない。 References: PCS Sec. 11. 13.25: 「ライブラリのルーチンが未定義」というエラーが出たままである。 正しいヘッダファイルをすべて#includeしたのに。 A: 一般に、ヘッダファイルはライブラリ関数の宣言を与えるだけで、 ライブラリ関数そのものを与えるわけではない。いくつかの場合 (とくに関数が標準でない場合)、プログラムをリンクするときに 正しいライブラリが見つかるように、パスを明記しなければならない (ヘッダファイルを#includeするだけではそんなことまでやってくれない)。 質問11.30, 13.26, 14.3を参照のこと。 13.26: それでも「ライブラリのルーチンが未定義」というエラーが出たまま である。今度はリンクするときにライブラリを明示的に指定したの に。 A: 多くのリンカは、指定したオブジェクトファイルとライブラリの リストを一回のパスで作り出し、ライブラリからその時点で未定義 の関数を含むモジュールだけを取り出す。だからオブジェクトファイ ルとライブラリのコマンドラインでの順序は重要である(オブジェ クトファイル間の順序も大事である)。普通はライブラリを最後に 探してもらいたい。(例えば、UNIXでは-lスイッチの類はすべてコマン ドラインの最後のほうに付けること。)質問13.28も参照のこと。 13.28: リンカが_endが未定義だと文句をつけてくるのはどういうときか。 A: このメッセージは、古いUnixのリンカの変なところである。_endが 未定義であるというエラーが出るのは、他にも未定義のものがあると きだけである。その他の部分を修正する。そうすれば_endのエラーは 消える。(質問13.25, 13.26も参照のこと。) 14章 浮動小数点 14.1: 浮動小数点の変数を、例えば3.1に設定すると、どうしてprintf()は 3.0999999と出力するのか。 A: たいていのコンピューターは浮動小数点数に、整数にと同じように、 2進数を用いている。2進数の1/1010(これは10進数では1/10)は無限循 環小数となる。2進表現は0.0001100110011...となる。コンパイラ の2進/10進変換ルーチン(たとえばprintf()で使われているもの)がど れくらい注意深く作られているかによって、2進数では正確に表現で きない数が(とくに精度の低い浮動小数が)代入されたり読み込まれて出 力されると(すなわち、2進から10進に変換し、10進から2進に変換し なおすと)食い違いが生じるかもしれない。質問14.6も参照のこと。 14.2: 平方根を求めようとしている。けれど、とんでもない数字が返ってく る。 A: まずを#includeしたことを確認して、次にdoubleを返す関数 を正しく宣言したことを確認する。(その他のライブラリルーチン で注意しなければいけないのは、atof()である。これはで 宣言されている。) 下の質問14.3も参照のこと。 References: CT&P Sec. 4.5 pp. 65-6. 14.3: ちょっとした三角関数の計算がしたくてを#includeしたけれ ど「_sin未定義」というコンパイルエラーが返ってきた。 A: まずは数学関数のライブラリが本当にリンクされていることを確認す ること。たとえばUnixではコンパイルあるいはリンクをする時に、コ マンドの一番最後に-lmオプションをつける必要がある。質問13.25と 13.26も参照のこと。 14.4: 浮動小数点の計算の結果が変で、しかもマシンによって違った答が返っ てくる。 A: まずは上の質問14.2を参照すること。 問題がそんなに単純でないときは、デジタルコンピュータが使ってい る浮動小数のフォーマットは、実数の演算のよくできたシミュレーショ ンを与えるものであって、決して正確なシミュレーションを与えるも のではないことをお忘れなく。桁落ちや、精度が徐々に失われていく ことや、その他の不合理なところが問題になることも多い。 浮動小数の結果が正確なんて考えないこと。とくに、浮動小数点数が 比較に使えるなんて考えないこと(いんちきな"誤差定数(fuzz factor)"を使うこともね。質問14.5参照)。 これらはC言語だけでなく他のプログラム言語でも問題となることで ある。浮動小数点の実装の多少の部分は、通常「プロセッサがやるこ とはなんでもあり」に定義されている。そうでなければ"正しい"浮動 小数点のモデルを持たないマシンではエミュレーションをするしかな く、そんなことになればコードのサイズと処理速度が犠牲になって使 い物にならない。 この記事では、浮動小数点を扱う際の落とし穴や抜け道をいちいち挙 げることはできない。よくできたプログラミングの教科書は、基礎的 な事柄を教えてくれる。以下の参考文献も参照のこと。 References: Kernighan and Plauger, _The Elements of Programming Style_ Sec. 6 pp. 115-8; Knuth, Volume 2 chapter 4; David Goldberg, "What Every Computer Scientist Should Know about Floating-Point Arithmetic". 14.5: 2つの浮動小数点の値が"十分近い"ことを判定するよい方法は。 A: 浮動小数点の値の絶対的な精度は、浮動小数点の定義により、その大 きさによって変化するので、2つの浮動小数点の値を比較する最上の 方法は精度のしきい値を使うことである。精度のしきい値は、比較し あう数の大きさに比例させる。 double a, b; ... if(a == b) /* 間違い */ ではなく、以下のコードを #include if(fabs(a - b) <= epsilon * a) 適切に選んだdegree of closenessのepsilonと共に使う(aが0でない 限り)。 References: Knuth Sec. 4.2.2 pp. 217-8. 14.6: 数を丸めるにはどうすればよいか。 A: 一番単純で容易なのは以下のようなコードを書くことである。 (int)(x + 0.5) しかしこの方法は負の数に対してはうまくいかない(負の数には (int)(x < 0 ? x - 0.5 : x + 0.5)のような式を使えばよい)。 14.7: なぜCに、べき乗が組み込み関数で用意されていないのか。 A: べき乗を組み込みの命令として持っているプロセッサが少ないからで ある。Cにはpow()が用意され、で宣言されている。ただし小 さな整数が対象なら、たいていは自分で掛け算で書いたほうがよい。 References: ANSI Sec. 4.5.5.1; ISO Sec. 7.5.5.1; H&S Sec. 17.6 p. 393. 14.8: 私が使ってるマシンのからあらかじめ定義されているはずの 定数M_PIが漏れているようだ。 A: この定数は(これはπの値を、マシンの精度一杯まで正確に、表わし ていることになっている)標準ではない。πが必要ならば、自分で #defineすること。あるいは4*atan(1.0)で計算する。 References: PCS Sec. 13 p. 237. 14.9: IEEEのNanとか、その他の特別な値かどうかのテストはどうやればよい か。 A: IEEEの浮動小数点数を高品質に実装しているシステムの多くは、これ らの値のテストを簡潔に行う機能を(例えばあらかじめ定義した定数 やisnan()マクロをまたはに)用意してい る。上のようなテストの標準化の作業が続けられている。Nanかどう かの力ずくだけれど、たいていはうまくいくテスト方法を以下の例で 示す。 #define isnan(x) ((x) != (x)) けれどIEEEフォーマットを知らないコンパイラは、このテストを最適 化の結果、削除するかもしれない。 その他の方法としては、問題にしている値をsprintf()で整形するこ とが考えられる。多くのシステムでsprintf()は"NaN"とか"Inf"といっ た文字列を生成するので、困ったときには役に立つ。 質問19.39も参照のこと。 14.11: Cで複素数を実装するよい方法は。 簡単なのは、単純な構造体とその構造体を操作する算術関数をいくつ か用意することである。質問2.7, 2.10, 14.12を参照のこと。 14.12: 以下の仕事をするコードを探している。 高速フーリエ変換(FFT's) 行列演算(乗算、逆行列など) 複素数演算 A: Ajay Shahがフリーで手に入る算術ソフトウエアの目録を管理してい る。これは定期的に投稿されているし、このFAQリストが入手できる のと同じ場所に保存されている(質問20.40参照)。質問 18.13,18.15c,18.16も参照のこと。 14.13: Turbo Cでプログラムを実行すると「浮動小数点フォーマットがリン クされていない」といってクラッシュするので困っている。 A: 小規模マシン用のコンパイラの中には、Borland社のC(Dennis RitchieのオリジナルのPDP-11用のコンパイラも)も含めて、必要がな さそうにみえるときは浮動小数点数への対応を実行しないようにでき ているものがある。特に浮動小数点数に対応しないprintf()や scanf()は%e、%f、%gを扱うコードを省略することでメモリを節約し ている。Borlandの、プログラムが浮動小数点数を使っているかどう かを判定する規則は、たまたま不十分なようである。そんなときはプ ログラマは浮動小数点数に対応したコードをロードさせるため、明示 的に浮動小数ライブラリルーチン(たとえばsqrt()。なんでもいい)を 呼ばなければならない(詳細はcomp.os.msdos.programmer FAQを参照 のこと) (訳注:Turbo Cではソースファイル内にextern void _ floatconvert(); #pragma extref _floatconvertと明示的に書くこと で、このエラーを回避することができる。) 15章 可変個数実引数リスト 15.1: printf()を呼ぶ前に必ずを必ず#includeするようにと言わ れた。なぜ? A: printf()の正しいプロトタイプがスコープに入るようにす るためにである。 コンパイラは可変個の実引数リストを取る関数には別の呼び出す仕組 みを使っているかもしれない。(そうするのは固定長の実引数リスト の関数で使っているのより効率の悪い呼び出しかたになっているから かもしれない。) よってプロトタイプは(省略記号「...」を使って実 引数リストが可変個数であることを示すことで)、可変個数実引数リ ストを使うときはいつも可変個数引数用の呼び出す仕組みを使う必要 があるとコンパイラに分かるように、スコープに入るようにしなけれ ばならない。 References: ANSI Sec. 3.3.2.2, Sec. 4.1.6; ISO Sec. 6.3.2.2, Sec. 7.1.7; Rationale Sec. 3.3.2.2, Sec. 4.1.6; H&S Sec. 9.2.4 pp. 268-9, Sec. 9.6 pp. 275-6. 15.2: どうして%fをprintf()のfloatの変数にもdoubleの変数にも使うこと ができるのか。この2つは別のデータ型ではないのか。 A: 可変個数実引数リストの可変個数の部分には「省略時の引数の格上げ (default argument promotions)」が適用される。charとshort intは intに、floatはdoubleにそれぞれ格上げされる(これはプロトタイプ がスコープ外の場合に適用される格上げと同じものである。これは" 古い書き方"として知られている。質問11.3を参照のこと)。したがっ てprintfの書式%fに見えるのはいつもdoubleの数である(同様に%cが 見るのは、%hdと同じく、intの数である)。質問12.9と12.13も参照の こと。 References: ANSI Sec. 3.3.2.2; ISO Sec. 6.3.2.2; H&S Sec. 6.3.5 p. 177, Sec. 9.4 pp. 272-3. 15.3: 解けなくてイライラしていた問題が実は、 printf("%d", n); で起きていることがわかった。ANSIの関数プロトタイプは上のような 引数の型の不一致から守ってくれると思っていたのだが。 A: 関数が可変個数の引数を取るとき、プロトタイプは可変個数の引数の 数やそのデータ型についての情報を与えない(そもそも出来ない)。よっ て通常の保護の仕組みは、可変個数の引数リストの可変個数の部分には あてはまらない。コンパイラは暗黙の変換も(通常の)データ型の不 一致も警告もできない。 質問5.2, 11.3, 12.9, 15.2も参照のこと。 15.4: 可変個数の引数を取る関数をどうやって書けばよいか。 A: にある機能を使う。 以下はmallocされた記憶領域に任意の個数の文字列を連結して格納す る関数である。 #include /* malloc, NULL, size_t用 */ #include /* va_ stuff用 */ #include /* strcatなど用 */ char *vstrcat(char *first, ...) { size_t len; char *retbuf; va_list argp; char *p; if(first == NULL) return NULL; len = strlen(first); va_start(argp, first); while((p = va_arg(argp, char *)) != NULL) len += strlen(p); va_end(argp); retbuf = malloc(len + 1); /* おしりの\0用に+1 */ if(retbuf == NULL) return NULL; /* エラー */ (void)strcpy(retbuf, first); va_start(argp, first); /* 2回目のスキャンのために再スタート */ while((p = va_arg(argp, char *)) != NULL) (void)strcat(retbuf, p); va_end(argp); return retbuf; } 使い方は以下のようになる。 char *str = vstrcat("Hello, ", "world!", (char *)NULL); 最後の引数をキャストする必要があることに注意。質問5.2と15.3参 照(使う人が、返ってきたmallocされた領域を解放する必要があるこ とにも注意)。 質問15.7も参照のこと。 References: K&R2 Sec. 7.3 p. 155, Sec. B7 p. 254; ANSI Sec. 4.8; ISO Sec. 7.8; Rationale Sec. 4.8; H&S Sec. 11.4 pp. 296-9; CT&P Sec. A.3 pp. 139-141; PCS Sec. 11 pp. 184-5, Sec. 13 p. 242. 15.5: printf()のように書式文字列と可変個数の引数をもらって、引数を printf()に渡して、仕事のほとんどprintf()にやらせるような関数を、 どうやって書けばよいのか。 A: vprintf()かvfprintf()かvsprintf()を使う。 「error: 」という文字列を先頭にエラーメッセージを出力し、改行 で終わる関数error()を以下に示す。 #include #include void error(char *fmt, ...) { va_list argp; fprintf(stderr, "error: "); va_start(argp, fmt); vfprintf(stderr, fmt, argp); va_end(argp); fprintf(stderr, "\n"); } 質問15.7も参照のこと。 References: K&R2 Sec. 8.3 p. 174, Sec. B1.2 p. 245; ANSI Secs. 4.9.6.7,4.9.6.8,4.9.6.9; ISO Secs. 7.9.6.7,7.9.6.8,7.9.6.9; H&S Sec. 15.12 pp. 379-80; PCS Sec. 11 pp. 186-7. 15.6: scanf()に類似の関数を書いて、scanf()を呼んでほとんどの仕事をさ せたい。どうすればよいか。 A: 残念ながら、vscanfの類は標準ではない。自分で書くしかない。 15.7: ANSI規格決定前に開発されたコンパイラを使っているが、 がない。どうすればいいか。 A: 古いヘッダというのがあって、これがほとんど 同じ機能を提供する。 References: H&S Sec. 11.4 pp. 296-9; CT&P Sec. A.2 pp. 134-139; PCS Sec. 11 pp. 184-5, Sec. 13 p. 250. 15.8: 関数が、実際にいくつ引数を渡されたか知る方法はあるか。 A: この情報は、移植性の高いプログラムには使えない。古いシステムに は、標準ではないnargs()関数を持つものもあるが、この関数が役に たつかどうかは必ず疑ったほうがいい。この関数は、たいてい引数の 数ではなくワード数を返す(構造体やlongの整数、浮動小数点表示の 数はたいてい、いくつかのワードとして渡される)。 可変個数引数を取る関数は、引数自身から引数の数を判断できるよう に作らなければならない。printfの類は、書式識別子(%dの類)を書式 文字列から捜して引数の数を判断する(だからこの手の関数は書式文 字列が実際の引数と一致していないとおかしな動作をする)。よく使 う別の手で、引数がすべて同じ型のときに便利なのは、番兵(0、-1ま たは適切な型にキャストしたヌルポインタ)をリストの最後に置くと いうものである(質問5.2と15.4のexeclとvstrcatの例を参照)。最後 に、各引数のデータ型が予測可能なら、可変個数の引数の数を明示的 に渡すことができる(ただし、呼び手にしてみれば、いちいち数える のは面倒だろう)。 References: PCS Sec. 11 pp. 167-8. 15.9: 今使っているコンパイラは以下のようには関数を宣言させてくれな い。 int f(...) { } すなわち固定の引数なしでは駄目なようだ。 A: 標準Cは少なくとも1つ固定の引数を必要とする。1つにはva_start() に渡せるように。質問15.10も参照のこと。 References: ANSI Sec. 3.5.4, Sec. 3.5.4.3, Sec. 4.8.1.1; ISO Sec. 6.5.4, Sec. 6.5.4.3, Sec. 7.8.1.1; H&S Sec. 9.2 p. 263. 15.10: 可変個数引数を取る関数があって、引数としてfloatを取る。なぜ、 va_arg(argp, float) ではうまくいかないのか。 A: 可変個数の実引数リストの可変個数の部分には、古い「省略時の引数 格上げ」が適応される。つまりfloatの引数は必ずdoubleに格上げさ れ(幅を広げられ)、charとshort intの引数はintに格上げされる。よっ てva_arg(argp, float);と書くのは常に誤りである。代わりに va_arg(argp, float)と常に書かなければならない。同じように、も ともとはcharやshortやintであった引数を取り出すのには va_arg(argp, int)と呼ばなければならない。質問11.3と15.2も参照 のこと。 References: ANSI Sec. 3.3.2.2; ISO Sec. 6.3.2.2; Rationale Sec. 4.8.1.2; H&S Sec. 11.4 p. 297. 15.11: va_arg()マクロを使ったが、関数へのポインタのデータ型を持つ引 数を取り出すことができない。 A: va_argマクロを使うことによるデータ型の書き換えは、関数へのポイ ンタなどの極端に複雑なデータ型を相手にすると困難となる。しかし、 関数へのポインタ型にtypedefを使えば、すべてうまくいくだろう。 質問1.21も参照のこと。 References: ANSI Sec. 4.8.1.2; ISO Sec. 7.8.1.2; Rationale Sec. 4.8.1.2. 15.12: 可変個数の引数を持ち、その引数を別の関数(その関数も可変個数の引 数を取る)に渡す関数をどうやって記述すればよいか。 A: 一般的にはできない。理想的には2番目の関数を書き直してva_listを 引数として取る(vfprintf()と同じように、上の質問15.5参照)ように する。引数を本当に引数として直接渡さなければならないときや、2 番目の関数をva_listを引数として取るように書き換える手が選べな いなら(言い換えれば、この2番目の、呼ばれる側の関数が、va_list ではなくて本当に可変個数の引数を取らなければならないなら)移植 性のある解は存在しない。(この問題は機種依存であるアセンブラ言語 まで持ち出せば解決できるだろう。以下の質問15.13も参照のこと)。 15.13: 実行時に、引数リストをつくって関数を呼び出すことができるか。 A: 動くことが保証される方法も移植性の高い方法もない。興味があるの ならこのFAQの編者に聞いてみること。彼はヘンテコリンなアイデア をいくつか持っている 引数リストの代わりに、汎用のポインタ(void *)の配列を渡すこと を考えたほうがいいかもしれない。呼ばれた関数は配列を、main()が argvを処理するのと同じように、一つ一つ処理することができ(すぐ わかるように、これは呼ばれる側の関数全体をこちらで管理している 場合に限られる)。 (質問19.36も参照のこと。)