
2026/10/08 22:44
Rust プログラマーのための C
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
最も重要な教訓は、C が安全性とメモリ管理のために手動管理を必要とする点であり、これに対し Rust はこれらの保証をネイティブに強制していることです。主な課題には、文字列の 널 ターミネーションを手動で処理することや、堅牢なネイティブ
Result 型ではなく「マジック整数」を用いてエラーチェックを行うことが挙げられます。さらに、標準的な整数サイズはプラットフォーム間で異なり、関数への引数として渡される際配列がポインタに変換(劣化)してしまうため、警告なし(例:Clang)ではサイズ計算が困難です。C99 で <stdbool.h> を介してボリアンヘッダーを後から追加したような歴史的な設計決定や、生ポインター算術の採用は、C が借用チェックや参照型といったモダンな安全性機能を欠く理由を説明します。したがって、メモリ割り当て関数(例:malloc)の戻り値をチェックしない場合、デベロッパーはバッファオーバーフローまたはセグメンテーションフォルトというリスクに直面します。これらの固有のリスクを軽減するため、C を使用する企業は手動のエラー処理を厳しく強制し、<stdint.h> から固定幅タイプを利用してプラットフォーム間の一貫性を確保する必要があります。これは、言語が不安全な使用パターンを制限するために可視修飾子を提供しないためです。本文
Rust プログラマーのための「C の驚きと落とし穴」
多くのプログラマーが C や C++ から Rust に移るのに対し、著者は最初から Rust を学びました。そのため、「C 向けに書かれた Rust ア.rticle は多いが」、「Rust プログラマー向けの C の記事は少ない」という状況にあります。 この投稿では、独学で C を習得した過程で見つかった 「眉をひそめさせるような」 詳細な事項を集めました。
注意: これは正式な C チュートリアルではありません。詳細な仕様については独自に調べる必要があります。
真偽値 (bool
) はプリミティブ型ではない(※)
boolC の初期バージョンでは、真偽値を表すプリミティブな型は存在せず、代わりに整数
0 と 1 を使用していました。
C99 で <stdbool.h> ヘッダーを追加することで bool が導入されましたが、true と false はキーワードではなく、以下のマクロ定義です。
#define true 1 #define false 0
したがって、他の言語のようにリテラルとして振る舞いません。C23 ではプリミティブ型になりましたが、旧バージョンとの互換性を考慮し
<stdbool.h> の使用が必要です。
Null テーミングされた文字列(Null-Terminated Strings)
Rust の
&str は 16 バイト(メモリアドレス 8 バイト + 長さ 8 バイト)を使用します。これに対し、C では文字列のサイズを保存せず、すべての文字列を null バイト (\0) で終了させる というアプローチをとります。
このトレードオフには以下のメリット・デメリットがあります。
メリット
- 文字列の隣に追加のサイズ変数を保持する必要がありません。
などで長さを効率的に取得できます。strlen
デメリット・リスク
- メモリ効率: 各文字列に null バイト分のスペースを確保する必要があります。空の文字列
でも 1 バイト消費します。"" - 安全上の問題:
- null バイトを文字列真ん中に使うと
の関数が誤動作します。<string.h> - **終了記号 (
) を忘れると「範囲外読み取り」**を引き起こし、セキュリティリスクになります。\0
- null バイトを文字列真ん中に使うと
実装の注意点:メモリ確保と終了記号
C で文字列を逆転させる場合、null バイトを考慮した追加スペースを確保し、最後に必ず
\0 を追加する必要があります。
char* reverse(char* forward) { // 文字列の長さを計算(null テーミネータを除く) unsigned long len = strlen(forward); // 【重要】文字列分 + null バイト分のスペースを確保する char* reversed = malloc(len + 1); for (int i = 0; i < len; i++) { reversed[i] = forward[len - 1 - i]; } // 【重要】末尾に null テーミネータを追加する reversed[len] = '\0'; return reversed; }
対照的に、Rust 関数は
&str の長さがメタデータに含まれているため、このような手動の管理が不要です。
ターゲット依存の整数幅
C の整数型(
int, long など)は正確なビット数を保証されていません。幅はターゲットプラットフォームによって異なります。特に Unix (long が 64 ビット) と Windows (long が 32 ビット) で挙動が変わるのは危険です。
クロスプラットフォーム互換性のための推奨事項
可変長の整数ではなく、**固定幅整数(
stdint.h 提供)**を使用する必要があります。
| Rust | C (固定幅) |
|---|---|
... , | ~ , |
... , | ~ , |
エラー処理は酷いものです
Rust の
と enum/match がエラー処理を強制し、安全な設計を促すのに対し、C のエラー処理は 「魔法の整数」に依存しています。Result
C の欠陥あるエラー処理
- 関数は
や-1
などを返してエラーをシグナル化します。NULL
を確認するだけで情報が限られます(スタックトレースや詳細なメッセージは困難)。errno- 言語がエラー処理を強制しません。失敗可能性のあるリソース操作(例:
)の後、手動でエラーチェックと NULL 処理を行う必要があります。malloc
char* reversed = malloc(len + 1); // 【必須】メモリ不足時の NULL チェック if (reversed == NULL) { perror("Error"); // エラーメッセージを表示 exit(1); // 優雅に終了 }
NULL ポインタチェックをしないと、セグメンテーションフォールト(Segfault)が発生します。 より安全な設計を試みるために「タグ付きユニオン」で Rust の
Result を模倣することもできますが、C は型システムによる強制力がなく、意図的な設計欠陥と見なされます。
フィールドアクセス構文
C では、値アクセス (
) と ポインターアクセス (.
) に演算子が異なります。Rust などのようにすべてに ->
. を使うことはできません。
struct Foo { int field; }; struct Foo value = { 103 }; // 値に対しては . でアクセス struct Foo* ptr = &value; // ポインターに対しては -> でアクセス printf("%d\n", value.field); // OK: 値アクセス printf("%d\n", ptr->field); // OK: ポインタ経由のフィールドアクセス
アレイは関数内ではポインター化される(ふざけて)
C のアレイには奇妙な挙動があります。
はコンテキストによって意味が異なります。
一般的に、アレイは「定義された場所」でサイズを持ちますが、関数のパラメータとして渡されるとポインタに変換されます。sizeof()
メイン関数内での動作(正しい)
void print_sizes() { char declared_size[3] = { 1, 2, 3 }; char inferred_size[] = { 4, 5, 6 }; // アレイのサイズは実際に保持されている printf("Declared size (main): %ld bytes\n", sizeof(declared_size)); // 3 bytes printf("Inferred size (main): %ld bytes\n", sizeof(inferred_size)); // 3 bytes }
関数引数での動作(ポインター化)
void function(char declared_size[3], char inferred_size[]) { // 【重要】ここではアレイではなくポインターとして扱われるため sizeof はポインタサイズ printf("Declared size (function): %ld bytes\n", sizeof(declared_size)); // 8 bytes (64bit システム) printf("Inferred size (function): %ld bytes\n", sizeof(inferred_size)); // 8 bytes }
- 原因:
の型定義は、コンパイル時に自動的にvoid function(char declared_size[3], ...)
に変換されます。void function(char* declared_size, ...) - 対策: Clang コンパイラはこれを検出し、警告を出力します。
$ clang main.c warning: sizeof on array function parameter will return size of 'char *' instead of 'char[3]' [-Wsizeof-array-argument]
おや、参照は存在しないではありませんか?
C には 参照(Reference)の概念はありません。すべてが生ポインターです。バウンディングチェックもありません。
そのため、以下のような「楽しい」ポインタ算術が可能になりますが、非常に危険です。
int array[] = { 0, 1, 2, 3, 4, 5 }; // インデックス操作ではなく、直接的なポインタ算術 for (int* x = &array[0]; x < &array[6]; x++) { *x = 5 - *x; }
※ これを実行するとバグやセキュリティホール(バッファオーバーフロー)の原因となります。
結論
C を学ぶのは非常に面白かったですが、システムプログラマーとして知っておくべき必須の知識でもあります。 著者の GitHub リポジトリには実装例が公開されていますので、興味があればぜひ触ってみてください。
BD103 :)
フットノート
- 技術的にはヘッダーなしで
を使用できますが、_Bool
/true
マクロを使用するにはfalse
が必要です。↩<stdbool.h> - Clang は C23 の完全サポートを持っていないため、C23 に移行するまで
への依存は継続される可能性があります。↩<stdbool.h> - Rust 版の逆転関数は簡易的な実装です。本番環境では
が推奨されます。↩forward.chars().rev().collect::<String>()
とusize
は C のisize
やsize_t
と完全にはマッピングされません。使用には注意が必要です。↩ptrdiff_t- Rust の型システムを「Fearless SIMD」のようなコード検証に利用する事例も参照推荐阅读します。↩