いまどきのコンピュータはたいてい1バイト(=8ビット)を基本としていますが、B言語が作られた頃は、8ビットより大きな「ワード」を基本としたマシンが多く存在していました。以下、Wikipediaへのリンク。
たとえば最初にBが動いたというPDP-7では1ワードが18ビットで、この18ビットのワードごとにアドレスが割り付けられます。メモリ容量は標準が4Kワードで64Kワードまで拡張できたということですが、Dennis RitchieによるThe Development of the C Languageによれば、Bの作者Ken ThompsonがUNIXの開発に使っていたPDP-7は8Kワードだったようです。1ワード18ビットで8Kワードといえばビット数で144Kビット、バイトにすれば18KBです。いまどきのパソコンならこの100万倍くらいのメモリを積んでいるのも普通です。よくまあこんなわずかなメモリでB言語やUNIXの開発をしていたものだと思います※1。
(初期の)Bはそういうコンピュータ向けに設計されていたので、18ビットなら18ビットの「1ワード」で表現できる整数型以外、データ型を持ちません。アドレス(ポインタ)も単なる整数も、同じワードで表現します。なので当時のBでは、a[5]が5[a]と書けるのも当たり前のことだったのです。a[5]は*(a + 5)のシンタックスシュガーであり、足し算は順序が逆でもよい、という事情はBもCと同じでしたから。
となると、カンのいい人は、「じゃあ文字列はどう表現していたんだ?」と思うでしょう。Cなら、str[i]でstrのi文字目(最初の文字は0文字目と数えるとして)を取り出せます。でもBはワード指向なので、str[i]だとstrのi番目のワードを指すわけで、i番目の文字は取り出せません。今なら1文字のために16ビットとか32ビットとかを割り当てることもありますが、当時の貧弱なマシンでそんな贅沢なメモリの使い方が出来るわけもありません。
Bでは、文字列は、1ワードの中に何文字かをパックする形で保持されていました。たとえばHoneywellの6070という(大型)コンピュータでもBは動いていましたが、このコンピュータのワード長は36ビットだったので、1ワードに4文字を詰め込むことができました。Honeywellの6070(以後、H6070と書きます)を前提としたBのチュートリアルにはこんなサンプルコードが載っています。
main( ) {
auto a;
a= 'hi!';
putchar(a);
putchar('*n' );
}
2行目のauto a;が変数宣言であるというのは想像できるでしょうが(型は1種類しかないのでCと違ってintとか書く必要はない)、3行目で、その変数に「hi!」という3文字を詰め込んでいます。hi!を囲んでいるのがダブルクォートではなくシングルクォートであることにも注目してください。Cではシングルクォートで囲むのは(普通は)1文字ですが、Bでは1ワードに入る分の文字数を囲むことができました。
「ではstrのi番目の文字を取り出したければどうするんだ?」と思うでしょうが、それにはそれ用の関数がありました。strのi文字目を取り出したければchar(str, i)、strのi文字目にcを書き込みたければlchar(str, i, c)です※2。
ところで、putchar('a');と書けばちゃんとa(だけ)が出力されますが、この時の'a'はASCIIコードなら10進数で65、7ビットに収まる値です。つまり、文字はワードの中に「右詰め」で格納されます。ただしダブルクォートで囲む文字列は、ワードの並びに「左詰め」で格納されます。ここで「左詰め」はワードの上位ビットから格納されるという意味です。今のPCはバイトマシンだからといってバイトの並びとして先頭から格納すると、リトルエンディアンのCPUでは期待した動きになりません。
Bも時代につれて進化してきたので、いくつかのバージョンがあります。ざっと探して見つかったのはこんな感じでした。
一番下のGCOS8版は、浮動小数点数が使えたり等かなり機能強化されています。マニュアルもなかなか長くて、私は読み通せていません。
上のふたつ、PDP-11版とMH-TSS版だと、以下のような違いがあります。
auto a 10;この10は初期化子ではなく配列の要素数です。
auto a[10];Cに慣れた身としては、PDP-11版の見た目はどうも気持ちが悪いです。MH-TSS版とかの「配列の要素数は指定した数+1」というのも気持ち悪いですが。
これはこれで気持ち悪いですが、PDP-11版のは面倒くさいですね……If the first use of a name is immediately followed by a left parenthesis '(', the name is typed external by default; thus the library functions need not normally be declared.
訳)名前の最初の使用直後に左括弧 『(』 が続く場合、その名前はデフォルトで外部型となる。したがってライブラリ関数は通常宣言不要である。
考えた結果、ここで作るB、Mae-Bでは、MH-TSS版をベースとすることにしました。どうせ実用に使おうというわけじゃなし、Bを知るなら一番低機能なPDP-11版で十分かもしれませんが、やっぱりなんか、いろいろ気持ちが悪いので。
まあCの前の言語なので、CにあってBにない機能はたくさんあります。そもそも型が1種類しかないので、構造体も共用体もありません。実用プログラムを書くにはこの辺が一番の障害かと思います。
その他の目につく違いはこんな感じ。
*0 null
*e end-of-file
*( {
*) }
*t tab
** *
*' '
*" "
*n new line
――「{」とか「}」とかのエスケープシーケンスがわざわざ用意されているということは、当時キーボードからこれが入力できないコンピュータがそれなりにあったんでしょうか。そうだとするとそもそもBのソースなんて入力できなそうですが(Cにおけるトライグラフのようなものは、私が探した範囲では見当たりませんでした)。
But the spaces around the operator are critical! For instance,――これが紛らわしいことがこのころからわかってたのなら、もっと早く変えておけよ、と思ってしまいますが…… 「The Development of The C Language」によればsets x to -10, while
x = -10subtracts 10 from x. When no space is present,
x =- 10also decreases x by 10. This is quite contrary to the experience of most programmers.
x=-10
訳)ただし演算子周辺のスペースが極めて重要です!例えば、
はxを-10に設定しますが、
x = -10
はxから10を引きます。スペースがない場合、
x =- 10
もxを10減らします。これはほとんどのプログラマーの経験とは正反対です。
x=-10
とのこと。this mistake, repaired in 1976, was induced by a seductively easy way of handling the first form in B's lexical analyzer.
訳)この誤りは1976年に修正されたが、Bの字句解析器において前者を扱う誘惑的に簡単な方法が原因で生じた。
Never try to pass a label as an argument to a function and then use that label to transfer to another function. The program will end up in the destination function, but with the previous function's stack pointer. This is bound to result in disaster eventually.関数の引数としてラベルが渡せるらしい。そしてちゃんとジャンプもするらしい。でも、その方法で別の関数に飛び込んだとしてもスタックがめちゃくちゃになる。そりゃそうだ。
訳)関数の引数としてラベルを渡した後、そのラベルを使って別の関数へ遷移しようとしないでください。プログラムは目的の関数に到達しますが、前の関数のスタックポインタを引き継いだ状態になります。これは最終的に必ず致命的な結果を招きます。
auto a[10];と書けばa[0]からa[10]の11個の要素が使えます。CおよびCから派生したいまどきの言語に慣れた人からすれば「え?」と思うでしょうが、Cの初心者はたいていa[10]に代入してしまうので、こっちの方が「自然」なのかもしれません。昔の8ビットパソコン時代のBASICはこうだった気がするし、VBAは今でもこうであるようだし。
function-definition:
declaration-specifiersopt declarator declaration-listopt compound-statement
Cの宣言の構文は変態なので前半部分はわけがわからないことになっていますが、最後に「compound-statement」がついていることがわかるでしょう。compound-statementというのは、{ }で囲まれた複合文のことです。つまりCでは、関数の処理本体は{ }で囲まなければいけません。
definition ::=
name ( {name {, name}0}01 ) statement
末尾はstatementです。よって、(while文とかがそうであるように)関数本体の処理が1文であれば{ }で囲む必要はありません。char(s,n) return((s[n/4]>>(27-9*(n%4)))&0777);
Notice that char is written without {}, because it can be expressed as a simple statement.
訳)charは単一の文として記述できるため、{}なしで記述されていることに注意。
concat( a, b1, b2, . . .. b10)Mae-Bでは、「隠れた最初の引数で引数の数を渡す」ようにしました。
そして、実際、MH-TSSでは、現在の入出力ユニットを示すグローバル変数としてrd.unit、wr.unitが用意されています。ピリオドが入っているので構造体かな? と思ってしまいますが、Bにそんなものはないので、ただのグローバル変数の名前の一部です。The characters A through Z, a through z, _, ., and backspace are alphabetic characters and may be used in names. The characters O through 9 are digits and may be used in constants or names; however, a name may not begin with a digit.
訳)AからZ、aからz、_、.、およびバックスペースはアルファベット文字であり、名前で使用できます。Oから9は数字であり、定数や名前で使用できます。ただし、名前は数字で始めてはいけません。
MH-TSSのリファレンスには以下の記述があります。
Declarations in B specify storage class of variables, and also, in some circumstances, specify initialization. There are three storage classes in B. Automatic storage is allocated at each function invocation, and becomes undefined upon return from the function. External storage is allocated before execution of the program, and is available to any and all functions. Internal storage is also allocated before execution, but is available to only one function; labels are the only current use of internal storage.
訳)B言語における宣言は変数の記憶域クラスを指定し、状況によっては初期化も指定する。B言語には三つの記憶域クラスが存在する。自動記憶域は各関数呼び出し時に割り当てられ、関数からの戻り時に未定義となる。外部記憶域はプログラム実行前に割り当てられ、あらゆる関数から利用可能である。内部記憶域も実行前に割り当てられるが、単一の関数からのみ利用可能である。現在のところ、内部記憶域はラベルのみに使用されている。
「自動記憶域は関数呼び出し時に割り当てられる」ふむふむCと同じだな、「外部記憶域はプログラム実行前に割り当てられ、あらゆる関数から利用可能である」なるほどグローバル変数だな、「内部記憶域も実行前に割り当てられるが、単一の関数からのみ利用可能である」これはCでいうところのstatic指定したローカル変数相当かな、と思っていると、「現在のところ、内部記憶域はラベルのみに使用されている」と続いて「あれっ?」となります。内部記憶域(Internal storage)というからには何か記憶するわけで、つまりstatic指定したローカル変数同様、ラベルに代入ができるのかなあ。それが役に立つケースは思いつきませんが。「現在のところ、内部記憶域はラベルのみに使用されている」なので、いずれstatic指定したローカル変数みたいな使い方も想定していたとか?
内部記憶域の説明(6.3 Internal Declaration)には以下のように書いてある。
The first reference to a variable not declared as external or automatic constitutes an internal declaration. The major use of internal declarations is with labels; at the end of each program, internal names not defined as labels will cause an error message. A label is defined by writing
name :※4preceding any statement.
訳)外部宣言または自動宣言として宣言されていない変数への最初の参照は、内部宣言を構成する。内部宣言の主な用途はラベルである。各プログラムの末尾において、ラベルとして定義されていない内部名はエラーメッセージを引き起こす。ラベルは、
name :を任意の文の前に記述することで定義される。
普通の変数は、自動変数のauto宣言であれグローバル変数のextrn宣言であれ、使うところより前に書く必要があります。ただ、ラベルはgotoで下の方に向かって飛ぶこともあるわけで、使うところより前に「name :」が書けるとは限りません。だから関数の最後まで来てからエラーになる。それはわかりますが、やっぱりこれも変数の説明に見える。
GCOS8版のマニュアルを見てみると以下のように書いてあって、
Labels are also stored in the body of a function's executable code, and are not directly accessible to the user.
訳)ラベルも関数の実行コード本体に格納され、ユーザーが直接アクセスすることはできません。
こちらは明確に代入不能と書いてある。でもGCOS8版はMH-TSS版までのBとはだいぶ違うので、GCOS8版での修正点かもしれないということで、Mae-Bでは「代入可能」の方に倒しました。だってそっちの方が面白そうだし。
Bではグローバル変数を初期化しつつ定義する場合、以下のように書きます。
a 10;
こう書けば、グローバル変数aが定義されて、10で初期化されます(=は要りません)。
それはいいのですが、構文規則を見ると、この「10」のところは非終端子ivalになっていて、その定義は定数または名前です。
an ival (initial value) is a constant or a name; the external name is defined and initialized with the value of the constant, or the lvalue of the name, respectively.
訳) ival(初期値)は定数または名前である。外部名は、それぞれ定数の値、または名前の左辺値で定義・初期化される。
名前を書くとその左辺値で初期化されるようです。
Mae-Bではそのように実装したつもりです。よって、
a 10; b a;
と書くとbはaのアドレスで初期化されます。aの右辺値で初期化する方がまだ使いでがあるのでは、とも思えますが、この実装だと以下のような書き方で多次元配列のようなもの(アイレフベクタ)が作れます。
v1[] 1, 2, 3, 4; v2[] 5, 6, 7, 8; v3[] 9, 10, 11, 12; v4[] 13, 14, 15, 16; matrix[] v1, v2, v3, v4;
Cだと、関数を呼び出す前に、関数定義するかプロトタイプ宣言しなければいけません。C89より前のCだとプロトタイプ宣言はありませんが、カッコ内が空の関数宣言はします。しかし、Bには、それ用の構文はなく、関数を定義や宣言なしで呼び出すのは完全に合法です。たとえばBのチュートリアルの冒頭に出ているサンプルでも、main()が先頭に書いてあります。
main( ) {
-- statements --
}
newfunc(arg1, arg2) {
-- statements --
}
fun3(arg) {
-- more statements --
}
Bでも関数呼び出しの( )は演算子であり、関数へのポインタを経由した関数呼び出しが可能です。よってfunc();という関数呼び出しを書いたとき、funcがそのまま関数なのか、それとも関数へのポインタが格納された変数なのかがわからないと困ります(呼び出し先を取得するための機械語なりバイトコートなりが異なってくるため)。今なら、メモリはたくさんあるのでソースを最後まで読み込んで判断できますが(Mae-Bではそうしていますが)、当時のBではどう判断していたんでしょう?
――でも、それを言い出すとグローバル変数の配列も同じなんだよな。もしかして私は根本的にBを勘違いしてますでしょうか。
Each of the languages (except for earliest versions of B) recognizes separate compilation, and provides a means for including text from named files.
訳)各言語(初期バージョンのBを除く)は分割コンパイルを認識し、名前付きファイルからテキストをインクルードする手段を提供する。
この「初期バージョンのB」(ealiest versions of B)というのがどこまでを含むのかわかりませんが、PDP-11版、MH-TSS版ともに分割コンパイルの機能はないように見えます。GCOS8版だと「%ファイル名」の形でCの#include相当のことができるようで、これのことを言っているのかもしれませんが、Dennis RitchieにとってBといえばDennis RitchieのWebページ以下にあるもののことなんじゃないのかなという先入観があるので、どうも違和感が。
(今の)Cなら、&, |はビット演算子で、&&, ||が論理演算子です。if文とかでAND条件を書きたければ今なら&&を使いますが、かつては&で代用していて、まあこれでほぼ困らないのですが、たとえば「1 & 2」は0でfalseになってしまう、そういう不便さから後になって&&演算子が導入されて、でもいまさら&演算子とかの優先順位を下げるわけにもいかないから「value & mask == HOGE_BIT」といった式で直感に反する動きになった、と私は理解していたのですが。
The Development of the C Languageには、以下の記述があります。
Rapid changes continued after the language had been named, for example the introduction of the && and || operators. In BCPL and B, the evaluation of expressions depends on context: within if and other conditional statements that compare an expression's value with zero, these languages place a special interpretation on the and (&) and or (|) operators. In ordinary contexts, they operate bitwise, but in the B statement
the compiler must evaluate e1 and if it is non-zero, evaluate e2, and if it too is non-zero, elaborate the statement dependent on the if. The requirement descends recursively on & and | operators within e1 and e2. The short-circuit semantics of the Boolean operators in such `truth-value' context seemed desirable, but the overloading of the operators was difficult to explain and use. At the suggestion of Alan Snyder, I introduced the && and || operators to make the mechanism more explicit.
if (e1 & e2) ...
訳)言語名が命名された後も急速な変化は続き、例えば&&演算子や||演算子の導入などがあった。BCPLやB言語では、式の評価は文脈に依存する。if文やその他の条件文内で式の値を0と比較する場合、これらの言語ではAND(&)演算子とOR(|)演算子に特別な解釈が適用される。通常の文脈ではビット単位で動作するが、B言語の文
if (e1 & e2) ...では、コンパイラはe1を評価し、それが0でない場合にe2を評価し、e2も0でない場合にifの中の文を処理対象にしなければならない。この要求はe1とe2内の&および|演算子に対して再帰的に適用される。このような「真偽値」文脈におけるブール演算子の短絡評価は望ましい特性だったが、演算子の多重定義は説明や使用が困難だった。アラン・スナイダーの提案を受け、この仕組みをより明示的にするため&&と||演算子を導入した。
この文章を読むと、&や|演算子は、条件式を評価している文脈では短絡演算子になるという特別な解釈がされる、かのようですが、boolean型もなければ非ゼロを気楽にtrueとして使うC(やB)のような言語で、そんなことは不可能だと思うのです。Mae-Bでは、&や|は単なるビット演算子として実装しています。都合に応じて短絡演算子になったりはしません。
公開日: 2026/01/18
間違い等ありましたら、掲示板にご連絡願います。