C 言語のポインタと配列の関係は? [1995.4]

UNIX の当初のウリのひとつが,オペレーティングシステムが "高級" 言語 C で書かれていることであった.他にも DEC の TOPS や VMS も BLISS で書かれていたし,C の先祖の BCPL もシステム記述に使われていた.しかし,生き残っているのは C だけだ.BLISS や BCPL に比べて C がずっと高級だった証かもしれない.

C を学ぶときのハードルのひとつがこのポインタと配列の理解だろう. 宣言 char *p; があったとき,これより後で,p に対して単項演算子 * を使うと,char 型のデータ (つまり文字) が結果となる.* はマイナス記号 - が符号の反転と減算に使われるようにポインタ参照演算と乗算の両方に使われる.まず *p の * が演算子であることを知ろう.この演算が実行されるときには,p は実体のある場所を指していないといけない.実体とはこの場合 char 型のデータを格納できる場所である.そこで次の問題は,場所の確保とその場所を指す方法である.場所の確保の一つの方法は配列を宣言することである.宣言 char a[16]; は,16個までの char 型のデータを格納できる連続した場所を確保する.p が確保した場所の先頭を指すようにするには,この配列の名前を使って p=a; とすればよい.ここで *p とすれば,配列 a の第1要素の char 型のデータを結果として返す.*(p+1) とすれば,配列 a の第2要素が得られる.配列の要素を得る場面は多いので,これを簡単に書けるように用意された演算子が [] である.p[1] は *(p+1) と同じである.また,配列名を使って a[1] とも書ける.これも *(a+1) である. 加算 + は可換であるから,*(1+a) と書くのはまったくかまわない.すると

a[1] → *(a+1) → *(1+a) → 1[a]

となるのだろうか? 実はそのとおりである.こんな式を書くのはまったく勧められないが,1[a] は「正しい」C の式である.このように配列要素の参照は,内部ではポインタ演算とポインタ参照からなっている.これを踏まえてあといくつかの点を押えよう.

C プログラミングにポインタの理解は必須である.小さなプログラムを作っていろいろ試してみよう.


Software - Practice and Experience に採録された The BLISS programming language: a history によれば,IA-32 の BLISS も作られていて,IA-64 への対応も進んでいるそうだ.立派に生き残っていました.すいません.BLISS に興味があれば BLISS language reference manual をどうぞ.[2003.8.7]
‡ 宣言 int x[10]; があるとき,式 *(x + 1) は 式 x[1] と同じである.char と int では格納するために必要な場所の量が異なるが,ポインタ演算では要素を単位に加減算が行なわれる.[2003.5.25]


C のセミコロンのつけかたは? [1995.5]

C に限らず,プログラミング言語には性質の違う要素がいくつも含まれている.宣言,文,式などがそうだ.実行時には,さらにプログラムとデータの区別がある.なかには,Lisp のようにプログラムとデータは同じ,文も式もなくみな関数,と少ない構成要素で成り立つものもある.少ないほうがわかりやすいと思うのだが,一般的な意見ではないそうだ.Lisp ならセミコロンで悩むことはない

C では,まず「式」と「文」の違いを理解しよう.式には,定数,変数,関数呼出し,これらに演算子を作用させたもの,式を ( ) で囲んだものなどがある.式を実行すると値が得られる.printf("Hi") も式である.実行すれば,整数値が得られ,それと共に Hi が標準出力に書き出される.文には,if, while, break; などのキーワードがつき,形が決まっているもの,いくつかの文を { } で囲んだ複文,そして,式にセミコロンをつけた式文がある.文は実行され,決められた実行の制御をしたり,式文の式を評価する.しかし,文は値をもたない.printf("Hi"); はセミコロンがついているので文である.この差は,( ),{ } で囲んでみるとわかるだろう.

(printf("Hi")); と {printf("Hi");} の二つは正しく,
(printf("Hi");) と {printf("Hi")}; は誤りである.

つまり ( 式 ) と { 文 } は正しく,( 文 ) と { 式 } は誤りである.{ } の後ろにはセミコロンがいらないことも,これでわかるだろう.else のない if 文は「if (式) 文」と構文が決まっている.

if (x) a = 1;

は式 a = 1 にセミコロンをつけて文としている.

if (x) { a = 1; b = 0; }

では「}」の後にはセミコロンはいらない.むしろ,else のある if 文では

if (x) { a = 1; }; else a = 0;

は誤りとなる.

if (x) { a = 1; };

は if 文と空文の二つの文だ.空文とは式文の式を省略したものである.

while (*++p != '.') ;

のように使える.「while (式) 文」の文が空文となっているので,while の文が省略されているのではない.セミコロンを必要とする文もある.例えば,関数の戻りは「return 式 ;」であって,「return 文」の文のセミコロンとはならない.よって,return {0} とは書けない.return (0); は書いてもよいが,この ( と ) は冗長である. break; や continue; もセミコロンがついて文である. さらに例外は goto 文で「goto 識別子 ;」である.識別子は goto 専用である.


† Lisp はセミコロンの代わりにかっこの数で悩むじゃないか,と言われることがある.そんな時には GNU Emacs エディタを使って (使いこなして) ほしい.閉じかっこを打つと対応している開きかっこにカーソルが一瞬移動するのに気づくだろう.対応する開きかっこが画面にないときにはエコー領域に対応するかっこが含まれている行が表示される.開きかっこにカーソルを置いて,C-M-k (ESC C-k でもよい) とすれば,対応する閉じかっこまでの文字が kill される.関数全体を kill/yank するなんてのは簡単だ.C プログラムでも { から } までを同じ要領で kill できるが,関数の名前や仮引数部分を含めて kill するには C-M-h (c-mark-funcion) して C-w といったことになる.Emacs の Control-Meta のプレフィックスはもともと Lisp の編集のためにある.それは Emacs の大部分が Lisp で書かれているからだ.使いこなせば,かっこが障害になることはない. [2003.7.18]


/bin にある[ というファイルはなんですか? [1995.8]

/bin/[ は /bin/test の別名だ.ls -li 'test' '/bin/[' でファイルの固有番号である inode 番号を表示させれば,test と [ が同じ inode 番号をもつことがわかる.これはファイル本体が同じの意味である.test は,引数で与えられた文字列の比較をしたり,ファイル名を与えてそのファイルの存在や実行可能であるかどうか,などを判定するプログラムである.[ は ] と対にして使うことになっているので,判定文の見映えがよくなる.例えばシェルスクリプトに書かれた次の二つは同じものだ.

if test " $list" = " "; then ...; fi
if [ " $list" = " " ] ; then ...; fi

ところで,test -f a.out と入力してプログラム test を実行してみよう.何が起きるだろうか.やってみればわかるが,何も起きない.a.out があってもなくても,何もプリントされない.シェルスクリプトの if はプログラム test の何を見ているのだろうか.実は,プログラムはちょうど C の関数が値を返すように,0 から 255 の整数値を返すことができる.出力ストリームに何かを書き出すのとはまったく別の流れである.プログラムを呼び出すシェルは,この値を使って if の判断をする.また,この値はシェルの組み込み変数に保持されている.test -f a.out を実行したあと,echo $status と打ってみよう (csh, tcsh など csh 系の場合.sh, bash など Bourne shell 系のときには echo $? と打つ).こんどは,a.out があれば 0,なければ 1 がプリントされるはずだ.この値に意味をもたせているのは test だけではない.例えば,grep も指定したパターンがひとつでも見つかれば 0 を返し,ひとつも見つからなければ 1 を返す.パターンが含まれる行を出力する grep プログラムに何も出力しないことを指定するオプションがあるのは,これを利用するためだ.

プログラムから返される値は,C の exit 関数の実引数,または,main 関数で return でプログラムを終了したときの return の値である.まさに,プログラムの戻り値であるわけだ.プログラムを呼び出した側では,これを wait システムコールで受取ることができる.C でプログラムを書くときには,main の戻り値にも気を配ろう.0が正常終了,1以上が異状終了である.


速いプログラムを作るには? [1995.10]

まず,プログラムを作るべきかどうかを考えよう.小さなテキスト操作であれば,Emacs のキーボードマクロでも,十分な速度が出るし,作成,デバッグが最小の手間で済む.C でプログラムすると決めたなら,次はデータ構造とアルゴリズムを精選しよう.ソートはクイックソートが一番速いわけではない.要素が少ないなら単純なもので十分だし,非常に多いのなら,一時ファイルを使ったソートとマージの組合せが必要だろう.次にプログラム全体の動きを概観するため,実行プロファイルを取ってみよう.特に実行回数の多い関数を調べるのは悪くない.ただし,あまり細部にこだわってはいけない.確かにいにしえには,i++ と ++i で実行時間が異なった.変数名の長さが速度に影響することもあった.いまやこのレベルはコンパイラの最適化オプションですべて解決と考えてよい.ただし,最適化オプションのあるなしで,プログラムの動きが変わることがありうる.C では可換な演算子の両側の項の評価順序や関数呼出しにおける実引数の評価順序は規定されていない.これをコンパイラは最適化に利用する.よって,評価順序に依存するプログラムは書いてはいけない.

実行環境も速度に影響する.ディスク入出力の頻度は特に効く.UNIX では一つのディレクトリに多数 (例えば,数万) のファイルがあると,そこでのファイルの作成,消去の操作が極端に遅くなる.これはカーネルでのファイル管理の問題であるから,プログラムの設計段階から考慮がいる.使用メモリ量も実行するマシンのメモリ容量と相談だ.同じプログラムを同時に走らせるときには共有できるメモリ量とプロセス固有に必要なメモリ量があることを思い出そう.コード部分は共有できるからいくつ走っても問題ない.伝統的な C では文字列は書き換えられるとされていて,例えばエラーメッセージの文字列でもプロセス固有のデータ部分に割り当てられる.これをコード扱いにして共有部分に割り当てるように工夫したのが xstr コマンドである.ANSI C では文字列は定数扱いである.

i++ と ++i のレベルに近いことを考えなければいけない世界もある.SPARC のレジスタウィンドウのあふれを気にしたり,キャッシュラインに心を配るという世界だ.オリンピック的な興味としても,なかなか楽しいものである.

とはいえ,新しい機種を買ってくるだけで実行速度が倍になるのも事実だ.ディスクもメモリもずいぶん安くなった.この恩恵をプログラムの実行速度増加だけでなく.プログラマの実働時間減少にもつなげてほしいものだ.


i ノードとはなんでしょうか [1995.11]

UNIX は小さくて軽い OS である,いや,あった.当初の UNIX カーネルは C 10,000行と PDP-11 アセンブラ 1,000行でできていた.すっきりした概念を簡潔に実現した OS である.こういうシステムでは,利用者からも OS のインプリメンテーションが透けてみえてくる.抽象化されていない,と言われればそれまでだが,システムイメージが直観的に伝わってくるとも言えよう.アドレスセレクト線につながったランプを見てプログラムを追えた時代はさすがに過ぎたが,ディスクのシーク音からは計算機の働きぶりがうかがえる.銀行の自動支払い機のガラガラ音は,利用者に安心感を与えるために残してあると聞く.UNIX もデバイスやプロセスに直結する番号,ポインタが利用者の目の前においてある.

i ノードはファイル実体の管理簿である.ファイルのモード,リンク数,オーナー,サイズ,アクセス時刻,ファイル本体のディスク上のブロック番号などが納められている.i ノードを並べたものが i リストで各 i ノードにはリストの順に i 番号 (index number) がついている.i リストはファイルシステムごとにもつので,ファイル実体はファイルシステムと i 番号から一意に決まる.ファイル実体と書いたのは,ひとつのファイル実体に複数のファイル名 (パス名) をつけることができるからである.名前をつけると書いたが,実際,ディレクトリのひとつのエントリは,名前と i 番号の組であり,同じ i 番号をもったエントリがいくつあっても構わない.ただし,i ノードには,どのディレクトリエントリが自分の i 番号を保持しているかの逆ポインタはない.ディスクブロックにも自分がどの i ノードで管理されているファイル実体の一部であるかの逆ポインタはない.ディスクの一部が壊れて,コンソールにディスクのブロック番号が読めない等のメッセージが出たとき,そのブロックを使っているファイルの名前を調べるのはたいへんである.まず,icheck -b block 番号 で i ノードをすべてチェックして,該当するブロック番号を保持している i ノードの番号を得る.次に,ncheck -i i 番号 でディレクトリエントリをすべてチェックして,i 番号が一致するエントリの組の名前部分を表示する.しらみつぶしを2度しないとわからない.

icheck や ncheck はファイルシステムの一貫性をチェックするコマンドだが,チェックは fsck で十分だ.icheck, ncheck は非常時用といえよう.


"Lions' Commentary on UNIX 6th Edition, with Source Code" (邦訳) 参照.[2003.8.7]


C のプログラムはなぜ a.out に変換しないと動かないのですか [1996.1]

コマンド名を入力すると,決められた動作をして結果が表示されたり,ファイルが変更されたりする.同じような動作をさせるコマンドも UNIX と例えば DOS では名前が違っている.とすると,UNIX という計算機,DOS という計算機があってコマンドを受け付けているのだろうか.

実際のつくりはそうではない.計算機といっている本体はたいていマイクロプロセッサとか CPU とか呼ばれている部品で,これが受け付けることができるのは,二つの数を加えるだとか,数が 0 かどうかを判定する,といった単純なものだけだ.これは機械命令と呼ばれている.機械命令の種類はせいぜい数十個である.これを並べて実行していくことでコマンドが動作する.では,並べ方をどう指示すればよいのだろうか.もちろん機械命令をコマンドの動作になるように直接人間が指定してもかまわない.だが,単純な命令を多数並べるとまちがいも入るし,第一書くのが大変だ.そこで,機械命令とは別に規則 (プログラム言語) を作ってそれでコマンド動作を書き,別のコマンドでこれを機械命令に直す (翻訳する) ことが考えられた.この翻訳プログラムをコンパイラと呼ぶ.最初に作られたコンパイラは言語 FORTRAN 用のもので,1956年ごろのことである.当時,コンパイラのようなプログラムを作ることは不可能と主張する人がいたようだが,コンパイラは確かに完成した.これ以降,コンパイラについては技法,理論とも大いに研究され計算機科学の分野としても重要な地位を占めている.

C のプログラムを a.out に変換しないと動かないのは,その計算機は C のプログラムを直接受け付けることができないからである. では,C を直接受け付けることができる計算機があれば,この変換は必要なくなるのだろうか.これはそのとおりである.C ではないが,プログラム言語 Pascal を (簡単な翻訳だけで) 受け付けることのできる計算機が一時期あった.だが,この種の計算機はほぼ姿を消してしまった.プログラム言語を直接受ける計算機より,少数の機械命令を高速に実行するほうが,トータルでよほど速いものができることが実証されたためだ.

C で書かれた動作を直接実行するプログラムを作ることはできる.インタプリタと呼ばれるプログラムがそれだ.コンパイルの過程が不要で,実行を一時止めたり,変数の内容を確認したり変更したりが容易にできるので,デバッグ用と言われたこともあった.しかし,インタプリタでなくても,別にデバッグ用のプログラムを使って一時停止などができるようになり,インタプリタはあまり使われない.C は a.out に直して実行するものと思ってよい.


   タイトル一覧  ホーム
webmaster