モダンなC

Jens GustedtのModern C読書メモ

レベル0 はじめに

C言語は1972年に登場しました.1989年,1999年,2011年にISOによって標準化されて,その時代にあった言語の拡張が行なわれてきました(それぞれ,C89,C99,C11とよびます).

最初のプログラム

#include <stdlib.h>
#include <stdio.h>

/* ここがプログラムの本体 */
int main(void) {
  double A[5] = {
    [0] = 9.0,
    [1] = 2.9,
    [4] = 3.E+25,
    [3] = .00007,
  };
  for (size_t i = 0; i < 5; ++i) {
    printf("element %zu is %g,\tits square is %g\n", i, A[i], A[i] * A[i]);
  }
  return EXIT_SUCCESS;
}

このプログラムを実行すると,次のような5行を出力します.

> ./getting-started
element 0 is 9,	its square is 81
element 1 is 2.9,	its square is 8.41
element 2 is 0,	its square is 0
element 3 is 7e-05,	its square is 4.9e-09
element 4 is 3e+25,	its square is 9e+50

printfという関数(処理のまとまり)が出力をしています.関数はデータを受け付けることができて,それを引数とよびます."element...ではじまる変わったテキストは,文字列リテラル(データそのもの)とよばれ,ここでは出力の形式,フォーマットを指定するために使われています.%からはじまる変わった単語が3つありますね.これはフォーマット指定子というもので,どのような形式で出力するかを決めています.カンマで区切られて,i,A[i],A[i] * A[i]の3つの値がありますが,3つのフォーマット指定子がそれぞれに対応していて,1つ目の%zuがiの出力を,2つ目の%gがA[i]の出力を,3つ目の%gがA[i] * A[i]の出力を決めています.さらに,バックスラッシュからはじまる\tや\nといった特殊な記号も見られます.

C言語のテキストそのものはマシンには理解ができません.そこで,マシンが理解できる命令,バイナリコードに変換してあげます.この作業をコンパイルといい,コンパイラというプログラムを使います.コンパイラとしては,clang,gccが有名です.

> clang -Wall -lm -o getting-started getting-started.c
> gcc -std=c99 -Wall -lm -o getting-started getting-started.c

ここで,-Wallはおかしな記述があれば,すべて警告を出すという設定,-lmは数学に関する機能を追加する設定(ここでは要らないが後で使用),-oとこれに続くgetting-startedはコンパイル結果の出力名,そして最後にあるのがC言語のテキストのファイルです.不適切な記述のばあい,どのような警告が出るか見てみましょう.ここでは,mainの左にあるintvoidに置き換えてみます.

#include <stdlib.h>
#include <stdio.h>

/* ここがプログラムの本体 */
void main(void) {
  double A[5] = {
    [0] = 9.0,
    [1] = 2.9,
    [4] = 3.E+25,
    [3] = .00007,
  };
  for (size_t i = 0; i < 5; ++i) {
    printf("element %zu is %g,\tits square is %g\n", i, A[i], A[i] * A[i]);
  }
  return EXIT_SUCCESS;
}

clangでコンパイルすると次のように出力されます.

> clang -Wall -lm -o getting-started-badly getting-started-badly.c
getting-started-badly.c:4:1: warning: return type of 'main' is not 'int' [-Wmain-return-type]
void main(void) {
^
getting-started-badly.c:4:1: note: change return type to 'int'
void main(void) {
^~~~
int
getting-started-badly.c:14:3: error: void function 'main' should not return a value [-Wreturn-type]
  return EXIT_SUCCESS;
  ^      ~~~~~~~~~~~~
1 warning and 1 error generated.

ちゃんとintに使ってね,という警告が出ています.警告のないプログラムを書くのが原則です.

基本的な構造

プログラムの文法は大きく分けて,宣言部,定義部,命令文があります.C言語のテキストは,予約語,符号,リテラル,識別子,関数,演算子からできています.予約語は,C言語であらかじめ定められた言葉で,最初のプログラムではincludeintvoiddoubleforreturn予約語です.ここではわかりやすいように予約語は太字にしています.符号には,括弧や句読点があります.括弧は{...},(...),[...],/*...*/,<...>があります.句読点としては,カンマやセミコロンが使われています./*...*/はコメントとよばれ,コンパイラが読み飛ばしてくれるため,説明などを書くことができます.多くのコンパイラでは,C++言語で導入されたコメントの形式(//...から文の終わりまで無視)も理解してくれます.リテラルは,プログラム中に書かれたデータそのものを指す言葉で,0,1,3,5,9.0,2.9,3.E+25,.00007,"element %zu is %g,\tits square is %g\n"が当たります.識別子は,ものとものとを区別するための名前で,A,i,main,printf,size_t,EXIT_SUCCESSが当たります.識別子にも種類があり,A,iは変数(データの入れ物),size_tは型(データの種類)の別名(_tは型,typeであることを指すC言語の慣習です),mainやprintfは関数,EXIT_SUCCESSは定数です.関数は,処理のまとまりですが,int main(void)とそれに続く{...}の部分({...}をブロックといいます)は,宣言部とよばれ,mainの定義が書かれています.main関数はC言語では特別な関数で,プログラムのはじまる場所を示しています.演算子は,=のような初期化,代入に使うもの,<のように比較するもの,変数のインクリメント(1増やすこと)に使う++,*のような掛け算に使うものなどがあります.
すべての識別子には宣言部があります.宣言しないとコンパイラが解釈できません.これが予約語とちがうところです.先ほどのプログラムでは,int main(void);,double A[5];,size_t i;が宣言部です.書き方としては,まずデータの型があり,次に識別名があります.関数のばあい,これに括弧が続きます.Aの後ろの[...]は配列(同じ型が並んだもの)の宣言です.doubleが5つ並んでいることがわかります.5つの項目は数字で指すことができ,それを添字といいます.0からはじまり4まであります.printf,size_t,EXIT_SUCCESSの宣言部はどこでしょうか.実は,別の場所で宣言されており,そのことを最初の1,2行目の#include...で書いています.printfはstdio.hで,size_tとEXIT_SUCCESSはstdlib.hで宣言されています.このようなファイルをヘッダーファイルとよび,コンパイルが見ることのできるどこかに置かれています.宣言には有効範囲,スコープがあります.Aはmainのブロックの中だけで有効です.iはforのブロックの中だけで有効です.mainを囲むブロックはないので,ファイルの中で有効です.
宣言ができたら,次は定義をしてあげます.size_t i = 0;はiを宣言したのち,0という初期値で定義しています.double A[5]...;は,9.0,2.9,0.0,0.00007,3.0E+25という初期値で順に定義されています.配列の初期化で使っている{...}は指示付き初期化子というものです.原則として初期化されていないものは0になります(Aの[2]はないので0になっています).
mainには,for,printf,returnといった命令文があります.繰り返し処理のために,for文が使われます.for文は,for(...)以降のブロック,(...)内のセミコロンで区切られた3つのパーツに分解できます.(...)内の3つのパーツは,左から順に,繰り返しで使う変数の宣言と定義をするところ,繰り返しを行なうかの条件を書いたところ(繰り返しの前にチェックします),繰り返しの後に処理される命令文です.return文は,関数の呼び出し元に終わったことを伝える命令文です.

レベル1 C言語を知る

ここでは,簡単なプログラムをきちんと書けるようになることを目標にしています.そのため,ややこしいキャスト(型の変換)を避けています.また,主に符号なし整数(符号は正負の記号のことで,符号なし整数は正数のこと)を使い,ポインターを段階的に導入していきます.
(すでにC言語を知っている人向けに,)ここでは,修飾子を左に結合させ,識別名と切り離して書きます.

char* name;

char*という型にnameという識別名があるとみます.

char const* const path_name;

最初のconstcharに,*がそれをポインターに,2番目のconstがその左に結びつきます.

制御

C言語では,iffordowhileswitchの5つが制御文とよばれます.似たようなものとして,"cond ? A : B"といった形式で書く三項間演算子や#if-#elseと書くプリプロセッサー条件があります(コンパイルの前にプリプロセス,前処理が行なわれます.前処理を行なうプログラムをプリプロセッサーとよびます).if文を使うと条件による分岐ができます.

if (i > 25) {
  j = i - 25;
}

これはiが25よりも大きければ,jにi - 25の値を代入するという意味です.(...)内の式を条件式といいます.さらに,

if (i > 25) {
  j = i - 25;
} else {
  j = i;
}

という表現もあります.条件式が満たされないばあい,elseのブロックが実行されます.ブロックの代わりに命令文だけにすることもできます.

if (condition) statement0-or-block0
else statement1-or-block1

ちなみに,0が論理値の偽として扱われ,0以外が正です.条件式では,値が等しいことを確認する==,値が等しくないことを確認する!=という演算子も使います.数値が真偽値になるので,

if (i != 0) {
  ...
}

if (i) {
  ...
}

と書けば十分です.また,stdbool.hを読み込めば,true,falseという定数を使うこともできます(trueは1,falseは0です).
繰り返し文として,すでにfor文を説明しました.練習としていくつかの表現を見てみましょう.

for (size_t i = 10; i; --i) {
  something(i);
}
for (size_t i = 0, stop = upper_bound(); i < stop; ++i) {
  something_else(i);
}
for (size_t i = 9; i <= 9; --i) {
  something_else(i);
}

1番目のfor文は,iは10から1までカウントダウンしていきます.条件式がiだけですが,iが0のとき,偽になるので,繰り返しが終わります.2番目のfor文はiと上限値であるstopの2変数を定義しています.3番目のfor文は,条件式がi <= 9なので,繰り返しが終わりそうにないですが,実は,size_tは決して負にならないのでiが9から0までカウントダウンして止まります(これについては後で触れます).繰り返し文には,ほかにもwhile文とdo文があります.while文は,

while (fabs(1.0 - a*x) >= eps) {
  x *= (2.0 - a*x);
}

のような形です(fabsは絶対値を計算する関数でtgmath.hで宣言されています).do文は,while文とよく似ていますが,ブロックを実行した後で,条件をチェックする点がちがいます.

do {
  x *= (2.0 - a*x);
} while (fabs(1.0 - a*x) >= eps);

繰り返し文は,breakcontinueを使うとより便利になります.breakは,その時点で繰り返しをやめ,ブロックから抜け出します.

while (true) {
  double prod = a*x;
  if (fabs(1.0 - prod) < eps)
    break;
  x *= (2.0 - prod);
}

while (true)は慣習的にfor(;;)とも書けます.

for (;;) {
  double prod = a*x;
  if (fabs(1.0 - prod) < eps)
    break;
  x *= (2.0 - prod);
}

for文の条件式が省略されたばあいは,常に正となっているため,先ほどのプログラムと同等です.continueは,その時点のループをスキップし,次のループをはじめます.

for (size_t i = 0; i < max_iterations; ++i) {
  if (x > 1.0) {
    x = 1.0/x;
    continue;
  }
  double prod = a*x;
  if (fabs(1.0 - prod) < eps)
    break;
  x *= (2.0 -prod);
}

ここで使っているfabsはtgmath.hにあるマクロです .複数に分岐する方法もあります.switchif-elseで書くと冗長になるときに使います.

if (arg == 'm') {
  puts("this is a magpie");
} else if (arg == 'r') {
  puts("this is a raven");
} else if (arg == 'j') {
  puts("this is a jay");
} else if (arg == 'c') {
  puts("this is a chough");
} else {
  puts("this is an unkown corvid");
}

これはswitchを使うと次のように書けます.

switch (arg) {
  case 'm': puts("this is a magpie");
            break;
  case 'r': puts("this is a raven");
            break;
  case 'j': puts("this is a jay");
            break;
  case 'c': puts("this is a chough");
            break;
  default: puts("this is an unkown corvid");
}

putsはprintf同様にstdio.hで定義されています.putsは引数の文字列をそのまま出力します.argの値が'm','r','j','c'のそれぞれに一致したときの処理が書かれています.どれにも一致しなかったときは,defaultにある処理をします.break文まで来ると,switchのブロックから抜け出します.switch文はbreakがないと,そのまま次のcaseに移るため,if-elseよりも柔軟な書き方ができます.

switch (count) {
  default: puts("++++ ... +++");
  case 4: puts("++++");
  case 3: puts("+++");
  case 2: puts("++");
  case 1: puts("+");
  case 0:;
} 

演算

ここでは,主にsize_t型を使っていきます.サイズを表す型なので,値が負になることがありません.size_tは0からSIZE_MAXまでの値を取ります.SIZE_MAXの値はプラットフォームによって異なりますが,現代のマシンでは以下が主流です.
2^{32} - 1 = 4294967295
2^{64} - 1 = 18446744073709551615
算術の演算子には,まず+,-,*があります.それぞれ.和,差,積を求める演算子です.

size_t a = 45;
size_t b = 7;
size_t c = (a - b)*2;
size_t d = a - b*2;

また,+や-は一つの項(単項)についても使えます.-bは,bの負の値です.算術の結果がsize_tの範囲にある限り正確ですが,たとえば,巨大な2数の積を計算してSIZE_MAXを超えたばあいは,算術あふれ(算術オーバーフロー)が発生します(これについてはすぐ後で説明します).つぎに,/,%を見てみます.それぞれ,整数での商と余りを求める演算子です.z = a/bならば,a%b = a - z*bです.当然ながら,0で割ることはできません.余りの考え方がわかると,算術あふれしたときの振る舞いがわかりやすくなります.size_tの値は常に(SIZE_MEX + 1)で割った余りなのです.だから,SIZE_MAX + 1は0です.算術あふれしたばあいは,値が周り,0 - 1はSIZE_MAXになります.
C言語には演算と代入を同時にする書き方があります.@を適当な演算子だとすると,

an_object @= some_expression

は,

an_object = (an_object @ (some_expression))

を省略したものです.+=,-=,*=,/=,%=といった書き方が使われます.たとえば,forループでは,

for (size_t i = 0; i < 25; i += 7) {
  ...
}

という書き方もできます.さらに,1増やしたり,1減らしたりすることがよくあるので,そのための表現,インクリメント,デクリメントがあり,それぞれ++i,--iと書きます.インクリメント,デクリメントには後置する表現もありますが,プログラムが読みにくなることもあるので,名前の紹介だけに留めます.
0と1からなるブール値について見てみます.==,!=,<.>は比較演算子といいます.さらに,<=,>=もあり,それぞれ小なりイコール,大なりイコールを表します.比較演算子はfalseまたはtrueを返すのですが,実体は0と1なので,算術に使ったり,配列の添字に使うこともできます.

size_t c = (a < b) + (a == b) + (a > b);
size_t d = (a <= b) + (a >= b) - 1;

この例だと,cは常に1ですし,dはaとbが等しいとき以外は0です.

double largeA[N] = {0};
...
/* largeAに適当な値を代入する処理 */

size_t sign[2] = {0, 0};
for (size_t i = 0; i < N; ++i) {
  sign[(largeA[i] < 1.0)] += 1;
}

sign[0]にはlargeAの要素のうち1.0より等しいか大きいものの個数が,sign[1]には1.0より小さいものの個数が入ります.条件を組み合わせたり,否定するための論理演算子というものがあります.!は否定,&&は論理積(かつ),||は論理和(または)を表します.

double largeA[N] = {0};
...
/* largeAに適当な値を代入する処理 */

size_t isset[2] = {0, 0};
for (size_t i = 0; i < N; ++i) {
  isset[!!largeA[i]] += 1;
}

largeAには適当な値が入っているのですが,最初の!によって,要素が0のときは1,それ以外のときは0になります.さらに,次の!によって,要素が0のときは0,それ以外のときは1となり,isset[0]には要素が0の個数,isset[1]には要素が0以外の個数がカウントされます.また,論理積論理和には短絡回路という性質があります.これは,左側の値で結論が出たら,右側の値は見ないということです.たとえば,isgreat,issmallという関数が数値を出すと考えてください.

if (isgreat(a) && issmall(b))
  ++x;
if (issmall(c) || issmall(d))
  ++y;

もし,isgreat(a)が0なら,issmall(b)は無視され,issmall(c)が0でないなら,issamll(d)は無視されます.
3項間演算子というif文に似た書き方もあります.

size_t size_min(size_t a, size_t b) {
  return (a < b) ? a : b;
}

a < bが正のとき,aがそれ以外でbが返ります.

基本的なデータ

データの種類である型を見てみます.第一に,signedintdoubleといった予約語として定義されている型があります.size_tやboolのようにヘッダーを読み込むことで使える,プログラマーによって作られた型があります.charは符号ありかなしかはプラットフォームよって異なります.また,数値の精度はコンパイラの実装に依ります.

大分類 小分類 名称 別名
整数 符号なし _Bool bool
unsigned char
unsigned short
unsigned int unsigned
unsigned long
unsigned long long
符号あり,なし char
符号あり signed char
signed short short
signed int signed,int
signed long long
signed long long long long
浮動小数 実数 float
double
double long
複素数 float _Complex float complex
double _Complex double complex
long double _Complex long double complex

通常の算術計算ならsize_tで十分です.小さな値で決して負にならないのであれば,unsignedを,負になるのであれば,signedを使います.Cの標準ライブラリでも多数の型が定義されています.time_t,clock_tは時間を扱うときに使う型です.

ヘッダー 意味
size_t stddef.h サイズを表す型
ptrdiff_t stddef.h 差のサイズを表す型
uintmax_t stdint.h 符号なし整数の最大値
intmax_t stdint.h 符号あり整数の最大値
errno_t errono.h intの代わりに返されるエラー
rsize_t stddef.h 境界値のチェック付きサイズ
time_t time.h エポックからの時刻
clock_t time.h プロセッサー時刻

値の書き方にはいろいろあります.123のような10進数,077のような先頭に0をつけた8進数,0xFFFFのような先頭に0xをつけた16進数(大文字小文字は区別しない),1.7E-13のような10進浮動小数点(mEeはm \cdot 10^{e}),0x1.7aP-13のように16進浮動小数点0XhPeはh \cdot 2^{e})もあります.'a'のように,文字は引用符で括ります.C言語ではaという文字に対応する数字として扱われます.文字の中ではバックスラッシュ\は特殊な意味を持ち,たとえば,'\n'は改行を表します."hello"などは文字列とよばれます.長い文字列を書けるように,連続する文字列は1つにくっつけられます.

puts("first line\n"
     "another line\n"
     "first and "
     "second part of the third line");

数値のリテラルに関して,数値のリテラルは負になることはありません.-34や-1.5E-23にある先頭の符号は数値の一部ではなく,負の演算子がくっついたものとみなされます(ややこしいですが,Eの後ろの符号は,浮動小数点の一部とします).整数のリテラルは,末尾にU,L.LLをつけることで,型を指定できます.1Uはunsigned,1Lはsigned long,1ULLはunsigned long longです.0は特殊な数値で,0,0x0,'\0'はどれも同じ値です.浮動小数点は,近似値でしかありません.プラットフォームによっては,0.2と0.2000000000000000111は同値です.浮動小数点の末尾に,f,Fをつければ,floatで,lやLをつければ,long doubleで,何もなければ,doubleです.
どんな変数も初期化をお勧めします.数値はこれまで説明したようにリテラルを書くと記述できます.{}を使って初期化することもできます.

double a = 7.8;
double b = 2* a;
double c = { 7.8 };
double d = { 0 };

その他の型では{}が必須です.

double A[] = {7.8,};
double B[3] = { 2 * A[0], 7, 33, };
double C[] = { [0] = 7.8, [7] = 0, };

配列の初期化の例です.配列の大きさは,明記しなければ,初期化のときに決まります.Cのように,指示付き初期化子を使った方がわかりやすくなります.どう初期化したらいいかわからない場合は,T a = {0}とします.

特定の値などに意味を持たせていると,ちょっと変更しただけで,プログラムはうまく機能しなくなります.

char const*const animal[3] = {
  "raven",  // カラス
  "magpie", // カササギ
  "jay",    // カケス
};
char const*const pronoun[3] = {
  "we",
  "you",
  "they",
};
char const*const ordinal[3] = {
  "first",
  "second",
  "third",
};
...
for (unsigned i = 0; i < 3; ++i)
  printf("Corvid %u is the %s\n", i, animal[i]);
for (unsigned i = 0; i < 3; ++i)
  printf("%s plural pronoun is %s\n", ordinal[i],
    pronoun[i]);

いろんな場所で3という数字を使っていますが,カラス科の動物は他とは独立に増やすこともあるでしょうし,3はそれぞれで独立した意味を持っています.そこで.こういった定数には名前をつけて区別してあげたほうがよさそうです.上のコードでは,constが付いていますが,これは定数ではなく,const修飾子のついた変数といいます.const修飾子がつくとそのオブジェクトを変更することができません.animalでは,配列の要素,文字列そのものを変更しようとするとコンパイラはエラーを出します.constを使うことで,読込専用の変数を作ることができます.さらに,文字列リテラルも読込専用で,char const[]とconst付きの文字の配列なのですが,constが文字列よりも後に導入されたため,互換性を保つため(それ以前のコードが動かなくなることを避けるため),変更からうまく守られていません.
数字を数える(列挙する)ために,列挙型enumがあります.

enum corvid { magpie, raven, jay, corvid_num, };
char const*const animal[corvid_num] = {
  [raven] = "raven",
  [magpie] = "magpie",
  [jay] = "jay",
};
...
for (unsigned i = 0; i < corvid_num; ++i)
  printf("Corvid %u is the %s\n", i, animal[i]);

これでenum corvidという新しい型ができました.値は0から始まるので,ravenが0,magpieが1,jayが2,corvid_numが3です.corvid_numの3は先ほど話題にしていた数字です.カラス科に新しく要素を追加しても,corivd_numがそのまま使えます.

enum corvid { magpie, raven, jay, chough, corvid_num, };
char const*const animal[corvid_num] = {
  [chough] = "chough",
  [raven] = "raven",
  [magpie] = "magpie",
  [jay] = "jay",
};

列挙型のそれぞれの値の型はsigned intです.なので,実は,型名を使うことなく,signed intの定数を宣言するために,使うこともできます.

enum { p0 = 1, p1 = 2*p0, p2 = 2*p1, p3 = 2*p2, };

この定数はコンパイル時に決まっていないといけないため,関数の結果やオブジェクト(変数など)は使えません.

signed const o42 = 42;
enum {
  b42 = 42,       // 42はリテラルなのでOK
  c52 = o42 + 10, // o42はオブジェクトなのでエラー
  b52 = b42 + 10, // b42はオブジェクトでないのでOK
};

Cでは,siged intしか定数を宣言できないため,代わりにテキストの置換であるマクロが使われます.マクロはプリプロセッサーによって処理されます.

#define M_PI 3.14159265358979323846

プログラム上にM_PIという識別名があれば,3.14...のdoubleと置換されます.実は,EXIT_SUCCESS,false,trueなどもマクロです.
2進数での表現について.符号なし整数は簡単です.最大値は,_MAXのようなマクロが用意されています.UINT_MAX,ULONG_MAX,ULLING_MAXです.2進表現は,ビット(0か1)がb_{0}, b_{1}, \cdots, b_{p-1}だとすると,以下のように計算されます.

\displaystyle \sum_{i=0}^{p-1} b_{i}2^{i}

ここで,pは精度とよばれます.

名称 [最小,最大] ヘッダー よくある例
size_t [0, SIZE_MAX] stdint.h [0, 2^{w}-1], w = 32, 64
double [±DBL_MIN, ±DBL_MAX] float.h [\pm 2^{-w-2}, \pm 2^{w}], w = 1024
signed [INT_MIN, INT_MAX] limits.h [-2^{w}, 2^{w}-1], w = 31
unsigned [0, UINT_MAX] limits.h [0, 2^{w}-1], w = 32
bool [false, true] stdbool.h [0,1]
ptrdiff_t [PTRDIFF_MIN, PTRDIFF_MAX] stdint.h [-2^{w},2^{w}-1], w = 31, 63
char [CHAR_MIN, CHAR_MAX] limits.h [0, 2^{w}-1], w = 7, 8
unsigned char [0, UCHAR_MAX] limits.h [0, 255]

符号なし型のビット表現により,ビット集合とビット演算を考えることができます.ビット集合は,ある値でビットb_{i}があれば,そのiの集合は集合V = \{0, \cdots, p - 1\}の部分集合になっていると考えるものです.ビット集合の演算,|,&,^の3つがあり,それぞれ和集合A \cup B,積集合A \cap B,対称差(排他的論理和ともいう)A \Delta Bです.A = 240のビット集合は,{4,5,6,7}で,B = 287のビット集合は,{0,1,2,3,4,8}です.すると以下のような計算になります.

ビット演算 16進 b_{15} \cdots b_{0} 集合演算 集合
V 65535 0xFFFF 1111111111111111 {0,...,15}
A 240 0x00F0 0000000011110000 {4,5,6,7}
~A 65295 0xFF0F 1111111100001111 V\A {0,1,2,3,8,9,10,11,12,13,14,15}
-A 65296 0xFF10 1111111100010000 {4,8,9,10,11,12,13,14,15}
B 287 0x011F 0000000100011111 {0,1,2,3,4,8}
A| B 511 0x01FF 0000000111111111 A \cup B {0,1,2,3,4,5,6,7,8}
A&B 16 0x0010 0000000000010000 A \cap B {4}
A^B 495 0x01EF 0000000111101111 A \Delta B {0,1,2,3,5,6,7,8}

演算の結果を代入するばあい,&=,|=,^=を使います.さらに,2の補数をとる~という演算子もあります.2の補数は,~B = V - Bであり,-B = ~B + 1です.

集合型のデータ

基本的なデータを集めて集合型のデータを作ることができます.集合型のデータには,同じ型を並べた配列,オブジェクトを指し示すポインター,異なる型を組み合わせる構造体,メモリー上の同じ場所に異なる型を重ね合わせる共用体の4種類があります.配列は,宣言の後に[N]と書きます.

double a[16];
signed b[N];

aはdoubleが16個並んだもので,bはsignedがN個並んだものです.さらに,配列の配列を作ることもできます.

double C[M][N];
double (D[M])[N];

は左に結合するという規則のため,CとDはどちらもdouble[N]がM個並んだものです.CはM個並んだもので,何が並んでいるかというとdoubleがN個並んだものという読み方をします.添字を使って値を見ることができます.a[0]はdoubleですし,C[0]は配列で,C[0][0]は(C[0])[0]の意味なので,doubleの要素です.配列はこれまでのような演算ができません.まず,配列は条件式として扱うと常にtrueです.配列には固定長配列と可変長配列があります.可変長配列はC99で導入されたもので初期化できない,関数の外では宣言できないといった制約があります.固定長配列では,に数字を入れずに宣言することもできます(初期化するときに決まります).

double C[] = { [3] = 42.0, [2] = 37.0, };
double D[] = { 22.0, 17.0, 1, 0.5, };

CとDはいずれもdouble[4]の型です.配列の長さは,sizeof演算子を使います.sizeof演算子はオブジェクト(変数や配列などをひっくるめた言い方)のサイズを返すので,割り算をしてあげます.つまり,配列Aの長さは,(sizeof A) / (sizeof A[0])です.文字列は末尾が0になっている文字の配列です."hello"という文字列があるとすると,見えない0という文字が末尾にあるので,配列の長さは6です.

char chough0[] = "chough";
char chough1[] = { "chough" };
char chough2[] = { 'c', 'h', 'o', 'u', 'g', 'h', 0, };
char chough3[7] = { 'c', 'h', 'o', 'u', 'g', 'h', };

これらはすべて同じ文字列の初期化です.いっぽうで,

char chough4[6] = {'c', 'h', 'o', 'u', 'g', 'h', };

は,0で終わっていないため,文字列ではありません.charの配列と文字列を扱う関数が標準ライブラリにあり,string.hを読み込むことで使えます.引数として配列を想定しているものはmemはじまり,文字列を想定しているものはstrはじまりです.memcpy(target, source, len)は,lenの数だけ配列の要素をコピーします.memcmp(s0, s1, len)は,先頭からlenの数だけ比較して,等しければ0を返します.memchr(s, c, len)は,配列sにcという文字が存在するか検索します.strlen(s)は,文字列sの長さを返します.配列の長さではなく,最初に要素が0になるまでの長さです.sは,0で終わっていなければなりません.strcpy(target, source)は,sourceの文字列の長さだけコピーします.これもsourceは0で終わっていなければなりません.strchr(s, c)は,memchrと似ていますが,sは0で終わっていなければなりません.文字列の関数を0で終わっていない配列に対して使うと,どのように処理されるかは決まっていません.いつでも処理が終わらなかったり,誤ったデータの書き換えが発生することがあります.そこで,C11では,文字列の最大長を指定できるstrnlen_s,strcpy_sが追加されました.関数にはプロトタイプというものがあり,ここで紹介した関数のプロトタイプは以下です.

size_t strlen(char const s[static 1]);
char* strcpy(char target[static 1], char const source[static 1]);
signed strcmp(char const s0[static 1],char const s1[static 1]);
char* strchr(const char s[static 1], int c);

ポインターのたくさんの性質は以降の章で説明するので,ここでは最小限のことだけ導入します.ポインターは有効,ナル,不定のいずれかの状態をとります.0によって初期化,代入されたポインターをナルポインターといいます.ポインターはナルのときのみ,falseとして扱われます.不定ポインターがあると,動作も未定義になるため,ポインターは常に初期化するように心がけてください.
配列は同じ型の要素を組み合わせるものでした.異なる型の要素を組み合わせたい場合はどうしたらいいでしょうか.そのために,structで書かれる構造体というものがあります.corvidsのコードを構造体を使って表してみると,以下のようになります.

struct animalStruct {
  const char* jay;
  const char* magpie;
  const char* raven;
  const char* chough;
};
struct animalStruct const animal = {
  .chough = "chough",
  .raven = "raven",
  .magpie = "magpie",
  .jay = "jay",
};

最初の6行で構造体による新しい型struct animalStructが 宣言されています.構造体の宣言のなかに,変数の宣言がありますが,これらはフィールドとよばれるものです.7行目以降は変数の宣言と定義です.フィールドを表すために.ドットを使います.配列だとanimal[chough]ですが,構造体ではanimal.choughです.つぎに,タイムスタンプについて,考えてみます.日時の情報は,年,月,日,時,分,秒とたくさんの情報があります.たとえば,配列を使うこともできるでしょう.

typedef signed calArray[6];

しかしながら,これだと年の情報が0番目に入るのか,5番目に入るのかわかりません.さらに,enumを使えばよいかもしれませんが,C言語ではstructを使う方法が採用されています.

strunct tm {
  int tm_sec;  // 秒 [0, 60]
  int tm_min;  // 分 [0, 59]
  int tm_hour; // 時 [0, 23]
  int tm_mday; // 日 [1, 31]
  int tm_mon;  // 月 [0, 11]
  int tm_year; // 1900年からの年
  int tm_wday; // 日曜からの日数 [0, 6]
  int tm_yday; // 1月からの日数 [0, 365]
  int tm_isdst;// サマータイム(Daylight Saveing Time)のフラグ
};
struct tm today = {
  .tm_year = 2014,
  .tm_mon  = 2,
  .tm_mday = 29,
  .tm_hour = 16,
  .tm_min  = 7,
  .tm_sec  = 5,
};

ある日時を初期化するには,このようにします.構造体のフィールドの値を見るには,同様にドットを使います.

printf("thie year is %d, next year will be %d\n",
       today.tm_year, today.tm_year+1);

初期化しなかったフィールド,tm_wday,tm_yday,tm_isdistは自動的に0になります.そのため,ここではtm_wdayやtm_ydayが本来あるべき値とは異なっています.tm_mdayを正しい値にする関数は次のようになります.

struct tm time_set_yday(struct tm t) {
  // tm_mdaysは1からはじまる
  t.tm_yday += DAYS_BEFORE[t.tm_mon] + t.tm_mday - 1;  
  // うるう年を考慮する
  if ((t.tm_mon > 1) && leapyear(t.tm_year))
    ++t.tm_yday;
  return t;
}

このように,関数に渡されたstructは値がコピーされて渡されており,tは元のオブジェクトとは異なります.そのため,元の変数に反映させるには,代入してあげます.

today = time_set_yday(today);

実際には,ポインター型で対応することが多く,それについては,後ほど説明します.また,代入の演算子=がでてきましたが,これに似た,比較演算子==や!=は構造体対して利用できません.

関数

関数にはプロトタイプ(引数と戻り値の型を含めて宣言のこと)があります.関数に引数がないとき,関数に戻り値がないとき,voidという型を使います.

extern double fbar(double x);
...
double fbar2 = fbar(2)/2;

プロトタイプのおかげで,コンパイラは引数を適切な型に変換できます.たとえば,上の例だと,fbarはdouble型を受け取るのですが,実際に渡されているのはsigned int型です.コンパイラは,2を2.0に変換して処理しています.main関数はプログラムのはじまる点を示すと特別な関数です.プロトタイプは複数あります.

int main(void);
int main(int argc, char* argv[argc+1]);

mainの戻り値は,EXIT_SUCCESSかEXIT_FAILUREを使います.再帰の話は一旦割愛.

Cライブラリ関数

プログラミングでよく使うような関数はライブラリとして提供されています.printf,puts,strtodもライブラリの関数です.ライブラリの関数は,処理に失敗した際,失敗したことがわかるように特徴的な戻り値を返します.たとえば,putsは書き込みに失敗すると,EOF(end-of-fileの意味)を返します.

if (puts("hello world") == EOF) {
  perror("can't output to terminal:");
  exit(EXIT_FAILURE);
}

算術の関数割愛.入出力の関数を見てみます.フォーマットのいらないテキストの出力を考えます.1文字だけ出力するputcharがあります.putcharのプロトタイプで引数の型がintなのは歴史的なものです.

int putchar(int c);
int puts(char const s[static 1]);

ファイルに書き出すこともできます.ストリーム(ファイルなどを抽象化した概念)の型は,FILEです.fputc,fputsがあります.

int fputs(int c, FILE* stream);
int fputs(char const s[static 1], FILE* stream);

FILEの横にある*は,ポインターの型であることを示してます.何もしなくても使えるストリームとして,stdout,stderrがあります.実は,putchar,putsはstdoutに書き出していました.stderrはstdoutと似ていて,端末に出力されますが,stdoutは通常の出力,stderrは緊急時の出力という区別がされています.

int putchar_manually(int c) {
  return fputs(c, stdout);
}
int puts_manually(char const s[static 1]) {
  if (fputs(s[i], stdout) == EOF) return EOF;
  if (fputs('\n', stdout) == EOF) return EOF;
  return 0;
}

いろいろ割愛.フォーマットされていないテキストの入力を考えます.文字列を取得するには,fgetsを使います.端末からの入力のストリームはstdinです.元々は,putsに対応して,getsもあったのですが,安全でないことが多かったため,C11で廃止されました.

int getchar(void);
int fgets(FILE* stream);

文字列を数値に変換するstrtod,strtoul,strtol,strtoumax,strtoimax,strtoull,strtoll,strtold,strtofがあります.

レベル2 深く知る

ポインター

doubleの変数d0とd1を入れ替えるdouble_swapという関数を作ってみます.アドレス演算子&はオブジェクトのアドレスを得るために使います.関数を呼び出すときは,こんな形になります.アドレス演算子によって返される型をポインター型とよびます

double_swap(&d0, &d1);
void double_swap(double* p0, double* p1) {
  double tmpe = *p0;
  *p0 = *p1;
  *p1 = tmp;
}
void double_swap(double p0[static 1], double[static 1]) {
  double tmp = p0[0];
  p0[0] = p1[0];
  p1[0] = tmp;
}

ポインターから,元のオブジェクトを取り出すにはオブジェクト演算子(一般には,ポインターを介して間接的にオブジェクトを扱うので間接参照演算子といいます)*を使います.なので,上のように呼び出されたなら,*p0はd0を,*p1はd1を指しています.不定やナルのポインターを使った場合,ふるまいは未定義なので気をつけないといけません.ポインターを使わなくても配列を使った同様のコードを書くこともできます.

ポインターは配列の先頭の要素のアドレスを指すこともあります.そして,ポインターに対し足し算をすることで,後に続く要素を参照することができます.同じことをいくつかのパターンで書いてみます.

double sum0(size_t len, double const* a) {
  double ret = 0.0;
  for (size_t i = 0; i < len; ++i) {
    ret += *(a + i);
  }
  return ret;
}
double sum1(size_t len, double const* a) {
  double ret = 0.0;
  for (double const* p = a; p < a+len; ++p) {
    ret += *p;
  }
  return ret;
}
double sum2(size_t len, double const* a) {
  double ret = 0.0;
  for (double const*const aStop = a+len; a < aStop; ++a) {
    ret += *a;
  }
  return ret;
}

呼び出すときは,以下のようになります.

double A[7] = {0, 1, 2, 3, 4, 5, 6,};
double s0_7 = sum0(7, &A[0]);
double s1_6 = sum0(6, &A[1]);
double s2_3 = sum0(3, &A[2]);

実はポインターになってしまうと,もはや配列の長さを知ることができません.そこで,配列の長さを渡しています.ポインターと配列は別物なのです.ポインターが配列の最後の要素を通り越してしまうと,未定義のふるまいがおきます.

double A[2] = {0.0, 1.0,};
double* p = &A[0];
printf("element %g\n", *p); // オブジェクトを参照している
++p;                        //  有効なポインター
printf("element %g\n", *p); // オブジェクトを参照している
++p;                        //  有効なポインター
printf("element %g\n", *p); // オブジェクト以外のものを参照している
                            // 未定義なふるまい

ここまでは,ポインターに整数の値を足す方法を見てきましたが,逆に,ポインターの計算から整数の値を得ることもあります.ポインターの差を求めると整数の値が得られます.ただし,この計算は2つのポインターが同じ配列を指しているときしかできません.

double A[4] = {0.0, 1.0, 2.0, -3.0,};
double* p = &A[1];
double* q = &A[3];
assert(p-q == -2);

ポインターの差の結果はintの範囲に収まるかはわからないので,ptrdiff_tというstddef.hで定義された型を使います.

/ ** @brief 時刻の差を計算する
  **
  ** 
  **
  **
  **
  **
  **
  **/
double timespec_diff(struct timespec const* later,
                     struct timespec const* sooner) {
  /* tv_sec は符号なし型かもしれないので,注意する */
  if (later->tv_sec < sooner->tv_sec)
    return -timespec_diff(sooner, later);
  else
    return 
      (later->tv_sec - sooner->tv_sec)
      /* tv_nsec は符号ありとわかっている */
      + (later->tv_nsec - sooner->tv_nsec) * 1E-9;
}

ここで.->という演算子を使っています.これは,左側の要素がstructのフィールドを指しているという意味です.*と.を使って,書き直すこともできて,a->tv_secは(*a).tv_secと同じです.
配列とポインターはとても密接な関係があります.A[i]と*(A+i)は同じものです.Aがポインターだとすると,2番目の表現は説明しましたが,実はA[i]という表現もできます.逆に,Aが配列だとすると,*(A+i)は配列からポインターへの減衰といい,配列に関する情報が失われます.Aを評価(解釈すること)した結果は,&A[0]になります.
やや割愛