前回(2019/12/06)の内容 メモリモデル(コンピュータの「生」のイメージ) メモリはセルの集まり : セルには 1 byte 分の情報を記録 セルには、セルの場所を表す「番地番号」が一意の連続した整数値がついている # 「番号番号」が整数値なので、「計算」が可能 セルの値の取り出しや設定(代入)は、「番地番号」を利用してセルを指定して行う C 言語の配列との比較 メモリは一つ : 配列は、複数 (先頭の場所を配列名、サイズは宣言で指定) セルのサイズは 1 byte : 配列は要素のサイズが色々(宣言型できまる) セルの指定は番地番号で行う : 配列は配列名と添字 (一般の変数は名前だけ) 「番地番号」=> 整数値 => 「値」を持つ / 計算ができる 「値」を持つ => 関数の引数に渡せる/関数の帰り値にする事ができる 計算ができる => 要素を、間接的に参照 => 関数の実引数に配列名を指定し、関数の中で配列要素の値が変更(代入)ができる理由(根拠/仕組み) 配列名 ary 対し ary[i] (要素の参照) <=> *(ary + i) # 「変数」: C 言語から見た(「生」でない、抽象化された)イメージ メモリと変数(普通の単純[配列の要素でない]変数)の関係 セルの(1 以上の..)並びが、変数 (変数の特質はセルの特質) char 型の場合 => 1 つのセル int 型の場合(32bit CPU であれば) => 4 つのセル double 型の場合 (64bit) => 8 つのセル # sizeof 演算子を利用して、セル数(byte 数)を得る事できる # # その処理系において # # => C 言語では、int 型のサイズは、処理系によって異なる # # !! 同じ PC でも、OS が違ったり、同じ OS でも違う会社の C Compiler だと異なるサイズになる可能性がある # # !!! なぜ、C 言語のサイズが違うか # # !!! => C 言語は、そのシステムで、最も効率(都合..)が良いサイスを選ぶ 「ポインター値」とは何か ? 基本は番地番号(実行時)だが、型属性も持つ(コンパイル時) 「番地番号」=> printf( "%p" ) 表示されるもの(整数値:16進数) 「型情報」も持っている 型情報 = メモリ上のセル(byte)サイズ(メモリへ処理) + 値に適用する演算 # セル数が利用される状況の例 # 代入 # char ch, dh; ch = dh: 1 byte コピー # int i, j; i = j; 4 byte コピー # 値に適用する演算の例 # int 型の割り算 : 10/3 # => 整数割り算なので、答えは 3 # double 型の割り算 : 10.0/3.0 # => 浮動小数点数割り算なので答えは 3.3333 => ポインタ値を操作する時にも、その型情報が利用される 例 : +1 とすると番地番号としては sizeof(型)だけ増える [今日の話] ポインター値とポインターの話 ポインター値 変数に & につけると得られる # 「変数」は、「メモリのセル」に対応づけられていて # その「メモリのセル」が「番地番号」を持っている # ポインター値の %p で表示される「値」は、その「番地番号」 # => 実行時に「値」して扱われるのは「型情報」が取り除かれた番地番号に対応 # # 「型」情報は、コンパイル時だけの情報で、実行時は失われている # # 「型」の情報は、「値」にではなく、「値」の使う側にある # 変数には型があるので # ポインター値には、その型の情報が引き継がれる # 型の情報は、コンパイル時は、保持されているが # その情報はデータから、そのデータの扱う側に移動されていて # データとしては、その情報は残らない ポインター値の操作方法 番地番号の変更 => 整数値の加算、減算が可能 int *pa; pa がポインター値をもっている場合 pa = pa + 1; 番地番号が変化 # sizeof(int) = 4 だけ増える # pa + 1 <-> &*( pa + 1 ) <-> & (pa[1]) <-> & pa[1] cf. double *pb; pb = pb + 1; 番地番号が変化 # sizeof(double) = 8 だけ増える 引き算も可能 int *pa; int *pc; pc = pa - 10; 番地が変化 # sizeof(int) * 10 = 40 だけ減る # pc <-> pa - 10 <-> &*( pa - 10 ) <-> & pa[-10] # pc[0] <-> pa[-10] # pc[10] <-> pa[0] # pc[10] <-> *(pc+10) <-> *( pa - 10 + 10 ) <-> pa[0] int pa[10]; /* pa[0] 〜 pa[9] */ int *pb = pa - 1; /* pb[1] 〜 pb[10] <-> *(pb+1) 〜 *(pb+10) <-> *(pa-1+1) 〜 *(pa-1+10) <-> *(pa+0)〜 *(pa+9) <-> pa[0] 〜 pa[9] */ ポインター同士の引き算は、整数値になる int ia[10]; int *pa = ia + 4; /* pa <- &ia[4] */ ini *pb = ia + 7; /* pb <- &ia[7] */ printf ( "%d\n", pb - pa ); /* 3 = ( 7 - 4 ) = pb - pa */ /* pb = pa + ( pb - pa ) */ # int *ip; # ip++; /* ip = ip + 1 => ip が指しているであろう配列要素の次の要素を指す */ 番地 型情報の変更 ポインター値の前に「(型名) : キャスト演算子」を付ける事により 型情報を変更する事ができる 動的領域 変数宣言によるメモリ確保は、コンパイル時に「確保量」が確定する。 動的領域確保 : 実行時に、好きなサイズで領域を確保する事ができる malloc(領域サイズ(byte 数))で、領域が確保され、その先頭の要素を指すポインター値がかえる => 非常に便利 確保できる保証はない 確保できなかった場合は、NULL 値が返るのでチェックが必要 確保できれば、そのまま、利用できる # 配列とは違い、名前がないので、ポインタ変数を用意して、 # その値を保持する必要がある # 確保した領域の参照は、このポインタ変数を経由して行う 確保した、領域は、free をつかって、開放する必要がある # 開放した後に、使ってしまう # (初期化問題)確保するまえに使ってしまう