K&R CとANSI Cを比較する:プ
ログラミング言語の哲学的変遷

プログラミング言語の歴史を振り返ると、言語の進化には単なる技術的な改良だけでなく、その背後にある哲学や価値観の変化も映し出されています。
特にC言語の初期バージョン(K&R C)と後の標準化バージョン(ANSI C)の間には、興味深い対比があります。

関連記事

1. K&R Cとは何か

K&R Cとは、ブライアン・カーニハン(Kernighan)とデニス・リッチー(Ritchie)によって開発され、1978年の著書「プログラミング言語C」で紹介されたC言語の初期バージョンを指します。
この本はC言語の「聖典」として広く読まれ、多くのプログラマーに影響を与えました。

K&R Cとは何か Kernighan & Ritchie 1978年 著書 「プログラミング言語C」 C言語の原典 UNIX開発と共に誕生 システムプログラミング向け シンプルさと効率性 最小限の構文・機械寄りの思想 プログラマーへの信頼 自由と責任を与える設計 → 1989年 ANSI Cとして標準化 互換性・厳格な型チェックを追加

K&R Cは、当時のUNIXオペレーティングシステムの開発と共に発展し、システムプログラミングに適した言語として設計されました。シンプルさと効率性を重視し、プログラマーに多くの自由と責任を与える設計思想が特徴です。

1.1. ANSI Cへの標準化

1980年代になると、C言語の人気が高まるにつれて、様々な実装の間で互換性の問題が生じるようになりました。そこで1989年に米国規格協会(ANSI)によってC言語の標準規格が制定されました。これがANSI Cです。

ANSI Cは、K&R Cの基本的な特徴を保ちながらも、より厳格な型チェックや新しい機能の追加、そして何より明確な言語仕様の策定を行いました。この標準化により、異なるプラットフォーム間での互換性が向上し、大規模な開発プロジェクトでの利用が容易になりました。

2. コードで見る両者の違い

K&R C vs ANSI C:主な違い K&R C ANSI C 関数定義(2段階) 関数定義(一体) int power(x, n) int x, n; { … } int power( int x, int n) { … } 型チェック 型チェック 緩やか・暗黙変換OK プログラマーの意図を信頼 厳格・警告・エラー 安全性を優先 「機械寄り・自由」→「人間寄り・安全」へのシフト ANSI Cは標準化により異なるプラットフォーム間の互換性を確保

2.1. 関数定義の方法

K&R CとANSI Cの最も分かりやすい違いの一つが、関数の定義方法です。

K&R Cでは、関数定義が二段階に分かれていました:

/* K&R スタイル */
int power(x, n)  /* まず関数名と引数名だけを宣言 */
int x, n;        /* 次に引数の型を別に宣言 */
{
    int i, p;
    p = 1;
    for (i = 1; i <= n; i++)
        p = p * x;
    return p;
}
Code language: JavaScript (javascript)

これに対し、ANSI Cでは関数の宣言と定義がより明示的になりました:

/* ANSI C スタイル */
int power(int x, int n) {
    int i, p;
    p = 1;
    for (i = 1; i <= n; i++)
        p = p * x;
    return p;
}
Code language: JavaScript (javascript)

K&Rスタイルは一見すると奇妙に見えますが、この書き方には独自の理由がありました。この二段階の定義方法がアセンブラコードのスタックフレーム構造と自然に対応している点です。アセンブラでの関数(サブルーチン)では、スタックに積まれた引数を取り出す際、まず「何個の引数があるか」と「それぞれの名前(位置)」が重要で、次に「各引数をどう解釈するか(型)」を考えます:

; アセンブラでの関数呼び出し(イメージ)
power:
    ; スタックフレームの設定
    push rbp
    mov rbp, rsp

    ; 引数の取り出し(位置で識別)
    mov eax, [rbp+8]   ; 最初の引数 x
    mov ecx, [rbp+12]  ; 2番目の引数 n

    ; あとは型がどうであれ計算処理
    ; ...Code language: CSS (css)

K&R Cの関数定義は、この「位置による引数の識別」という低レベルの考え方に近く、メモリレイアウトやスタックフレームを意識したプログラマーにとって直感的でした。

ANSI Cでは、より人間が読みやすく理解しやすい形に整理されましたが、同時にプログラミング言語が低レベルのハードウェアから少し遠ざかったとも言えます。この変化は「機械寄り」から「人間寄り」へのシフトを表しています。

2.2. 型チェックの厳格さ

K&R Cでは型チェックが比較的緩やかで、プログラマーの意図を尊重する姿勢がありました。例えば:

/* K&R スタイル */
main() {
    int i = 1;
    float f = 2.5;
    i = f;  /* 警告なしで暗黙の型変換が行われる */
    
    char c = 'A';
    i = c + 2;  /* 文字と整数の演算も自然に行われる */
    
    printf("i = %d\n", i);  /* 67が出力される('A'のASCII値65に2を足した値) */
}
Code language: PHP (php)

このコードでは、異なる型の間で暗黙の型変換が行われていますが、K&R Cではこれを許容します。プログラマーが意図的にそうしているのだろうと信頼する姿勢です。

ANSI Cではより厳格になり、型の不一致に対して警告やエラーが出るようになりました:

/* ANSI C スタイル */
#include <stdio.h>

int main(void) {
    int i = 1;
    float f = 2.5;
    i = f;  /* 警告:精度の損失の可能性 */
    
    char c = 'A';
    i = c + 2;  /* キャストを推奨: i = (int)c + 2; */
    
    printf("i = %d\n", i);
    return 0;
}
Code language: PHP (php)

これは、「約束事は明確にしておく」という考え方の表れです。学校のルールブックのように、すべてのルールが明文化されているイメージです。

3. Cのエレガンスが光るコード例

K&R時代のCコードには、独特の美しさがあります。

K&R のエレガンス:簡潔さの美学 文字列コピー while (*s++ = *t++) ; 1行でコピー完結 終端で自動停止 条件演算子の入れ子 m==2 ? (閏年) ? 29 : 28 : … 月の日数を1式で表現 閏年判定も内包 ビット操作 for(; n!=0; n>>=1) if(n & 01) b++; 右シフトで1ビットずつ 1の個数を計上 ポインタ・演算子・副作用を組み合わせた表現 読み解くほどに意図が見える「詩的な簡潔さ」 ANSI C以降:同じ処理でも明示的・冗長に 安全性と可読性を得た代わりに、表現の密度は薄れた

データと処理をまとめた「オブジェクト」が一般的になる前の時代、原始的なデータを使いながら抽象的な操作を簡潔に書く表現は、C言語の特色です。
このような簡潔さと効率性が調和した表現は、今のC言語にも引き継がれていると思います。

3.1. 文字列コピーの簡潔な表現

/* 文字列コピーのK&Rスタイル */
while (*s++ = *t++)
    ;
Code language: JavaScript (javascript)

文字列オブジェクトなどを使わずに、たった1行で文字列のコピー処理を表現しています。
「s」というポインタが指す場所に「t」の内容をコピーし、両方のポインタを次に進める、という操作を繰り返すコードです。
文字列の終端(値が0)になると自動的に終了します。

3.2. 文字列反転の洗練された実装

この関数は文字列を反転させます。

/* 文字列を反転するK&Rスタイルの関数 */
reverse(s)
char s[];
{
    char *p, *q, c;
    
    for (p = s, q = s + strlen(s) - 1; p < q; p++, q--) {
        c = *p;
        *p = *q;
        *q = c;
    }
}
Code language: HTML, XML (xml)

配列名sがポインタとして扱われ、ポインタ演算で文字列の末尾にアクセスする手法が簡潔です。
K&R Cでは「配列はポインタ」という視点が自然に表現されています。
これは箱に入った玉を、両端から交換していくようなイメージです。

3.3. 条件演算子の芸術的な使用

この関数は与えられた月(m)と年(y)に対して、その月の日数を返します。

/* 月の日数を返す関数 */
monthdays(m, y)
int m, y;
{
    return m==2 ? (y%4==0 && y%100!=0 || y%400==0) ? 29 : 28
                : m==4 || m==6 || m==9 || m==11 ? 30 : 31;
}
Code language: JavaScript (javascript)

条件演算子(三項演算子)を入れ子にして、うるう年の判定も含めたロジックを1行で表現しています。
一見複雑ですが、慣れると美しく読めるようになります。

3.4. ビット操作の芸術性

このコードは整数の二進表現で「1」のビットがいくつあるかを数えます。

/* 整数の2進表現での1のビット数を数える */
bitcount(n)
unsigned n;
{
    int b;
    
    for (b = 0; n != 0; n >>= 1)
        if (n & 01)
            b++;
    return b;
}
Code language: JavaScript (javascript)

右シフト演算子(>>)とビットごとのAND演算子(&)を使い、シンプルながら効率的なアルゴリズムを実現しています。

3.5. 複合代入演算子の巧みな活用

特に (*s++ = *t++) という表現は、「値をコピーしながらポインタを進める」という操作を一行で表現する職人技です。

/* 文字列sをt(最大n文字)にコピー */
mystrcpy(s, t, n)
char s[], t[];
int n;
{
    while (--n >= 0 && (*s++ = *t++))
        ;
    *s = '\0';
}
Code language: JavaScript (javascript)

現代の目から見ると「読みにくい」かもしれませんが、ポインタによって配列を塊として扱っているのです。

4. プリプロセッサによる言語拡張

K&R時代のCは、プリプロセッサ(コンパイル前に処理を行うツール)を使った「言語拡張」が花開いていました:

プリプロセッサ:言語の上に言語を作る #define による拡張 #define LOOP(v,s,e) \ for(v=s;v<=e;v++) #define BEGIN { #define END } → Pascal風の見た目に テキスト置換による 「方言」の創造 vs LISP マクロ (defmacro when (condition &rest body) `(if ,condition (progn ,@body))) → 構文木を直接操作 言語の構造を理解した上で 真のメタプログラミング C プリプロセッサ 「料理の言葉」の入れ替え テキスト置換のみ LISP マクロ 「料理」を理解した変形 構文木の直接操作
<code>#define LOOP(var, start, end) for(var = start; var <= end; var++)
#define BEGIN {
#define END }

main()
BEGIN
    int i, sum = 0;
    LOOP(i, 1, 10)
        sum += i;
    printf("Sum: %d\n", sum);
END</code>Code language: PHP (php)

これはまるでPascalやALGOLのような見た目になっています。こうした「言語の上に言語を作る」文化は、初期C言語の大きな特徴でした。UNIXの開発者たちは自分たちの考えを表現するため、常に言語を押し広げていたのです。これは自分だけの方言を作るようなものです。

実は、この「マクロによる言語拡張」という考え方は、LISP言語のマクロシステムと哲学的に通じるものがあります。LISPでは、マクロを使ってプログラム自体を変形・拡張する「メタプログラミング」が基本的な考え方でした:

lisp;; LISPのマクロ例
(defmacro when (condition &rest body)
  `(if ,condition
       (progn ,@body)))

;; 使用例
(when (> x 0)
  (print "xは正の数です")
  (setq positive-count (+ positive-count 1)))

LISPのマクロとCのプリプロセッサには、「コードがコードを生成する」という共通の考え方があります。これは料理人が新しい料理を作るために、基本的な材料から独自のソースや調味料を作るようなものです。

ただし、大きな違いもあります。LISPのマクロは言語自体の一部であり、プログラムの構造(構文木)を直接操作できますが、Cのプリプロセッサは単純なテキスト置換しかできません。言い換えれば、LISPのマクロは「料理」を理解した上で新しいレシピを作り出せますが、Cのプリプロセッサは「料理の言葉」を入れ替えるだけです。

K&R時代のプログラマーたちはこの制限を理解しながらも、プリプロセッサを創造的に使って言語を拡張し、自分たちの考えをより直感的に表現しようとしました。この「言語を道具として押し広げる」姿勢は、初期ハッカー文化の重要な側面でした。

ANSI Cでもプリプロセッサの機能は維持されましたが、「標準的な書き方」が推奨されるようになり、こうした創造的な使い方は徐々に減っていきました。これは自由な方言から「標準語」への移行とも言えるでしょう。

4.1. ヘッダーファイルとインターフェイスの哲学

K&R時代のCでは、プリプロセッサは単なるマクロの定義だけでなく、ヘッダーファイルという重要な概念と結びついていました。ヘッダーファイルはプログラムの「インターフェイス」を定義する重要な役割を担っていました:

/* stdio.h の一部(イメージ) */
#define NULL 0
#define EOF (-1)

/* 関数宣言 */
int getchar();
int putchar();
int printf();
FILE *fopen();Code language: CSS (css)
/* K&R スタイルのプログラム */
#include "stdio.h"  /* ヘッダーファイルの取り込み */

main()
{
    int c;
    while ((c = getchar()) != EOF)
        putchar(c);
}Code language: PHP (php)

この「#include」という仕組みは、表面上はシンプルなテキスト置換ですが、実際には「モジュール間のインターフェイス」という重要な考え方を具現化したものでした。これは契約書のようなものです。「stdio.h」は「標準入出力を使うなら、これらの関数や定数が使えますよ」という約束ごとを示しています。

4.2. インターフェイス分離

この考え方は現代のプログラミングでも重要な「インターフェイス分離」や「情報隠蔽」の先駆けでした。例えば:

/* mylib.h - インターフェイス */
int add(int a, int b);
int subtract(int a, int b);

/* mylib.c - 実装 */
int add(a, b)
int a, b;
{
    return a + b;
}

int subtract(a, b)
int a, b;
{
    return a - b;
}

/* main.c - 利用側 */
#include "mylib.h"

main()
{
    printf("%d\n", add(10, 5));     /* 関数の実装は知らなくても使える */
    printf("%d\n", subtract(10, 5));
}Code language: PHP (php)

この分離は「何ができるか」と「どうやって実現するか」を区別するという重要な設計思想です。これは店の「メニュー」(インターフェイス)と「調理方法」(実装)が分かれているのに似ています。お客さん(プログラマー)はメニューだけ見れば料理を注文できますが、調理方法を知る必要はありません。

K&R時代のヘッダーファイルは、形式的にはシンプルでしたが、この「インターフェイスと実装の分離」という考え方が自然に育まれていました。これは、後の時代のオブジェクト指向プログラミングにおける「インターフェイス」や「抽象クラス」の概念、さらには現代の「API設計」の原型とも言えるでしょう。

ANSI Cでは、この考え方がより明確に制度化されました。関数プロトタイプの導入により、インターフェイスはより厳格になりました:

/* ANSI C スタイルのヘッダーファイル */
#ifndef MYLIB_H
#define MYLIB_H

int add(int a, int b);  /* 引数の型も明示的に宣言 */
int subtract(int a, int b);

#endifCode language: CSS (css)

この変化は、「大きなプログラムを作るには、明確な約束ごとが必要」という認識の高まりを反映しています。これは村社会から都市社会への移行に似ています。村では暗黙の了解で多くのことが進みますが、都市では明文化されたルールが必要になるのです。

K&R時代の「自由な表現」から、ANSI C以降の「標準化された表現」への移行は、プログラミングが「小さな集団の芸術」から「産業としての工学」へと発展していく過程を表しています。この変化は必然的なものであり、どちらが優れているというよりは、時代の要請に応じた進化と言えるでしょう。

4.3. 「暗黙知」の尊重

K&R Cの美しい点は、「プログラマーの暗黙知」を尊重する姿勢です。
例えば、次のような配列初期化の方法:

K&R の美学:暗黙知の尊重 コードは 「会話」 プログラマーの暗黙知 /* ASCII判定テーブル */ char ctype[] = { 0, 0, 0, 0, _S, _S, _S, /* … */ }; 言葉少なに多くを語る while (*s++ = *t++) ; ・コピー ・ポインタ進行 ・終端検出 → すべて1行に凝縮 暗黙知 vs 明示 K&R:信頼・共通理解が前提 ANSI:すべてを明文化する文化
/* ASCII文字の種類を判定するテーブル */
char ctype[] = {
    0, 0, 0, 0, 0, 0, 0, 0,
    _S, _S, _S, 0, 0, _S, 0, 0,
    /* 省略 */
};
Code language: JavaScript (javascript)

ここでマクロ_Sは空白文字を表し、配列のインデックスはASCII値に対応しています。
このような「暗黙の理解」に基づくコードは、初期C言語の「言葉少なに多くを語る」美学を表しています。

5. ポインタの表現力

C言語の最も強力な機能の一つがポインタです。
ポインタの扱いが柔軟で強力でした。

ポインタの表現力 配列 ≈ ポインタ(K&R Cの核心) a[0]=1 a[1]=2 a[2]=3 a[3]=4 a[4]=5 p = a a[2] = p[2] = *(a+2) 文字列 = ポインタ char *s = “hello”; 文字列はアドレス K&R の直感:データと参照の分離 現代:オブジェクト参照 String s = “hello”; // Java 「すべてはオブジェクトへの参照」 K&R Cのポインタ思想は、OOP参照モデルの先駆け

5.1. 配列とポインタの親密な関係

K&R Cでは、配列とポインタの関係が特に密接です。
この二つの概念は、ほぼ同じものとして扱われることが多くありました:

/<em style="background-color: initial; font-family: inherit; font-size: inherit; text-align: initial; color: inherit;">* K&R スタイルの配列とポインタ */</em><code>main()
{
    int a[5] = {1, 2, 3, 4, 5};
    int *p;
    
    p = a;      <em>/* 配列名はポインタとして扱われる */</em>
    
    printf("%d %d\n", a[2], p[2]);  <em>/* どちらも3が出力される */</em>
    printf("%d %d\n", *(a+2), *(p+2));  <em>/* 同じく3が出力される */</em>
}</code>Code language: HTML, XML (xml)

この例では、配列名aが自動的にその先頭要素へのポインタとして扱われています。
この考え方は、データとそのアドレス(場所)を密接に結びつけるという哲学の表れです。

これは本の「目次」と似ています。
目次(配列名)は本の内容(データ)がどこにあるかを示すもので、目次自体が内容なのではなく、内容の「ありか」を示しているのです。

ANSI Cでもこの関係は基本的に維持されましたが、配列とポインタの違いがより明確に区別されるようになりました:

/* ANSI C スタイル */
int main(void) {
    int a[5] = {1, 2, 3, 4, 5};
    int *p;
    
    p = a;  <em>/* 配列からポインタへの変換は維持 */</em>
    
    <em>/* しかし、以下のような操作は不可能 */</em>
    a = p;  <em>/* エラー: 配列名は変更不可能なポインタ */</em>
    
    <em>/* sizeof演算子は配列とポインタで異なる結果に */</em>
    printf("sizeof(a) = %d\n", (int)sizeof(a));  <em>/* 20(5要素 × 4バイト) */</em>
    printf("sizeof(p) = %d\n", (int)sizeof(p));  <em>/* 4(ポインタのサイズ) */</em>
    
    return 0;
}Code language: JavaScript (javascript)

この変化は、「似ているが同じではない」という微妙な違いをコード上で明確にした例です。
K&R Cでは、この違いがぼんやりとしていましたが、ANSI Cでは違いを明確にすることで安全性を高めました。

5.2. 文字列のポインタ表現と現代の参照モデル

K&R Cでは、文字列が本質的にはポインタであるという考え方が自然に表現されています:

/* K&R スタイルの文字列表現 */
char *s = "hello";     /* 文字列リテラルを直接ポインタに代入 */
char t[] = "world";    /* 配列としての文字列 */

/* どちらも同じように扱える */
printf("%s %s\n", s, t);

/* 配列とポインタの交換可能性 */
while (*t)
    putchar(*t++);     /* tをポインタとして扱う */
Code language: JavaScript (javascript)

この例では、文字列を「文字へのポインタ」として扱うK&R Cの直感的な表現が示されています。
配列とポインタの境界が曖昧なこの設計は、多くの表現の可能性を生み出しました。
このアプローチは、本の「ページ」と「そのページに書かれている内容」が分離できないのと同じように、コンテナとコンテンツを一体として扱う考え方です。

オブジェクトの参照モデル

実はこの「データの実体と、それを指し示す参照」という概念は、現代のオブジェクト指向言語の参照モデルの先駆けとも言えます。
例えばJavaやPythonなどの言語では、オブジェクトに対する「参照」を通じてデータにアクセスするという考え方が基本になっています。

// Javaでの参照の例
String s = "hello";  // 文字列オブジェクトへの参照
Code language: JavaScript (javascript)

K&R Cの文字列ポインタの考え方は、当時としては非常に先進的な「データと参照の分離」という視点を提供していました。
いわば、オブジェクト指向プログラミングの「すべてはオブジェクトへの参照である」という思想の原型が、すでにここに存在していたのです。

ANSI Cではこの区別がより明確になり、特にconst修飾子の導入により、文字列リテラルは変更不可能であることが強調されるようになりました:

/* ANSI C スタイル */
const char *s = "hello";  /* 変更できない文字列としてマーク */
char t[] = "world";       /* 変更可能な文字の配列 */

/* これは危険な操作としてコンパイラが警告 */
s[0] = 'H';               /* 文字列リテラルの変更を試みる */
Code language: JavaScript (javascript)

5.3. 関数ポインタの活用

K&R Cでは関数ポインタの宣言と使用も特徴的でした:

/* K&R スタイルの関数ポインタ */
int (*compare)();    /* 任意の数の引数を取る関数へのポインタ */

/* 比較関数 */
int ascending(a, b)
int a, b;
{
    return a - b;
}

int descending(a, b)
int a, b;
{
    return b - a;
}

/* ソート関数 */
sort(arr, n, cmpfn)
int arr[];
int n;
int (*cmpfn)();      /* 関数ポインタを引数に */
{
    int i, j, temp;
    
    for (i = 0; i < n-1; i++)
        for (j = i+1; j < n; j++)
            if ((*cmpfn)(arr[i], arr[j]) > 0) {
                temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
}

/* 使用例 */
main()
{
    int numbers[] = {5, 2, 7, 3, 1};
    
    /* 昇順ソート */
    sort(numbers, 5, ascending);
    
    /* 降順ソート */
    sort(numbers, 5, descending);
}
Code language: JavaScript (javascript)

この例では、関数自体をデータとして扱うK&R Cの柔軟性が示されています。
関数ポインタを使うことで、ソートアルゴリズムの動作を変更することができます。

ANSI Cでは関数ポインタの宣言がより明示的になりました:

/* ANSI C スタイル */
int (*compare)(int, int);   /* 引数と戻り値の型を明示 */

/* 比較関数 */
int ascending(int a, int b) {
    return a - b;
}

/* ソート関数 */
void sort(int arr[], int n, int (*cmpfn)(int, int)) {
    /* ... */
}
Code language: JavaScript (javascript)

K&R Cでは関数ポインタの宣言がより自由でしたが、それは「プログラマーが意図を理解している」という前提に立っていました。
ANSI Cではより安全性を重視し、型情報を明示することが求められるようになりました。

5.4. gotoの適切な使用

K&R時代のCでは、goto文(プログラムの実行位置を直接指定する命令)が悪とされる前に、適切な使い方が示されていました:

/* エラー処理のためのgoto */
doit()
{
    int *p;
    FILE *fp;
    
    p = malloc(sizeof(int) * 10);
    if (p == NULL)
        goto error;
        
    fp = fopen("data.txt", "r");
    if (fp == NULL)
        goto error;
        
    /* 処理 */
    fclose(fp);
    free(p);
    return 0;
    
error:
    if (p != NULL)
        free(p);
    if (fp != NULL)
        fclose(fp);
    return -1;
}
Code language: PHP (php)

このようなリソース管理のためのgoto文は実用的で、コードの流れを乱すことなくエラー処理ができます。
構造化プログラミングが主流になる前の、実用的な知恵の表れです。
これは迷路の中で「もし行き止まりになったら入口に戻る」という指示に似ています。

6. エラー処理の哲学

K&R Cではエラー処理が最小限でした。

エラー処理の哲学 K&R C 「エラーは例外的な状況」 fp = fopen(“f.txt”,”r”); if (fp == NULL) exit(1); × 即時終了 シンプルだが情報なし ANSI C 「エラーは通常処理の一部」 fp = fopen(“f.txt”,”r”); if (fp == NULL) { perror(“Error”); } エラーメッセージ EXIT_FAILURE 情報付き・構造化 goto によるリソース管理(K&R)→ 構造化エラーハンドリング(ANSI)へ

例えば:

/* K&R スタイル */
FILE *fp;
fp = fopen("file.txt", "r");
if (fp == NULL)
    exit(1);  /* シンプルに終了 */
Code language: PHP (php)

ANSI Cになると、より構造化されたエラー処理が推奨されるようになりました:

/* ANSI C スタイル */
FILE *fp;
fp = fopen("file.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    return EXIT_FAILURE;
}
Code language: PHP (php)

この違いは「エラーは例外的な状況」(K&R)から「エラーは通常の処理の一部」(ANSI)という思想の変化を表しています。これは「雨が降ったら予定を中止する」から「雨が降っても代替案がある」という考え方の変化に似ています。

6.1. 型システムの進化

K&R Cでは、型システムが現代の目から見るとかなり寛容でした:

main() {
    int a[10];
    int *p = a;       /* 配列と同じように添字アクセスできる */
    p[5] = 123;       /* ポインタに配列のような添字を使える */
    
    void *generic;    /* K&Rでは明確にvoid*の概念はなかった */
    generic = a;      /* ポインタ型の自動変換 */
    p = generic;      /* 警告なし */
}
Code language: JavaScript (javascript)

ANSI Cでは型の互換性がより厳格になり、警告や場合によってはエラーが出るようになりました。この変化は、大規模プロジェクトでの安全性を高めるためのものですが、同時に言語の「遊び心」や「表現の自由度」は減少したとも言えます。

これは子供の遊び場に例えられます。K&R Cは見守る大人が少ない自由な遊び場、ANSI Cは安全柵が設置された遊び場のようなものです。

7. structとunionの洗練された使い方

Cの構造体(struct)と共用体(union)の扱いは、データ構造を表現する強力な手段でした。
特に、メモリレイアウトを直接制御できる点が特徴的です。

struct と union の表現力 struct 各フィールドが別メモリ struct point { int x; int y; }; x y 別々に確保 → 多目的に応用 点・時刻・座標 union 同じメモリを共有 union value { int i; float f; }; i または f 同一メモリを共有 → 省メモリ 型を切り替えて解釈 タグ付き共用体 型情報 + データの組合せ struct data { int type; union { int i; float f; } value; }; 「何か」を示すタグで 値の解釈を切り替える → OOPの多態性の原型 JS Object / Python dict

7.1. 構造体の柔軟な表現

K&R Cの構造体宣言は今日のそれとは少し異なっていました:

/* K&R スタイルの構造体宣言 */
struct point {
    int x;
    int y;
};

/* 構造体変数の定義 */
struct point p1;
struct point p2;

/* 初期化 */
p1.x = 10;
p1.y = 20;

/* 構造体ポインタの使用 */
struct point *pp = &p1;
(*pp).x = 30;  /* 間接参照と . 演算子の組み合わせ */
Code language: JavaScript (javascript)

特筆すべきは、K&R CではK&R Cの後のバージョンで導入された「->演算子」(構造体ポインタからメンバーへのアクセス)がなく、ポインタの間接参照と.演算子を組み合わせる必要があったことです。これは車のエンジンにアクセスするために、まずボンネットを開け(*)、それから特定の部品に手を伸ばす(.)ようなものでした。

後にANSI Cでは、この操作を簡略化する->演算子が標準化されました:

/* ANSI C スタイル */
struct point *pp = &p1;
pp->x = 30;  /* より簡潔な表現 */
Code language: PHP (php)

7.2. タグの省略と匿名構造体

K&R Cでは、構造体のタグ(名前)を省略することも一般的でした:

/* 匿名構造体 */
struct {    /* タグ名なし */
    int hour;
    int minute;
    int second;
} time1, time2;  /* 変数のみで型に名前がない */

/* 使い方 */
time1.hour = 9;
time1.minute = 30;
Code language: JavaScript (javascript)

この書き方は、使い捨てのデータ構造を簡潔に表現できます。一度しか使わない型に名前をつける必要がない、という考え方です。これは使い捨ての容器のようなもので、長く使うものには名前を付けますが、一度だけ使うものには名前を付ける必要はありません。

7.3. unionの創造的な使用

K&R Cのunion(共用体)は、メモリの効率的な使用と型の柔軟な解釈を可能にしました:

/* K&R スタイルのunion */
union value {
    int i;
    float f;
    char *s;
};

/* 使い方 */
union value v;
v.i = 10;      /* 整数として使用 */
printf("%d\n", v.i);

v.f = 3.14;    /* 浮動小数点として使用 */
printf("%f\n", v.f);

v.s = "hello"; /* 文字列ポインタとして使用 */
printf("%s\n", v.s);
Code language: JavaScript (javascript)

unionは同じメモリ領域を異なる型として解釈するという、ハードウェアに近い考え方を表現しています。これは同じスペースを目的に応じて「寝室」にも「書斎」にも使える多機能な部屋のようなものです。

7.4. structとunionの組み合わせ

K&R時代には、structとunionを組み合わせた独創的なデータ構造も多く見られました:

/* 汎用データ構造 */
struct data {
    int type;  /* データ型を示すフラグ */
    union {
        int i;
        float f;
        char *s;
    } value;   /* 匿名unionをメンバーとして持つ */
};

/* 使い方 */
struct data d;
d.type = 0;     /* 0: 整数, 1: 浮動小数点, 2: 文字列 */
d.value.i = 42;

if (d.type == 0)
    printf("整数: %d\n", d.value.i);
else if (d.type == 1)
    printf("浮動小数点: %f\n", d.value.f);
else
    printf("文字列: %s\n", d.value.s);
Code language: JavaScript (javascript)

この設計は、今日の多くのプログラミング言語に見られる「タグ付き共用体」や「バリアント型」、あるいはオブジェクト指向言語の「多態性」の先駆けとなる考え方でした。データとその型情報を一緒に持つことで、データの意味を状況に応じて変えられるという柔軟性を提供しています。

実際、この「データとその解釈の組み合わせ」という考え方は、現代のJavaScriptにおける「オブジェクト」やPythonの「ディクショナリ」などの動的なデータ構造の概念的な祖先と見ることもできます。これは荷物にタグを付けて、中身が何かを示すようなものです。

ANSI Cではこれらの機能は基本的に維持されましたが、より厳格な型チェックが導入され、型の不一致や不適切な使用に対する警告が増えました。これは「自由に使えるが、責任も伴う」というK&R Cの哲学から、「間違いを防ぐための柵」を設ける方向へのシフトを表しています。

8. プログラミング言語の「官僚化」現象

K&R CからANSI Cへの変化は、プログラミング言語の「官僚化」とも呼べる現象を表しています。これはプログラミングという活動が「芸術」から「産業」へと変化していく過程の反映です。

プログラミング言語の「官僚化」 K&R時代 〜1980年代 ANSI C 1989年〜 Java / ADA 1990年代〜 職人集団 芸術・研究 暗黙知・自由 標準化・移行期 互換性の確保 規則の明文化 産業化・分業 誰が書いても同じ 工場製品化 「芸術」から「工学」へ K&R の forever ループ #define forever for(;;) 現代:while(true) 明確・標準的・個性なし

初期のプログラミングは、いわば「研究者」や「職人」「芸術家」が行う活動でした。
UNIXを開発したベル研究所のプログラマーたちは、ある種の職人集団であり、各々が言語の可能性を探求していました。

/* K&Rスタイルの表現 */
#define forever for(;;)    /* 無限ループを表す詩的な表現 */

main()
{
    int c, nl = 0, nw = 0, nc = 0, inword = 0;
    
    forever {
        if ((c = getchar()) == EOF)
            break;
        nc++;
        if (c == '\n')
            nl++;
        if (c == ' ' || c == '\n' || c == '\t')
            inword = 0;
        else if (inword == 0) {
            inword = 1;
            nw++;
        }
    }
    printf("%d %d %d\n", nl, nw, nc);
}
Code language: PHP (php)

このコードは「wc」コマンド(テキストファイルの行数、単語数、文字数を数える)の簡易版ですが、foreverというマクロの使用は詩的であり、アルゴリズムの表現も簡潔です。

対して、プログラミングが職業として確立されると、「誰が書いても同じような結果になる」ことが重視されるようになりました。これは手作りの家具から工場製品への変化のようなものです。職人の作った家具には個性がありますが、工場製品は誰が作っても同じ形になります。

8.1. 言葉から工業製品へ

初期C言語の魅力は、「プログラマーの言葉」としての親密さにありました。
人間同士の会話のように、多くを語らずとも通じ合えるという信頼関係がそこにはあったのです。
言葉は曖昧さを含みますが、話し手と聞き手の間に共通理解があれば、簡潔に多くを表現できます。

ANSI C以降の言語は「工業製品」としての性質を強めました。
誰が使っても同じ結果が得られるように標準化され、安全装置が組み込まれています。
「言葉としての親密さ」が薄れつつありますが、その代わりに多くの人が安全に使えるという利点を獲得しました。

K&R Cの「詩的な簡潔さ」と、ANSI Cの「明示的な安全性」。
どちらが優れているというわけではなく、時代と用途の変化に合わせて進化してきた結果なのです。