Yabu.log

ITなどの雑記

C言語の復習その6メモリについて

メモリ管理について学びました。malloc,free,reallocなど

コンパイルするときに、知識不足によるコンパイルエラーがかなり減りました。*1 またコンパイルエラー時に、何が間違っているかすぐわかるようになり、なぜ動いているのか・動かないのかわからないというソースがかなり減りました。

mallocについて

ローカル変数との違い

  • ローカル変数は確保できる合計サイズに限りがある
  • mallocの方が柔軟。
  • staticをつけた変数はデータセグメントに格納される
  • ではローカル変数は?
    • スタック領域に確保されます
      • ある程度の領域を抑えてスタックのベースアドレスから、
        • 下に伸びる
        • 上に伸びる
      • みたいな感じで空き容量を埋めていく感じです。
  • mallocではヒープ領域にメモリを確保する

    • heapの上限はない?

      この「ヒープ領域」には上限が決められていません。p223より

      • スタックはコンパイル、リンク時に大きさが決まるらしい。
  • mallocは失敗するとNULL=(((void*)0))が帰ってくる*2

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
  • freeをしないとヒープの領域を食いつぶしてメモリリークが発生する
    • 厄介なバグの一つ
    • malloc()で確保した領域は必ずfreeせよ!!!

realloc

void *realloc(void *ptr, size_t size);

  • 一度mallocした領域の拡張を行うことができる
  • ただしmallocで確保したサイズより大きいデータを書き込んでも普通に実行できてしまった。
  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);

Linuxmallocmmapについて

liunuxではmalloc()は以下のような動作になっている

  • mmap()関数で似たようなことができる
    • mmap関数ではページ単位でメモリを獲得する
    • malloc関数ではバイト単位で管理する
      • 内部ではmmapシステムコールを使ってページ単位で確保し、それをプールしている
      • malloc呼び出し時にプールから指定バイト数分切り出して渡している

Linuxの仕組みより

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

hariboteOSでは?

memmann_alloc(カーネル用のmalloc)

こちらは MEMMAN_ADDRというmammann_allocで割り当てる領域の開始アドレスがdefineで(0x003c0000)に指定されています。

api_malloc(アプリ用のmalloc)

mallocで割り当てるアドレスがどこから始まるかをhariboteOS用の実行ファイル.hrbファイルに書きこまれています。 あまり自信がありませんが、これはデータセグメントのベースからのオフセットアドレスだと思います。

感想

メモリリークは大変だと思うが、 発生した際に時間をかければ、問題箇所の調査ぐらいはできるくらいの知識はついた気がする。

今まではメモリリークの調査など絶対できそうになかったが、最近は結構自信がついてきた。(まだやったことないけど)

ただし、調査に時間はすごくかかりそうだ。

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

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

C言語の復習その5。関数ポインタについて

関数ポインタは「宣言方法」と「引数、返値があっていない適当に実装されたコードを読む」のがややこしいが、綺麗に書けばそこまで難解ではない。という認識です。

関数ポインタについて

  • 宣言 void(*p)()
    • 引数付き void(*p)(char *str)
  • *pを囲ってる()が若干気持ち悪い
    • これがないと演算子の優先度の都合上、ポインタ型の変数を返す関数になってしまう。
      • int *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の仕組み

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

*1:JSなら全てstrictモードにするなど

*2:パズルチックなコードや他人が書いたコードで型変換のルールを調べ直す日が来るかもしれません。

C言語の復習その4。enum,3項演算子,goto,関数

関数ポインタをちょっと読み込んだくらい進みましたが、 関数ポインタは難しそうなので、それで一つ記事を書きます。

というわけでキリが良いので投稿♪

enum

  • enumのメンバは本来値を持たない。(持つべきではない)
  • 実は数値型の値を持っている。
    • 先頭から0,1,2,3...と割り振られている。
typedef enum
{
  SUCCESS, //0
  FAILURE  //1
}Result;
  • ただしこの値を直接計算・表示に使うのはenumの利用方法としてはふさわしくない。
    • K&Rにこの「ふさわしくない利用例」が乗ってしまっているらしい。
  • 確かJavaenumも内部では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)さんからこんな指摘をいただきました。

Javaと違いC言語のemumは単なるint型です。 C言語は弱い型付けなので普通にintのように扱えます。

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のラベルは関数ブロックの最後尾にかけない?

    • 手元のGCCでは上記のGOTOを使った関数は最終行のprintf("\n");を抜くとエラーになった。
      • またまた某所で教えてもらいましたが、

      ラベルの次行に空文であるセミコロン ; を置けば、ブロックの最後尾にラベルを書くことが出来ます。

      • とのことです。検証したところ確かにコンパイル&実行できました。
  • 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はだいたい上記の認識でいいが変数は別。

fa11enprince.hatenablog.com

C言語能力が着実にレベルアップしています。かなり良い本だと思います。

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

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

プログラマのための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://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%83%E3%82%AB%E3%83%BC%E3%83%9E%E3%83%B3%E9%96%A2%E6%95%B0

だそうです。

郵便番号はユニークではない。

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この業界意外と狭い!

プログラマのためのSQL 第4版

プログラマのためのSQL 第4版

*1:急に出てきたサブクエリが中途半端だな...と読んでて思ったのです。

C言語復習その3構造体、共用体について

構造体と共用体について学びました。

構造体のサイズについて

  • コンパイラが勝手にサイズを偶数長になるように変更する
  • 構造体の正確なサイズはわからない。
    • sizeof演算子で大きさを求めるべき。
      • 実際の現場でもsizeof演算子で大きさを求める場面は結構あるらしい。

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",&registers.ah);
//bitフィールドを使った構造体のアドレスは取得可能。
printf("registers adress is %p\n",&registers);

共用体

  • ある値の何バイト目まで、というような値の参照ができる。
  • 見た瞬間にCPUのレジスタを扱うのに向いているのでは?と思った。
  • hikaliumさんが過去に作られていたOSで適当にunionとググってみたところcpuのレジスタのbitを共用体で表現しているところがありました。

github.com

  • EFLAGSやCRnレジスタはnビット目がこれ!という表現を共用体を使わないと混乱すると思います。
  • Linuxでも共用体でレジスタっぽいものを表現していました。*1
  • たぶんCPUのレジスタを共用体で表現するということは普通に行われていることなのでしょう。
    • 本書ではほとんど使われない、と書かれているが、ある程度低レイヤーの分野の書籍を読んだことがあると用意の活用イメージが湧く。

確かに面白いのですが、実際のところ普通のプログラミングをしている限り、ほとんど用途がありません。ですから、こういうものがある、ということだけ覚えておけば十分でしょう。

  • COBOLに似たような機能があった気がする。レベル変数?とか言った気が。

tallercolibri.com

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の仕組み

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

*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
  • 大きな配列をローカル変数として宣言するとスタックを使い過ぎてしまう。

    • コンパイラは特にこの件に関して警告を出さない
    • 対策
      • コンパイル時にスタックの領域を大きくする
      • staticやグローバル変数を使うようにしてスタックを使わなくする
      • mallocを使い明示的にメモリを確保する
      • メモリ上の変数を使わない(ファイルで扱う)
  • C言語では関数には値そのものではなく、値のコピーが渡るらしい。

    • アセンブラ(objdump)で見て見たけど、よくわからなんかった
    • 配列を渡すと、配列のアドレス(のコピー)が渡る
      • 配列のコピーを渡す方法はない。
  • ポインタの正確な定義は難解らしい

    ポインタという言葉の定義は、実はあんまりはっきりとは決まってないらしい 注2-A正確にいうとANSIの仕様として正確に決まってはいます。が、それを定めた文章はあまりに難解で、とてもではありませんが簡単に理解できるようなものではありません。そのせいもあるでしょうが、一般的には「ポインタ」という言葉の定義は、ないに等しい状態です。p105

  • ポインタという呼称はややこしいのでやめた方が良い

    • 文脈に応じて
      • ポインタ変数
      • アドレス
    • に言い換えること。p106
  • 配列を引数として受け取る関数の宣言

    • void func(int *arg)と書くのが一般的
    • void func(int arg[])と書くこともできる。
    • 俺は後者の方が好き。
  • %pの出力に関して。

    • 6Byteのものが表示された。
      • 0x7ffee878ea86
    • これは何かのオフセット?
    • 一瞬ページングのページオフセットかと思ったけどあっちは12bit
    • 物理アドレスではないと思う。
  • ポインタの添字とラベルを変えることができる豆知識の説明

    • i[5]5[i]と同じ意味
    • OS自作本ではアセンブリのレベルで説明していた
    • この本ではポインタアクセスの中身を加算にして説明している
      • i[5]=*(a+5)=*(5+a)=5[a]
  • 2次配列はただの配列の配列

  • 2次元配列はアドレスで受け取れない
    • void func(int arg[])=void func(int *arg)
    • void func(int arg[3][5]) = void func(int arg[][5])
      • void func(int *arg)これで多次元配列は受け取れない
      • 配列の関数が受け取るときには、要素数は最初の要素数だけが省略できる(p117より)

    • なぜか本書の内容を検証したがうまくいかなかった。
//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の仕組み

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

C言語の復習を始めました。

こんにちは。大学の1年目のC言語の授業で予約語と非予約語の違いが判らずに 1週間くらい変数には何でもwight,hightを付けていた不届きものです。

最近C言語を再勉強することにしました。

一応大学ではC言語を一番書いていましたが、 C言語そのものの知識というのはあまり深くありません。

ちょっとした事業の数値計算の課題などで使った程度で、 (ポインタなどは積極的に避けていた)

Makeファイルを書いたり、テストを書いたり、 ということはできていませんでした。

C言語は一応読めるけど、複雑なものはわからないし、 大きな規模だったり本格的なモノづくりに活用できるようなスキル ではないということです。

これは今後hariboteOSを改築するのに大きな障害になると思ったのでC言語力を高めたいわけです。

というわけで、もう10年近くプログラミングをやっていますので、 繰り返しはfor文で書きますよ~等は説明されるとくどいような気がするので、

ある程度知っているが深く知らない、人向けの本を探してみました。

ちょうどこんなエントリが目に付いたので早速本を買ってみました。

hatomu555.hatenablog.com

早速ポインタの節の手前まで終わりました。

github.com

本書中の面白いトリビアでも書こうと思います。

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)進めました。 ここはフレッシュな脳みそで読みたいの今晩はこの辺にしておきます。