ドットインストールでC言語入門しなおした

とはいっても

やったのはポインタまわりだけなんだけど、「ポインタをつかうとこうメモリが節約できるんだよ」という話が腑に落ちて面白かった。以下コードにコメント書いてく。vimのquickrunでやっててなんか実行時に警告が出てるけどまあそこらへんは無視してる。
やったこと

  • static 変数
  • ポインタの生成
  • ポインタをつかったプログラミング
#include <stdio.h>

void static_add(){
  /*自動変数の定義*/
  static int b = 0;
  b++;
  printf("static added %d\r\n", b);

}

void add(int arg){
  arg += 10;
  printf("added %d\r\n", arg);

}

/*メイン関数の決まり文句*/
/*return 0 で正常終了を返すために int 型っぽい*/
/*引数はよくわからない。neosnippetで自動生成した*/
int main(int argc, char const* argv[])
{

/*以下の警告が出るけど無視*/
/*/Users/atasatamatara/Dropbox/a.c: In function ‘main’:*/
/*/Users/atasatamatara/Dropbox/a.c:21: warning: initialization makes pointer from integer without a cast*/
/*/Users/atasatamatara/Dropbox/a.c:23: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘int *’*/
/*/Users/atasatamatara/Dropbox/a.c:23: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘int *’*/

  int a = 10;
  printf("a: %d\r\n", a);
  /*a: 10*/

  int *pa = a;
  pa = &a;
  printf("pa(address): %d\r\n", pa);
  /*pa(address): 1865700996*/

  /*ポインタをわたす関数呼び出し*/
  add(*pa);
  /*added 20*/

  /*自動変数が結果を保持していることを確認*/
  static_add();
  /*static added 1*/
  static_add();
  /*static added 2*/
  static_add();
  /*static added 3*/

  return 0;
}
  • static 変数について

mainプログラムが終わるまで他の関数を呼び出していても変数の内容を保持しておく。通常の(ローカル)変数だと関数呼び出しが終わってしまうと値は破棄されるが、static をつけることで保持することができる。なので上記のように連続して呼び出すと値が保持されているのでインクリメントがされていることがわかる。
個人的にはこういうのはむしろJavaScriptのクロージャからはいった節があるので、static という変数でやってくれるならそれのほうが素直な感じする。クロージャとかレキシカルスコープとかなんか面倒な感じする。

  • ポインタをわたしたプログラミング

まあめっちゃ駆使した組み込みの世界とかこわいからしらないけど……。
ポイントとしては通常関数呼び出しするときに long arg とかすると8バイト(64bit環境だとlong long で16バイトとかになるんかな?)とか、あと文字列などの配列を大量に生成する場合新規にメモリ領域を確保してしまうためメモリ効率が悪い。なので、ポインタ=メモリアドレスをわたしてやることで直接そのメモリ領域(この場合 int a)を使うことが出来る。int *pa = a と * でポインタ定義するが、変数は pa のまま。アドレスは &a で参照。ポインタの入った変数は *pa というように間接変数というのを参照することでポインタかつ中身を渡せるという認識。

まあ

ぶっちゃけCで開発することとかないと思うんだけど、メモリをどう使用しているかとか、もしカーネル読むことになってC++な世界を見るとしたらまあ手がかりにはなるんじゃないかとか思う。ゆるふわLLどころかゆるふわJavaScriptから入門したのでそこらへんかなり知らんし。情報工学系とか計算機科学の分野のバックグラウンドがないぶんちょっと補完、って感じ。

追記

コメント欄よりツッコミいただいた

一つ目のwarningは、
int *pa = a;
で int型の変数を int*型の変数に代入しているのがNG、というwarningです。
異なる型の変数に代入する場合は、
int* pa = (int*)a;
としてキャストしてやる必要があります。

int* 型ってのがよくわからなかったんですよね。いや、いまでも微妙ですが、 int *a で a のポインタが作成できるけど、 int* a だと int* 型っていうの新しい型を作成するということになるのかな?

2つ目の warning は、printf のフォーマット指定子 %d にポインタ変数を渡していることです。
printf では %p がポインタ用のフォーマット指定子です。

そうか、 %p というのがあるのか

あと、
add(*pa);
と書いた場合、ポインタ渡しではなく値渡しです。

ポインタを収めた変数に * で参照すると値渡し、って理解でいいのかな

ポインタ渡しにするなら、
[関数定義]
void add(int arg){}

void add(int* arg){}
[呼び出し側]
int a = 0;
int* pa = &a;
add(pa);
または、
int a = 0;
add(&a);
でも同じです。

なるほど、ポインタを受け取るにはポインタで渡さなければならない、ということでしょうか。これが参照(ポインタ)渡しっていうのかな?

追記その2

コメント欄よりその2。心強い

int* pa;
int * pa;
int *pa;
どれでも同じ意味です。

まじか!それすごい混乱してたのに……

「ポインタ」っていう特殊なものがあるわけでは無くて、ただの int* 型とか char* 型の変数がポインタと呼ばれているだけです。

なるほど!

ただしこの変数の用途は決まっていて、メモリアドレスしか格納できません(なので int型とかの数値型を代入するとコンパイラにおこられます)。逆に言えば、この変数に適当な数値を突っ込めばメモリアドレスとして扱われるので、(32bitなら)0番地〜 0xFFFFFFFF番地までの好きなメモリにアクセスできます。例えば、
unsigned int* pa = (unsigned int*)0x40000000;
pa = 0xdeadbeef;
これでメモリの 0x40000000番地に 0xdeadbeef という4バイトの値を書き込んだことになります。
(まあ普通はOSの保護が効いてるので大抵は例外とかが起きますが。)

ふむ

pa という書き方は、変数 pa (という名前がついているメモリ領域に格納されている値)をメモリアドレスとして解釈して、そのメモリアドレスに書いてある値を読んできて、という指示になります。逆に &pa という書き方は変数 pa という名前がついているメモリ領域のアドレスを教えて、という指示です。

なるほど。値を読んでくるかメモリアドレスを読んでくるか。

まとめると、C言語のオブジェクト(変数や関数)は全部メモリアドレスに名前がついてるだけですw なのでメモリを節約したければ、いろんな関数で共有するものはメモリの一箇所にだけ書いておいて、そのメモリアドレスだけを関数同士でやりとりする(==参照渡し)というわけです。

なるなる。なるなるなるなる〜〜