技術日誌

DB,Java,セキュリティ,機械学習など。興味のあることを雑多に学ぶ

「はじめて学ぶCの仕組み2」を読んだ

大体1週間程度で読み終わりました。

プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み

プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み

この本を読み始めた動機は

  • C言語力が低いとOSのコードを読み書きするのに障害になると感じたから
  • C++の本でC言語初心者が門前払いされていたから

C++の本を開いた理由は、大局的にはオブジェクト指向の機能がある言語でOSを読み書きしようという野望があったからです。

C++プログラミング入門

C++プログラミング入門

よかったところ

C言語から手続き型プログラミングを引いた部分を一通り学べる

などの使い方について一通り学べます。

if文やfor文とかに関する長い説明はいらないけど、 C言語の基本的な機能に自信がない・・・という私みたいな人にぴったりな本でした。

よくなかったところ

  • 演習問題があまり充実していない。
  • volatileの説明がない。まぁそもそもあまり使わないものらしいが。
  • 構造体を->で利用する方法が書かれていない

わからなかったところ

配列が持っている長さ(添え字)の情報

char s[5];この分の添え字が結局何のためにあるのかよくわかっていません。

   char s[5] = "hello";
  puts(s);

これは変数をヒープに割り当てる際に、 s[0]のアドレスから変数型*5バイト分後ろがsの領域になるように割り当てているということでしょうか。

この添え字は多分変数sの領域の確保時に、以前確保した変数と被らないように領域を確保するだけに思います。

実行時はこの配列sへの書き込み以外の方法でも書き込みに対して保護などされておらず、上書きされるように思います。

例えばsの前後に配列を確保し、アドレスがかぶるように不正なindexを設定して代入するなどして簡単にメモリ破壊ができるようです。

char a[1];
char s[6] = "hello\0";
char b[1];

puts(s); //hello

printf("a at:%p\n",a);
printf("s at:%p\n",s);
printf("b at:%p\n",b);

// a at:0x7ffee9992acf
// s at:0x7ffee9992ac9
// b at:0x7ffee9992ac8

printf("a[1] at:%p\n",&a[1]);
printf("s[1] at:%p\n",&s[1]);
printf("b[1] at:%p\n",&b[1]);

// a[1] at:0x7ffee9992ad0
// s[1] at:0x7ffee9992aca
// b[1] at:0x7ffee9992ac9

//この環境だとbはsの1バイト小さいアドレスに確保される
//b+1のアドレスはsと一致している

//注意)b[0]までが正規の範囲
//コンパイル時に警告
// test.c:32:3: warning: array index 1 is past the end of the array (which contains
//       1 element) [-Warray-bounds]
//   b[1] = 'x';
//   ^ ~
// test.c:7:3: note: array 'b' declared here
//   char b[1];
//   ^

b[1] = 'x';
//*(b+1) = 'x'; //←こんか感じでもアクセスできる

puts(s); // xello

}

変数を確保するときだけ使っているように思えます。

この本にはいわゆる「C言語は自分の足を打てる言語」的なエッセンスは書かれていません。

文字列リテラルは3つの例外を除いてポインタになっている

わからないというか、そもそもきちんと検証していなかったので検証してた。

「sizeof演算子オペランドの場合(読み替えが抑止され、配列全体のサイズが返される)」

  printf("%lu\n",sizeof "test");//5

「&演算子オペランドの場合(配列全体へのポインタが返される)」

printf("%p\n", &"test");
printf("%p\n",  "test");
//出力は同じ値になる
// 0x1052edfb3
// 0x1052edfb3

「配列初期化時の文字列リテラル(文字列リテラルは普通はcharの配列だが、この場合特別に初期化子の省略形とみなされる)」です。

char test0[5];
char test1[5] = {'t','e','s','t'};
char test2[5] = "test";

char *test3;
test3 = "test";
//宣言と代入を分けているので、静的な領域に確保した"test"という文字列のアドレスを
//格納したことになっていると思う。

printf("%p\n",test0);
printf("%p\n",test1);
printf("%p\n",test2);
printf("%p\n",test3);


//test3だけアドレスが遠い
// 0x7ffee95c8acb
// 0x7ffee95c8ac6
// 0x7ffee95c8ac1
// 0x106637fab

規格

規格と実装の差分

C11やC99があるようですが、こちらの具体的な紹介などはありませんでした。 言語としてコアな部分は変わっていないと思いますが、 本書の感想をブログに投稿中は、標準規格ではこうなっている、ということを教えてもらうことが多かったです。

ちなみに色々ネットや本を見ているとC++では結構規格が重要になってくるようです。C++11,C++14,C++17などあるようですが、

  • このバージョン以前はコードがかなり煩雑になる。
  • あるバージョンでシンプルにコードを書く方法が入ったので、時代遅れの方法を使うべきではない等。

次に学習すべきはC++かRust?

本格的に規模の大きいコードを書くとなるとクラスやインターフェースの機能がないC言語だときつくなると思います。

おそらくこれから低レイヤープログラミングを続けていくならば、 勉強だけならC言語だけでも問題ないかもしれませんが、より実践的なことをするなら、オブジェクト指向などのモダンなパラダイムを持っている言語の習得が必須なのではないかと感じます。

いきなりRustを勉強してもいいのかもしれませんが、C++を知っていないとRustの有り難みがわかりにくいということと聞いたので、 とりあえず習得したC言語を利用してOSを学習しつつ、並行してC++を少し齧ってみようと思います。

https://www.amazon.co.jp/プログラミングRust-Jim-Blandy/dp/4873118557