C言語

C言語はシステム記述用のプログラミング言語です。コンパイルすることにより機械語に変換されるため、実行速度が非常に速いことが特徴です。主にOS、デバイスドライバ、プログラミング言語の作成に使われています。

識別子

変数や関数、記号定数の名前のことを識別子と呼びます。識別子は自由に命名できますが、以下の制約があります。

予約語

予約語とはC言語において固有の意味を持つ単語です。そのため、予約語は識別子として用いることができません。C言語の予約語を以下に示します。

コメント

C言語のソースコードにコメント(注釈)を入れることができる。/* から始まり、*/ までがコメントとして扱われる。コメントを入れても、プログラムの実行には何の影響も与えない。ひとつのコメントを複数行に渡って記述してもよい。/**/ の間に、さらに /**/ を入れる「入れ子」はできない。

/* 
  メイン関数
  argc: コマンドライン引数の数
  argv: コマンドライン引数の値
*/
int main(int argc, char **argv) {
    return 0; /* 正常終了 */
}

一行コメント

C99 から導入されたコメント形式で、// から始まり、行末までがコメントとして扱われる。行末でコメントが終了するため、ひとつのコメントを複数行に渡って記述することはできない。

// メイン関数
// argc: コマンドライン引数の数
// argv: コマンドライン引数の値
int main(int argc, char **argv) {
    return 0; // 正常終了
}

変数

変数とは、プログラムで扱われる数値や文字列などの値を一時的に記憶しておく領域のことです。変数を識別するために、ひとつひとつの変数に固有の名前を付けておきます。これを変数名といいます。

変数を使用する前に、どのような変数を使用するか、その変数名や変数に記憶できるデータ型を明らかにしておくことを変数の宣言といいます。

C 言語で変数を宣言する書式を次に示します。

[ 記憶クラス ] データ型 変数名 [ = 初期値 ] ;

記憶クラスおよび初期値は省略することができます。

記憶クラスおよびデータ型が同じ変数は、カンマで区切って複数宣言することができます。

[ 記憶クラス ] データ型 変数名 [ = 初期値 ] , 変数名 [ = 初期値 ] , 変数名 [ = 初期値 ] ;

コンパイル

Intel C++ Compiler

Intel C++ Compilerとは、インテルが開発及び販売しているコンパイラである。実行に必要なライブラリやリンカなどは付属していないため、他のコンパイラと組み合わせて使用する。WindowsではMicrosoft Visual C++、LinuxではGCCが必要である。

auto

自動変数はスタック領域に変数の領域が確保される。自動変数を宣言するには、記憶クラスに auto を指定する。

記憶クラスとは、宣言する変数をどこに記憶するかを指定します。

auto int n;

変数を宣言する際、記憶クラスの指定を省略した場合は自動変数となる。

int n;

自動変数を宣言する際、明示的に初期化を行わなければ初期値は不定となるため、注意が必要である。

static

静的変数は、プログラム実行中に常に同じ場所に配置され、値を保持します。

静的変数を宣言するには、記憶クラスに static を指定します。静的変数を宣言する例を次に示します。

static int n;

静的変数を宣言する際、明示的に初期化を行わなければ、初期値は0になります。

変数の初期値
記憶クラス 初期値
自動変数 不定
静的変数 0

register

変数の領域が確保される物理的な記憶装置は主記憶装置である。主記憶装置よりもCPU内のレジスタの方が高速にアクセスできるため、変数の領域を確保する物理的な記憶装置をレジスタに指定した変数がレジスタ変数である。ただし、可能な限りレジスタに確保するよう要求するだけで、必ずしもレジスタに確保される保障はない。

レジスタ変数を宣言するには、記憶クラスに register を指定する。

register int n;

typedef

typdefは、既存の型に対して新しい名前を付けるときに使用します。

typedef 型名 新しい型名;

新しい型名を付けると、既存の型名のように使用することができます。

typedef unsigned int BYTE;
BYTE data;

この例は、次のように変数を宣言するのと同じです。

unsigned int data;

型名の代りに、構造体や共用体の宣言を指定することもできます。

typedef struct {
  変数宣言のリスト
} 新しい型名;
typedef union {
  変数宣言のリスト
} 新しい型名;

これらは次のように記述することもできる。

struct tag_name {
  変数宣言のリスト
};

typedef struct tag_name 新しい型名;
union tag_name {
  変数宣言のリスト
};

typedef union tag_name 新しい型名;

const

const は定数を表す修飾子である。

例としてstrcpy関数のプロトタイプ宣言を見てみよう。

char *strcpy( char *string1, const char *string2 );

strcpy 関数は文字列 string2string1 へコピーする関数である。

string1 はコピー先の領域であり、コピー後は内容が変化する。strcpy 関数にとっては出力パラメーターであり、変数であるため const 修飾子が付いていない。

一方、string2 はコピー元の領域であり、コピー後も内容は変化しない。strcpy関数にとっては入力パラメーターであり、定数であるため const 修飾子が付いている。

たとえば、次のようなソースコードはコンパイル時にエラーとなる。

void example(const char *dest, char *src) {
  strcpy(dest, src);
}

この場合、char *の引数にconst char *を渡しているためエラーとなる。

なお、const char *の引数にchar *を渡しても問題ない。

volatile

volatile修飾子は最適化を抑制するオブジェクトを宣言します。

volatile int comm = 1;
while (1) if (comm == 0) break;

もし volatile 修飾子が無ければ、Cコンパイラーにより最適化が行われます。 ソースプログラムではループの中で変数が0に等しいかどうか比較していますが、変数が0になることは無いのでCコンパイラーの最適化機能で無駄な比較が削除されることがあります。 このため無条件で無限ループとなります。 volatile 修飾子があればコンパイラーによる最適化が行われないため、(他のプロセスから値を変更するなどして)変数 comm の値が0になるとループを脱出します。 これは、タスク間の同期制御に利用できます。

union

union (共用体)は、同一のデータ領域を複数の異なるデータ型が共用するようにしたものです。

union タグ名 {
  変数宣言のリスト
};

以下の場合には、タグ名を省略することが可能です。

変数宣言のリストの中に共用体を入れて、入れ子構造にすることも可能です。

共用体内の最初のメンバの初期化が可能です。

union x {
  char a[4];
  int  b;
};

union x ux = { 'U', 'N', 'I', 'X' };

関数の定義

type name([ptype parameter[, ptype parameter...]])
{
 // 処理
}
type
関数の戻り値の型
name
関数の名前
ptype
仮引数の型
parameter
仮引数の名前

関数で可変個数の引数を扱う

printf()scanf() では、引数の数や型が不定です。これらの関数のように、引数の個数が可変である関数の作成方法を説明します。

引数の個数が可変の関数を作成するには、ヘッダファイル stdarg.h をインクルードします。

関数の定義は次のようにします。

関数名( 変数名 , ... )

可変個数の引数は "..." で表します。引数を "..." で表すと、コンパイル時に引数の個数をチェックしなくなります。ただし、可変個数でない引数が最低ひとつは必要です。引数が全て可変個数の引数の関数は作成できません。したがって、次のような関数を定義することはできません。

関数名( ... )

最初の引数はどんなものでも構いませんが、通常は可変引数の個数や型を示すものが普通です。

関数内では、可変引数を扱うために va_list 型の変数を宣言します。

va_list 変数名 ;

可変引数の扱いを開始するには、va_start マクロを呼び出します。

void va_start( va_list 変数1 , void 変数2 );

変数1 には、va_list 型の変数を指定します。変数2 には、不定引数になる前の最後の固定引数を指定します。

可変の変数を取り出すには va_arg を使用します。

 va_arg( va_list 変数名 ,  );

可変引数を扱う必要がなくなったら va_end マクロを呼び出します。

void va_end( va_list 変数名 );

可変個数の引数を扱う関数は何らかの方法で引数の数と型を特定しなければなりません。その方法として、次のようなものが考えられます。

型はあらかじめ決まっていて、引数の数を固定引数で渡す

可変個数の引数を扱う関数の例を次に示します。

#include <stdio.h>
#include <stdarg.h>

void print_varargs(int n, ...) {
  va_list pvar;
  char *p;

  va_start(pvar, n);
  for (; n > 0; n--) {
      p = va_arg(pvar, char *);
      printf("%s\n", p);
  }
  va_end(pvar);
}

void main(void) {
  print_varargs(1, "hello");
  print_varargs(2, "hello", "world");
}

上記の例では、型は char * の固定で、引数の個数を第1引数で与えています。

引数の数と型を固定引数で渡す

printf() でも最初の引数は "%d %s" のように引数の型や個数を表していますね。

型はあらかじめ決まっていて、特定の値が現れるまで引数を取り出す

Linuxのシステムコール execl(), execle(), execlp() のように、特定の値が現れるまでパラメータを取り出します。

可変引数の型が char * で、NULL が現れるまで引数を取り出す例を次に示します。

#include <stdio.h>
#include <stdarg.h>

void print_varargs(int dummy, ...) {
  va_list pvar;
  char *p;

  va_start(pvar, dummy);
  while ((p = va_arg(pvar, char *)) != NULL) {
      printf("%s\n", p);
  }
  va_end(pvar);
}

void main(void) {
  print_varargs(0, "hello", NULL);
  print_varargs(0, "hello", "world", NULL);
}

引数の数を渡すのではなく、最後の引数であるという目印として特定の値を決めておく方法です。ただし、最後の引数であることを表す値(上記の例では NULL)を通常値として渡すことはできません。

va_list型を直接扱う

va_list 型の変数を直接扱える関数もあります。

#include <stdio.h>
#include <stdarg.h>

int vprintf(const char *format, va_list ap);

int vfprintf(FILE *stream, const char *format, va_list ap);

int vsprintf(char *s, const char *format, va_list ap);

int vsnprintf(char *s, size_tn, const char *format,  va_list ap);

上記の関数を使えば、引数をひとつひとつ取り出すことなくストリームに出力することができます。

#include <stdio.h>
#include <stdarg.h>

void print_varargs(char *format, ...) {
    va_list pvar;

    va_start(pvar, format);
    vprintf(format, pvar);
    va_end(pvar);
}

void main(void) {
    print_varargs("%s\n", "hello");
    print_varargs("%s %s\n", "hello", "world");
}

関数の呼び出し

name([argument[, argument...]])
name
関数の名前
argument
実引数

printf()

与えられた書式で文字列を生成して、標準出力へ出力する。

修飾子 説明
d 10進符号付き整数
i 10進符号付き整数
u 10進符号無し整数
c 文字
s 文字列
p ポインタ

データ型を型修飾子で指定することもできる。

型修飾子
修飾子 説明
hh char
h short
l long
ll long long
z size_t
L long double

fopen

テキストファイル(example.c)から1行ずつ文字列を読み込んで、標準出力へ出力するプログラムを次に示す。

#include <stdio.h>
#define BUFSIZE 1024
#define FILENAME "example.c"

int main(int argc, char *argv[]) {
  char buf[BUFSIZE];
  FILE *fp = fopen(FILENAME, "r");
  if (fp != NULL) {
    while(fgets(buf, BUFSIZE, fp)) {
      printf("%s", buf);
    }
    fclose(fp);
  } else {
    perror(NULL);
    return 1;
  }
}

OSがファイルを操作するためのシステムコールを用意している。これらをC言語から呼び出すこともできる。これらのシステムコールは低水準入出力関数とも呼ばれ、OSに依存する。

とはいえ、Linux系のOSではほぼ共通で、Microsoft Windowsでさえ高い互換性がある。ただし、それぞれインクルードするヘッダファイルが異なるので、確認が必要である。

低水準入出力関数
関数 説明
open ファイルをオープンする。
read ファイルから文字列を読み込む。
write ファイルへ文字列を書き込む。
close ファイルを閉じる。

システムコールを使った場合の例を次に示す。

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#define BUFSIZE 1024
#define FILENAME "example.c"

int main(int argc, char *argv[]) {
  char buf[BUFSIZE];
  int fd = open(FILENAME, O_RDONLY);
  if (fd != -1) {
    while(read(fd, buf, BUFSIZE)) {
      printf("%s", buf);
    }
    close(fd);
  } else {
    perror(NULL);
    return 1;
  }
}

ここでは低水準入出力関数でファイルを操作する例を示したが、Unix系OSでは汎用的に使える関数である。たとえば、プロセス間通信やネットワーク通信にも利用できる。

strtok()

#include <string.h>

char *strtok(char *s1, const char *s2);

引数s1で指定した文字列の中に引数s2で指定した区切り文字(文字列形式で複数種類の文字を指定可能)が存在すれば、全てNUL文字('\0')に置き換えます。

つまり、1つの文字列を区切り文字によって複数の文字列に分割します。この分割されたそれぞれの文字列をトークンといいます。

例えば、以下のような文字列 s1 があったとします。

A B , C D , E F \0

strtok(s1, ",") を実行すると、文字列 s1 は次のようになります。

A B \0 C D \0 E F \0

また、strtokは最初のトークン ("AB") の先頭の文字 ('A') のポインタを返します。

次に、strtok(NULL, ",") を実行すると、strtokは次のトークン ("CD") の先頭文字 ('C') のポインタを返します。

次にstrtok(NULL, ",") を実行すると、strtokは次のトークン ("EF") の先頭文字 ('E') のポインタを返します。

最後のトークンの先頭文字のポインタを返した後にstrtok(NULL, ",") を実行すると、strtokは戻り値として NULL を返します。

rand()

C言語で乱数を生成するには、次の関数を使用します。

#include <stdlib.h>

int rand(void);

rand() 関数は 0 から RAND_MAX までの乱数を返します(RAND_MAX はヘッダファイル stdlib.h の中で定義されています)。

0 から 99 までの乱数を発生させたいときは、100 で割った余りを使います。

srand()

rand() ライブラリ関数は、あらかじめ決められた乱数表から順番に値を返しているので、完全にランダムな数を生成しているわけではありません。これは、rand() 関数の戻り値を printf() 関数で出力するプログラムを作成すれば確認できます。実行するたびに毎回同じ数値が出力されるはずです。

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

int main(int argc, char *argv[]) {
  int i, r;
  for (i = 0; i < 10; i++) {
    r = rand();
    printf("%i\n", r);
  }
}

上記のプログラムを何度実行しても、常に同じ乱数が出力されます。そこで乱数の初期化が必要です。乱数の初期化とは、乱数表を先頭からではなく別のところから読み出すようにすることでです。乱数の初期化は srand() 関数で行います。

#include <stdlib.h>

void srand(unsigned int seed);

引数 seed には初期値を指定します。通常は time() 関数の戻り値を指定します。プログラムを実行するたびにそのときの時刻は変わるので、プログラムを実行するたびに異なる乱数が発生するようになります。

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

int main(int argc, char *argv[]) {
  int i, r;
  srand(time(NULL));
  for (i = 0; i < 10; i++) {
    r = rand();
    printf("%i\n", r);
  }
}

時刻

現在の時刻を取得するには、time() 関数を使用します。

#include <sys/types.h>
#include <time.h>

time_t time(time_t *tloc);

time_t は sys/types.h の中で typedef により unsigned int または unsigned long として定義されています。

引数 tlocには time_t 型の変数のポインタを指定します。time() 関数により、tloc で指定した time_t 型の変数の中に1970年1月1日0時0分0秒からの経過秒数が格納されます。

戻り値には 1970年1月1日0時0分0秒からの経過秒数が返されます。

tloc に格納される値と time() 関数の戻り値は同じです。引数に NULL を指定することもできます。

time_t t;
t = time(&t);
time_t t;
t = time(NULL); /* 引数にNULL指定 */
time_t t;
time(&t); /* 戻り値は無視 */

時刻を表すデータ型には、time_tstruct tm があります。これらを相互に変換したり、文字列に変換する時刻関数群が用意されています。これらの時刻関数の相関図を次に示します。

時刻関数の相関図
Figure 1. 時刻関数の相関図

たとえば、time() 関数で取得した現在の時刻(time_t型)を localtime() 関数を用いて struct tm 型に変換することができます。time_t 型の時刻は、ctime() 関数を用いて文字列に変換することができます。

#include <time.h>

char *ctime(const time_t *clock);

struct tm *localtime(const time_t *clock);

struct tm *gmtime(const time_t *clock);

char *asctime(const struct tm *tm);

モジュール結合度

モジュール結合度とは、モジュール間の関係性の強さを表す尺度である。モジュール結合度が低いほど他モジュールの変更による影響を受けにくくなるため、保守容易性、拡張性、汎用性及び再利用性が高くなる。モジュール結合度は次表の概念モデルに分類される。内部結合が最も結合度が高く、順に低くなっていく。

モジュール結合度
概念モデル 説明
内部結合 他のモジュール内部を直接参照したり、一部を共有する。
共有結合 共通領域に定義したデータを参照する。
外部結合 外部宣言しているデータを参照する。
制御結合 呼び出すモジュールの制御構造を知っていることを前提として、パラメータに処理のスイッチフラグなどのデータを受け渡す。
スタンプ結合 使用するデータ要素を、構造体やレコードといったデータ構造で受け渡す。
データ結合 使用するデータ要素を単一のパラメータとして受け渡す。

外部結合

外部結合では、外部宣言しているデータを参照する。

sub.c

extern int a;
extern int b;

int add() {
  return(a + b);
}

int sub() {
  return(a - b);
}

main.c

int a = 3;
int b = 2;

int main(int argc, char *argv[]) {
  int x, y;
  x = add();
  y = sub();
}

制御結合

制御結合では、呼び出すモジュールの制御構造を知っていることを前提として、パラメータに処理のスイッチフラグなどのデータを受け渡す。

モジュール結合度が制御結合である例を次に示す。

int calc(int flag, int a, int b) {
  int x = 0;
  switch flag {
    case 0:
      x = a + b;
      break;
    case 1:
      x = a - b;
      break;
  }
  return(x);
}

int main(int argc, char *argv[]) {
  int x, y;
  x = calc(0, 3, 2);
  y = calc(1, 3, 2);
}

スタンプ結合

スタンプ結合では、使用するデータ要素を、構造体やレコードといったデータ構造で受け渡す。

struct complex {
  int a;
  int b;
  int c;
};

int add(struct complex comp) {
  return(comp.a + comp.b);
}

int sub(struct complex comp) {
  return(comp.a - comp.b);
}

int main(int argc, char *argv[]) {
  int x, y;
  struct complex comp;
  comp.a = 3;
  comp.b = 2;
  x = add(comp);
  y = sub(comp);
}

データ結合

データ結合では、使用するデータ要素を単一のパラメータとして受け渡す。

struct complex {
  int a;
  int b;
  int c;
};

int add(int a, int b) {
  return(a + b);
}

int sub(int a, int b) {
  return(a - b);
}

int main(int argc, char *argv[]) {
  int x, y;
  struct complex comp;
  comp.a = 3;
  comp.b = 2;
  x = add(comp.a, comp.b);
  y = sub(comp.a, comp.b);
}

参考文献

Brian Kernighan and Dennis Ritchie (1989) プログラミング言語C

ISO (2018) ISO - ISO/IEC 9899:2018 - Information technology — Programming languages — C