はじめに
プログラミング言語の世界には多くの種類がありますが、その中でC言語は特別な存在です。現在では多くのプログラミング言語の源流となっているC言語ですが、誕生した当時はとても奇抜な存在でした。なぜそう言えるのか、そしてなぜそのような言語が今日の標準になったのかを考えてみましょう。
高級言語とアセンブリ言語の間に立つC言語
C言語の特徴的な点は、アセンブリ言語と高級言語の中間に位置するような性質を持っていることです。アセンブリ言語はコンピュータのハードウェアに直接対応する低レベルの命令を使うプログラミング言語で、高級言語は人間が理解しやすい抽象的な表現を使うプログラミング言語です。
しかし、これはC言語が古いプログラミング言語だからではありません。むしろ、C言語より古いプログラミング言語であるFORTRANやCOBOLの方が、より高級言語としての性格が強いものでした。これらの言語は、計算式や経理書類をそのままプログラミング言語へと変換できるように設計されています。つまり、計算をしたいユーザーと機械コードを生成する部分が明確に分業されていたのです。
一方、C言語はコンピューターの内部構造により自然な言語設計になっています。文法は構造化プログラミングの書き方(ALGOL由来)を採用していますが、構造化原理主義というわけではなく、プリプロセッサ(コンパイル前に処理を行う仕組み)やアセンブリ言語との混合状態で、実用性を重視していました。同じ時期に開発されたPascalとも、この点で大きく異なります。
つまり、C言語はハッカーのためのプログラミング言語だったのです。
OSを作るための言語
C言語の大きな特徴は、オペレーティングシステム(OS)を作るために開発された言語だという点です。OSは以前はアセンブリ言語で書かれていて、コンピューターのプロセッサーごとに作り直す必要がありました。これは新しいコンピューターが登場するたびに、OSも一から作り直さなければならないことを意味します。
昔のコンピューターは、その機械専用のプログラムシステムを必要としていました。しかしPDP-7(当時の小型コンピューター)のようなコンピューターを活用するには、システムの移植性が重要になってきます。コンピューターごとに毎回アセンブリで書いていたのでは大変だからです。
だからこそC言語は、アセンブリ言語で記述するような処理を簡単にすることが目的だったのです。これが計算や集計処理をするビジネス向けのプログラミング言語と違う点です。C言語はシステムを作るためのプログラミング言語なのです。
コード例で見る進化
プログラミング言語の進化を理解するには、同じ処理を異なる言語でどのように表現するかを比較するのが効果的です。ここでは「1から10までの数字を合計する」という単純な処理を例にとり、それぞれの言語でどのように記述されるか見ていきましょう。
アセンブリ言語(IBM 704)
アセンブリ言語は機械に最も近い言語です。プロセッサーのための命令を直接指定します。
* 1から10までの合計を計算するIBM 704アセンブリプログラム
ORG 100 * プログラム開始位置
START CLA ZERO * アキュムレータをゼロクリア
STO SUM * 合計値を初期化
CLA ONE * カウンタを1で初期化
STO COUNT
LOOP CLA SUM * 現在の合計を取得
ADD COUNT * カウンタの値を加える
STO SUM * 新しい合計を保存
CLA COUNT * カウンタをロード
ADD ONE * カウンタを1増やす
STO COUNT * 新しいカウンタ値を保存
CLA TEN * 上限値をロード
SUB COUNT * 10-カウンタ
TMI EXIT * カウンタが10を超えたら終了
TRA LOOP * まだ10以下なら繰り返し
EXIT HTR 0,0 * プログラム終了
* データ領域
ONE DEC 1 * 定数1
TEN DEC 10 * 定数10
ZERO DEC 0 * 定数0
COUNT BSS 1 * カウンタ変数
SUM BSS 1 * 合計値を格納する変数
END START
Code language: PHP (php)
この例では、IBM 704の命令セットを使っています。CLA(Clear and Add)やSTO(Store)、ADD(Add)などの命令は当時の多くのアセンブリ言語で共通して使われていました。TMI(Transfer on Minus)やTRA(Transfer)は分岐命令です。
レジスタと呼ばれるコンピューターの内部記憶装置を直接操作しています。値を増やしたり比較したりする命令が一つひとつ書かれていて、コンピューターに近い表現になっています。
FORTRAN(1957年頃)
FORTRANは科学技術計算のための最初の高級言語です。
PROGRAM SUM
INTEGER I, TOTAL
TOTAL = 0
DO 10 I = 1, 10
TOTAL = TOTAL + I
10 CONTINUE
PRINT *, 'THE SUM IS: ', TOTAL
END
Code language: PHP (php)
科学計算向けに、計算方法(アルゴリズム)をそのまま書けるように設計されています。変数による計算処理はリッチ(オーバーフローしにくい)なのですが、行番号へジャンプする構造はまだ原始的で、アセンブリ言語に近いです。
COBOL(1959年頃)
COBOLはビジネス処理を目的として開発された言語です。英語に近い表現を用います。
IDENTIFICATION DIVISION.
PROGRAM-ID. SUMPROGRAM.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 COUNTER PIC 9(2) VALUE 1.
01 TOTAL PIC 9(3) VALUE 0.
PROCEDURE DIVISION.
PERFORM UNTIL COUNTER > 10
ADD COUNTER TO TOTAL
ADD 1 TO COUNTER
END-PERFORM.
DISPLAY "THE SUM IS: " TOTAL.
STOP RUN.
Code language: JavaScript (javascript)
大文字ばかりなのでちょっと読みにくいですが、COBOLは英語の文章のように読める構文を持ち、ビジネス書類のような区分けがされています。COBOLの構造要素の予約語を日本語にしてみると、ほとんど「手順書(マニュアル)」であることがわかります。
識別部.
Program-ID. SumProgram.
データ部.
作業場所節.
01 counter PIC 9(2) VALUE 1.
01 total PIC 9(3) VALUE 0.
手続き部.
perform until counter > 10
add counter to total
add 1 to counter
end-perform.
display "The sum is: " total.
stop run.
Code language: JavaScript (javascript)
COBOLも数値データのメモリ表現が10進数で冗長な設計になっているため、計算結果に予期せぬ誤差が含まれにくいようになっています。金融システムなどで代替されにくいのは、このようなデータの扱いによるところが大きいです。
C言語の広がり(効率性と脆弱性)
今でこそC言語は難しい、使いにくい、危険だとよく言われますが、それはもともと万人向けのプログラミング言語ではなかったからです。C言語は、大企業や研究機関が大規模なプロジェクトとして作ったわけではなく、少数の開発者が実用的な問題を解決するために作られました。それが当時のコンピュータの性能の制約の中で、多くの人が「汎用プログラミング言語」として使うようになっただけなのです。
ベル研究所のデニス・リッチーらが、自分たちが使うために作った言語が、今や世界中のコンピューターシステムの基盤になっています。いわば、ガレージから生まれた発明品が世界を変えたようなものです。この「小さな工房から生まれた奇抜なアイデアが世界を変える」というストーリーは、プログラミングの世界では珍しくありません。
K&R C(1970年代)
K&R C(Kernighan and Ritchie C)はC言語の初期バージョンです。UNIXのために開発されました。
main()
{
int i, sum = 0;
for (i = 1; i <= 10; i++)
sum = sum + i;
printf("The sum is: %d\n", sum);
return 0;
}
Code language: JavaScript (javascript)
初期のC言語はシンプルですが、forループなどの制御構文によって繰り返し処理をコンパクトに表現しています。K&R Cでは関数の戻り値の型は明示的に書かなくてもデフォルトでint型と見なされる点も、実用性を重視していた特徴が色濃く現れています。
PDP-7アセンブリ言語
PDP-7は18ビットのミニコンピューターで、Ken ThompsonがC言語の前身となる最初のUNIXシステムを開発したマシンです。
* PDP-7アセンブリ言語で1から10までの合計を計算するプログラム
* 注:これは推測に基づいた再現例です
.org 100 / プログラムは100から開始
start: lac zero / アキュムレータをゼロにクリア
dac sum / 合計値を初期化
lac one / カウンタを1に設定
dac count
loop: lac sum / 現在の合計をロード
add count / カウンタの値を加算
dac sum / 新しい合計を保存
lac count / カウンタをロード
add one / カウンタを1増やす
dac count / 新しいカウンタ値を保存
lac ten / 比較値(10)をロード
cma / 1の補数を取る(ビット反転)
add count / count-10の計算
sza / 結果がゼロならスキップ(count=10の場合)
jmp loop / まだ10未満ならループを続ける
hlt / 終了
* データ領域
one: 1 / 定数1
ten: 10 / 定数10
zero: 0 / 定数0
count: 0 / カウンタ変数
sum: 0 / 合計値
.end
このような長いアセンブリのコードの代わりに、(多少のオーバーヘッドはあるものの)短く書けるのがC言語の良さだったのです。
現代に続く流れ
多くのプログラムでC言語が使われるようになったのは、当時としては、書きやすさと生成されるコードの効率のバランスが良かったためです。この効率性は、主にコンピュータのメモリをどのように使うかをプログラマーが細かく決められることに由来します。利用目的に合わせて余分なメモリを使わずに済みますが、その分確保したメモリを超えないように動作させることは、プログラマーに任せられました。このような設計は高いパフォーマンスを実現する一方で、バッファオーバーフローなどのセキュリティ脆弱性も生み出しやすくなりました。
C言語は多くの現代プログラミング言語に影響を与えました。C++はC言語に直接オブジェクト指向の概念を追加したものです。JavaやC#もC言語の構文をベースにしながら、より安全で使いやすい機能を追加しています。「;」による文の区切り、「{}」で囲むブロック構造や「for」「if」などの制御構文が、現代のプログラミング言語の多くで標準的なのは、C言語の影響が大きいです。
ANSI C(1989年以降)
ANSI C(C89/C90)以降は、より厳格な文法規則が導入されました。
#include <stdio.h>
int main(void)
{
int i;
int sum = 0;
for (i = 1; i <= 10; i++) {
sum += i;
}
printf("The sum is: %d\n", sum);
return 0;
}
Code language: PHP (php)
関数の型宣言が明示的になり、より安全なコードになっています。
Java(1995年)
Javaはオブジェクト指向言語で、C言語の構文を受け継ぎながらメモリ管理などの安全性を向上させています。
public class SumExample {
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
System.out.println("The sum is: " + sum);
}
}
Code language: JavaScript (javascript)
Javaではすべてのコードをクラスの中に書く必要があります。またforループの初期化部分で変数宣言ができるようになっています。
Rust(2010年代)
Rustは安全性とパフォーマンスの両立を目指した現代的な言語です。
fn main() {
let sum: i32 = (1..=10).sum();
println!("The sum is: {}", sum);
}
Code language: JavaScript (javascript)
Rustでは関数型プログラミングの影響を受けたイテレータを使って、同じ処理をより簡潔に記述されています。ほかにも型推論や不変変数(let)などの現代的な機能があります。
言語の進化から見えること
これらの例から、プログラミング言語の進化が見えてきます。初期のアセンブリ言語やFORTRANでは、コンピューターの動作や科学計算に合わせた表現が中心でした。COBOLはビジネス文書に近い形式で、プログラミングの門戸を広げました。
C言語はこれらと異なり、コンピューターの動作を理解しつつも、より簡潔に記述できるように設計されています。その後のJavaやRustは、C言語の強みを引き継ぎながら、安全性や抽象化など現代のニーズに応える機能を追加しています。
特にRustの例では、1行で同じ処理を実現していますが、これはコンピューター内部の複雑な処理を高度に抽象化したものです。「1から10までの数値の範囲を作成し、その合計を求める」という人間の意図をそのまま表現できるようになっています。
C言語は、このような言語進化の重要な転換点にあります。低レベルの操作とわかりやすい表現のバランスが取れた言語として、その後の言語設計に大きな影響を与えたのです。
まとめ
C言語は、アセンブリ言語と高級言語の間の奇抜な位置づけを持つ言語として誕生しました。システムプログラミングという特定の目的のために作られた言語が、そのバランスの良さから広く使われるようになり、今日のプログラミング言語の源流となりました。万人向けに作られたわけではない言語が、結果的に万人に使われるようになったというのは、技術の歴史における興味深い一例です。
C言語は今でこそ多くのプログラミング言語の源流になっていますが、当時はとてもエキセントリックな感じでした。このことを説明します。C言語はアセンブリ言語と高級言語の中間に位置するような性質があります。しかしこれは古いプログラミング言語だからではありません。もっと古いプログラミング言語であるFortranやCOBOLは、より高級言語として型が決まっていて、それをもとにマシンコードを生成していくものです。計算をしたいユーザと機械コードを作っていく生成部分が明確に分業されていたように思います。計算式や経理書類をそのままプログラミング言語へと整えていったような作りになっています。一方、C言語はそういったものよりもよりコンピューターの性質に自然な言語設計になっているように思います。構文としては、ALGOL由来の構造化の書き方ですが、構造化原理主義と言うわけではなく、プリプロセッサやアセンブリ言語とごった煮状態で実用に重きを置いていたように思います。この点は、似たような時期に作られたPascalとも違います。つまりハッカーのためのプログラミング言語なのです。C言語がOSを作るために作られた言語であるのが特徴です。これは従来アセンブリ言語で機械のプロセッサーごとに作っていく必要がありました。しかしこれはコンピューターが変わると作り直す必要がありました。昔のコンピューターはその機械のためのプログラムシステムとして作られていたんだと思います。しかしPDPのような、そこにあるコンピューターを活用しようと思ったときにはシステムのポータビリティーというのが大事になってきます。ちまちまアセンブリで書いているようではしんどいからです。だからこそC言語はアセンブリ言語で記述するような処理を楽にするのがもともとの目的だったように思います。この点が計算や集計処理をするようなビジネスを目的とするプログラミング言語と違う点です。システムを作るためのプログラミング言語がC言語だからです。ただこのC言語は、当時としては記述のしやすさと生成コードの冗長さのバランスが良かったため、多くのプログラムで利用されるようになりました。C言語はよくわかりにくいとか使いにくいとか危険だとか言われるけれども、それはそういうもので、もともと万人向けしたプログラミング言語ではない。たまたま万人が使ったと言うことなのです。