C言語の復習その6メモリについて
メモリ管理について学びました。malloc,free,reallocなど
コンパイルするときに、知識不足によるコンパイルエラーがかなり減りました。*1 またコンパイルエラー時に、何が間違っているかすぐわかるようになり、なぜ動いているのか・動かないのかわからないというソースがかなり減りました。
mallocについて
ローカル変数との違い
- ローカル変数は確保できる合計サイズに限りがある
- mallocの方が柔軟。
- staticをつけた変数はデータセグメントに格納される
- ではローカル変数は?
- スタック領域に確保されます
- ある程度の領域を抑えてスタックのベースアドレスから、
- 下に伸びる
- 上に伸びる
- みたいな感じで空き容量を埋めていく感じです。
- ある程度の領域を抑えてスタックのベースアドレスから、
- スタック領域に確保されます
mallocではヒープ領域にメモリを確保する
- heapの上限はない?
この「ヒープ領域」には上限が決められていません。p223より
- スタックはコンパイル、リンク時に大きさが決まるらしい。
- heapの上限はない?
mallocで戻ってきたアドレスを紛失しないこと
ローカル変数でmallocの返値アドレスを確保して、スコープを抜けるなどで噴出すると、 プログラム実行終了までに確保したアドレスは回収されない。
javaのようなメソッドエリア(静的領域とも)からの参照がなくなった参照を自動的に解放対象にする(ガベージコレクション)のような仕組みはない。
対策としては、
- アドレスを管理している変数を関数の返値として呼び出し元に返す
- グローバル変数で管理する
などがある。
free()について
- mallocで確保した引数を与えるとそこを解放する
void free(void *ptr);
- 宣言を見る限り
void*
->char
のキャストはもちろんできるが、char
->void*
のキャストも可能なのか。
- mallocで確保していないアドレスや解放済みのアドレスをfree()に渡すと実行時エラーになる
- コンパイル時にわかりそうなもんだが。。。
p = (char *) malloc(strlen(str)+1); void* vp = (void*) p; free(p); //free(vp); // 間違えて、同じアドレスを指している変数を2回freeしてしまった。 // その時のエラーを記念に残しておく // a.out(20725,0x7fffad00f380) malloc: *** error for object 0x7f8ebd402710: pointer being freed was not allocated // *** set a breakpoint in malloc_error_break to debug // Abort trap: 6 char localv = 'c'; //free(&localv); //ローカル変数をfreeした結果。 // a.out(20787,0x7fffad00f380) malloc: *** error for object 0x7ffee5ed7ab7: pointer being freed was not allocated // *** set a breakpoint in malloc_error_break to debug // Abort trap: 6
realloc
void *realloc(void *ptr, size_t size);
char *str1 = "hello"; char *str2 = " world"; p = malloc(strlen(str1)+1); strcpy(p,str1); puts(p);//hello //なぜか入れても抜いても同じ結果になる。 //p = realloc(p,strlen(str1)+ strlen(str2) +1); strcat(p,str2); puts(p);//hello world; free(p);
- 確保した領域の後ろのアドレスが空で、たまたま使えていたということ?
保証されていない動作?ということだろうか。
mallocを2回続けて、1回目の領域に大きなデータを書いてみたが、バグらせることができなかった。
reallocやらないとどうなるか。
複数回連続でmallocをして確保したアドレスを
printf()
してみたが、アドレスは綺麗に順番で確保されているわけではないようだ以下は複数char型のポインタ変数のアドレスを確保して実験したソース
char *vp1 = malloc(1); char *vp2 = malloc(1); char *vp3 = malloc(1); char *vp4 = malloc(1); char *vp5 = malloc(1); char *vp6 = malloc(1); //1バイトしか確保していない領域に複数バイト分の文字を書き込み //文字列リテラルはそれ自体が静的データへのアドレスなので以下のコードは実験としてふさわしくない //vp1 = "test"; strcpy(vp1,"testtesttesttesttesttesttesttest"); strcpy(vp2,"computercomputercomputercomputer"); strcpy(vp3,"abcdeabcdeabcdeabcdeabcdeabcde"); strcpy(vp4,"englishenglishenglishenglishenglish"); strcpy(vp5,"boyboyboyboyboyboyboyboyboyboy"); strcpy(vp6,"girlgirlgirlgirlgirlgirlgirlgirlgirl"); puts(vp1); puts(vp2); puts(vp3); puts(vp4); puts(vp5); puts(vp6); //結果:データ破壊ができた // bcdeabcdeabcde // computercomputerabcdeabcdeabcdeabcdeabcdeabcde // abcdeabcdeabcdeabcdeabcdeabcde // englishenglishenboyboyboyboyboybgirlgirlgirlgirlgirlgirlgirlgirlgirl // boyboyboyboyboybgirlgirlgirlgirlgirlgirlgirlgirlgirl // girlgirlgirlgirlgirlgirlgirlgirlgirl printf("vp1 addr is %p\n",vp1); printf("vp1 value is %c\n",*vp1); printf("vp2 addr is %p\n",vp2); printf("vp2 value is %c\n",*vp2); printf("vp3 addr is %p\n",vp3); printf("vp3 value is %c\n",*vp3); printf("vp4 addr is %p\n",vp4); printf("vp4 value is %c\n",*vp4); printf("vp5 addr is %p\n",vp5); printf("vp5 value is %c\n",*vp5); printf("vp6 addr is %p\n",vp6); printf("vp6 value is %c\n",*vp6); //出力 // vp1 addr is 0x7fc072402730 // vp1 value is b // vp2 addr is 0x7fc072402710 // vp2 value is c // vp3 addr is 0x7fc072402720 // vp3 value is a // vp4 addr is 0x7fc072402750 // vp4 value is e // vp5 addr is 0x7fc072402760 // vp5 value is b // vp6 addr is 0x7fc072402770 // vp6 value is g
- 値の欠損、上書きなどが起こったが、想定したものと微妙に違う。
- mallocは10バイト間隔を開けてアドレスを確保している
- mallocが確保するアドレスはアドレスの並び順通りではない
- 上の例だと
vp1 addr is 0x7fc072402730
vp2 addr is 0x7fc072402710
vp3 addr is 0x7fc072402720
vp4 addr is 0x7fc072402750
- 最近のコンパイラはバッファオーバーフロー対策でこういう細工がしてあるということを少し聞きかじったことがあるが、もしかしてそれなのでしょうか。
- コンパイラのオプションでこの細工を無効化できたはずだが。
realloc実践
確保した領域より大きい文字列を書き込む前に、reallocすることで適切な動作を実現できた。
char *vp1 = malloc(1); char *vp2 = malloc(1); char *vp3 = malloc(1); char *vp4 = malloc(1); char *vp5 = malloc(1); char *vp6 = malloc(1); char* str1 = "testtesttesttesttesttesttesttest"; char* str2 = "computercomputercomputercomputer"; char* str3 = "abcdeabcdeabcdeabcdeabcdeabcde"; char* str4 = "englishenglishenglishenglishenglish"; char* str5 = "boyboyboyboyboyboyboyboyboyboy"; char* str6 = "girlgirlgirlgirlgirlgirlgirlgirlgirl"; vp1 = realloc(vp1,strlen(str1)+1); vp2 = realloc(vp2,strlen(str2)+1); vp3 = realloc(vp3,strlen(str3)+1); vp4 = realloc(vp4,strlen(str4)+1); vp5 = realloc(vp5,strlen(str5)+1); vp6 = realloc(vp6,strlen(str6)+1); strcpy(vp1,str1); strcpy(vp2,str2); strcpy(vp3,str3); strcpy(vp4,str4); strcpy(vp5,str5); strcpy(vp6,str6); puts(vp1); puts(vp2); puts(vp3); puts(vp4); puts(vp5); puts(vp6); printf("vp1 addr is %p\n",vp1); printf("vp1 value is %c\n",*vp1); printf("vp2 addr is %p\n",vp2); printf("vp2 value is %c\n",*vp2); printf("vp3 addr is %p\n",vp3); printf("vp3 value is %c\n",*vp3); printf("vp4 addr is %p\n",vp4); printf("vp4 value is %c\n",*vp4); printf("vp5 addr is %p\n",vp5); printf("vp5 value is %c\n",*vp5); printf("vp6 addr is %p\n",vp6); printf("vp6 value is %c\n",*vp6); //出力 // testtesttesttesttesttesttesttest // computercomputercomputercomputer // abcdeabcdeabcdeabcdeabcdeabcde // englishenglishenglishenglishenglish // boyboyboyboyboyboyboyboyboyboy // girlgirlgirlgirlgirlgirlgirlgirlgirl // vp1 addr is 0x7fa623402770 // vp1 value is t // vp2 addr is 0x7fa6234027a0 // vp2 value is c // vp3 addr is 0x7fa6234027d0 // vp3 value is a // vp4 addr is 0x7fa623402810 // vp4 value is e // vp5 addr is 0x7fa6234027f0 // vp5 value is b // vp6 addr is 0x7fa623402710 // vp6 value is g
void型のポインタ
- mallocの結果はvoid型のポインタとして帰ってくる
void *malloc(size_t size);
- これは任意のポインタ型に代入(暗黙の型変換を伴う)可能になっている
- もちろん明示的にキャストすることも可能
char *p; char *str = "Getting memories!!"; p = (char *) malloc(strlen(str)+1); //キャストしなくてもOK //p = malloc(strlen(str)+1); void* vp = (void*) p; printf("void pointer type size is %d\n",sizeof vp); strcpy(p,str); puts(p); //vp型のポインタ変数もputsに渡せるようだ。 puts(vp); //putsは引数にchar型のポインタをとるので、渡った先で暗黙の型変換がされているものと思われる //int puts(char * s);
Linuxのmallocとmmapについて
liunuxではmalloc()
は以下のような動作になっている
- mmap()関数で似たようなことができる
Linuxの仕組みより
[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識
- 作者: 武内覚
- 出版社/メーカー: 技術評論社
- 発売日: 2018/02/23
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
hariboteOSでは?
memmann_alloc(カーネル用のmalloc)
こちらは MEMMAN_ADDRというmammann_allocで割り当てる領域の開始アドレスがdefineで(0x003c0000)に指定されています。
api_malloc(アプリ用のmalloc)
mallocで割り当てるアドレスがどこから始まるかをhariboteOS用の実行ファイル.hrbファイルに書きこまれています。 あまり自信がありませんが、これはデータセグメントのベースからのオフセットアドレスだと思います。
感想
メモリリークは大変だと思うが、 発生した際に時間をかければ、問題箇所の調査ぐらいはできるくらいの知識はついた気がする。
今まではメモリリークの調査など絶対できそうになかったが、最近は結構自信がついてきた。(まだやったことないけど)
ただし、調査に時間はすごくかかりそうだ。
プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み
- 作者: 倉薫
- 出版社/メーカー: 翔泳社
- 発売日: 2009/02/13
- メディア: 大型本
- 購入: 2人 クリック: 6回
- この商品を含むブログ (3件) を見る
C言語の復習その5。関数ポインタについて
関数ポインタは「宣言方法」と「引数、返値があっていない適当に実装されたコードを読む」のがややこしいが、綺麗に書けばそこまで難解ではない。という認識です。
関数ポインタについて
- 宣言
void(*p)()
- 引数付き
void(*p)(char *str)
- 引数付き
*p
を囲ってる()
が若干気持ち悪い
引数や戻り値は暗黙の型変換が絡んで来るとややこしい
この記事を書いた当初、gccのコンパイルオプションを-W -Wall
と書くべきところを-w -Wall
と書いていまい、
警告を抑制するという馬鹿なことをやってしまいました。
※警告を最大限に出すようにオプション(-W -Wall
)を与えて*1、悪いプログラミングの癖は強制するべきだと思っています。
//引数なし、返値なしの関数の場合 void (*fp)(void) = func1;//この宣言代入がもっともふさわしい fp(); fp(2);//エラー void型の関数の引数にintを渡したため //関数ポインタの型を不適切なものにする int(*fp2)(int a) = func1;//警告がでる //関数ポインタの宣言時の引数の個数にふさわしく無い呼び出しをすると、 //fp2();//コンパイルエラー // error: too few arguments to function call, expected 1, // have 0 // fp2(); //関数ポインタの宣言時の引数にふさわしい引数を与えているが、代入した関数には引数はない //void func1(); //一見不正なことをしているように見えるが、そもそも引数の指定が無い関数には引数を与えて呼び出すこともできる func1(1);//ただし警告は出る // other/typeConvert.c:44:12: warning: too many arguments in call to 'func1' // func1(1); // ~~~~~ ^ //この挙動が気に入らないなら、関数ポインタの引数としてvoidを定義すれば良い。 //ちなみに関数ポインタの宣言で引数が決められている場合は、代入する関数の引数がvoidであっても、 //引数を与えて呼び出すことができてしまう。 //void voidf(void) void (*fp3)(int a) = voidf;//ただしこの代入は警告が出る // warning: incompatible pointer types initializing // 'void (*)(int)' with an expression of type 'void (void)' // [-Wincompatible-pointer-types] // void (*fp3)(int a) = voidf; // ^ ~~~~~ fp3(1); //防ぐには代入する関数の引数をvoidにして、関数ポインタの引数もvoidに、そしてコンパイラの警告オプションを最大にすることが重要。 ・・・ //代入されている関数達 void func1(){ puts("引数なしのfunc1が呼ばれました"); } void func2(char *str){ puts(str); } int intfunc(){ return 3; } int intfunc2(int i){ int result = i+3; printf("%d\n",result); return result; } void voidf(void){ puts("引数を渡すとエラーになる関数が呼ばれました"); }
- 暗黙の型変換を期待したコードでvoidが絡んでいると失敗する
大体のルールはここに記載されている。
関数ポインタと、ポインタ変数に代入する関数の
- 引数
- 戻り値
をきちんと一致するように宣言・代入し、コンパイルオプションで警告を細かく出せばこの問題に頭を悩ますことはないと思います。*2
関数ポインタの配列
宣言がちょっとわかりにくいけど、基本は同じなので特に解説なし。
//宣言 int(*p3[4])(char *output); //代入 p3[0] = func1; p3[1] = func2; p3[2] = intfunc; p3[3] = intfunc2; //誤った使用 //以下のコンパイルはすべて失敗する // p3[0](); // p3[1](); // p3[2](); // p3[3](); //コンパイル結果 // chap5/functionp.c:65:10: error: too few arguments to function call, expected 1, // have 0 // p3[0](); // ~~~~~ ^ // chap5/functionp.c:66:10: error: too few arguments to function call, expected 1, // have 0 // p3[1](); // ~~~~~ ^ // chap5/functionp.c:67:10: error: too few arguments to function call, expected 1, // have 0 // p3[2](); // ~~~~~ ^ // chap5/functionp.c:68:10: error: too few arguments to function call, expected 1, // have 0 // p3[3](); // ~~~~~ ^ //使用 p3[0]("123");//出力結果::引数なしのfunc1が呼ばれました p3[1]("123");//出力結果:123 p3[2]("123"); p3[3]("123");//出力結果:152838021 多分不正に数値にキャストした文字列に3を足した結果。 ・・・ //代入されている関数達 void func1(){ puts("引数なしのfunc1が呼ばれました"); } void func2(char *str){ puts(str); } int intfunc(){ return 3; } int intfunc2(int i){ int result = i+3; printf("%d\n",result); return result; }
補足、某所でいただいたアドバイスについて
引数なし、ではなくvoid型の引数というものがある。
DCL20-C. 引数を受け付けない関数の場合も必ず void を指定する
こちらを関数ポインタの型変数に宣言すれば、 引数をつけて読んだときにエラーになります。
//void引数と関数ポインタの実験 int (*fp1)(int a); //int voidf(void); fp1 = voidf;//代入可能 //fp1();//エラー fp1(1);//実行可能 //voidf(1);//コンパイルエラー int (*fp2)(void); fp2 = voidf; fp2(); //fp2(10);//コンパイルエラー
暗黙の型変換は、コンパイラ依存のものも結構あり、gccで動くからといって油断はしないこと
INT36-C. ポインタから整数への変換、整数からポインタへの変換
プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み
- 作者: 倉薫
- 出版社/メーカー: 翔泳社
- 発売日: 2009/02/13
- メディア: 大型本
- 購入: 2人 クリック: 6回
- この商品を含むブログ (3件) を見る
C言語の復習その4。enum,3項演算子,goto,関数
関数ポインタをちょっと読み込んだくらい進みましたが、 関数ポインタは難しそうなので、それで一つ記事を書きます。
というわけでキリが良いので投稿♪
enum
- enumのメンバは本来値を持たない。(持つべきではない)
- 実は数値型の値を持っている。
- 先頭から0,1,2,3...と割り振られている。
typedef enum { SUCCESS, //0 FAILURE //1 }Result;
- ただしこの値を直接計算・表示に使うのはenumの利用方法としてはふさわしくない。
- K&Rにこの「ふさわしくない利用例」が乗ってしまっているらしい。
- 確かJavaのenumも内部ではintか何かだったが、内部の数値を直接利用できなかった気がする。
- 残念ながら本来想定されていた用途で利用されていることは少ないらしい
- C言語の仕事をしたことがないので分かりません。
typedef enum { SUCCESS,FAILURE }Result; /** * print divided result. * * @param dividend [description] * @param divisor [description] * @return [description] */ Result divide(int dividend,int divisor){ if(b==0){ return FAILURE; } printf("calc result is %d\n",dividend / divisor); return SUCCESS; } int main(){ Result result = divide(6,0); if(result == FAILURE){ printf("divide is fail\n"); } //enumのメンバは数値を割り当てられているが、それを表示したり処理に使うのはenum本来の使い方ではない。 printf("success num is %d\n",SUCCESS); //0 printf("failure num is %d\n",FAILURE); //1 }
- OSのコードとして使うなら例外の種類を表現したりするのに使えるのではないかなと思いました。
- トラップ
- フォールト
- アボート
typedef enum { trap,fault,abort }Exception;
コメントの指摘
Kaz Mu (id:fa11enprince)さんからこんな指摘をいただきました。
OSを作る、読むという目標から、あまり重要でないと思い検証していなかったのですが、 せっかくコメントを頂いので検証してみます。
本書ではenumは整数型である必要はあるが、int型である必要は無いと明記されていました。
C言語の使用には「enum型の値は、整数としても使える」とはっきり定められています 注)これはあくまでも「整数」であって、「int」では無いことに注意してください。本書に付属のgccでは,enum型はこの部分はかなり合理的に最適化することができ、定数の数が255以下の場合には1バイト、それ以外の場合には2バイト取られるようにするオプションが用意されています。
少し調べたところ、-fshort-enums
というオプションがそれのようです。
http://www.keil.com/support/man/docs/armclang_ref/armclang_ref_chr1411640303038.htm を参考に検証するコードを書いてみました
enum defaultEnum{test,test1,test2}; // Largest value is 8-bit integer enum int8Enum {int8Val1 =0x01, int8Val2 =0x02, int8Val3 =0xF1 }; // Largest value is 16-bit integer enum int16Enum {int16Val1=0x01, int16Val2=0x02, int16Val3=0xFFF1 }; // Largest value is 32-bit integer enum int32Enum {int32Val1=0x01, int32Val2=0x02, int32Val3=0xFFFFFFF1 }; // Largest value is 64-bit integer enum int64Enum {int64Val1=0x01, int64Val2=0x02, int64Val3=0xFFFFFFFFFFFFFFF1 }; //コンパイルできず //enum int128Enum{test3=0x01,test4=0x02,int128Val=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1} /* chap2/enum.c:22:49: error: integer literal is too large to be represented in any integer type enum int128Enum{test3=0x01,test4=0x02,int128Val=0xFFFFFFFFFFFFFFFFFFFFFF... */
printf("size of default is %zd\n", sizeof (enum defaultEnum)); printf("size of int8Enum is %zd\n", sizeof (enum int8Enum)); printf("size of int16Enum is %zd\n", sizeof (enum int16Enum)); printf("size of int32Enum is %zd\n", sizeof (enum int32Enum)); printf("size of int64Enum is %zd\n", sizeof (enum int64Enum)); //printf("size of int128Enum is %zd\n", sizeof (enum int128Enum));
$ gcc chap2/enum.c $ ./a.out size of default is 4 size of int8Enum is 4 size of int16Enum is 4 size of int32Enum is 4 size of int64Enum is 8 $ gcc -fshort-enums chap2/enum.c $ ./a.out size of default is 1 size of int8Enum is 1 size of int16Enum is 2 size of int32Enum is 4 size of int64Enum is 8
検証の結果から手元のapple clangでは
- enumのメンバはデフォルト4バイト
- 4バイトで表現できない場合は8バイトになる
-fshort-enums
オプションをつけるとなるべく小さい値でenumをコンパイルする- 値の代入がなければ4バイトが割り振られる
ということがわかりました。
3項演算子
int a =-1; printf("%d\n",a); //入れ子にできる char *judge_result = 0!= (a>0? a: 0)? "正の数":"0か負の数"; //奇妙にみえるが以下と同じ意味 //int fixed_number = a>0? a: 0; //char *judge_result = 0!= fixed_number ? "正の数":"0か負の数"; printf("%s\n",judge_result);
3項演算子は多分、式なので入れ子にできます。 可読性が無くなりますが。
- 個人的に3項演算子と言うネーミングはいまいちしっくりこない。
- if式と言う名前ならしっくりきてたかな。
- Kotliのwhen式みたいな感じ
- ?式、q式とかでもいいかな?
- 逆にわかりにくくなってしまった。
- osdev-jpで教えてもらいましたが、3項演算子というのは俗称で、JIS C99 規格書 § 6.5.15*1 では条件演算子と書かれているそうです
GOTO
- gotoが有用な場合もある
- 例えば2重ループから抜ける場合
- 以下サンプル
/** * 九九を計算して表示する * 引数limitの数字に到達した時点で処理をやめる * @param limit [description] */ void nine_nine(int limit){ for(int i=1;i<10;i++){ for(int j=1;j<10;j++){ int result = i*j; printf("%d\t",result); if(result==limit){ goto last; //2重ループを一気に抜ける } } printf("\n"); } last: printf("\n"); //ちなみにこの1行を抜くとコンパイルエラーになる }
このコードをgotoをつかわずに書くとこんな感じになります。
/** * 九九を計算して表示する * 引数limitの数字に到達した時点で処理をやめる * @param limit [description] */ void nine_nine(int limit){ for(int i=1;i<10;i++){ int result; for(int j=1;j<10;j++){ result = i*j; printf("%d\t",result); if(result==limit){ break; } } printf("\n"); if(result==limit){ break; } } printf("\n"); }
- まぁ抜けたい分のループだけ別の関数に括り出して
return
すると言うのもアリですが。 疑問:GOTOのラベルは関数ブロックの最後尾にかけない?
gotoが移動できるのは同じ関数の中だけ。
- near jumpみたいだなと思いました。
$ gcc chap4/control.c chap4/control.c:25:1: error: expected statement } ^
関数
気になる記述があったのでメモ。
実際には以下のようなことがあることは、実務を行なっているC言語プログラマでも知らない人もいるようです。 呼び出し側は返値は受け取らなくても良い 返値を返さない関数も定義できる 返値の型を省略できる場合もある 返値を受け取る側の変数と、返値の肩は必ずしも一致していなくても良い
P204より。C言語の仕事をしたことがないので(以下略)
- printfは数値を返している
- 成功した場合は表示した文字数を
- 失敗した場合は-1を
int num = printf("test\n"); printf("%d\n",num);//5
- 値を返さないのに関数?
数学的に考えれば、「関数」と言うのは、必ず結果(つまり返値)が導かれるものです。
- この話は非常に納得がいく。
ちょっと昔、こういう細かい言い方にこだわってて関数っぽいものは以下のように言い分けるように心がけている
- サブルーチン:戻り値を返さないこと
- コマンドクエリ分離の原則(CQRS)でいうコマンドに対応するもの
- メソッド:メンバに対する処理(参照更新代入等)があること
- 関数:値を返すこと、副作用がないこと
- サブルーチン:戻り値を返さないこと
int型の関数は型の宣言を省略できる
- 引数も省略できる
test(a){ return a+3; } ・・・ printf("%d\n",test(5));//8
$ gcc --version Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1 Apple LLVM version 9.1.0 (clang-902.0.39.2) Target: x86_64-apple-darwin17.7.0 Thread model: posix InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin $ gcc chap5/function.c chap5/function.c:8:17: warning: implicit declaration of function 'test' is invalid in C99 [-Wimplicit-function-declaration] printf("%d\n",test(5)); ^ chap5/function.c:14:1: warning: type specifier missing, defaults to 'int' [-Wimplicit-int] test(a){ ^ 2 warnings generated.
main関数の型をvoidにするのは間違い
- C言語の仕様ではない。
int main(int args,char *argv[])
がより良い。
externとstatic
- externは他のファイルから呼び出せる
- staticは呼び出せない。
- extern,staticの指定がない場合はexternになる。
- C++ではstaticになるらしい?
- 関数のextern,staticはだいたい上記の認識でいいが変数は別。
C言語能力が着実にレベルアップしています。かなり良い本だと思います。
プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み
- 作者: 倉薫
- 出版社/メーカー: 翔泳社
- 発売日: 2009/02/13
- メディア: 大型本
- 購入: 2人 クリック: 6回
- この商品を含むブログ (3件) を見る
プログラマのためのSQL 読書会(22)に参加
26.7から27.1.3まで読みました。
p478の複雑なクエリの解説
基底テーブル
--▼P.476 CREATE TABLE AnnualSales1 (salesman CHAR(15) NOT NULL PRIMARY KEY, jan DECIMAL(5,2), feb DECIMAL(5,2), mar DECIMAL(5,2), apr DECIMAL(5,2), may DECIMAL(5,2), jun DECIMAL(5,2), jul DECIMAL(5,2), aug DECIMAL(5,2), sep DECIMAL(5,2), oct DECIMAL(5,2), nov DECIMAL(5,2), "dec" DECIMAL(5,2)); -- decは予約語のため、ダブルクォートが必要
イメージとしては1~12月の売上とセールスマンの情報をペアに持つテーブルになります。 とりあえずこのテーブルに入るデータを用意します。
投入データ(オリジナル)
insert into AnnualSales1 (salesman,jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,"dec") values ('yabu',200.2,190.3,220.21,240.12,220.65,510.78,190.89,220.11,230.43,210.32,190.43,480.23)
1個目のビュー
--▼P.477 CREATE VIEW NumberedSales AS SELECT salesman, 1 AS M01, jan, 2 AS M02, feb, 3 AS M03, mar, 4 AS M04, apr, 5 AS M05, may, 6 AS M06, jun, 7 AS M07, jul, 8 AS M08, aug, 9 AS M09, sep, 10 AS M10, oct, 11 AS M11, nov, 12 AS M12, "dec" FROM AnnualSales1;
2個目のビュー
--▼P.478 CREATE VIEW AnnualSales2 (salesman, month, sales_amt) AS SELECT S1.salesman, (CASE WHEN A.nbr = M01 THEN 'Jan' WHEN A.nbr = M02 THEN 'Feb' WHEN A.nbr = M03 THEN 'Mar' WHEN A.nbr = M04 THEN 'Apr' WHEN A.nbr = M05 THEN 'May' WHEN A.nbr = M06 THEN 'Jun' WHEN A.nbr = M07 THEN 'Jul' WHEN A.nbr = M08 THEN 'Aug' WHEN A.nbr = M09 THEN 'Sep' WHEN A.nbr = M10 THEN 'Oct' WHEN A.nbr = M11 THEN 'Nov' WHEN A.nbr = M12 THEN 'Dec' ELSE NULL END), (CASE WHEN A.nbr = M01 THEN jan WHEN A.nbr = M02 THEN feb WHEN A.nbr = M03 THEN mar WHEN A.nbr = M04 THEN apr WHEN A.nbr = M05 THEN may WHEN A.nbr = M06 THEN jun WHEN A.nbr = M07 THEN jul WHEN A.nbr = M08 THEN aug WHEN A.nbr = M09 THEN sep WHEN A.nbr = M10 THEN oct WHEN A.nbr = M11 THEN nov WHEN A.nbr = M12 THEN "dec" ELSE NULL END) FROM NumberedSales AS S1 CROSS JOIN (VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10), (11), (12)) AS A(nbr);
- select * from AnnualSales1
salesman | jan | feb | mar | apr | may | jun | jul | aug | sep | oct | nov | dec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
yabu | 200.20 | 190.30 | 220.21 | 240.12 | 220.65 | 510.78 | 190.89 | 220.11 | 230.43 | 210.32 | 190.43 | 480.23 |
- select * from NumberedSales;
salesman | M01 | jan | M02 | feb | M03 | mar | M04 | apr | M05 | may | M06 | jun | M07 | jul | M08 | aug | M09 | sep | M10 | oct | M11 | nov | M12 | dec |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
yabu | 1 | 200.20 | 2 | 190.30 | 3 | 220.21 | 4 | 240.12 | 5 | 220.65 | 6 | 510.78 | 7 | 190.89 | 8 | 220.11 | 9 | 230.43 | 10 | 210.32 | 11 | 190.43 | 12 | 480.23 |
- select * from AnnualSales2;
salesman | month | sales_amt |
---|---|---|
yabu | Jan | 200.20 |
yabu | Feb | 190.30 |
yabu | Mar | 220.21 |
yabu | Apr | 240.12 |
yabu | May | 220.65 |
yabu | Jun | 510.78 |
yabu | Jul | 190.89 |
yabu | Aug | 220.11 |
yabu | Sep | 230.43 |
yabu | Oct | 210.32 |
yabu | Nov | 190.43 |
yabu | Dec | 480.23 |
イメージ的には
水平展開されている規定テーブルを縦横変換する感じでしょうか。
p479のクエリについて
自宅で帰って動かしたところ、PostgreSQL 10では動作しませんでした。
SELECT * FROM ((SELECT x FROM Foo) --このサブクエリの結果に名前を付ける必要あり LEFT OUTER JOIN (SELECT x FROM Bar) --このサブクエリの結果に名前を付ける必要あり ON Foo.x = Bar.x) AS Foobar(x1, x2) INNER JOIN Floob ON Floob.y = x1;
以下のように変えると動きました。
with foo(x) as (values(1),(2),(3)), bar(x) as (values(1),(4),(3)), floob(x,y) as (values(1,1),(9,2)) --↑共通テーブル式でクエリ実行に必要なテーブルを宣言しています。 SELECT * FROM ((SELECT x FROM Foo) as foo --修正 LEFT OUTER JOIN (SELECT x FROM Bar) as bar --修正 ON Foo.x = Bar.x) AS Foobar(x1, x2) INNER JOIN Floob ON Floob.y = foobar.x1;
結果
x1 | x2 | x | y |
---|---|---|---|
1 | 1 | 1 | 1 |
2 | 9 | 2 |
そもそもの私の疑問はこれの一つ上の「まだ間違い」とコメントされているクエリの 結合結果に別名を付け足せれば動くのでは?というものでした。
SELECT * FROM (Foo AS F1 LEFT OUTER JOIN Bar AS B1 ON F1.x = B1.x) as Foo(x,y) --この行を付け足しています! INNER JOIN Floob ON Floob.y = Foo.x; -- まだ間違い
私が付け足した行で正しくうごきました。
x1 | x2 | x | y |
---|---|---|---|
1 | 1 | 1 | 1 |
2 | 9 | 2 |
結果も本書で正しい、とされている最終のクエリと同じものになりました。*1
MySQLと艦これ
昔艦これでMySQL Clusterのエラーメッセージが出ていたらしい。
http://nice-boat.jp/archives/46
- 最近のMySQLでは全てのエラーにエラーコードがついた。
- エラーコードといえば。。。
と言う文脈でこの話題が出てきました。
そもそもエンドユーザーにはミドルウェアのエラーメッセージは出さないのが基本です!!!
https://www.ipa.go.jp/security/awareness/vendor/programmingv1/b09_03.html
一時テーブル
create global temporary table などはこの本が出た後にSQL標準に入ったかも? という話だが、 「create global temporary table iso」などでググっても それらしいものが出なかった。
- 一時表はmysqlはクエリのチューニングでよく使う
アッカーマン関数
- 再帰の説明に出てきてなにこれ?となった
与える数が大きくなると爆発的に計算量が大きくなるという特徴があり、性能測定などに用いられることもある。
だそうです。
郵便番号はユニークではない。
https://qiita.com/_takwat/items/3a121656425fac7bb820
事態は結構深刻だと思う。
- 郵便番号は必ず1つの町名に紐づいているわけではない
- 市区町村をまたいで同じ郵便番号を持つケースがある
市区町村はおろか県を飛び越えて同じ郵便番号を持ちうるケースがある
まぁ郵便番号をナチュラルキーにするのはやめましょうということですね。
- ナチュラルキー事態があまり好きではありません。
UNION互換
行式、テーブル式の結果の変数型と順番が同じこと。
UNION 互換とは、以下の二つの条件を共に満たすことです。 1.列の数が同一であること 2.同じ列位置の列のデータ型が同じ(または自動的に型変換可能)であること
http://www.geocities.jp/mickindex/database/celko/celko_so.html
ミックさんのサイトより引用。
和両立ともいう
和両立(union compatible)とは、2つの関係において、次数(属性数/列数)が等しく、対応する属性同士のドメイン(値の型・範囲)が等しく、2つの関係の型が適合している様子をいいます。次の例は和両立である2つの関係です。
https://www.db-siken.com/kakomon/21_haru/am2_8.html
デスペの過去問解説から引用。
一応本書にも34章に解説がある😅
これはイカの全ての条件を満たすことを言う。 1.2つのテーブルが同じ列数である 2.同じ位置の隠れつは同じデータ型(または自動的な変換が可能)である。
本書34賞より引用。 ミックさんのサイトのものとほぼ同じですね。
この本後半で解説するものを断りもなく前半でバンバン出し過ぎな気がする。
誤植
p.488 ZipCodeにcity列がない
▼P.488 CREATE TABLE ZipCodes (state_code CHAR(2) NOT NULL PRIMARY KEY, low_zip CHAR(5) NOT NULL UNIQUE, high_zip CHAR(5) NOT NULL UNIQUE, CONSTRAINT zip_order_okay CHECK(low_zip < high_zip)); --- SELECT A1.name, A1.street, SZ.city, SZ.state_code, A1.zip FROM ZipCodes AS SZ, AddressBook AS A1 WHERE A1.zip BETWEEN SZ.low_zip AND SZ.high_zip;
p489 Address.zip => AdressBook.zipでは?との指摘がありました。
▼P.489 SELECT name, street, city, state, zip FROM StateZip2, AddressBook WHERE state = (SELECT state FROM StateZip2 WHERE high_zip = (SELECT MIN(high_zip) FROM StateZip2 WHERE Address.zip <= StateZip2.high_zip));
スモールワールド
参加者の方、実は同じ建物で働いている人でしたwこの業界意外と狭い!
- 作者: ジョー・セルコ,Joe Celko,ミック
- 出版社/メーカー: 翔泳社
- 発売日: 2013/05/24
- メディア: 大型本
- この商品を含むブログ (16件) を見る
*1:急に出てきたサブクエリが中途半端だな...と読んでて思ったのです。
C言語復習その3構造体、共用体について
構造体と共用体について学びました。
構造体のサイズについて
Bitフィールド
- char(1Byte)より小さい長さで構造体のメンバを分割
- 1bit,2bitの長さの型はないのかと常に疑問だったのでこれは嬉しい。
- 低レイヤー勢は使いまくってそう。
- 1Byte以下の長さをもつレジスタの分割なんかはこれを利用するといいんじゃないかなと思いました。
- 型はint,unsigned int,signed intのみ
- メンバの長さが中途半端だとアドレスをまたぐことがありえる
- 名前なしのビットフィールドを活用してワードアライメント的な埋めを開発者が設定できる。
- bitフィールドのアドレスは参照できない。
- バイトの単位でアドレスは振られる
- ビットフィールドはバイトより小さな長さを持ち得るのでアドレスが割り振れない
- そのメンバの開始地点のアドレスくらいわかってもいいんじゃないかな〜と思った。
- バイトの単位でアドレスは振られる
typedef struct{ unsigned al:8; unsigned ah:8; } registers; ・・・ //bitフィールドのメンバのアドレスは参照できない。 //chap2/struct.c:37:30: error: address of bit-field requested printf("ah adress is %p\n",®isters.ah); //bitフィールドを使った構造体のアドレスは取得可能。 printf("registers adress is %p\n",®isters);
共用体
- ある値の何バイト目まで、というような値の参照ができる。
- 見た瞬間にCPUのレジスタを扱うのに向いているのでは?と思った。
- hikaliumさんが過去に作られていたOSで適当にunionとググってみたところcpuのレジスタのbitを共用体で表現しているところがありました。
- EFLAGSやCRnレジスタはnビット目がこれ!という表現を共用体を使わないと混乱すると思います。
- Linuxでも共用体でレジスタっぽいものを表現していました。*1
- たぶんCPUのレジスタを共用体で表現するということは普通に行われていることなのでしょう。
- 本書ではほとんど使われない、と書かれているが、ある程度低レイヤーの分野の書籍を読んだことがあると用意の活用イメージが湧く。
確かに面白いのですが、実際のところ普通のプログラミングをしている限り、ほとんど用途がありません。ですから、こういうものがある、ということだけ覚えておけば十分でしょう。
- COBOLに似たような機能があった気がする。レベル変数?とか言った気が。
eaxを共用体で表現
- eat ax ah alを共用体で作って見ましたが。。。
#include <stdio.h> typedef struct{ //unsigned al:8; unsigned char al; //unsigned ah:8; unsigned char ah; } ax_8bit; typedef union { //unsigned ax:16 short ax; ax_8bit _8bit; } ax; typedef union{ //unsigned eax:32 int eax; ax axpart; }eax; }
出力
eax is 0x1d2c65b8 ax is 0x 65b8 al is 0x b8 ah is 0x 65 eax size is 4 ax size is 2 ah size is 1 al size is 1 eax addr is 0x7ffeec3b9ae0 eax addr is 0x7ffeec3b9ae0 ax addr is 0x7ffeec3b9ae0 ah addr is 0x7ffeec3b9ae1 al addr is 0x7ffeec3b9ae0 0x7ffeec3b9ae0 is b8 0x7ffeec3b9ae1 is 65 0x7ffeec3b9ae2 is 2c 0x7ffeec3b9ae3 is 1d
0x7ffeec3b9ae0
をベースアドレスにして整理すると
offset | 0 | 1 | 2 | 3 |
---|---|---|---|---|
value | b8 | 65 | 2c | 1d |
8bit r | [al] | [ah] | - | - |
16bit r | [ax | ax] | - | - |
32bit r | [eax | eax | eax | eax] |
下位アドレス、上位アドレスなどの並びがちょっと混乱してわからない。これあってるのかな?
プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み
- 作者: 倉薫
- 出版社/メーカー: 翔泳社
- 発売日: 2009/02/13
- メディア: 大型本
- 購入: 2人 クリック: 6回
- この商品を含むブログ (3件) を見る
*1:ネットワークのパケット周りでも使われている
C言語の復習その2ポインタ
- 各型に対するポインタ変数の型がある
- どのかたのポインタも同じサイズ(私の環境では8バイト)
- 構造体のポインタ型もある
- ポインタ変数型のポインタもある
char c = 'c'; char* pc = &c; char **ppc = &pc; printf("-------------------\n"); printf("c value is %x\n",c); //63 = asciiで'c' printf("pc value is %x\n",pc); //ec1daabf printf("ppc value is %x\n",ppc); //ec1daab0 printf("-------------------\n"); printf("c addr(&c) is %x\n",&c); //ec1daabf printf("pc addr(&pc) is %x\n",&pc); //ec1daab0 printf("ppc addr(&ppc) is %x\n",&ppc); //ec1daa98 printf("-------------------\n"); printf("c reference is nothing\n"); //なし printf("pc reference(*pc) is %x\n",*pc); //63 printf("ppc reference(*ppc) is %x\n",*ppc); //ec1daabf printf("-------------------\n"); printf("ppc referencese reference(**ppc) is %x\n",**ppc);//63
この例を表に起こすとこんな感じ
変数 | 型 | 値 | アドレス | ポインタ参照(*変数) | ポインタ参照の参照(**変数) |
---|---|---|---|---|---|
c | char | 0x63 | 0xec1daabf | なし | なし |
pc | char* | 0xec1daabf | 0xec1daab0 | 0x63 | なし |
pcc | char** | 0xec1daab0 | 0xec1daa98 | ec1daabf | 0x63 |
大きな配列をローカル変数として宣言するとスタックを使い過ぎてしまう。
C言語では関数には値そのものではなく、値のコピーが渡るらしい。
- 逆アセンブラ(objdump)で見て見たけど、よくわからなんかった
- 配列を渡すと、配列のアドレス(のコピー)が渡る
- 配列のコピーを渡す方法はない。
ポインタの正確な定義は難解らしい
ポインタという言葉の定義は、実はあんまりはっきりとは決まってないらしい 注2-A正確にいうとANSIの仕様として正確に決まってはいます。が、それを定めた文章はあまりに難解で、とてもではありませんが簡単に理解できるようなものではありません。そのせいもあるでしょうが、一般的には「ポインタ」という言葉の定義は、ないに等しい状態です。p105
ポインタという呼称はややこしいのでやめた方が良い
- 文脈に応じて
- ポインタ変数
- アドレス
- に言い換えること。p106
- 文脈に応じて
配列を引数として受け取る関数の宣言
void func(int *arg)
と書くのが一般的void func(int arg[])
と書くこともできる。- 俺は後者の方が好き。
%p
の出力に関して。- 6Byteのものが表示された。
0x7ffee878ea86
- これは何かのオフセット?
- 一瞬ページングのページオフセットかと思ったけどあっちは12bit
- 物理アドレスではないと思う。
- 6Byteのものが表示された。
ポインタの添字とラベルを変えることができる豆知識の説明
i[5]
は5[i]
と同じ意味- OS自作本ではアセンブリのレベルで説明していた
- この本ではポインタアクセスの中身を加算にして説明している
i[5]
=*(a+5)
=*(5+a)
=5[a]
2次配列はただの配列の配列
- 2次元配列はアドレスで受け取れない
//OK void func1(int array[3][5]){ puts(array); puts(array[1]); } //OK void func2(int array[][5]){ puts(array); puts(array[1]); } //不正 void func3(int *array){ puts(array); puts(array[1]); } void main(){ func1(color); func2(color); func3(color); }
結果
Blue Blue Blue Segmentation fault: 11
うーん最後のセグフォは納得だけど、Grayが渡せていない?
プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み
- 作者: 倉薫
- 出版社/メーカー: 翔泳社
- 発売日: 2009/02/13
- メディア: 大型本
- 購入: 2人 クリック: 6回
- この商品を含むブログ (3件) を見る
C言語の復習を始めました。
こんにちは。大学の1年目のC言語の授業で予約語と非予約語の違いが判らずに 1週間くらい変数には何でもwight,hightを付けていた不届きものです。
最近C言語を再勉強することにしました。
一応大学ではC言語を一番書いていましたが、 C言語そのものの知識というのはあまり深くありません。
ちょっとした事業の数値計算の課題などで使った程度で、 (ポインタなどは積極的に避けていた)
Makeファイルを書いたり、テストを書いたり、 ということはできていませんでした。
C言語は一応読めるけど、複雑なものはわからないし、 大きな規模だったり本格的なモノづくりに活用できるようなスキル ではないということです。
これは今後hariboteOSを改築するのに大きな障害になると思ったのでC言語力を高めたいわけです。
というわけで、もう10年近くプログラミングをやっていますので、 繰り返しはfor文で書きますよ~等は説明されるとくどいような気がするので、
ある程度知っているが深く知らない、人向けの本を探してみました。
ちょうどこんなエントリが目に付いたので早速本を買ってみました。
早速ポインタの節の手前まで終わりました。
本書中の面白いトリビアでも書こうと思います。
charに算術演算子を使って計算することが可能
#include <stdio.h> int main(void){ // Your code here! char a = 3; char b = -5; printf("%d\n",a+b);//-2 a +=253; printf("%d",a);//0 2byte overflow }
こんなことができたなんて。
ポインタにconstをつけると中身の書き換えを防ぐことができる。
//凡例 char* p; このconstはポインタ変数が指している領域を保護 👇 const char* const p; ☝️ こっちのconstはポインタ変数の値を保護
前のconstで参照先をがっちりガード。 後ろのconstで参照そのものを変更させない。
という2重ガードができる。
/** * const char* pで参照先の領域のみを保護する */ void referenceOnlyProtect(const char* p){ p = "test"; //書き換え可能。ただし呼び出し元に影響なし。 //ローカルスコープのvalueというポインタの値のみを書き換えている。 //*p = 's'; //error: read-only variable is not assignable //この変更ができてしまうと呼び出し元の値も変わってしまう。 } /** * char const *pでポインタの値のみを保護する。 * ポインタの指す先が変更できなくなる。 * 参照は書き換え可能 */ void valueOnlyProtect(char* const p){ //p = "test";//error: cannot assign to variable 'p' with const-qualified type 'char *const' //ポインタの値を"test"の参照で書き換えることができない。 //*p = 't';//Bus error: 10 //Busエラーという謎のエラーがでた。 //🚌プップ~ //この書き換えは本書的にはできるっぽいが。。。このエラーは謎。 } /** * 参照先、参照そのものも両方保護する。 */ void valueAndReferenceProtect(const char *const p){ //p="test";//error: cannot assign to variable 'p' with const-qualified type 'const char *const' //参照の変更は許可されない //*p = 't';//error: read-only variable is not assignable //参照先の値も保護されている。 }
概念としては理解できますが、 手前のconstが参照先を保護、奥のconstがポインタのアドレス値を保護という順番が覚えられません。 多分明日には忘れています。 何かいい覚え方はないもんでしょうか。
とりあえず鬼門のポインターまで(p70)進めました。 ここはフレッシュな脳みそで読みたいの今晩はこの辺にしておきます。