2016年度「プログラミング言語」配布資料 (6)

五十嵐 淳 (京都大学大学院情報学研究科 通信情報システム専攻)

2016年11月27日

[ 前の資料 | 講義ホームページ・トップ | 次の資料 ]

under construction2017年版に向け改訂中

C言語

C言語はJavaやOCamlに比べると,プログラムが実行される環境(ハードウェア・オペレーティングシステム)をより強く意識する必要がある,という意味で低水準(low-level)な言語であると言われる.データのメモリ上の配置について非常に細かく操作できる一方で,メモリ操作の安全性は低く,コンパイラの検査を回避してプログラムが触れるべきでないメモリ領域を読み書きすることも容易にできてしまう.一方で,アセンブリ言語に比べれば関数・繰り返しの構文が備わっていたり,ハードウェア毎の差異が抽象化されており,移植性が高いプログラムを記述することができる.

典型的なCプログラムの構造

Cプログラムは,おおまかにいうと,

  1. ライブラリ関数を使うための #include による ヘッダファイル の読み込み.
  2. 関数定義と関数プロトタイプ宣言の列
  3. プログラム実行の開始点となる main 関数の定義

で構成される.

以下は,階乗を計算する関数 fact を定義し,5の階乗をライブラリ関数 printf を使ってディスプレイに出力するためのプログラムである.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// C/samples/fact.c

#include <stdio.h>  // for printf

int fact(int n) {
  if (n == 1) {
    return 1; 
  } else {
    return n * fact(n - 1);
  }
}

int main(void) {
  printf("fact(5) = %d.\n", fact(5));
}

// 以降行末まで,もしくは,/**/ に挟まれた部分はコメントになる.3行目が,14行目で呼び出されている printf 関数を使うための include 宣言,5〜11行目が階乗関数 fact の定義,13〜15行目が main 関数の定義である.構文が Java のメソッドとよく似ていることがわかるだろう.1

#include

#include は,他のファイルに書かれたプログラムを読み込むための記法で,ファイルの内容がそこに展開されたのとほぼ同じ効果が得られる.通常のCプログラムにはファイル名に拡張子 .c がつけられるのに対し,拡張子が .h であるファイルはヘッダファイルと呼ばれる.ヘッダファイルには特定のライブラリ関数を使うためのその関数の型や関連する様々な定数や型定義が書かれている.

ライブラリ関数のドキュメントにはどのヘッダファイルを include すればよいかが書かれている.stdio.h は standard input and output の略で,入出力に関する関数を使う際に読みこむ.

関数定義

関数 fact の定義は Java に慣れた者であれば容易に読めるだろう.(ただし,真のCプログラマは再帰を使わずに繰り返しを使って定義するところだろう.)

C言語では,ファイルの後ろの方に書かれた関数を何もせずに呼び出す(ちなみにこのようなファイルの後の方に書かれている定義を参照することを(大変紛らわしいが) 前方参照(forward reference) という)と,コンパイラに警告される.例えば,mainfact の順番を変更してコンパイルすると,

fact.c:4:28: warning: implicit declaration of function 'fact' is invalid in C99
      [-Wimplicit-function-declaration]
  printf("fact(5) = %d\n", fact(5));
                           ^
1 warning generated.

これは警告なので,コンパイラはfactを「(引数はよくわからないが) int を返す」関数だと仮定して処理を進める.しかも,後で fact が全く違う型の関数だったとしてもお構いなしなので,これでコンパイルが成功しても,うまく実行できるか全く安心できない.

ファイルの後方で定義される関数を呼ぶためには関数の型情報(返り値型,関数名,引数の型)だけを先に宣言しておく.これをプロトタイプ宣言という.

int fact(int);

main の前に宣言しておけば,コンパイラも fact が整数をひとつ引数として整数を返す関数だと理解してコンパイル処理を進めることができる.この例では,単に main を後に置けば済む話だが,例えば関数同士が相互に参照する場合にはプロトタイプ宣言は必須である.

真偽値

C の真偽値は実質的に整数である.両辺が等しいかを比較する == は比較結果が等しければ 1 を,等しくなければ 0 を返すものの,ifwhile などの条件判定は 0 を偽として扱い,それ以外の値全て を真として扱う.このことに依存して書かれたプログラムも多い.比較結果は int 型の整数に格納することもできるが,C99 では,01 だけが格納できる(1 より大きい値も全て 1 につぶれてしまう) bool 型も(stdbool.h を読み込むことで)使うことができる.stdbool.h を読み込むと,bool 型だけでなく,true, false という定数も同時に(1, 0 の別名として)定義される.

main 関数

先に述べたように main 関数は,Cプログラムの実行開始地点を示す.プログラム起動時に外から情報をもらわない場合,引数部分には無引数を表す (void) を書く.(これは他の無引数関数も同様である.)文字列をもらうこともできるが,ここでは扱わない.また返値の型は必ず int である.この返り値で,プログラム呼び出し側に,プログラム終了時に情報を送ることもできる.2

printf 関数

printf 関数は文字列やデータを整形して(printff は format に由来すると思われる)出力するための関数である.基本的には,第一引数の文字列(二重引用符で囲まれた部分)を出力するが,% がついている部分は,format directive と呼ばれ,そのまま出力されず,第二引数以降のデータを適宜文字列化して埋め込んで出力する.このプログラムの %d は整数を十進表記(decimal の d)で整形する,という意味である.format directive には様々な種類があるがここではカバーしない.

末尾の \n は改行を表す文字の特殊表記である.このようなソースコード上に直接表記できない文字を示すための表記を エスケープシーケンス(escape sequence) という.

構造体

C言語で OCaml のレコードに相当するものが 構造体(struct) である.以下は OCaml で示した2次元座標の点の構造体 point,中点を計算する関数 middle を定義し,原点と (3,8) の中点を計算している.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// C/samples/point.c

#include <stdio.h>

struct point {
  int x;
  int y;
};

struct point middle(struct point p1, struct point p2) {
  struct point result = {(p1.x + p2.x) / 2, (p1.y + p2.y) / 2};
  return result;
}

int main() {
  struct point origin = {0, 0};
  struct point p;
  p.x = 3;
  p.y = 8;

  struct point m = middle(origin, p);
  
  printf("The middle point between (%d, %d) and (%d, %d) is (%d, %d)\n", origin.x, origin.y, p.x, p.y, m.x, m.y);

  return 0;
}

5〜8行目が構造体 point の定義である.pointx という名前で読み書きできる整数,y という名前で読み書きできる整数が,この順序で並んだようなデータであることを意味している.ここで宣言される名前 point構造体タグ と呼ばれ,struct と組み合わせることで型の名前として使うことができる.また内部のデータ x, y は(構造体の) メンバ(member) と呼ばれる.

構造体のメンバ x にアクセスするためには .x という記法を使う.p1 は構造体を格納した変数であり,このメンバ xp1.x でアクセスすることができる.p1.x の型は point の定義を見ることで知ることができる.

10〜13行目が中点を計算する middle である.構造体 struct point の引数をふたつ取り,struct point を返すものとして定義されている.struct point は型なので,このように関数パラメータや,局所変数の型として使うことができる.変数 result の宣言の右辺は,構造体(や複数の値から構成されるデータ)を初期化するための表記で,中括弧の中にデータを並べる.この際,(OCaml のレコードと違って)メンバの名前は指定できず,定義の時の順序に従うので,並べる順序は重要である.

初期化構文を使わずに初期化する場合は,18,19行目のようにメンバアクセスの記法を使って代入を行うことができる.(OCaml と構文が違うので注意されたし.)

このプログラムでは,構造体を関数を使ってやりとりしているが,この引数・返値渡しにあたっては,構造体のメンバが全てコピーされるため,(構造体が大きい場合には特に)避けることも多い.大きなデータ構造のコピーを避けて計算するには---というより,C言語を理解するには---ポインタ(pointer) の概念を理解することが非常に重要である.3

ポインタ

C言語のポインタを理解するためのいくつかの用語を導入したい.

オブジェクト
連続したメモリ領域のこと.
識別子
型やオブジェクト,構造体などにつけられる名前,もしくは名前として使うことのできる文字列のこと.
変数
識別子によって名付けられたオブジェクト
左辺値(L-value)
変数が指すオブジェクトの先頭アドレス
右辺値(R-value)
変数が指すオブジェクトに格納されたデータ

int x; といった変数宣言は,細かくは,メモリに int を格納するための領域(オブジェクト)を確保して,それに x という名前をつける作業と考えることができる.格納された領域は一定の規則に従って解放される.例えば,関数のパラメータであれば,関数の実行開始時に(各呼び出し毎に別の領域が)確保され,実引数の内容がコピーされて初期化され,関数の実行終了時に解放される.

構造体のメンバアクセス p1.y のような表現は,オブジェクト p1 の後半部分のオブジェクトを示すためのものである.オブジェクトは使われる文脈によって,その(先頭)アドレスまたは格納されたデータとして解釈される.例えば,

x = x + 1;

という代入文は「変数 xx + 1 を代入する」と説明してきたが,細かくは,「オブジェクト x の先頭アドレスに(オブジェクト x から整数データを読み出しそれに 1 を足した整数)を格納せよ」と解釈される.代入文の左辺では先頭アドレスとして,右辺では格納されたデータとして解釈されるため,それぞれオブジェクトの左辺値,右辺値と呼ばれる.

アドレス演算子 &

前置演算子 & はオブジェクトの先頭アドレス(左辺値)を返す演算子4である.& が適用されるのはオブジェクトなので &1 のように整数などに直接適用することはできない.

以下は,2変数 x, y を宣言して,そのアドレスを出力するプログラム断片である.

  int x = 100;
  int y = 200;

  printf("x and y are allocated at %p and %p\n", &x, &y);
  printf("their sizes are %zd\n", sizeof(int));

実行結果は以下のようになる(具体的な値は,OS毎,さらには同じOSでも各実行毎に違うだろう).

x and y are allocated at 0x7fff52271978 and 0x7fff52271974
their sizes are 4

プログラム最後の行に現れる sizeof(int)sizeof 演算子と呼ばれ型やオブジェクトを引数としてそのサイズをバイト単位で計算する.int のデータのサイズは実行環境に依存するがここでは4バイトのようである.&x&y はどうやら,連続したメモリ領域に確保されているらしいことがわかる.(アドレスを printf で表示するためには %p を使い,サイズを表す整数には %zd を使う.) 先に宣言した x の方が大きいアドレスに格納されている.

この & で取得したアドレスは,データとして変数に格納することができる.これがポインタ変数である.ポインタ変数は名前の前に * をつけて宣言する.(が,型としては「整数へのポインタ型」 int * のように扱うので,「名前の前に」という言い方はベストではないかもしれない.)

  int *px = &x;
  int *py = &y;

pxpy にはそれぞれ xy のアドレスが格納される.pxpy も変数なので,そのアドレスを取得することもできる.

  printf("px and py are allocated at %p and %p\n", &px, &py);
  printf("their sizes are %zd\n", sizeof(int *));

実行結果:

px and py are allocated at 0x7fff52271968 and 0x7fff52271960
their sizes are 8

pxpyxy と同様連続した領域に格納されているようだ.(が,ypx の間には空白があるようだ.これは アラインメント といって,データのサイズに応じて,格納場所のアドレスの下位数ビットを0に揃えなければいけないというCPUの制約に由来するものである.プロセッサによっては,ロード命令で複数バイト(例えば4バイト)を一度に読み出す際に,アドレスが4の倍数でないといけないという制約がある.)

さらにこれらのアドレスを別の変数に格納することもできる(この場合,その変数の型は int ** になる)がここでは行わないでおく.

さて,アドレスからは * 前置演算子を使って,その先のオブジェクトを参照することができる.例えば *pxpx が指す(変数 pxに格納された右辺値であるところのアドレスに格納された)オブジェクトになる.そのため x*px は同じオブジェクトを表す表現として使うことができる.よって x に代入をした後に *px の(右辺)値を表示すると,x に代入された値が表示される.*px を代入文の左辺に持ってくることもできる.
この場合,*px の指すオブジェクトの内容,すなわち x が書き変わる.

  printf("px at %p points to x (%d)\n", px, x);
  x = 300;
  printf("px at %p points to x (%d)\n", px, *px);
  *px = *px + 1;  // reads (*px) + 1
  printf("the value of x is %d\n", x);
/* 表示結果
the value of *px is 100
the value of *px is 300
the value of x is 301
*/

ちなみに ++ 演算子は変数の値を 1 増やすことができるが,*px = *px + 1;(*px)++; と括弧をつける必要がある.括弧を付けないと px の値(つまりアドレス)を増やしてから,(増やす前の元のアドレスを) * で参照する,という全く違った意味になるので注意すること.5(「アドレスを増やす」という意味は次にみる.)

ポインタ演算

(この節のポインタ演算による参照は illegal なので,例として不適切.次の節と順番を変えるべきかな.)

さて,C言語のポインタは,Javaの参照と違って足し算・引き算などの演算をすることができる.これが ポインタ演算(pointer arithmetic) である.実は,ポインタ演算の原理はそれほど難しくはない.むしろ難しいのはポインタ演算を使ったプログラムの意図を理解することである.

  • ポインタに n を足した結果は,指しているオブジェクトのサイズn個分増えたアドレスになる

これだけである.ただし,結果のアドレスにどんなデータが格納されているかはお構いなし,である.例えば int へのポインタなら,演算の結果得られたアドレスには int が格納されていると「思い込んで」データの読み出し・書き込みを行う.

xy の領域が連続していた(xyの「次のアドレス」に割り当てられていた)ことを思い出してもらいたい.以下のプログラムは,実質的に x の値に5を足して,y に,y の値 + 100 を x に書き込むプログラムになっている.

  *py = *(py + 1) + 5;   // py + 1 points to the integer "next" to y
  printf("px at %p points to x (%d)\n", px, x);
  printf("py at %p points to y (%d)\n", py, y);

  *(py+1) = *py + 100;
  printf("px at %p points to x (%d)\n", px, x);
  printf("py at %p points to y (%d)\n", py, y);
/* 実行結果
px at 0x7fff52271978 points to x (301)
py at 0x7fff52271974 points to y (306)
px at 0x7fff52271978 points to x (406)
py at 0x7fff52271974 points to y (306)
*/

今回はたまたま xy が連続していたからよいようなものの,そうでない場合には全く違う結果になる場合もある(コンパイラに依存する).また,これを濫用すると全く関係ない領域のデータを破壊することも簡単にできる.

  *(py-3) = 255;
  printf("px is %p\n", px);
  printf("it points to %d\n", px, *px);
/* 実行結果
px is 0x7fff000000ff
Segmentation fault: 11
*/

この例の場合 py-3 のアドレスに px の領域の下位32ビットが格納されていたために,px の値が破壊されてしまっている(000000ff が代入された 255 の痕跡である).そして,そのアドレスから読み込みをしようと試みたところ,どうやらアクセス禁止領域だったらしく OS の Segmentation fault エラーとなって実行が異常終了してしまっている.

構造体へのポインタ,キャスト (C/samples/pointer2.c)

構造体の場合も,同様にして&演算子で構造体が格納された領域の先頭を指すポインタを取得することができる.

  struct point p1 = {500, 100};
  struct point p2 = {300, 400};

  struct point *pp1 = &p1;
  struct point *pp2 = &p2;

  printf("pp1 at %p points to a point (%d, %d)\n", pp1, p1.x, p1.y);
  printf("pp2 at %p points to a point (%d, %d)\n", pp2, p2.x, p2.y);
  printf("their sizes are %zd\n", sizeof(struct point));
/* 実行結果
pp1 at 0x7fff50488960 points to a point (500, 100)
pp2 at 0x7fff50488958 points to a point (300, 400)
their sizes are 8
*/

構造体pointintふたつ分の領域を占めるので,サイズは8バイト,xy は連続した領域に配置されていることがわかる.

ポインタを通じて,メンバの読み書きをするには,ポインタの参照を行う * と,メンバを選択する . 演算子を用いればできる(. の方が結合が強いので括弧が必要である):

  (*pp1).x = 900;   // parentheses needed

が,この演算子の組み合わせパターンは頻出なので,それらを組み合わせた->という演算子も容易されている.

  pp2->y = 200;     // abbreviation for (*pp2).y

このふたつの代入の結果を見てみよう.

  printf("pp1 at %p points to a point (%d, %d)\n", pp1, p1.x, p1.y);
  printf("pp2 at %p points to a point (%d, %d)\n", pp2, p2.x, p2.y);

/* 実行結果
pp1 at 0x7fff50488960 points to a point (900, 100)
pp2 at 0x7fff50488958 points to a point (300, 200)
*/

p1.xp2.y が書き変わっている.

さて,構造体のメンバも記憶領域の一部を占めているオブジェクトであるので,そのアドレスを & で取得することができる.

  int *p = &pp1->y;  // p points to a middle of the object named p2

  printf("p at %p points to %d\n", p, *p);
/* 実行結果
p at 0x7fff50488964 points to 100
*/

このようにして,pp1->y (すなわちp1.y)のアドレスを取得することができる.アドレスを見てみると確かに pp1pp2 の真ん中である.言い換えると,この p は構造体の途中を指している.この指している先には p1.y の値である 100 があることがわかる.さらに,このポインタを通じて,p1.y の値を書き換えることもできる.

  (*p)++;   // equivalent to *p = *p + 1;  incrementing the content of p
            // Do not confuse with *p++;, which is equal to *(p++);.
            // It increments p and read the value pointed to by p (and discards, in this case).
  printf("pp1 at %p points to a point (%d, %d)\n", pp1, p1.x, p1.y);
  printf("pp2 at %p points to a point (%d, %d)\n", pp2, p2.x, p2.y);

/* 実行結果
pp1 at 0x7fff50488960 points to a point (900, 101)
pp2 at 0x7fff50488958 points to a point (300, 200)
*/

実行結果で表示される数値が 101 になっていることに注目してもらいたい.

(以下は,少し行儀の悪いプログラムである.)さて,ポインタ自体は単に計算機のアドレスなので,指す先のデータが何であっても同じサイズのデータである.C言語では,型変換(これはコンパイラにポインタが指す先のデータの大きさを教えるだけで,実行時には何も起こらない操作となる)という操作を行うことで,型Tへのポインタがある時にそれを異なる型T'を指すポインタである,としてプログラムを実行することができる.型変換は キャスト とも呼ばれる.キャストは括弧の中に変換先の型を書いて式の前に置く.例えば,pp1pp2 は構造体の先頭アドレスであると同時に,int メンバ x を格納した領域の先頭アドレスでもあるので,struct point * から int * へのキャストを行うことで,メンバ x の値だけを取りだすことができる.

  // It's OK to assign a pointer to a different type with explicit
  // type conversion (called casts), if the resulting pointer is
  // correctly aligned.  In this case, p points to the member x.
  p = (int *)pp2;

  printf("p at %p points to %d\n", p, *p);
/* 実行結果
p at 0x7fff50488958 points to 300
*/

これは p = &pp2->x; とするのと同じである.

しかも,メンバ yx の次の領域に確保されているので,ポインタ演算を使うことで y にアクセスすることができる.以下の p+1 は次の p から見て int ひとつ分先のアドレス,つまり p2.y があるべき位置を指す.

  // y is the next member, and so the following assignment will update
  // p2.y.  (It's not recommended, though -- in general, members x and
  // y may or may not be allocated to contiguous regions (when they
  // have different types), so don't assume p+1 will always point to
  // the next member.
  *(p+1) = -100;

  printf("pp1 at %p points to a point (%d, %d)\n", pp1, p1.x, p1.y);
  printf("pp2 at %p points to a point (%d, %d)\n", pp2, p2.x, p2.y);

/* 実行結果
pp1 at 0x7fff50488960 points to a point (900, 101)
pp2 at 0x7fff50488958 points to a point (300, -100)
*/

このようにして p2.y が更新される.

このようにふたつのメンバが同じ型でしかも宣言時に隣りあっていれば,ポインタ演算で正しい位置を指すことができる6が,メンバの型が異なるとうまく行かないこともあるので注意してもらいたい.例えば,メンバ x の型が char (これは1バイト),メンバ y の型が int の場合,

struct foo {
  char x;
  int y;
}

と隣りあって宣言されていても,おそらく xy の間には数バイト(int が4バイトなら3バイト)のギャップがあるはずである.これは,アラインメントにより y のアドレスが4の倍数でないといけないことによるものである.この時 x を指す char * 型のポインタに1を足しても,y の先頭アドレスは得られない.

また,ポインタ型のキャストの使用にも細心の注意が必要である.コンパイラは,どんなポインタ型間のキャストも許しているが(コンパイラを -Wincompatible-pointer-types というオプション付きで起動すれば警告は発生する),変換後の型が指す領域が実際に確保されているかは何もチェックしないので,実行時に,ポインタの指すオブジェクトとその型(サイズ)の間に齟齬がる場合には,これまた未定義動作になってしまう.

例えば,

  p = &pp2->y;
  struct point *pp3 = (struct foo *)p;
  printf("pp3 at %p points to a point (%d, %d)\n", pp3, pp3->x, pp3->y);
/* 実行結果
pp3 at 0x7fff5048896c points to a point (-100, 900)
*/

とすると,pp3pp2 の真ん中から,p2 の範囲を越えて8バイト分の領域を指すポインタとなる.今回はたまたま,はみでた領域に p1 が割り当てられているので,その前半部分(つまり x の部分)を読んで 900 が得られている.

ポインタ渡し

ポインタは,単独の変数で使うだけでなく,関数の引数や返り値でやりとりすることもできるし,ポインタをメンバとして持つ構造体を作ることもできる.

まずは,ポインタ渡しについて.

関数呼び出しの際,引数に変数の名前を書いたとしても,関数型に渡されるものは,その変数の(右辺)値である.既にJavaやOCamlの関数呼び出しの動作の説明でふれたように,関数側のパラメータ変数については新たに領域が確保されて,渡された値はその変数の初期値になる.よって,以下のように,関数側でパラメータ変数に代入を行っても,その影響を呼び出し側で観察することはできない.

void novice_swap(int a, int b) {
  int tmp = a;
  a = b;
  b = tmp;
  return;
}

int main(void) {
  int x = 2;
  int y = 3;
  
  novice_swap(x, y);
  
  printf("x = %d; y = %d", x, y);  // prints "x = 2, y = 3" not "x = 3, y = 2" as one might expect
}

しかし,関数に呼び出し側の変数のアドレスを渡してやることで,呼び出し側(caller)の変数の値を呼び出され側(callee)から変更することができる.以下は,2変数の値を入れ替える「正しい」swap 関数の定義とその使用例である(C/sample/swap.c).

#include <stdio.h>

void swap(int *a, int *b) {
  int tmp = *a;
  *a = *b;
  *b = tmp;
  return;
}

int main(void) {
  int x = 4;
  int y = 100;

  printf("(x, y) = (%d, %d)\n", x, y);
  swap(&x, &y);
  printf("(x, y) = (%d, %d)\n", x, y);

  return 0;
}

この関数 swap は,整数へのポインタを受け取る関数となっていて,そのポインタの指す先を入れ替える動作になっている.一方,呼び出し側では,変数のアドレスを & で取得して渡すことで,swap 側で入れ替えてもらっている.このように,ポインタを渡して関数側で書き換えてもらう,というのは,関数から複数の値を呼び出し側に返したい・伝えたい場合などに(return だけでは賄えないので)使うパターンで,C言語のライブラリ関数でよく見る.

ダングリング・ポインタ,malloc と free

さて,ポインタを関数をまたいでやりとりする時には,ポインタの指す先の領域が確保されているかを常に気にする必要がある.上の swap の例であれば,swap の実行中ずっと,xy の領域は確保されているので大丈夫である.大丈夫でない典型例は,関数側の局所変数へのポインタを返してしまうような関数である.

int *foo(int x) {
  int y = x * 2;
  return &y;
}

この関数は,局所変数 y に引数の倍の数を入れて,y のアドレスを返している.しかし,y の領域は foo の実行が終わった途端に解放されてしまうので,呼び出し側で foo の返り値の先を参照するのはまずい(未定義動作).このような,解放された領域を指すポインタを ダングリング・ポインタ(dangling pointer) と呼ぶ.

  int *p = foo(100);
  printf("*p is %d", *p);  // Illegal!

つまり,新たにメモリ領域を確保してその領域へのポインタを呼び出し側に返したい場合に局所変数を使うことはできない.(ポインタを返すのではなく return y; とするのであれば,単に変数の中身(右辺値)が呼び出し元にコピーされて返されるので問題はない.もちろん返り値の型は int * ではなく int にする必要があるが.)このような場合には,標準ライブラリの malloc 関数を使って,ヒープ領域と呼ばれる別のメモリ領域から領域を確保する必要がある.

malloc 関数を使うには<stdlib.h> を include する.型は,

void *malloc(size_t);

というもので,確保したい領域のサイズ(型 size_t の値,典型的には sizeof がこの型を返す)を渡すと確保した領域へのポインタを返してくれるという関数である.(領域の確保に失敗した場合にはどこの領域も指さないーよって * で参照した途端に未定義動作になるーnull pointerが返ってくる.) void * というのは,特殊なポインタ型で(いくつかの例外を除いて),何を指しているのか不明なポインタに対して使われる.void * 型のポインタは,通常,キャストを使ってその指している(べきものの)先を明示してから使う.型 T を格納する領域を確保する時には,以下のように呼び出すのが定石である.

(T *)malloc(sizeof(T))

malloc を使うと,上の foo

int *foo(int x) {
  int *p = (int *)malloc(sizeof(int));
  *p = x * 2;
  return p;
}

と書き換えることができる.本当に行儀のよいプログラムを書きたかったら,以下のように *p にアクセスする前に malloc の返り値が null pointer でないかどうかを確認する処理を書くべきである.

int *foo(int x) {
  int *p = (int *)malloc(sizeof(int));
  if (p == NULL) { printf("malloc failed!!\n"; exit(0); }
  *p = x * 2;
  return p;
}

NULL が null pointer を表す定数,exit はプログラムの実行をいきなり終了するためのライブラリ関数である.

malloc で確保された領域は,対になる free というライブラリ関数で解放することができる(解放すべきである).

void free(void *);

free 関数は,(malloc で確保された領域の)ポインタを受け取って,その領域を解放する.解放した後にそのポインタの指す先にアクセスしてはいけない(未定義動作).また,malloc で確保されたわけでもない領域のポインタを渡したり,同じポインタに対し二度 free を行った時の動作は未定義である.(ちなみに null pointer を渡した場合には,何もしない,という動作になる.)

free の使用例については後ほど詳しく見ていくが,新しい foo は例えば以下のように使うことができる.

  int *p = foo(100);
  printf("*p is %d", *p);  // prints "*p is 200"
  free(p);
  // p shouldn't be used any longer

共用体 (C/sample/union.c)

構造体は複数のデータを並べたようなオブジェクトを作るための仕組みであった.これに対し, 共用体(union) は,異なる種類のデータを同じ領域に重ねあわせて,ひとつのオブジェクトを異なる型のデータとして扱うための仕組みである.

以下は char を3つ並べた構造体 triplechar と,int を重ねあわせた共用体 foo の宣言である.

struct triplechar {
  char fst;
  char snd;
  char trd;
};

union foo {
  int i;
  struct triplechar ch;
};

foo は(構造体と同様に)タグと呼ばれ,union foo の形で型として使うことができる.また,ich は(これも構造体と同様に)メンバと呼ばれる.共用体 foo は,i という名前で読み書きできる整数と ch という名前で読み書きできる構造体(struct triplechar)が,同じ領域に重ねあわされているようなデータであることを意味している.「重ねあわされている」というのは,メンバ i を通じてアクセスした時には int として,メンバ ch を通じてアクセスした時には構造体として見えるという感じである.以下の,コード断片は,union foo の変数を用意して,そこに整数 0x12345678 を書き込んでいる.f を整数として扱いたいため,メンバ i を使っている.(0x は整数定数を16進数表記して与える時の接頭辞である.)

  union foo f;
  f.i = 0x12345678;

  printf("The size of f is %zd\n", sizeof(union foo));
  printf("f as integer is %d (decimal) and 0x%x (hexadecimal)\n", f.i, f.i);
/* 実行結果:
The size of f is 4
f as integer is 305419896 (decimal) and 0x12345678 (hexadecimal)
*/

実行結果にあるように,f のサイズは 4 である.これは int のサイズ 4 と struct triplechar のサイズ 3 の最大をとった値になっている.共用体はメンバが同じ領域を占めるので,そのサイズは,全メンバの中の最大サイズになる.

さて,今,4バイトの領域に 0x12345678 が書き込まれているわけだが,これを,メンバ ch を通じてアクセスすると,先頭3バイト分が struct triplechar としてアクセスできる.構造体なので,その中にアクセスするためにはさらに .fst などをつけることになる.以下は,f の先頭3バイトの値を個別に取り出して表示するためのコードである.%hhd%hhxchar 型の値を十進表記と16進表記で表示するためのフォーマット文字列である.hh の部分が char であることを,dx が基数を指定している.

  printf("f's first byte is %hhd; second is %hhd; third is %hhd\n",
         f.ch.fst, f.ch.snd, f.ch.trd);

  printf("In the hexadecimal notation, they are 0x%hhx, 0x%hhx, and 0x%hhx\n",
         f.ch.fst, f.ch.snd, f.ch.trd);

/* 実行結果
f's first byte is 120; second is 86; third is 52
In the hexadecimal notation, they are 0x78, 0x56, and 0x34
*/

この実行環境の場合,32ビット整数は下位8ビットから順に(アドレスの小さい方に)格納されている7ことがわかる.

さて,この構造体の値を書き換えれば,当然 i を通じて int としてアクセスした際の値にも影響を及ぼす.次のコード辺では,2バイト目に 1 を足して f.i の値を表示している.前と出力がどう変わったかに注意してほしい.

  // Modify the second byte
  f.ch.snd = f.ch.snd + 1;
  printf("f as integer is %d (decimal) and 0x%x (hexadecimal)\n", f.i, f.i);

/* 実行結果
f as integer is 305420152 (decimal) and 0x12345778 (hexadecimal)
*/

この例では,共用体の中に構造体メンバが入れ子になっていた.一般に,共用体の中に共用体メンバ,構造体の中に構造体メンバ,構造体の中に共用体メンバが入れ子になるどのパターンも許されている.入れ子になった○○体の宣言をする際には以下のようにまとめて宣言してもよい.

union foo {
  int i;
  struct triplechar {
    char fst;
    char snd;
    char trd;
  } ch;
};

この例のように,あるデータを意図的に別の表現で見たい場合に共用体は便利である.表現の変換までは必要なくても,OCaml のヴァリアントのように異なる型の値を混ぜて扱うためにも使う.例えば,2分探索木のデータを

union tree {
  struct leaf {...};
  struct branch {...};
}

のように表現することが考えられるだろう.この場合,leaf 構造体のオブジェクトを branch 構造体のオブジェクトとして変換したいというより,ひとつの型(union tee)で,ふたつの種類の値を保持している可能性を表現したい,というのが用途である.しかし,OCaml のヴァリアントと違って,共用体の表す値がどのメンバの値であるかを知る術は用意されていない.つまり,union tree 型の変数があっても,それが leaf を表しているか,branch を表しているデータなのかわからないということである.我々は後で2分探索木をプログラムする際には,共用体だけでなく leaf/branch の区別をするための情報も付加してデータを設計することになる.

列挙型

列挙型は,OCaml のヴァリアントの最初の例(furikake)でみたような,いくつかの定数からなる型を定義するための仕組みである.OCaml の

type furikake = Shake | Katsuo | Nori;;

に対応する型は,

enum furikake {
  shake, katsuo, nori
};

のように定義できる.構造体・共用体の時と同じく furikake はタグと呼ばれ enum furikake は型として使える.{} の中で宣言された名前は 列挙定数 (enumeration constant) と呼ばれる.
しかし,OCaml とは違って,列挙型は単なる整数型の別名であり,また列挙定数も整数定数の別名である.(左から順に0,1,2と割り当てられていく.実は enum 宣言の時に katsuo=10 などと,どの定数なのか指定することもできる.しかも恐しいことに,= で指定しない列挙定数の数が 10 に達すると異なる名前に同じ整数が割り当てられる!) 所詮は整数なので,四則演算なども全く問題なく(?)行うことができる.

(以下は C/sample/enum.c より)

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

enum day {
  monday, tuesday, wednesday, thursday, friday, saturday, sunday
};

int main(void) {
  printf("today is %d\n", monday);

  int x = wednesday * saturday;
  printf("today is %d\n", x);
}

[ 前の資料 | 講義ホームページ・トップ | 次の資料 ]


  1. もちろん Java が後発なので,Java が C によく似せて作られているのである.

  2. Java では main メソッドの返値型は void で何も返せない.

  3. Java のオブジェクトは変数のためのメモリ領域とは別の領域に割り当てられていて,変数にはオブジェクトへの参照が格納されているだけなので,引数でオブジェクトを渡しているように見えても参照が渡されているだけでオーバーヘッドは小さい.

  4. & は中置演算子としても使えるが,これはビット毎の論理積を取る演算子で意味が全く違うので注意すること.

  5. C言語には,表記を簡潔にするための演算子が沢山あるが,習熟するまで優先度を間違えやすく,しかも間違えても(コンパイラがエラーを発するわけでもなく)単に別の意味になるだけのことが多く,エラーの温床となる.

  6. これはおそらく正しいと思うのですが,正直自信がないです.隣りあった,サイズが同じメンバの間にギャップが生じることがあるとしたらどういう状況でしょう.(誰に聞いているのか…)

  7. このように,多バイト長データを格納する際にアドレスの小さい方に下位ビットを割り当てる方式をリトル・エンディアンという.逆はビッグ・エンディアン.(「計算機の構成」でやりました…よね?)


Copyright 五十嵐 淳, 2016, 2017