C 言語の基礎

2021年3月14日

C 言語とは

C 言語は、UNIX とともに広まったプログラミング言語で、現在主に使われている言語 (Python なども含む) に、良くも悪くも、肯定的にも否定的にも、広く影響を与えている。現在は、主に C++ の一部として利用されていることが多いと思われるが、Linux カーネルや組み込み系などで C 言語そのものとしても使われている。言語の歴史的には、Fortran とは別系統で、ALGOL の系統のようである。

C 言語にはバージョンがあり、C89 (C90)、C99、C11、C17 (C18)、C2x がある。C99 で結構大きな機能追加が行われているが、そのすべてが C++ に採用されているわけではない (コンパイラの拡張機能で対応できる場合もあるが)。一方、// によるコメント機能の追加など、C++ に近づいている部分もある。

コンパイラ

フリーで使えるコンパイラとして gcc や clang がある。ここでは macOS の Homebrew 版 gcc 10.2.0 で動作を確認した。ちなみに、Apple の gcc は実は clang なので注意。

プログラムの構造とコンパイル

プログラムは関数からなる。

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("Hello\n");
    return 0;
}

1 行目は C 言語の記述ではなく、前処理を行うプリプロセッサへの指示 (ディレクティブ) である。下で出力用の printf 関数を使うための準備として、stdio.h という標準のファイルを読み込むように指示している。main が C 言語のプログラムの入口となる関数で、関数の中身 (ブロック) は {...} で表される。printf 関数で文字列を出力させ、プログラムの終了コードとして 0 を返している。

上記の内容を含むファイルを "hello.c" とする。gcc でコンパイルするには次のようにする。

$ gcc -o hello hello.c

実行。

$ ./hello
Hello

誤りを見つけやすくするために、オプションをつけたほうがよい。

$ gcc -std=c99 -Wall --pedantic-error -o hello hello.c

-std は C 言語のバージョン指定、-Wall は警告をすべて出す指定、--pedantic-error は文法を厳密に扱う指定である。

文

文はセミコロン (;) で終わる。

printf("Hello\n");

コメント

/* ... */ や // ... でコメントを入れることができる。

/* 文字列を表示 */
printf("Hello\n");

もともと // ... は C++ のものだったが (厳密には BCPL のものだが)、C99 で正式に C 言語でも使えるようになっている。

変数の定義

変数は、"型 変数1, 変数2,...;" という形で定義する。

int i, j;
float a, b;

もともとブロック内ではブロックの最初でのみ変数を定義できたが、C99 からブロック内のどこででも定義できるようになっている (gcc ではオプション --pedantic-error を指定しないと C89 であってもどこでも定義できてしまうが)。

定義と同時に初期値を指定することもできる。

int i = 0;

変数の値を変更させたくない場合は、const をつける。

const int num = 10;

関数の外で変数を定義した場合は、どの関数からでも見えるようになる。これを大域 (グローバル) 変数という。これに対して、関数や関数内のブロックの中で定義されたものは、そのブロック内でしか見えず、これを局所 (ローカル) 変数という。

初期値を指定しなかった場合、局所変数の値は未定義 (なんでもありうる) である。大域変数は 0 に初期化されるが、まあ当てにしないでちゃんと初期化したほうがよい。

データ型

整数型

整数型は int である。

int i = 0;
printf("%d\n", i);

C 言語ではデータ型のサイズはこれと決まってないが、整数型は一般的に 4 バイト長である。符号なしの unsigned int というものもある。バイト長の短い short int (おそらく 2 バイト) や、バイト長の長い long int (おそらく 8 バイト)、long long int (おそらく 8 バイト) というものもある。数字の表し方 (リテラル) は、unsigned int は "123u"、long int は "123l"、unsigned long int は "123ul" などとする。

変数のサイズを確認するには、sizeof 演算子を使えばよい。

printf("%d\n", sizeof i);

sizeof 演算子は、よく関数のように sizeof(i) などと書かれることがある。

浮動小数点数型

いわゆる実数型であり、単精度 (4 バイト) の float と、倍精度 (8 バイト) の double がある。

float a = 1.23f;
double b = 1.23;
printf("%f %lf\n", a, b);

バイト長の長い long double (16 バイト) というのもある。

long double c = 1.23L;
printf("%Lf\n", c);

複素数型

C99 で複素数型が定義されている。complex.h をインクルードすることで、複素数型 complex と虚数単位 I が使えるようになる。

#include <complex.h>

...

float complex c = 1. + 2.*I;
printf("%f + %fi\n", creal(c), cimag(c));

関数 creal と cimag で実部と虚部を取り出せる。

文字型と文字列

文字型は char である。C 言語の文字列は char の配列で表し、文字列の終わりは終端文字 '\0' で判別する。Fortran の文字列のように、文字列の終端がわからないとかいうことにはならない。

char s[11] = "1234567890";
printf("%s\n", s);

終端文字も文字なので、10 文字の文字列を入れるには少なくとも 11 文字入る配列が必要なことに注意。初期値からサイズを決めてよければ、サイズを指定しなくてもよい。

char s[] = "1234567890";

文字列の長さは string.h をインクルードして使える strlen 関数で得られる。

#include <string.h>

...

printf("%d\n", strlen(s));

C 言語の文字列は、あくまで文字型の配列にすぎないという単純さに起因する使いにくさがある (配列全体のコピーに代入は使えない)。文字列をコピーしたり比較したりするのに、いちいち関数を使ったりループを回したりする必要がある。

構造体を使えば、代入ぐらいは多少マシにはなる。

struct String {
    char str[11];
};

struct String s1, s2;
s1 = (struct String){.str = "omochi"};
s2 = s1;

C 言語では引用符 "..." と '...' は意味が異なり、"..." は文字列だが、'...' は文字である。

char c = 'a';

したがって、文字配列のある要素に値を入れる場合は '...' を用いる必要がある。

s[i] = '\0';

論理値型

論理値は、C 言語ではもともと整数の 0 が偽でそれ以外が真と扱われていたが、C99 ではブール型が定義されている。stdbool.h をインクルードすることで、ブール型 bool と、真偽値の true、false が使えるようになる。

#include <stdbool.h>

...

bool b = true;

列挙型

ユーザーが定義した値をとる列挙型というものがある。

enum { sabatora, chatora, mike } type;
type = sabatora;

enum {...} までが列挙型で、type は変数名である。type にはここで定義した sabatora、chatora、mike を値として入れられる。といっても、実態はただの整数であり、sabatora から 0、1、2 と値が割り当てられている。また、type に整数 3 を入れても怒られたりはしない。割り当てられる整数は変更できる。

enum { sabatora, chatora=2, mike } type;

上の例だと、それぞれの値は 0、2、3 になる。

列挙型には名前をつけられる。

enum NekoType { sabatora, chatora, mike };

これの変数を定義するには、次のようにすればよい。

enum NekoType type;

配列

すでに文字列のところで少し例を見たが、データ型の配列は次のように定義することができる。

float a[3] = {1., 2., 3.}, b[4] = {1., 2., 3., 4.};

これを見ると配列への値の代入は簡単そうに見えるが、これは初期化のときだけで、ふつうは要素ごとに値を入れる必要がある。

float a[3];
a[0] = 1.;
a[1] = 2.;
a[2] = 3.;

配列の各要素には a[i] の形でアクセスする。Fortran と異なり、添字は 0 からはじまる。

もう少し簡単にするなら、C99 の複合リテラルというものを用いて、次のようにはできる。

#include <string.h>

...

float a[3]
memcpy(a, (float []){1., 2., 3.}, sizeof(float)*3);

あんまり簡単でもないかもしれない。

また、配列のコピーに代入は使えない (文字列のところで示した構造体を使う方法はあるが)。

多次元配列は、次のように定義する。

float a[2][2] = {{1., 2.}, {3., 4.}};

メモリ上の配置は a[0][0]、a[0][1]、... と最後の添字が先に動くように並ぶ。Fortran とは異なる。

もし一部の要素しか初期化されない場合は、それ以外の値は 0 になる。したがって、すべての要素を 0 にしたければ、最初の要素に 0 を指定すればよい。

float a[2][2] = {{0.}};

ところで、細かいが、初期化では最後にカンマがあってもよい。

float a[2][2] = {
    {1., 2.},
    {3., 4.},
};

あとで項目が増えた時に追加しやすいようにらしい (これは enum でも同様)。Python で配列や辞書において同様になっているのはこのためである。これは便利だが、JSON では受け付けないので、戸惑ったりする。

C99 以降では、要素を指定して初期化することができる。

float a[2][2] = {
    [0][0] = 1.
}

この場合、a[0][0] に 1 が入って、それ以外は 0 になる。

C 言語の配列のサイズは、以前は定数である必要があったが (const 変数でもだめ)、C99 から変数でもよいことになっている。

構造体

複数の変数をひとまとめにした構造体というものを作ることができる。

#include 
#include 

int main(int argc, char *argv[])
{
    struct {
        char name[11];
        char type[11];
        int age;
    } prof;

    strncpy(prof.name, "omochi", 7);
    strncpy(prof.type, "sabatora", 9);
    prof.age = 4;

    printf("%s\n", prof.name);
    printf("%s\n", prof.type);
    printf("%d\n", prof.age);

    return 0;
}

struct {...} が構造体の定義である。ここでは name、type、age の 3 つの変数がひとまとめにされている。それぞれの変数のことをフィールドという。prof は構造体の変数である。各フィールドにアクセスするには、prof.name、prof.type などと、変数とフィールド名をドットでつなぐ。上の例では strncpy で prof.name、prof.type に文字列をコピーし (代入が使えないのが残念なところ)、prof.age に値を代入している。

構造体の初期化は、つぎのようにできる。

struct {
    char name[11];
    char type[11];
    int age;
} prof = {
    "omochi",
    "sabatora",
    4,
};

C99 以降では、フィールド名を指定した初期化ができる。

struct {
    char name[11];
    char type[11];
    int age;
} prof = {
    .name = "omochi",
    .type = "sabatora",
    .age = 4
};

構造体には名前をつけられる。

struct Profile {
    char name[11];
    char type[11];
    int age;
};

構造体の変数を定義したければ、次のようにすればよい。

struct Profile prof;

構造体同士は単純に代入することができる。

struct Profile prof1, prof2;

strncpy(prof2.name, "annko", 7);
strncpy(prof2.type, "chatora", 8);
prof2.age = 4;

prof1 = prof2;

C99 以降で使える複合リテラルを使えば、次のような代入が可能である。

prof = (struct Profile){
    .name = "omochi",
    .type = "sabatora",
    .age = 4
};

ユーザー定義型

ユーザーが型を定義できるようになっている。

たとえば、構造体の型 Profile を定義する場合、以下のようになる。

typedef struct {
    char name[10];
    char type[10];
    int age;
} Profile;

この型の変数を定義するには、次のようにすればよい。

Profile prof;

列挙型の例だと、次のようになる。

typedef enum { sabatora, chatora, mike } NekoType;
NekoType type;

複雑な型だけでなく、型の別名をつけるのにも使える。たとえば、独自の実数型を定義したければ、次のようにできる。

typedef float real;

こうしておけば、あとで実数を倍精度にしたいなと思えば、float を double に変えるだけでよい。

ポインタ

ポインタというのは、変数を参照するためのものである。たとえば、整数 i のポインタを作るには、次のようにする。

int i = 0;
int *p = &i;

"*p" の "*" はポインタを表す。"&i" というのは、i のメモリのアドレスを表し、p は i のアドレスを保持することで i を参照する。参照時、p はアドレスを表すだけだが、*p は p の指している先、p のアドレスに相当するメモリの場所、つまり i の中身を表す。アセンブリ言語の間接アドレッシングのようなものである。

*p = 1;
printf("%d\n", i);

上のようにすると、i の値が変わる。

だからなんだ、という話だが、これを使えば、データ構造をつないだようなデータ構造、連結リストや木構造などを作ることができる。また、関数に巨大なデータを受け渡す時に、データをコピーする代わりにそのアドレスを渡すような使い方ができる。これと関連して、C 言語の配列はポインタと同一視される。

float a[] = {1., 2., 3.};
float *p = a;

上の例では、ポインタの初期化時の a には "&" が付いていない。つまり、a 自体がポインタと見なされているのである。これにより、関数に配列を渡す場合には、ポインタを想定すればよいことになる。これは "&" をつけるかどうかの話にすぎないように見えるが、多次元配列の話とも絡んでくる。多次元配列とは言っているが、配列 a[i][j] は、a[i] 単体でもアクセスできたりする。配列をポインタと同一視できるということは、(a[i])[j] と見ることができて (実際、この形でもアクセスできる)、2 次元配列はポインタの配列である。

float a[2][2] = {{1., 2.}, {3., 4.}};
float *p = a[0];

上の例では、2 次元配列の 0 行をポインタ p で参照している。ポインタもまた配列とみなせるので、次のようにアクセスすることができる。

printf("%f\n", p[1]); /* a[0][1] */

ポインタが何も指していないことを意味するために、NULL というものがある。

p = NULL;

ポインタはまた、メモリの動的確保で必要になる。

メモリの動的確保

配列のサイズを動的に決めたい場合などは、メモリを動的に確保する必要がある。メモリを確保するには、malloc 関数を使う。たとえば、配列を動的に確保する場合、次のようにする。

#include <stdlib.h>

float *a;
a = malloc(sizeof(float)*10);

上の例では、float のサイズ 10 個分のサイズを指定して、要素 10 個の配列の領域を確保している。領域のアドレスが返ってくるので、それをポインタで受ける。ただし、何らかの問題で領域を確保できなかった場合は、NULL が返ってくる。

領域は書き手の責任で解放する必要があり、それには free 関数を使う。

free(a);

C 言語はかなり書き手任せなところがある。確保した領域外にアクセスしても怒られないし (メモリアクセスエラーになることもあれば、うっかり動いてしまうこともある。後者の場合かなり厄介)、領域を解放し忘れてもそのままである (メモリリークという)。これを C 言語の欠点と見ることもできるし、勝手なことをしないという利点として見ることもできる。いずれにしても、メモリ・ポインタまわりは、雑に扱うとみょうちくりんなバグを作り込みかねないので、慎重さが必要である。

メモリの型はなんでもよくて、構造体でもよい。

Profile *prof;
prof = malloc(sizeof(Profile));

領域サイズを変更することもできる。

a = realloc(a, sizeof(float)*20);

Fortran と異なり、多次元配列の動的確保が意外とやっかいで、2 次元配列がポインタの配列であることを考えれば、ポインタのポインタに対してポインタの配列を確保し、それぞれの要素に対して配列を確保し...などとやるか、一度に全体の領域を確保しておいて、ポインタの配列の要素にそれぞれデータの配列のアドレスをセットしていく...などとすれば、一応作れはするが、色々と処理が面倒なので、たぶん 1 次元配列のまま扱ったほうが早いと思う。m x n の配列なら、a[i][j] は a[i*n + j] とすればよい。動的な多次元配列をごりごり使う必要があるなら、諦めて Fortran を使ったほうがよいかもしれない。

式と演算子

文は式からなるが、式はデータと演算子で構成される。

整数、浮動小数点数については、以下のような演算子が使える。

演算子例意味
+a + b加算
-a - b減算
*a * b乗算
/a / b除算
--a負符号

また、整数については、以下の演算子もある。

演算子例意味
%a % b剰余
++i++, ++iインクリメント (1足す)
--i--, --iデクリメント (1引く)

インクリメント・デクリメントは、前置と後置がある。式には値があるが、i++ の値は i に 1 を足す前の値、++i の値は i に 1 を足したあとの値になる。ただ、わかりにくいので、あまりこれを前提にしたコードは書かないほうがよい。

i += 1 で i = i + 1 を意味するような代入演算子というものもある。また、& や << などのビット演算子というものもある。

Fortran と違って、配列に対して直接使える演算子はない。また、文字列も文字の配列にすぎないので、便利な演算子は使えない (代わりに関数を使う)。

制御構造

ブロック文

{...} でブロックを作る。

{
    float a = 1.2;
    printf("%f\n", a);
}
printf("%f\n", a); // エラー: a は見えない

ブロックは変数のスコープを規定し、その中で定義された変数はその外では参照できない。ブロックは入れ子にすることもできる。

if...else 文

if...else 文で条件分岐を作れる。

if(v == 1){
  console.log("sabatora");
}else if(v == 2){
  console.log("chatora");
}else{
  console.log("mike");
}

if のあとの括弧の中は、条件式になり、値が 0 以外ならその下のブロックの中身が実行され、値が 0 だと実行されない。else if は次の条件式で、else はどれにも当てはまらない場合である。if の下は 1 文ぶら下がるようになっていて、上の例はそこにブロックがぶら下がっている状態になっている。また、else if も else に if 文がぶら下がっている状態である。ブロック内に文が 1 行しかないなら、次のようにも書ける。

if(v == 1)
  console.log("sabatora");
else if(v == 2)
  console.log("chatora");
else
  console.log("mike");

値を分岐させたい場合は、if...else 文の代わりに、三項演算子を使うこともできる。

int i = v == 1 ? 0 : 1;

v が 1 なら c は 0、さもなければ c は 1 になる

条件文では、以下のような比較演算子や論理演算子が使える。

比較演算子

値を比較する演算子として、<、>、<=、>= がある。等しいを表すものは == である (= と間違いやすい)。"等しくない" は != である。

配列を直接比較したりはできない。したがって、(Fortran と違って) 文字の配列にすぎない文字列を直接的に演算子で比較することはできない (代わりに関数を使う)。

論理演算子

条件を複数記述するための論理演算子として、&& (AND)、|| (OR) がある。また、式の頭に ! をつけると否定になる。途中で結果が決まったらその先は評価しない (短絡という)。たとえば、&& で早々に false になったり、|| で早々に true になったりすると、その先は評価されない。関数を呼んでいたりすると、呼ばれないことになる。

たとえば、x が 0 以上でかつ y が 5 以下であるときになにかしたい、という場合は、次のようになる。

if(x >= 0 && y <= 5){
    ...

この場合、そもそも x が 0 より小さかったら、y の式はまったく評価されない。

switch 文

switch 文は、if 文の特殊なやつである。

switch(v){
case 1:
    printf("sabatora\n");
    break;
case 2:
    printf("chatora\n");
    break;
case 3:
    printf("mike\n");
    break;
default:
    printf("none\n");
    break;
}

v の値に応じて、対応する case の下の文が実行される。値は整数以外に文字 (文字列ではない) でもよい。break は必須でなく、書き忘れると下の条件に流れ落ちる (フォールスルーという)。最後の default の break は必要ないのだが、習慣として書かれる。default がいらない場合も習慣として default が書かれる (うっかり通ることがある)。

for ループ

for ループは規定の回数だけ繰り返したりするときに使う。

for(i = 0; i < 10; i++){
    printf("%d\n", i);
}

括弧の中はセミコロンで区切られており、それぞれ初期化、ループ条件、更新となっている。C99 以降では、C++ のように初期化部分でループ変数を定義できる。

for(int i = 0; i < 10; i++){
    printf("%d\n", i);
}

while ループ

while ループは、条件が成り立つあいだ繰り返す。

int i = 0;
while(i < 10){
    printf("%d\n", i);
    i++;
}

do...while ループ

do...while ループは、while ループに似ているが、条件判定が最後にあるため、少なくとも 1 回はループの内容が実行される。

int i = 0;
do{
    printf("%d\n", i);
    i++;
}while(i < 10);

continue と break

continue でループを次に進めることができる。

for(int i = 0; i < 10; i++){
    if(i % 2 == 0) continue;
    printf("%d\n", i);
}

出力は次のようになる。

1
3
5
7
9

break でループを抜けることができる。

for(int i = 0; i < 10; i++){
    if(i == 5) break;
    printf("%d\n", i);
}

出力は次のようになる。

0
1
2
3
4

goto 文

入れ子になったループを一気に抜けたい場合は、goto 文が使える。

for(int i = 0; i < 5; i++){
    for(int j = 0; j < 5; j++){
        if (i == 3) goto loop_end;
        printf("%d %d\n", i, j);
    }
}
loop_end:

"loop_end:" がラベルの定義に当たり、このラベルを指定して goto 文で移動する。また、エラー処理をメインの処理と分けたい場合に goto 文を使うこともできる。

    if(p == NULL) goto allocation_error

    ...

    return 0;

allocation_error:
    fprintf(stderr, "allocation error\n");
    return 1;
}

goto 文は、深いループを抜ける場合か、エラー処理くらいにしか使わない (というか使わないほうがよい)。

プログラムの停止

プログラムを止めるには、exit 関数を使う。

#include <stdlib.h>

...

if(p == NULL) {
    fprintf(stderr, "allocation, error\n");
    exit(1);
}

exit で設定しているのは終了コードである。

これとは別に、abort 関数というものもある。

#include <stdlib.h>

...

abort();

こちらは緊急停止のイメージである。何か本当に致命的なことが起こった場合に使う。

関数

関数

処理のまとまり (手続き) はすべて関数で表される。関数は次のように定義する。

float dbl(float x)
{
    return 2.*x;
}

最初の float は返り値の型で、dbl は関数名、括弧の中は関数の引数の定義である。

次のようにして呼び出す。

int main(int argc, char *argv[])
{
    printf("%f\n", dbl(2.));
    return 0;
}

返り値のない変数 (Fortran でいうところのサブルーチン) は、返り値の型を void とする。

void hello(void)
{
    printf("Hello\n");
}

ここで、引数がないことを示すために、引数の定義のところに void を指定している。これについては、以下のような事情がある。

関数の引数の定義には、次のような古い書き方もある。

float dbl(x)
float x;
{
    return 2.*x;
}

この書き方の場合、引数がない場合も含めて、引数をチェックしないことになっている。どういうことかというと、たとえば、呼び出しで "dbl(2., 3., 4.)" のように余計な引数を渡してもエラーにならない。ということは、hello のように引数のない関数でも "hello(2, 3, 4)" のように無意味に引数を渡すことができて、引数があると思い込むことによる変なバグの原因になりかねない。そこで、引数がない場合は void を書いておいて、引数がほんとうにないことを明示しておく (エラーになるようにする) わけである。

ちなみに、C++ の場合、古い書き方が許されていないので、引数がないときに引数を渡したらちゃんとエラーになる。

プロトタイプ宣言

関数を呼び出すには、呼び出す前に定義されていなければならない。

float dbl(float x)
{
    return 2.*x;
}

int main(int argc, char *argv[])
{
    printf("%f\n", dbl(2.));
    return 0;
}

逆にはできない。

int main(int argc, char *argv[])
{
    printf("%f\n", dbl(2.)); /* エラー: 関数が定義されていない */
    return 0;
}

float dbl(float x)
{
    return 2.*x;
}

ただ、これだと不便なので、前もって予告しておけば、あとで定義してもよいことになっている。

float dbl(float x); /* 予告 */

int main(int argc, char *argv[])
{
    printf("%f\n", dbl(2.));
    return 0;
}

float dbl(float x)
{
    return 2.*x;
}

これをプロトタイプ宣言という。プロトタイプ宣言は、型がわかればよいので、次のように書いてもよい。

float dbl(float);

関数の入れ子

C 言語では、関数の中で関数を定義することはできない。できないのだが、gcc ではできてしまうので注意 (オプション --pedantic-error を指定すればちゃんとエラーになる)。

引数で値を受け取る

C 言語の関数の引数は (Fortran と違って) 値渡しである。呼び出し側の引数 (実引数) の値をコピーして関数側の引数 (仮引数) に渡すので、関数側の引数の値を変更しても呼び出し側は影響を受けない。あえて呼び出し側の変数の値を関数で変更したい場合は、ポインタを用いる。

void set_value(float *v)
{
    *v = 1.;
}

int main(int argc, char *argv[])
{
    float a = 0.;
    set_value(&a);
    printf("%f\n", a);
    return 0;
}

set_value の呼び出しでは変数の値ではなくアドレスを渡す必要があるので、"&" をつけている。set_value の中では、ポインタの参照先にアクセスするために "*" を用いている。

配列や文字列の受け渡し

配列や文字列 (文字の配列) を関数に渡すには、ポインタを用いる。

void set_value(float *a, int n, float v)
{
    for(int i = 0; i < n; i++){
        a[i] = v;
    }
}

void print_value(const float *a, int n)
{
    for(int i = 0; i < n; i++){
        printf("%f ", a[i]);
        if((i+1) % 5 == 0) printf("\n");
        else if(i == n-1) printf("\n");
    }
    printf("\n");
}

int main(int argc, char *argv[])
{
    const int n = 10;
    float a[n];
    set_value(a, n, 1.);
    print_value(a, n);
    return 0;
}

set_value でポインタ引数に対して配列を渡しているが、配列がポインタと解釈されるので、"&" をつける必要はない。print_value のポインタ引数に const がついているのは、配列の値を変更する必要がないためである。ポインタ引数だと常に実引数の値が変更される可能性があるが、const をつけることで、これが入力のためのポインタ引数であると使い手に明示することができる (Fortran の intent(in) のような意味合いである)。

ポインタ引数は、"*a" と書く代わりに "a[]" と書いてもよい。main 関数の引数に "*argv[]" というのがあるが、これは「ポインタの配列だよ」といった意味合いであり、"**argv" (ポインタのポインタ) と書いても動く。

関数の受け渡し

関数を関数に渡すのも、例のごとくポインタでよいのだが、少しややこしい。

float dbl(float x)
{
    return 2.*x;
}

typedef float (*func1)(float);

void map(float a[], int n, func1 f)
{
    for(int i = 0; i < n; i++) a[i] = f(a[i]);
}

int main(int argc, char *argv[])
{
    float a[] = {1., 3., 5.};
    const int n = sizeof(a)/sizeof(float);
    map(a, n, dbl);
    for(int i = 0; i < n; i++) printf("%f ", a[i]);
    printf("\n");
    return 0;
}

上の例は、配列の要素に関数を適用して書き換える関数 map を定義している。関数のポインタの型を typdef で定義している。型を定義せずに書くなら、次のようになる。

void map(float a[], int n, float (*f)(float))

つまり、返り値の型と引数の型をきっちり明示する必要がある。Fortran におけるインターフェイスを明示する方法に比べると、少しわかりにくい。

可変長引数

printf 文のような可変長引数を持つ関数を自分で作ることができる。

#include 

double sum(int n, ...)
{
    va_list valist;
    va_start(valist, n);

    double s = 0.;
    for(int i = 0; i < n; i++){
        s += va_arg(valist, double);
    }

    va_end(valist);

    return s;
}

int main(int argc, char *argv[])
{
    printf("%f\n", sum(5, 1., 2., 3., 4., 5.));
    return 0;
}

sum 関数の "..." が可変長引数を意味する。可変長引数を処理するには、stdarg.h をインクルードして va_list や va_arg などを使う。

局所変数の値の保持

あまりやるものではないが、関数内の局所変数の値を保持しておくことができる。

int count(void)
{
    static int i = 0;
    return ++i;
}

この関数は、呼び出すたびに 1、2、3、... と値を 1 足しながら返す。static がつけられた変数 i は、関数を抜けたあとでも値を保持している。これを静的変数という (Fortran の save に対応する)。一方、ブロックを抜けたらなくなってしまうふつうの変数は、自動変数と呼ばれる。あえて対比するとしたら、以下のようになるだろうか。

void count(void)
{
    static int i = 0;
    auto int j = 0;
    i++;
    j++;
    printf("%d %d\n", i, j);
}

これを実行していくと、i はカウントアップされるが、j はされない。auto は自動変数を意味するキーワードであるが、ふつうは使わない。最近の C++ が別の意味で auto キーワードを使うようになったので、なおさら使わない。

再帰

関数の再帰は (Fortran と違って) 特に何の制限もなく行える。

int fibonacci(int n)
{
    switch(n){
    case 0:
        return 0;
        break;
    case 1:
        return 1;
        break;
    default:
        return fibonacci(n-2) + fibonacci(n-1);
        break;
    }
}

ところで、この場合、return 使っているなら break はいらないのだが、うっかりフォールスルーが怖いので習慣としてこうされる (まあ if 文でもいいのだが)。

その他

C 言語には、デフォルト引数 (オプション引数) とか、キーワード引数とか、余計な機能はない。

ただ、構造体と複合リテラルを利用して、無理やり似たようなことができなくはない。

struct func_keywords {
    int key1;
    int key2;
};

void func(struct func_keywords keywords)
{
    printf("%d %d\n", keywords.key1, keywords.key2);
}

int main(int argc, char *argv[])
{
    func((struct func_keywords){
        .key1 = 1,
        .key2 = 2,
    });

    return 0;
}

うまくやればデフォルト引数も実現できるかもしれない (いらん気もするが)。まあ、どうしても必要なら C++ を使った方が早いだろう。

分割コンパイル

コードを複数のファイルに分けることができる。たとえば、次のようなファイルを用意する。

profile.h

#ifndef PROFILE_H
#define PROFILE_H

typedef struct {
    char name[11];
    char type[11];
    int age;
} Profile;

void Profile_init(Profile*, const char*, const char*, int);
void Profile_set_name(Profile*, const char*);
void Profile_set_type(Profile*, const char*);
void Profile_set_age(Profile*, int);

#endif /* PROFILE_H */

profile.h は、profile.c と対応したインクルード用ファイルで、ヘッダーファイルと呼ばれる。ここで、最初の #ifndef はプリプロセッサディレクティブで、PROFILE_H が定義されていなければその下に行く。その下では #define で PROFILE_H が定義される。#ifndef は、一番下の #endif と対応している。これは何かというと、ヘッダーファイルは一連のコンパイルの過程で複数回インクルードされることがあるが、中のコードを複数回は実行したくないため、1回切りだけ実行するようにしている。これはインクルードガードと呼ばれるテクニックである。

上のコードは、構造体のユーザー定義型 Profile の定義と、それを操作する関数のプロトタイプ宣言からなっている。これは profile.c のコードを使う準備をするためのものである。対応する profile.c は、以下のようになる。

profile.c

#include <string.h>
#include "profile.h"

void Profile_init(Profile *prof, const char *name, const char *type, int age)
{
    Profile_set_name(prof, name);
    Profile_set_type(prof, type);
    Profile_set_age(prof, age);
}

void Profile_set_name(Profile *prof, const char *name)
{
    strncpy(prof->name, name, sizeof(prof->name));
}

void Profile_set_type(Profile *prof, const char *type)
{
    strncpy(prof->type, type, sizeof(prof->type));
}

void Profile_set_age(Profile *prof, int age)
{
    prof->age = age;
}

Profile 型の変数に値をセットする関数を用意している。Profile の変数はポインタで受け取っているが、これは構造体のコピーが一般的には大変だからである。prof->name などは、構造体のポインタで構造体のフィールドにアクセスするための書き方で、(*prof).name などと同じ意味である。

Profile 型の使い方は、次のようになる。

example.c

#include <stdio.h>
#include "profile.h"

int main(int argc, char *argv[])
{
    Profile prof;
    Profile_init(&prof, "omochi", "sabatora", 4);
    printf("%s %s %d\n", prof.name, prof.type, prof.age);
    return 0;
}

Profile 型を使うために profile.h をインクルードしている。ファイルの囲みが <...> だったり "..." だったりしているが、前者はシステムのパスからファイルを探し、後者はローカルからファイルを探す。stdio.h のほうを "..." にしても実は大丈夫なのだが、逆はダメである。

コンパイルは別々に行う。

$ gcc -c profile.c
$ gcc -c example.c
$ gcc -o example example.o profile.o

profile.c/profile.h のセットのようなものをモジュールと呼ぶ。プロトタイプ宣言は、すべてをヘッダーファイルに書く必要はなく、外部に公開したいものだけを書けばよい (残りは .c ファイルに書けばよい)。それでも他のファイルから関数を呼べなくはないという意味で、外からまだ見えてはいる。だが、.c ファイルの中で関数やグローバル変数の前に static をつけると、それらはほんとうに外のファイルから見えなくなる。

static int count;

static void f(void)
{
    ...
}

この仕組みにより、あるデータ型を中心にユーザーの操作に必要な関数だけ提供し、その実装の詳細は隠すといったデータ抽象が可能になる。

ところで、モジュールにより変数を提供したい場合、ヘッダーファイルに書くと問題がある。というのも、構造体の宣言や関数のプロトタイプ宣言であれば、それぞれ見ている人が一度きりしか見ていない限りは問題ない。上の例では、profile.c も example.c も profile.h を見ているという意味で 2 回見ていそうだが、それぞれは 1 回ずつしか見ていないので問題ない。ただ、変数のように実体があるものをヘッダーファイルに書いてしまうと、複数回同じ実体を定義してしまうことになるため問題がある。そういう場合は、モジュールの .c ファイルの中でグローバル変数として定義しておいて、ヘッダーファイルのほうでは extern 宣言というものを書く。

extern int count;

これは、実体はどこかにあるよ、と教えるだけのものである。当然、static 変数は外から見えないのでダメである。

ファイル

ファイルの新規作成

ファイルを開いて書き込むには、次のように書く。

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

...

FILE *fp;

fp = fopen("test", "w");

if(fp == NULL){
    fprintf(stderr, "error: file open error\n");
    exit(1);
}

fprintf(fp , "test\n");

fclose(fp);

fopen 関数でファイル名を指定してファイルを開く。2 つめの引数 "w" は、新規書き込みを意味する。同名のファイルがある場合は、上書きされる。追記する場合は "a" を指定する。fopen は FILE 型のポインタを返す。ファイルを開くのに失敗した場合は、値が NULL になるので、チェックする必要がある。開いたファイルは fclose 関数で閉じる。

fprintf は、ファイル書き込み用の printf であり、FILE 型のポインタを指定する。上記の例では、エラー時に stderr を指定して fprintf を用いているが、これは標準エラー出力 (普通の出力である標準出力とは別の、エラー用の出力) に書き出すためのものである。

ファイルの読み込み

ファイルの読み込みは、次のように書く。

FILE *fp;
char buffer[128];

fp = fopen("test", "r");

if(fp == NULL){
    fprintf(stderr, "error: file open error\n");
    exit(1);
}

fgets(buffer, sizeof(buffer), fp);
printf("%s", buffer);

fclose(fp);

fopen で "r" を指定して、読み込みようにファイルを開いている。文字列の読み込みには、fgets を用いている。

もし、ファイル全体を 1 行ずつ読み込んで処理したい場合は、次のようにすればよい。

while(fgets(buffer, sizeof(buffer), fp)){
    printf("%s", buffer);
}

数値の読み込み

キーボードから数値を読み込む場合、空白区切りであれば、次のように書けばよい。

int a, b;
scanf("%d%d", &a, &b);

scanf には文字列から読み込むバージョンの sscanf があるので、ファイルから読み込む場合はこれを使えばよい。

sscanf(buffer, "%d%d", &a, &b);

固定フォーマット (たとえば 10 文字で数値を表すなど) であれば、文字幅を指定すればよい。

sscanf(buffer, "%10d%10d", &a, &b);

バイナリファイルの読み書き

バイナリファイルを読み書きするときは、ファイルを開くときに "b" を指定する。書き込みは "wb"、読み込みは "rb" である。読み書きには fwrite/fread 関数を用いる。たとえば、整数の配列を書き込むには次のようにする。

FILE *fp;
fp = fopen("test", "wb");

fwrite((int []){1, 2, 3}, sizeof(int), 3, fp);

fclose(fp);

これを読み込むには、次のようにする。

fp = fopen("test", "rb");

int a[3];
fread(a, sizeof(int), 3, fp);

for(int i = 0; i < 3; i++){
    printf("%d\n", a[i]);
}

fclose(fp);

fwrite/fread 関数の引数はそれぞれ、配列のポインタ、要素のサイズ、要素の個数、ファイルポインタである。

書式

printf 系の関数や scanf 系の関数では、書式が用いられる。

int a = 123;
printf("a = %d\n", a);

1 つめの文字列が書式である。ふつうの文字列は、そのままの文字列として表される。"%" がついたものは、特殊なもので、指定したデータに置き換えられる。

整数は、int は "%d"、long int は "%ld" で表す。実数は、float は "%f"、double は "%lf"、long double は "%Lf" で表す。また、123.45 を 1.2345e+02 のような指数表示で表す場合は、"%e" や "%le"、"%Le" を用いる。

"%" の後に表示の幅を指定することができる。整数の幅を 10 文字にしたければ、"%10d" とする。実数の場合は、7 文字で小数点以下は 3 桁にしたければ、"%7.3f" とする。

float a = 123.45;
printf("%7.3f\n", a);

幅の指定を別途行うことができる。数字をアスタリスク "*" で表す。

printf("%*.*f\n", 7, 3, a);

"%s" で文字列を、"%c" で文字を表す。"%" そのものを出力したい場合は "%%" を用いる。

書式というより文字列の話だが、バックスラッシュ "\" も特別な意味を持つ。これはエスケープシーケンスと呼ばれる。最もよく使うのは改行を表す "\n" である。また、二重引用符を出力したい場合は、"\"" を用いる。バックスラッシュ自体は "\\" である。

文字列

C 言語では、文字列は関数で処理する。文字列の関数は string.h で定義されている。

文字の長さ

文字の長さは strlen 関数で得られる。

char s[] = "mike";
printf("%ld\n", strlen(s));

ただし、指定した文字列が終端文字を持たない場合は、どういう結果になるかはわからない。

コピー

文字列をセットしたり、コピーしたりするには、strncpy 関数を用いる。

char s[10];
strncpy(s, "mike", 5);

引数はそれぞれ、コピー先、コピー元、文字数である。4 文字の文字列に対して 5 を指定しているのは、終端文字を含む必要があるためである。次のようにしてもよい。

strncpy(s, "mike", strlen("mike")+1);

ただ、コピー先のサイズがコピー元より小さいとまずいので、コピー先のサイズを指定したほうがよいかもしれない。

strncpy(s, "mike", sizeof(s));

連結

文字列の連結には strncat 関数を用いる。

strncpy(s, "mike", sizeof(s));
strncat(s, "neko", sizeof(s)-strlen(s));

引数はそれぞれ、連結先、コピー元、文字数である。

比較

文字列の比較は strncmp 関数で行う。

printf("%d\n", strncmp(s, "mikeneko", sizeof(s)));
printf("%d\n", strncmp(s, "mikeneka", sizeof(s)));
printf("%d\n", strncmp(s, "mikeneku", sizeof(s)));

等しい場合は 0、辞書的に s のほうが大きい場合は正の整数、小さい場合は負の整数を返す。

文字の探索

文字の探索は strchr 関数で行う。

printf("%s\n", strchr(s, 'k'));

strchr は、指定した文字の位置でのポインタを返す。文字列として見れば、指定した位置以降の文字列ということになる。見つからなければ NULL を返す。

もし末尾から検索したければ、strrchr 関数を使う。

文字列の探索

文字列の探索は strstr 関数で行う。

printf("%s\n", strstr(s, "ne"));

返り値は strchr 同様である。

区切り文字での分割

文字列を区切り文字で分割するには、strtok 関数を用いる。たとえば、カンマ区切りの文字列を分割するには、次のようにする。

char s[] = "aaa,bbb,ccc";
char *p, *q;

q = s;
while((p = strtok(q, ",")) != NULL){
    printf("%s\n", p);
    q = NULL;
}

strtok は、文字列と区切り文字 (文字列) を指定したら、最初の分割された文字列 (この場合だと "aaa") を返す。そのあと、第 1 引数に NULL を渡して再度呼び出すと、次の文字列 (この場合は "bbb") を返すようになっている。最後まで行ったら NULL が返ってくる。

文字列の文字の巡回

文字列の文字を巡回するには、単純には次のようにすればよい。

for(int i = 0; i < strlen(s); i++){
    printf("%c\n", s[i]);
}

次のようにも書ける。

for(int i = 0; s[i] != '\0'; i++){
    printf("%c\n", s[i]);
}

あまりやるものではないが、ポインタを使うこともできる。

for(char *p = s; *p != '\0'; p++){
    printf("%c\n", *p);
}

コマンドライン引数

コマンドライン引数は、main 関数の引数として渡ってきているものである。

int main(int argc, char *argv[])
{
    ...

argc は実行プログラム名を含んだ引数の個数である。したがって、最低 1 であり、引数が 1 つ指定されていたら 2 になる。argv は引数の文字列の配列で、argv[0] は指定した実行プログラム名になる。指定した文字列になるので、カレントディレクトリで "./a.out" を実行したら、そのまま "./a.out" という文字列が得られる。パスを含まない実行プログラム名のみが欲しい場合は、ちょっと雑だが、次のように得られる。

printf("%s\n", strrchr(argv[0], '/')+1);

たとえば、プログラムが引数を 1 つだけ要求する場合、次のように書ける。

int main(int argc, char *argv[])
{
    if(argc < 2){
        printf("usage: example <file>\n");
        return 0;
    }

    char *filename = argv[1];
    
    ...

オプションを処理したい場合は、1 文字目が '-' かどうかを調べて処理する。

for(int i = 1; i < argc; i++){
    if(argv[i][0] == '-'){
        switch(argv[i][1]){
        case 'a':
          ...
        }
    }
}

計算時間の計測

計算時間を計測するには、次のようにする。

#include <time.h>

clock_t start_time = clock();

...

clock_t end_time = clock();

double elapsed_time = (end_time - start_time)/(double)CLOCKS_PER_SEC;
printf("%7.3f s\n", elapsed_time);

たとえば、行列の積の計算速度を測る場合、次のようになる。

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

int main(int argc, char *argv[])
{
    clock_t start, end;

    start = clock();

    const int n = 500;
    double a[n][n];
    double b[n][n];
    double c[n][n];

    srand(time(NULL));

    for(int i = 0; i < n; i++){
        for(int j = 0; j < n; j++){
            a[i][j] = rand();
            b[i][j] = rand();
        }
    }

    for(int i = 0; i < n; i++){
        for(int j = 0; j < n; j++){
            c[i][j] = 0.;
            for(int k = 0; k < n; k++){
                c[i][j] += a[i][k]*b[k][j];
            }
        }
    }

    end = clock();

    printf("%7.3f s\n", ((double)(end - start))/CLOCKS_PER_SEC);

    return 0;
}

ここで、rand 関数 (stdlib.h) は、0 から RAND_MAX の範囲の乱数を与える関数である。srand 関数は、乱数の種 (seed) を設定するもので、seed が同じだと乱数の系列が同じになるため、time 関数を使って設定している。

乱数

上の例で乱数を用いたが、任意の範囲の乱数を得るには、次のような関数を用意すればよい。

#include 

int randi(int rmin, int rmax)
{
    return (int)(rmin + (rand()/(double)RAND_MAX)*(rmax - rmin));
}

double randf(double rmin, double rmax)
{
    return rmin + (rand()/(double)RAND_MAX)*(rmax - rmin);
}

アサーション

関数の実装の上で、必ず成り立っていなければならない条件がある。たとえば、if 文では else には絶対行かないはずとか、switch では default には絶対行かないはずとか、関数の引数は絶対正であるはずとか、ヌルポインタが来るはずがないとか、そういう条件である。「はず」とは言っても、人間なのでうっかり間違いはあるわけで、起こるはずがないことはしばしば起こるものである。そういうときのために、アサーション (assertion) というチェックが行われる。

アサーションとは、主張という意味で、こうなってるはず! という宣言である。たとえば、ある関数の引数の値が 0 以上のはず! と思うなら、次のように書く。

#include <assert.h>

void f(int a)
{
    assert(a >= 0.);
    ...

a の値がうっかり負になっていたら、プログラムは assert の行でメッセージを出して唐突に停止する。これはあくまでバグを発見するためのもので、変な状態で下手に動き続けるくらいなら止めてしまえという考え方である。これはプログラムの書き手に対するメッセージなので、ユーザーに対するものとしてこれを用いてはいけない。

上のようなチェックは、関数の利用条件を定めていることになる。このような考え方を、契約による設計とか、契約プログラミングなどという。

都度チェックを入れていくと、パフォーマンスに影響しそうに見えるが、これらは本番では取り除くこともできる。プリプロセッサで NDEBUG が定義されていたら、assert は無効になる。gcc だとオプション "-DNASSERT" を指定すればよい。

アサーションは C 言語以外でも使われているが、バグも含めて普通のエラーとして対処すべきではないか、というアサーション不要論もある。特にサーバー系だと、突然全体が停止されると困るという事情がある。

プリプロセッサ

#include のように "#" から始まる指示は、C 言語ではなく前処理 (preprocess) を行うプリプロセッサ (preprocessor) のものである。プリプロセッサの指示をプリプロセッサディレクティブ (directive) という。

#define

プリプロセッサでは、#define マクロによってマクロと呼ばれるものを定義できる。単純には、名前だけ定義できる。

#define PROFILE_H

これはインクルードガードに用いたものだが、PROFILE_H という名前を定義している。#ifdef/#ifndef でこれが定義されているかどうかをチェックできる。

また、文字列の置き換えに用いることができる。

#define MAX_PATH 128

これは、MAX_PATH を 128 と置き換えることを定義している。これはプログラムの中で用いることができる。

char path[MAX_PATH];

マクロは一般的には大文字で定義されるが、小文字も使える。従来、マクロは定数を定義するのに用いられてきた (他の言語で定数を大文字で表す習慣があったら、これのせいである)。特に、配列のサイズは定数しかダメだったという理由がある。数値を直接コードに書くと、あとで変更が必要になったときにめんどくさいことになるため、避けられる (直接書かれた数値はマジックコードと呼ばれる)。だが、配列のサイズに変数 (const 含む) は指定できなかったため、マクロが使われてきた。C99 では配列のサイズは変数でもよいことになったので、この状況はなくなっている。最近は、マクロだとコンパイラのチェックが働かないということで、推奨されないようになってきている。

マクロは、単純に文字列を置き換えるだけなので、式を書いてもよい。

#define PI2 (2.*3.14)

ただし、本当に置き換えるだけなので、注意が必要である。ちょっとわざとらしい例を示せば、たとえば次のようなマクロを定義したとする。

#define THREE 1+2

これをプログラムの中で使ったとする。

int a = 2*THREE;

これは 2*1+2 に展開されるので、a の値は望んだ結果である 6 ではなくて、4 になってしまう。そのため、式を書く場合は必ず括弧で囲む習慣がある。

#define THREE (1+2)

これなら 2*(1+2) になるので、望んだ通り 6 になる。

引数を持たせることもできる。

#define MAX(a, b) (a >= b ? a : b)

これは a, b どちらか大きなほうを返す関数のような働きをする。普通の関数でよい気もするが、これの利点は、コードに埋め込まれるため呼び出しのオーバーヘッドがないことと、そもそも型に依存しないこと (整数でも実数でもよい) ということが挙げられる。いわゆるインライン関数やジェネリック関数のようなものを定義できる (まあ inline は C 言語でも使えるが)。ただ、あまりやりすぎるとわかりにくくなることがある。

もっと変なこともできる (しないほうがよいが)。

#define MAX_IMP(T) T max_##T(T a, T b) \
    { \
        if(a >= b){ \
            return a; \
        }else{ \
            return b; \
        } \
    }

MAX_IMP(int)
MAX_IMP(float)

#define MAX(T, a, b) max_##T(a, b)

MAX_IMP では、型を変数として関数の実装を書いている (マクロは 1 行で定義する必要があるため、バックスラッシュで継続行にしている)。いわゆるジェネリックプログラミング、C++ でいうテンプレートのようなものである。その下で、MAX_IMP に型を指定してそれぞれの型の関数を定義している。MAX は呼び出し用のマクロである。ここで、##T と書いてあるのは、T を展開した文字列を意味する。したがって、MAX_IMP(int) では int max_int(int a, int b) という関数が定義されることになる。

使い方は、たとえば次のようになる。

int a = 1, b = 2;
printf("%d\n", MAX(int, a, b));

int c = 1., d = 2.;
printf("%f\n", MAX(float, c, d));

マクロを無効化したい場合は、#undef が使える。

#if ... #endif

#if ... #endif で条件分岐ができる。#if のあとに 0 以外を指定すると、#if ... #endif の間のコードが有効になり、0 を指定すると無効になる。簡単な使い方は、直接 0 を指定してコメントの代わりにするものである。

#if 0

...

#endif

終わりがわかりにくいので、コメントを付けたりする。

#ifndef PROFILE_H
#define PROFILE_H

...

#endif /* PROFILE_H */

else にあたるものは #else、else if に当たるものは #elif である。

#if の他に、マクロが定義されていたら真になる #ifdef、マクロが定義されていなかったら真になる #ifndef (インクルードガードで用いた) がある。これらを用いたものを条件付きコンパイルと呼ぶが、たとえば、あるライブラリを使うかどうかを切り替えたり、プラットフォーム依存のコードをプラットフォームごとに切り替えたりといったことに用いられる。

assert は NDEBUG を定義すると無効になるが、これは次のようになっている。

#ifdef NDEBUG
#define assert(e)       ((void)0)
#else
#define assert(e)  \
    ((void) ((e) ? ((void)0) : __assert (#e, __FILE__, __LINE__)))
#define __assert(e, file, line) \
    ((void)printf ("%s:%u: failed assertion `%s'\n", file, line, e), abort())
#endif

assert 自体がマクロになっていて、定義を切り替えている。

定義されたマクロ

上の assert の例にもあるが、ファイル名と行番号がそれぞれ __FILE__、__LINE__ で参照できる。また、__func__ で関数名を参照できる (これだけ小文字なのは C99 で追加されたため)。