デニス・リッチーのCがUnixを形作り、カーネルや組み込み機器、高速ソフトウェアを今も支えている理由。移植性、性能、安全性についての要点を解説します。

Cは多くの人が直接触ることは少ない技術の一つですが、ほとんどの人が依存しています。あなたがスマートフォン、ノートPC、ルーター、自動車、スマートウォッチ、あるいは表示付きのコーヒーメーカーを使っているなら、そのスタックのどこかにCが関わっていて、デバイスの起動、ハードウェアとのやり取り、あるいは「瞬時」に感じられるだけの十分な高速動作を支えています。
構築者にとってCは、制御性と移植性という稀な組み合わせを提供するため実用的な道具です。メモリやハードウェアを直接管理できるほどマシンに近く動作しますが、比較的少ない書き直しで異なるCPUやOSに移植できます。この組み合わせは代替が難しいものです。
Cの最大の足跡は次の三つの領域に表れます。
アプリが高級言語で書かれていても、その基盤や性能に敏感なモジュールはしばしばCに由来します。
この記事では、デニス・リッチー、Cの背後にある設計目標、そして現代製品でCが登場する理由をつなぎます。取り上げる内容:
これはCに特化した記事です。C++やRustは比較のために登場することがありますが、焦点はCそのもの、なぜそう設計されたか、そして実システムで選ばれ続ける理由にあります。
デニス・リッチー(1941–2011)は、AT&Tベル研究所での業績で最もよく知られるアメリカの計算機科学者です。ベル研は初期のコンピューティングと電気通信で中心的な役割を果たしました。
1960年代後半から1970年代のベル研で、リッチーはケン・トンプソンらとともにOS研究に取り組み、Unixにつながりました。トンプソンが初期のUnixを作り、リッチーはそれを保守・改良・共有できるシステムへと進化させる重要な共同制作者となりました。
リッチーはベル研で使われていた以前の言語の考えを取り入れてCを作りました。Cはシステムソフトウェアを書くために実用的に設計されており、メモリやデータ表現を直接扱える一方で、アセンブリより読みやすく移植しやすいという長所があります。
この組み合わせは重要でした。UnixがCで書き直されることで、単に見た目のためではなく、新しいハードウェアへ移すのが圧倒的に容易になり、時間とともに拡張できるようになりました。結果として強力なフィードバックループが生まれました:UnixはCにとって厳しい実運用のケースを提供し、CはUnixを一台の機械に留めず広めることを助けたのです。
UnixとCは「システムプログラミング」を定義しました:機械に近いけれど特定のプロセッサに縛られない言語でOSやコアライブラリ、ツールを作るというアプローチです。その影響は後のOSや開発ツール、今日の多くのエンジニアが学ぶ慣習に表れています――神話ではなく、実際にスケールで機能したからです。
初期のOSは大部分がアセンブリで書かれていました。アセンブリはハードウェアを完全に制御できますが、変更は遅く、エラーが起きやすく、特定のプロセッサに強く結びついていました。些細な機能でも低レベルのコードが膨大になり、別の機械に移すには大規模な書き直しが必要でした。
リッチーは真空の中でCを発明したわけではありません。Cはベル研で使われていた以前の簡潔なシステム言語から発展しました。
Cはコンピュータが実際に行うこと(メモリのバイト、レジスタでの算術、コードのジャンプ)にきれいに対応するよう作られました。シンプルなデータ型、明示的なメモリアクセス、CPU命令に対応する演算子が言語の中心にあるのはそのためです。大規模なコードベースを管理できるほど高水準でありながら、メモリ配置や性能を直接制御できる程度に低水準でもあります。
「移植性」とは同じCソースコードを別のコンピュータに移して、最小限の変更でそこでコンパイルし、同じ振る舞いを得られることを意味します。OSを各プロセッサごとに書き直す代わりに、ほとんどのコードを共有し、ハード依存の部分だけを小さく差し替える、というやり方がUnixの普及を促しました。
Cの速さは魔法ではなく、コンピュータの実際の動作に直接対応していることと、コードとCPUの間に余計な「仕事」がほとんど挟まれないことの結果です。
Cは通常コンパイルされます。つまり、人間に読みやすいソースを書き、コンパイラがそれを機械語に翻訳します。
実際にはコンパイラは実行可能ファイル(または後でリンクされるオブジェクトファイル)を生成します。重要なのは最終結果が実行時に逐次解釈されるのではなく、CPUが理解する形式で既に用意されていることです。これがオーバーヘッドを減らします。
Cは関数、ループ、整数、配列、ポインタといったシンプルな構成要素を与えます。言語が小さく明示的であるため、コンパイラはしばしば直截な機械語を生成できます。
必須のランタイムがなく、隠れたチェックや複雑なメタデータを挿入する仕組みが入らないことが多い点が重要です。ループを書けば概ねループが得られ、配列要素にアクセスすれば直接のメモリアクセスが行われます。この予測可能性がCが性能に強い理由の一つです。
Cは手動メモリ管理を採用しており、プログラムが明示的にメモリを要求(例:malloc)し、明示的に解放(例:free)します。これはいつメモリを割り当て、どれだけ保持するかを細かく制御したいシステムレベルのソフトウェアに必要なことが多く、隠れたオーバーヘッドを最小化できます。
代償は明白です:制御が増すほど責任も増えます。メモリを解放し忘れたり、二重解放したり、解放後のメモリを使ったりすると、重大で時にはセキュリティに関わるバグにつながります。
OSはソフトウェアとハードウェアの境界に位置します。カーネルはメモリ管理、CPUスケジューリング、割り込み処理、デバイスとの通信、システムコールの提供を担います。これらは抽象的な問題ではなく、特定のメモリ位置の読み書き、CPUレジスタ操作、都合の悪いタイミングで届くイベントへの対応などを含みます。
ドライバとカーネルは「これを正確にやれ」と表現できる言語を必要とします。実際には:
Cはそのコアモデルがバイト、アドレス、単純な制御フローに近いため、この用途に適しています。カーネルが起動する前にホストすべき必須のランタイムやGC、オブジェクトシステムがないのです。
Unixや初期システムの考え方は、ポータブルな言語でOSの大部分を実装しつつハードウェア寄りの部分は薄く保つ、というアプローチを普及させました。多くの現代カーネルもこのパターンに従います。アセンブリが必須な箇所(ブートコード、コンテキストスイッチなど)は別ですが、実装の大半はCで担われることが多いです。
Cはまた標準Cライブラリ、ネットワークの基盤コード、低レベルランタイムといったコアシステムライブラリで支配的です。Linux、BSD、macOS、Windows、RTOSのいずれを使っていても、裏側にCコードがある可能性は高いです。
OS分野でのCの魅力は単なる懐古趣味ではなく、工学上の経済性に根ざしています:
RustやC++など他の言語もOSの一部で使われ利点をもたらしますが、Cは共通分母として残ります。多くのカーネルがCで書かれており、低レベルインタフェースの多くがCを前提にしているため、他のシステム言語はCと相互運用しなければなりません。
「組み込み」は多くの場合、サーモスタットやスマートスピーカー、ルーター、車載機器、医療機器、工場のセンサ、家電など、普段コンピュータと認識されない装置内のマイコンを指します。これらは一つの目的を長期にわたり稼働し続け、コスト、電力、メモリに厳しい制約があります。
多くの組み込みターゲットはキロバイト単位のRAMしかなく、コード用のフラッシュも限られています。バッテリで動くものは大半の時間スリープしている必要があり、リアルタイム締め切りを持つものもあります。モーター制御ループが数ミリ秒遅れるだけでハードウェアが誤動作することもあります。
こうした制約はプログラムのサイズ、ウェイク頻度、タイミングの予測可能性などあらゆる決定に影響します。
Cは小さなバイナリを生成し、ランタイムオーバーヘッドが最小です。必須の仮想マシンはなく、動的割当を避けることも容易です。フラッシュにファームウェアを収めたり、デバイスが不意に「停止」しないことを保証したいときに重要です。
さらに重要なのは、Cがハードウェアとの対話を簡単にする点です。組み込みチップはGPIO、タイマ、UART/SPI/I2Cといった周辺装置をメモリマップドレジスタで公開することが多く、Cのモデルはこれらに自然にマッピングされます。特定のアドレスを読み書きし、個々のビットを操作できることが重要です。
多くの組み込みCは以下のいずれかです:
いずれの場合もハードウェアレジスタ(しばしばvolatile指定)、固定サイズバッファ、注意深いタイミングを扱うコードが中心になります。こうした「マシンに近い」スタイルが、フラッシュ/RAMや電力、デッドラインに厳しいファームウェアでCがデフォルト選択となる理由です。
「パフォーマンス重視」は時間や資源が製品の一部である状況を指します:ミリ秒がユーザー体験に影響し、CPUサイクルがサーバーコストに影響し、メモリ使用量がプログラムが収まるか否かを決めます。そうした場所でCは、データ配置、仕事のスケジューリング、コンパイラに許す最適化を制御できるためデフォルトの選択肢です。
Cは高頻度または厳しいレイテンシ要件のあるシステムの中心にあることが多いです:
これらの領域では通常、ほんの一部の内部ループが実行時間の大半を占めます。
チームは製品全体をCに書き換えることは稀で、プロファイルしてホットパスだけをCで最適化します。
Cはホットパスで役立ちます。低レベルの詳細(メモリアクセスパターン、キャッシュの振る舞い、分岐予測、割当オーバーヘッド)を調整できるため、データ構造をチューニングし不要なコピーを避け割当を制御すれば大幅な高速化が見込めます。
現代製品は多言語混在が普通です:全体はPython、Java、JavaScript、あるいはRustで書き、クリティカルなコアはCで実装します。
一般的な統合方法:
このモデルなら高級言語で素早く開発しつつ、重要箇所で予測可能な性能を得られます。代償は境界でのデータ変換や所有権、エラーハンドリングに注意が必要なことです。
Cが急速に普及した一因は「移動できる」ことです:同じコア言語が小さなマイコンからスーパーコンピュータまで実装され得ます。その移植性は魔法ではなく、共有された標準とそれに従う文化の成果です。
初期のC実装はベンダーごとに違い、コードの共有が難しかった。大きな転換はANSI C(C89/C90)とその後のISO C(C99、C11、C17、C23など)でした。バージョン番号を暗記する必要はありませんが、標準が公開された合意であり、言語と標準ライブラリの振る舞いを定義する点が重要です。
標準は次を与えます:
だから標準に沿って書かれたコードはコンパイラやプラットフォーム間で比較的少ない変更で移行できます。
移植性の問題は多くの場合、標準が保証していないことに依存するときに起きます:
intが必ず32ビットとは限らず、ポインタサイズも環境によって異なる良いデフォルトは標準ライブラリ優先で書き、非移植コードは小さく名前付けされたラッパーに隠すことです。
また、移植性を促すコンパイルフラグを使いましょう。一般的には:
-std=c11)-Wall -Wextra)真剣に扱う標準優先のコードと厳格なビルドは、どんなトリックより移植性を高めます。
Cの力は刃でもあります:メモリに近づける一方で、他の言語が防ぐミスを簡単に犯せてしまいます。これがCが高速かつ柔軟である理由の一方で、初心者や疲れた熟練者が間違える原因でもあります。
プログラムのメモリを番号付きの郵便箱が並ぶ通りだと想像してください。変数は何かを入れる箱です(整数など)。ポインタは箱そのものではなく、「どの箱を開けるかを書いた紙片」、つまり住所です。
これにより中身をコピーせずに住所を渡せますし、配列やバッファ、構造体、関数を指せます。しかし住所が間違っていれば、間違った箱を開けてしまいます。
これらはクラッシュ、検出されにくいデータ破損、セキュリティ脆弱性として現れます。システムコードではこれらの失敗が上位のソフトウェア全体に影響を与えます。
Cは「デフォルトで危険」ではなく許容的です:コンパイラはあなたが書いたものをそのまま意味すると仮定します。これは性能と低レベル制御には有利ですが、注意深い習慣、レビュー、良いツールと組み合わせないと誤用しやすいということです。
Cは直接的な制御を与えますが、ミスをめったに許しません。良いニュースは「安全なC」は魔法ではなく、規律ある習慣、明確なインタフェース、ツールによる自動チェックの組み合わせで実現できるという点です。
APIを誤用しにくく設計しましょう。ポインタと一緒にバッファサイズを取る関数、明示的なステータスコードを返す関数、誰が確保を解放するかを文書化することを好みます。
境界での長さチェックを常態化し、事前に検証して失敗を早期に返すこと。メモリ所有権は単純に保ちます:一つのアロケータとそれに対応する解放経路、呼び出し側か呼ばれ側かで明確にすること。
現代のコンパイラは危険なパターンを警告できます。CIで警告をエラー扱いにしてください。開発ではSanitizer(AddressSanitizer/UBSan/LSanなど)を有効にして、境界外書き込み、use-after-free、整数オーバーフローなどを検出します。
静的解析やリンタはテストで見つからない問題を掘り起こします。ファズィングはパーサやプロトコル処理に特に有効で、予期せぬ入力でバッファや状態マシンのバグを露呈します。
コードレビューではオフバイワン、NUL終端忘れ、符号付き/符号なしの混同、戻り値の未チェック、エラーパスでのメモリリークなど、C固有の失敗モードを明示的にチェックしてください。
言語が保護してくれない分、テストの重要性は増します。ユニットテストは基本で、統合テスト、既知のバグに対する回帰テストがより効果的です。
厳密な信頼性や安全性が求められる場合は、Cの制限されたサブセットと明文化されたコーディング規則を採用することを検討してください(ポインタ算術の制限、特定のライブラリ呼び出しの禁止、ラッパーの要求など)。重要なのは一貫性です:ツールやレビューで強制できる現実的な規則を選びましょう。
Cは理解し切れるほど小さく、ハードウェアやOS境界に近いほど「糊(のり)」として不可欠です。この組み合わせが、見た目が良い新しい言語があってもチームがCに手を伸ばす理由です。
C++はクラスやテンプレート、RAIIといった強力な抽象化機構を加えることを目指しており、多くのCとソース互換性を保ちますが「互換」であって「同一」ではありません。暗黙の変換やオーバーロード解決、限界ケースでの宣言有効性などに違いがあります。
実製品では混在が一般的です:
橋渡しは通常CのAPI境界です。C++側はextern "C"で関数をエクスポートし名前修飾を避け、双方がプレーンなデータ構造で合意します。これにより段階的なモダン化が可能になります。
Rustの大きな約束はGCなしでのメモリ安全性であり、強力なツールとパッケージ生態系に支えられて多くのバグクラス(use-after-free、データ競合)を減らせます。
しかし採用には代価が伴います:
RustはCと相互運用できますが、境界は複雑さを追加し、すべての組み込みターゲットやビルド環境で同等にサポートされているわけではありません。
世界の基盤コードの多くがCで書かれており、書き換えはリスクとコストが高いです。Cは予測可能なバイナリ、小さなランタイム仮定、幅広いコンパイラ入手性に合致し、マイコンから一般CPUまでカバーします。
最大の到達範囲、安定したインタフェース、実績あるツールチェーンが必要ならCは合理的選択です。安全性最優先なら新しい言語を検討すべきです。最良の判断はターゲットハードウェアとツール、長期保守計画に基づきます。
Cは「消える」わけではありませんが、その重心は明確になりつつあります。メモリやタイミング、バイナリ制御が重要な領域では今後も活躍し、安全性や反復速度が最重要な領域では徐々に立ち位置を譲るでしょう。
Cは引き続きデフォルト選択になりやすい:
これらの領域はゆっくり進化し、巨大なレガシーコードベースがあり、バイトや呼び出し規約、失敗モードを推論できるエンジニアを評価します。
新規アプリ開発では、より強い安全保証と豊富なエコシステムを持つ言語が好まれています。メモリ安全バグはコストが高く、現代のプロダクトは迅速な提供や並行性、安全なデフォルトを優先する傾向があります。システムプログラミングでも新しいコンポーネントがより安全な言語へ移行する一方で、Cは既存の基盤として残るでしょう。
低レベルのコアがCでも、周辺にはWebダッシュボードやAPIサービス、デバイス管理ポータル、内部ツール、診断用の小さなモバイルアプリなどが必要です。これら上位層は反復速度が重視されることが多いです。
上位層を素早く構築したいなら、Koder.aiのようなツールでチャットを通じてReactのWebアプリ、Go+PostgreSQLのバックエンド、Flutterのモバイルをプロトタイプでき、管理UIやログビューア、フリート管理サービスを素早く作るといったワークフローが有効です。プランニングモードやソースコード書き出しにより、プロトタイプから本格実装へ移すことも現実的です。
基礎から始め、プロがCを使う方法で学ぶのが良いです:
さらにシステム寄りの記事や学習パスを読みたい場合は /blog を参照してください。
Cはメモリやデータレイアウト、ハードウェアアクセスといった低レベルの制御性と高い移植性を両立するため、マシンを起動し、制約の厳しい環境で予測可能に実行するコードに適しているため、いまでも重要です。
Cは主に以下の領域で依然として広く使われています:
アプリ全体が高級言語で書かれていても、基盤部分や性能に敏感なモジュールはしばしばCに依存しています。
デニス・リッチーはベル研究所でCを作り、システムソフトウェアを書く実用的な手段を目指しました。Cの登場によってUnixがCで書き直され、ハードウェアをまたいだ移植と拡張が格段に容易になったことが大きな意義です。
簡単に言えば、移植性とは同じCソースを異なるCPU/OSでほぼそのままコンパイルして一貫した振る舞いを得られることです。一般に多くのコードは共有され、機械依存の部分だけを小さく分離して差し替えます。
Cが速く予測可能である理由は、言語がCPUの動作に直接対応し、余計なランタイムオーバーヘッドが少ないからです。通常コンパイラはループや算術、メモリアクセスを直接的な命令に変換するため、ミリ秒やワット単位で厳しい箇所で有利になります。
多くのCプログラムは手動メモリ管理を使います:
malloc)free)これはいつ、どれだけのメモリを使うかを正確に制御したいカーネルや組み込み、ホットパスで有用です。代償として、ミスがクラッシュやセキュリティ問題を招く可能性があります。
カーネルやドライバがCで書かれるのは、次の必要性があるためです:
Cはこれらに合致し、成熟したツールチェーンと予測可能なバイナリを提供します。
組み込み向けのターゲットはしばしば数キロバイトのRAMや限られたフラッシュを持ち、電力やリアルタイム制約があるため、Cは小さなバイナリや低ランタイムオーバーヘッドを実現し、メモリマップドレジスタや割り込みを直接扱うのに適しています。
ほとんどの場合、チームは製品全体をCで書き直すのではなく、プロファイルしてホットパス(実行時間の大部分を占めるごく一部)だけをCで実装します。統合手段としては:
境界でのデータ変換や所有権・エラーハンドリングに注意が必要です。
実務での「安全なC」は、規律あるコーディングとツールの組み合わせにほかなりません。代表的な対策:
これらで一般的なバグクラスを大幅に減らせます。