HN 日本語サマリー

← 一覧へ戻る
プログラミング

RustプログラマーのためのC言語

C for Rust Programmers (bd103.dev)

26 pointsby xyproto4 コメント

要約

この記事は、Rustを最初に学んだプログラマーがC言語を学ぶ際に直面するであろう、C言語特有の奇妙な点や注意点をまとめたものです。C言語にはブーリアン型が組み込まれておらず、文字列はヌル終端であること、整数型の幅がターゲット依存であること、そしてエラーハンドリングがRustに比べて貧弱であることなどが挙げられています。これらの違いを理解することが、RustプログラマーがC言語を習得する上で役立つと述べています。

全文翻訳

RustプログラマーのためのC言語 2026-10-07 #rust #c 私が最初に学んだシステムプログラミング言語はRustでした。これは他の多くのプログラマーと比較すると珍しいことです。Rustに来る前にC言語やC++を最初に学んだ人の方が多いでしょう。 そのため、インターネット上には「Rust for C Programmers」の記事はたくさんありますが、「C for Rust Programmers」の記事はほとんどありません。さて、私はそれを変えようとしています! 最近C言語とC++を学んでいますが、本当に奇妙な言語ですね。この記事は、私がC言語を独学中に学んだ、眉をひそめるような詳細のコレクションです。(今日はC++は扱いません。あの厄介な問題に踏み込む準備はできていません。) これは、きちんとしたC言語のチュートリアルの代わりになるものではありません。自分でGoogleで探す必要があります。むしろ、その言語で作業する際に心に留めておくべきことのリストです。 前提が整ったところで、カーテンを開けてC言語が提供するものを見てみましょう! ブーリアンは組み込みではない C言語の最初のバージョンにはブーリアン型のプリミティブ型がなく、プログラムは代わりに整数0と1を使用していました。これはC99で変更され、オプションの<stdbool.h>ヘッダーでブーリアンが追加されました[1]。 #include <stdbool.h> int main() { bool yes = true; bool no = false; return 0; } それでも、trueとfalseは他の言語のようにリテラルやキーワードではありません。代わりに、それらは1と0に展開される定義です。 #define true 1 #define false 0 これはC23[2]で再び変更され、ブーリアンは真の言語プリミティブになりましたが、以前のバージョン用にコンパイルする場合は<stdbool.h>を含める必要があります。 ヌル終端文字列 Rustの&strは16バイトです。8バイトはメモリ上のアドレス、8バイトは文字列の長さです。これは、strが動的にサイズ変更可能な型であり、ポインタメタデータを使用して文字列の長さを追跡するためです。 // 通常の参照は8バイトしか使用しません... assert_eq!(std::mem::size_of::<&u8>(), 8); // ...文字列は16バイトを使用します。 assert_eq!(std::mem::size_of::<&str>(), 16); このアプローチにより、文字列の長さを取得するのは非常に効率的ですが、&str参照ごとにメモリをより多く消費します。 C言語は異なるアプローチを使用します。文字列のサイズを別途保存せず、すべての文字列をヌルバイト(\0)で終端します。これは意図的なトレードオフであり、いくつかの結果をもたらします。 Cプログラムは、文字列の横に余分なサイズ変数を保持する必要がありません[3] すべての文字列は、ヌルバイトのためのスペースを末尾に必要とします これは、空の文字列""でも1バイトのメモリを消費することを意味します! ヌルバイトは、<string.h>の関数を台無しにすることなく、文字列の途中で簡単に使用することはできません。 ヌル終端を忘れると、境界外読み取りが発生する可能性があります。 実際には、ヌル終端のために追加のスペースを確保し、文字列の末尾に追加することを覚えておく必要があります。たとえば、これはC言語で文字列を反転するプログラムです。 1char* reverse(char* forward) { 2 // ヌル終端を除いた文字列の長さを計算します。 3 unsigned long len = strlen(forward); 4 // 文字列とヌル終端のための十分なスペースを確保します。 5 char* reversed = malloc(len + 1); 6 7 for (int i = 0; i < len; i++) { 8 reversed[i] = forward[len - 1 - i]; 9 } 10 11 // 末尾にヌル終端を追加します。 12 reversed[len] = '\0'; 13 14 return reversed; 15} 5行目と12行目に注意してください。これらはヌル終端を考慮して特別な措置を講じています。 参考までに、対応するRustの関数[4]はそうする必要はありません。 fn reverse(forward: &[u8]) -> Box<[u8]> { let len = forward.len(); let mut reversed = Box::<[u8]>::new_uninit_slice(len); for i in 0..len { reversed[i].write(forward[len - 1 - i]); } unsafe { reversed.assume_init() } } ターゲット依存の整数幅 C言語の整数型は、正確なビット数を使用することが保証されていません。代わりに、幅はターゲットプラットフォームによって異なります。 Type | C Standard | 64-bit Unix | 64-bit Windows char | at least 8 bits | 8 bits | 8 bits short | at least 16 bits | 16 bits | 16 bits int | at least 16 bits | 32 bits | 32 bits long | at least 32 bits | 64 bits | 32 bits long long | at least 64 bits | 64 bits | 64 bits データはcppreference.comより 特に、Unixではlongが64ビット、Windowsでは32ビットであることは私をいら立たせます。クロスプラットフォームの互換性を気にする場合は、<stdint.h>で提供される固定幅整数のみを使用するという、友人が数年前に私にくれたアドバイスに従うことをお勧めします。 Rust | C u8 | uint8_t u16 | uint16_t u32 | uint32_t u64 | uint64_t usize | size_t [5] i8 | int8_t i16 | int16_t i32 | int32_t i64 | int64_t isize | ptrdiff_t [5] エラーハンドリングはひどい 私はRustのエラーハンドリングが本当に好きです。Resultはエラーに対処することを強制し、sum型(列挙型)とmatch文はそれを簡単に行えるようにします! それに比べて、C言語のエラーハンドリングの話は、まっすぐに悲劇的です。関数がエラーを示すために-1やヌルポインタのような「マジック整数」を返すということに尽きるようです。errno(特定のエラーの種類を確認するために使用できるスレッドローカル整数)を読むことで、もう少し情報を得ることができますが、実際のエラーメッセージやスタックトレースを取得するという点では、はるかに困難です。 C言語でコードを書く際に最も気づくことの1つは、言語がエラーを処理することを決して強制しないということです。関数が失敗する可能性があることを覚えておくのはあなた次第です。 たとえば、ヌル終端文字列のコードスニペットを次に示します。 // 文字列とヌル終端のための十分なスペースを確保します。 char* reversed = malloc(len + 1); for (int i = 0; i < len; i++) { reversed[i] = forward[len - 1 - i]; } 新しいプログラマーにとって、malloc()が失敗してヌルポインタを返す可能性があることはすぐには明らかではありません。マシンがメモリを使い果たした場合、reversed[i]にアクセスするとセグメンテーション違反が発生します。 役に立たないセグメンテーション違反を避けるために、プログラムはヌルポインタをチェックし、見つかった場合は正常に終了する必要があります。 char* reversed = malloc(len + 1); if (reversed == NULL) { perror("Error"); exit(1); } このようにすることで、セグメンテーション違反、あるいはさらに悪いことに、その他の意図しない動作よりもはるかに良い体験が得られます。 $ ./main Error: Cannot allocate memory もちろん、すべての割り当てられたポインタをチェックすることを覚えておくことは、素晴らしい開発者体験ではありません。 私が実験したアプローチの1つは、タグ付きユニオンを使用してC言語でRustのResultを再作成することでした。 struct MallocResult { enum Tag { OK, ERROR } tag; union Value { void* ptr; char* error_message; } value; }; しかし、使用するのは完全に厄介であり、エラーを処理する前にすぐにresult.value.ptrにアクセスすることを止めるものは何もありません。 privateやpublicのような可視性修飾子の欠如は、これを防ぐために使用できる可能性がありますが、意図的な設計上の決定のようです。 C言語は、プログラマーが正しく行うことを完全に信頼し™、契約や安全な抽象化のための施設をほとんど提供しません。 私は個人的にこのアプローチに同意しません。私は間違いを犯さないマスタープログラミングウィザードではありません。コンパイラがチェックできるように、プログラムの要件を型システムにエンコードする方がはるかに良いでしょう![6]そうすれば、コードがコンパイルされれば正しく書かれたと reasonably に確信できます。 しかし、話がそれてしまいました。 フィールドアクセス構文 C言語には2つの異なるフィールドアクセス演算子があります。値の場合は.、ポインタの場合は->です。 struct Foo { int field; }; struct Foo value = { 103 }; struct Foo* ptr = &value; // フィールドに直接アクセスします。 printf("%d\n", value.field); // ポインタ経由でフィールドにアクセスします。 printf("%d\n", ptr->field); これは最初は私を驚かせました。Rustはすべてに.を使用するためです。興味がある場合は、このStack Overflowの記事[7]に、->演算子が存在する興味深い歴史が記載されています。 配列は楽しいためにポインタになる 配列にはいくつかの奇妙なニュアンスがあります。時にはそれらはsizeof()を使用して長さをアクセスできる値であり、他の時にはサイズが不明なポインタです。一般的に、これは、配列が定義された関数内で扱われているかどうかによって決まります。 私が言いたいことを示すために、2つの配列のサイズを出力する非常に簡単なCプログラムを次に示します。 char declared_size[3] = { 1, 2, 3 }; char inferred_size[] = { 4, 5, 6 }; printf("Declared size (main): %ld bytes\n", sizeof(declared_size)); printf("Inferred size (ma