C言語:全ページ版

* 以下の各項が1ページ(約3k程)となっているスライド版もあります。内容やリンクに変化はありません。

目次

[C言語目次へ]

[この項を1頁で読む (1)]

0. C言語入門

 C言語の学習には,少なくとも二つの側面があります。一つは文法の正しい理解,もう一つはプログラミング技術の習得です。

[C言語目次へ]

[この項を1頁で読む (2)]

1. プログラミングと実行

 先ずは,幾つかの用語の確認です。

[C言語目次へ]

[この項を1頁で読む (3)]

1.1. 作業手順

 C言語では次の手順で作業します。

  1. プログラムの作成
  2. コンパイル
  3. 実行

ソースファイルの作成には,エディタを使えば良いです。UNIX なら emacs,vi,pico,或いは cat < EOF > など,Mac OS X ならば Terminal.app から UNIX と同じように作成できるだけでなく,TextEdit(但し,標準テキスト shfit+cmd+T で)や BBEdit などの GUI エディタも。また Windows ならばノートパッドやメモ帳など。テキストファイルを編集できるものであれば,何でも良い訳です。

[C言語目次へ]

[この項を1頁で読む (4)]

1.2. コンパイルと実行方法

 プログラムが一つのソースファイルから構成される場合のコンパイルと実行方法です。

* プログラムが複数のソースファイルから構成される場合については後述

Q a.out の由来
C言語の作者 Dennis M. Ritchie によると the output of the assembler の意。アセンブラ(assembler)とは,CPUが理解できる機械語に翻訳するもので,元のファイルはニーモニック(mnemonics)と呼ばれる略語で記述するアセンブリ言語で書く。アセンブリ言語は CPUによって異なるのに対し,C言語はコンパイラがアセンブリ言語に翻訳するため移植性が高い。
 C言語のソースファイルをアセンブリ言語に翻訳させるには,
   % cc -S xxx.c
とすれば良い。すると,拡張子 .s が付いたアセンブリ言語ファイルができる。これから実行ファイルを作るには,
   % cc xxx.s
とすれば良い。出来上がるのが a.out である。
 実行ファイルからアセンブリを見ることもできる。(「逆アセンブル」と呼ばれている。)コマンド gdb で開き,disassemble main と打てば良い。
   % gdb a.out
   ...
   (gdb) disassemble main

[C言語目次へ]

[この項を1頁で読む (5)]

1.3. コンパイル・オプション

 コンパイル時に様々なオプションを付すことができます。以下は gcc におけるオプションの一部です。

* 詳しくは,gcc のマニュアルを。Win32 用 Borland の場合,オプションを付さなくとも warning が出る。

-Wunused未使用の変数をチェック
-Wreturn-type関数の返却値と型の整合性
[例]main関数の終了が exit関数,或いは return文でないと次の warning が出る。
  control reaches end of non-void function
-Wformatprintfscanf の変換指定子のチェック
-Wimplict関数や引数の暗黙的宣言のチェック
-Wuninitialiezed 変数の初期化チェック(但し,最適化用オプション利用時)
-Wall上記オプションを含む warning用オプションのすべて。
どのオプションが含まれるかはコンパイラのマニュアルを。
-O処理の最適化。最適化については,他のオプション -O2 などあり。
どのような最適化が行われるかはコンパイラのマニュアルを。
-o実行ファイル名を指定。
-cオブジェクトファイルの生成。
-S翻訳処理のみ。アセンブリ言語ファイルが生成される。
-ansi-std=c89ANSI C (C89)/ISO C90 準拠でのコンパイル。
-std=c99C99 準拠でのコンパイル。但し,gcc 3.3.1 の段階で,
 "GCC has incomplete support for this standard version."
となっている。
 C.f. gcc の C99 への対応状況
-pedanticCXX に従った診断。-std=cXX と一緒に使う。

文法チェックをしたいのであれば,-Wall オプションを使用すれば良いでしょう。エラーだけでなく warning(注意)が出なくなるまでプログラムを書き直します。

% cc -O -Wall xxx.c

* lint という文法チェッカもあります。但し,ISO による規格がある現在,コンパイラはその規格に従って作られることが要求されるため,コンパイラが出す warning を無視しないことです。

[注意]文法上正しくとも,初期目的を果たさないという意味での「バグ」がない,或いはバッファ・オーバーフローやメモリ・リークが起こらない訳ではありません。これらは,プログラムを作成する側の問題として残ります。

[C言語目次へ]

[この項を1頁で読む (6)]

2. 文法の枠組み

 プログラムに書き込む字句には次の種類があります。

文法は,これらの組合せで構成され,仕様書(C99)にそれらすべてが500ページ以上にもわたり掲載されています。当然,それらのすべてを紹介することはできません。しかしながら,ソースファイルに書き込む文法は,次の6種類に分類できます。

[C言語目次へ]

[この項を1頁で読む (7)]

2.1. 字句要素(Lexical Elements)

 プログラムに書き込む各字句(トークン)の概略です。[C99, 6.4]

次を一つのソースファイルに書き込み,そのソースファイルのみから構成されるプログラムを考えましょう。ソースファイル名は任意ですが,拡張子 .c を必ず付します。

/* Example 2.1 */

#include <stdio.h>

int main(void)
{
       const int a = 0;
       register int m = 12;
       int n = 65;

       printf("a is %d; m is %d; n is %d.\n", a, m, n);

       return 0;
}

このプログラムでの各字句です。

・キーワードint void const register return
・識別子main a m n printf
・文字列リテラル "a is %d; m is %d; n is %d.\n"
・定数0 12 65 文字列リテラル内の各文字
・区切り子/ * # <> () {} = ; ,

しかしながら,文法自体は,これらの組合せでできています。

[C言語目次へ]

[この項を1頁で読む (8)]

2.2. 文法の構成要素

 コンパイラが実際に解釈する文法は,次の通りです。

/* Example 2.1 */コメント
(注釈文)
/**/ で挟まれた部分はコメントと解釈。C99 では C++ のように,// から改行まででも良いとなっている。[C99, 6.4.9]
#include <stdio.h>プリプロセッサ 
(前処理)
区切り子 # で始まるのは「プリプロセッサ」と解釈。ここでは,printf関数が定義されているライブラリ stdio.h の読み込み。printf関数を使うため。
int main(void) { }関数の定義main関数の定義
const int a = 0;宣言変数 a の定義
register int m = 12;宣言変数 m の定義
int n = 65;宣言変数 n の定義
printf("a is %d; m is %d; n is %d.\n", a, m, n); 文(式文)printf関数の呼出し文。関数の呼出しは「式」。printf関数は第1変数に指定した文字列リテラル内の文字列を標準出力。第2変数の a の値が文字列リテラル内の1つ目の %d,第3変数 m の値が2つ目の %d,第4変数の n の値が3つ目の %d 部分に出力される。%d を「変換指定子」(conversion specifier)といい,この場合十進法(digit)での出力を意味する。
return 0;文(return文)main関数の返却。main関数の従属変数の値(戻り値,返り値,返却値)を 0 に設定。

この例より理解できることは,字句それぞれが独立しているというよりは,字句の組合せで文法が成り立っていることです。そして,ソースファイルに書き込む文法は,次から構成されていることが理解できます。

先ずは,宣言の概略からです。

[C言語目次へ]

[この項を1頁で読む (9)]

3. 宣言(Declarations)

 「宣言」とは識別子の解釈,属性を指定するもので,必ずセミコロン ; で閉じます。Example 2.1 で見た宣言

const int a = 0;
register int m = 12;
int n = 65;

では,変数 amn の属性を指定しています。キーワード(予約語)const はデータの扱いを読み込み専用(read-only)とする「型修飾子」,int は扱うデータの種類が整数(integer)であることを意味する「型指定子」,register はデータの記憶域領域を CPU のレジスタにするようコンパイラに求める「記憶域クラス指定子」です(*)。また,識別子 am などを「宣言子」といい,= の右辺側を「初期化子」と言います。このように,宣言は次から構成されています。

これらの内,型指定子は必須です。[C99, 6.7.2, 2] (また,型指定子にタグ,列挙体メンバが含まれない場合には宣言子が必要ですが,これについては後程再述します。)

* register は元来,CPUのレジスタへの割り当てを意味しますが,仕様(C99)では「可能な限り速くアクセスすることを要求」となっており,実際の処理は処理系に任せています。[C99, 6.7.1, 4]

[C言語目次へ]

[この項を1頁で読む (10)]

3.1. 型指定子(Type Specifilers)

 キーワード(予約語)と型指定子の関係です。

型指定子
(Type Specifiers)
[C99, 6.7.2]
データ
の種類
メモリ
消費量
範囲(桁数)[C99, 5.2.4.2]
最小値最大値
void値を持たない。宣言子に制限あり。
char整数unsigned charsigned char のいずれかで,それは処理系に任されている。[C99, 6.2.5, 15]
unsigned char整数1バイト0255
signed char整数1バイト-127127
short
short int
整数2バイト-3276732767
int整数C99 の例では short と同じ。
long
long int
整数4バイト-21474836472147483647
long long
long long int
整数8バイト-92233720368547758079223372036854775807
float少数4バイト1.17546435E-383.40282347E+38
double少数8バイト2.2250738585072014E-3081.7976931348622157E+308
struct タグ {struct-宣言}
struct タグ
構造体タグ型。宣言子はオプション。
enum タグ {列挙体メンバ}
enum タグ
列挙体タグ型。宣言子はオプション。enum タグ {列挙体メンバ} ではタグもオプション。
* 「タグ」は,構造体や列挙体を特定化するための識別子。

char が1バイトであることを除き,メモリ消費量と範囲は処理系依存です。また,ブール型データを扱う _Bool や複素数を扱う型,浮動小数点については long double もあります。unsigned charsigned char を除く整数型については unsigned を前に付すとメモリ消費量は変わらずに非負領域が扱えるようになります。例えば,

unsigned short i;

とすると,i0 から 65535 までの整数が扱えます。

[C言語目次へ]

[この項を1頁で読む (11)]

3.2. 宣言子(Declarators)

 型指定子にタグ,列挙体メンバを含まない場合には,次のいずれかの宣言子を付す必要があります。[C99, 6.7, 2]

識別子自体も又,宣言子です。ポインタ宣言子はポインタ型,配列宣言子は配列型,関数宣言子は関数型のデータを扱うための宣言子です。

[C言語目次へ]

[この項を1頁で読む (12)]

3.3. 型(Type)の分類

3.1 で見た型指定子と 3.2 で見た宣言子を組み合わせると,扱えるデータは,つぎの「型(type)」に分類されます[C11 §6.2.5]。

オブジェクト型
Object Type
スカラ型
Scalar Type
算術型
Arithmetic
Type
実数型
Real Type
整数型
Integer Type
標準整数型
Standard
Integer Type
符号付き
Standard
Signed
Integer
Type
signed char
short
short int
int
long
long int
long longlong long int
符号無し
Standard
Unsigned
Integer
Type
_Bool
unsigned char
unsigned short
unsinged int
unsigned long
unsigned long long
拡張整数型
Extended
Integer Type
符号付きImplementation Defined
符号なし
列挙型(Enumerated Type)
浮動小数点型
Floating Type
float
double
long double
複素数型
Complex
Type
float _Complex
double _Complex
long double _Complex
ポインタ型(Pointer Type)
集成体型
Aggregate Type
配列型(Array Type)
構造体型(Structure Type)
関数型(Function Type)

[ノート]
(1) 標準符号付き整数型と拡張符号付き整数型をまとめて「符号付き整数型(Signed Integer Type)」と呼びます[C11 §6.2.5, 4]。
(2) 同様に,標準符号無し整数型と拡張符号無し整数型をまとめて「符号無し整数型(Unsigned Integer Type)」と呼びます[C11 §6.2.5, 6]。
(3) 「複素数型」のサポートは,処理系に任されています[C11 §6.2.5, 11]。
(4) charsigned charunsigned char で「文字型(Character Type)」を構成します[C11 §6.2.5, 15]。
(5) 上記表内の「ポインタ型」「配列型」「構造体型」「関数型」と,上記表に掲載していない「共用体型(Union Type)」と「アトミック型(Atomic Type)」の6つが「派生型(Derived Type)」を構成します[C11 §6.2.5, 20]。「派生型」とは,「オブジェクト型」と「関数型」から派生する型であり,「関数型」自体が「派生型」である理由は,返却値の型とパラメータの型によって特徴付けられるためで,「返却値の型より派生する」と考えます。
(6) 「アトミック型」のサポートは,処理系に任されています[C11 §6.2.5, 20]。

オブジェクト型は,コンパイル(翻訳)段階において,確保するメモリ領域の大きさ「サイズ(size)」の確定有無によって,つぎに分類されます。

後に見る演算子には,「スカラ型」に使えるものもあれば,「算術型」や「実数型」,さらには「整数型」に限定されたものもあります。演算子の利用制限にあたっては,上記表を確認すると良いでしょう。

[C言語目次へ]

[この項を1頁で読む (13)]

4. 実数型オブジェクト

 ソースファイルに書き込む字句は,

のまとりとなることを確認し,宣言における文法の概略とデータの種類を見ました。ここでは,その内,実数型オブジェクトの初期化を中心に,その宣言方法を具体的に見て行くこととします。

[C言語目次へ]

[この項を1頁で読む (14)]

4.1. char型変数

 char型は文字(character)を扱うために用意された型です。

/* Example 4.1 */

#include <stdio.h>

int main(void)
{
       char a = 'A';
       char b = '\101';
       char c = '\x41';

       printf("(a) %c (%d, %o, %x)\n", a, a, a, a);
       printf("(b) %c (%d, %o, %x)\n", b, b, b, b);
       printf("(c) %c (%d, %o, %x)\n", c, c, c, c);

       return 0;
}

'文字' は「文字定数」と呼ばれるものです(*)。変数 a は文字 A を意味する値に,変数 b は8進数で 101,そして変数 c は16進数で 41 を意味する値に初期化されます。'\101''\x41' も文字定数です。また,printf関数の変換指定子ですが,%c はASCII文字,%o は8進数,そして,%x は16進数での出力を意味します。

 実行結果です。

(a) A (65, 101, 41)
(b) A (65, 101, 41)
(c) A (65, 101, 41)

ASCIIコード表を確認すると,すべての変数が文字 A を意味していることが理解できます。

 文字を扱うための型が char ですが,「3.1. 型指定子」で見たように,char型は整数上の変数です。整数と文字は,ASCIIコード表に従って1対1で対応しています。上の例の場合,コンピュータは,printf において %c を指定されたら ASCIIコード65の文字である A を出力し,%d を指定されたら 10進法の65を出力するわけです。ASCIIコード表を見ると,ASCII文字は 127 までの整数に割り振られています。したがって,char型で扱える整数は,signed char-127 から 127 となります。

*「文字定数」については「付録 文字定数,整数定数,浮動小数点定数」を参照。

[C言語目次へ]

[この項を1頁で読む (15)]

4.2. shortintlong型変数

 型によって扱える整数の範囲が異なります。型指定子で見た通り,扱える整数の範囲は,

char, short (short int), int, long (long int), long long (long long int)

の順に大きくなります。(扱える整数の範囲の確認方法は,後程見ます。)

 これらの整数型変数の初期化方法は,基本的には,すべて同じです。ただ,char型は ASCII文字を扱うことを想定しているので,「4.1. char型変数」では文字定数による初期化を使いました。ここでは,整数という数を扱うことを前提にした初期化方法を見ます。

/* Example 4.2 */

#include <stdio.h>

int main(void)
{
       int a = 65, b = 0101, c = 0x41;
       long l = 65L, m = 0101L, n = 0x41L;

       printf("\t%%d\t%%o\t%%x\n");
       printf("(a)\t%d\t%o\t%x\n", a, a, a);
       printf("(b)\t%d\t%o\t%x\n", b, b, b);
       printf("(c)\t%d\t%o\t%x\n", c, c, c);

       printf("\t%%ld\t%%lo\t%%lx\n");
       printf("(l)\t%ld\t%lo\t%lx\n", l, l, l);
       printf("(m)\t%ld\t%lo\t%lx\n", m, m, m);
       printf("(n)\t%ld\t%lo\t%lx\n", n, n, n);

       return 0;
}

6501010x41 は「整数定数」と呼ばれているものです(*)。接尾子 Llong型という意味です。65 は10進数の65,0101 は8進数で101,0x41 は16進数で41という意味です。また,printf関数内の変換指定子ですが,long型変数の出力には,%d%odo の前に l を付します。また,\t は水平タブです。これは文字定数に使えるエスケープ文字です。

 実行結果です。

        %d      %o      %x
(a)     65      101     41
(b)     65      101     41
(c)     65      101     41
        %ld     %lo     %lx
(l)     65      101     41
(m)     65      101     41
(n)     65      101     41

すべての変数が整数 65 を意味しています。

*「整数定数」については「付録 文字定数,整数定数,浮動小数点定数」を参照。

[C言語目次へ]

[この項を1頁で読む (16)]

4.3. floatdoublelong double型変数

 これまで整数型変数を扱ってきました。ここでは,少数が扱える floatdoublelong double型変数の初期化方法を見ます。

/* Example 4.3 */

#include <stdio.h>

int main(void)
{
       float x = 3.14F;
       double y = 1E-5;
       long double z = 2.345L;

       printf("(x) %f, %E", x, x);
       printf("(y) %f, %E", y, y);
       printf("(z) %Lf, %LE", z, z);

       return 0;
}

3.14F2.345L1E-5 は「浮動小数点定数」(floating constant)と呼ばれる定数です。変換指定子 %f は小数点表示,%E は指数表示での出力を意味します。但し,long double の場合には L を付す必要があります。[C99, 7.19.6.1, 7] 実行すると,

(x) 3.140000, 3.140000E+00
(y) 0.000010, 1.000000E-05
(z) 2.345000, 2.345000E+00

という出力を得ます。

 計算の精度や桁数については,doublefloat の倍のメモリ領域を使うため,double の方が高い訳ですが,そのベースとなる浮動小数点の計算式は次の通りです。[C99, 5.2.4.2.2, 2]

sign beΣk=1pfkb-k

二進法の場合,b = 2fk01 をとります。精度と桁数は ep で決まることとなります。次は,be などの確認です。

/* Example 4.4 */

#include <stdio.h>
#include <float.h>

int main(void)
{
       printf("\tb\te\t\tp\tMIN\t\tMAX\n");
       printf("\t\tMIN\tMAX\n");
       printf("float\t%d\t%d\t%d\t%d\t%E\t%E\n",
              FLT_RADIX, FLT_MIN_EXP, FLT_MAX_EXP, FLT_MANT_DIG, FLT_MIN, FLT_MAX);
       printf("double\t%d\t%d\t%d\t%d\t%E\t%E\n",
              FLT_RADIX, DBL_MIN_EXP, DBL_MAX_EXP, DBL_MANT_DIG, DBL_MIN, DBL_MAX);

       return 0;
}

出力結果(奥山研究室の場合)

        b       e               p       MIN             MAX
                MIN     MAX
float   2       -125    128     24      1.175494E-38    3.402823E+38
double  2       -1021   1024    53      2.225074E-308   1.797693E+308

FLT_RADIXFLT_MIN_EXP などの定数(マクロ)はライブラリ float.h で定義されています。float の場合,e が -125 から 128 をとり,p が 24 です。一方,double は,e が -1021 から 1024 をとり,p が 53 です。桁数は float38double308 です。

[C言語目次へ]

[この項を1頁で読む (17)]

4.4. 列挙体 enum

 型指定子の中で整数型に属するものとして「列挙型」(enumerated type)があります。

enum(列挙体,enumeration)

enum タグ {const1, const2,...};
enum タグ 変数名 = 列挙定数;

{ } 内の「列挙体メンバ」const1const2 は,列挙定数(enumeration constant)と呼ばれ,整数定数や文字定数と同様に int型の定数として扱われます。[C99, 6.4.4, 1 & 6.4.4.3] const1 には 0 が入り,以下 12 といった具合に連続した整数が入ります。タグ は列挙体を特定化するために付す識別子です。変数 変数名 の型は enum タグ(列挙体タグ型)ですが,データとしては列挙定数のすべての値が収まる整数型(例えば,int型)で,処理系依存です。[C99, 6.7.2.2, 4]

/* Example 4.5 */

#include <stdio.h>

int main(void)
{
       enum weekday {sun, mon, tue, wed, thu, fri, sat};
       enum weekday day = tue;
       printf("weekday = %d\n", day);
       return 0;
}

実行結果です。

weekday = 2

タグを外した宣言も可能です。

enum {const1, const2,...} 変数名 = 列挙定数;

この場合,変数名enum {const1, const2,...}型です。

/* Example 4.6 */

#include <stdio.h>

int main(void)
{
       enum {sun, mon, tue, wed, thu, fri, sat} weekday = tue;
       printf("weekday = %d\n", weekday);
       return 0;
}

ただ単に,整数の定数の作成にも利用できます。

enum {const1, const2,...};
/* Example 4.7 */

#include <stdio.h>

int main(void)
{
       enum {a, b, c, d = 5, e, f};
       printf("c + e = %d\n", c + e);
       return 0;
}

実行結果です。

c + e = 8

a0b1c2 が入ります。d には 5 が入り,以下 67 と入ります。

[C言語目次へ]

[この項を1頁で読む (18)]

4.5. 変数のサイズ:size_t sizeof演算子

 型によって扱える数値の範囲が異なるのは,値を記憶するために消費するメモリ領域の大きさが型によって違うためです。メモリの1単位は1byte です。8-bit で1byte,1bit には0か1かが入るので,1byte のメモリ領域を使う変数の場合,28=256個の数字が扱えます。signed char型変数が -127 から 127 の255個の整数を扱えるのは,変数一つに付き1byte のメモリ領域を消費するからです。

 消費するメモリ領域の大きさをサイズ(size)と言います。変数のサイズを求める演算子が sizeof演算子です。

sizeof 演算子

sizeof(変数), sizeof 変数

sizeof演算子は非負整数(バイト数)を返します。(したがって,オブジェクト型に制限されます。[C99, 6.5.3.4])その戻り値の型は size_t で,intlong などの整数型とは区別しています。size_t はメモリのバイト数を表す型です。(その定義はヘッダ stddef.h 内にあります。[C99, 6.5.3.4, 4]) 但し,ある変数を size_t 型として宣言するには以下のいずれかの標準ライブラリを読み込む必要があります。

stddef.hstdio.hstdlib.hstring.htime.hwchar.h

/* Example 4.8 */

#include <stdio.h>

int main(void)
{
       printf("Type\t\tByte(s)\tBit(s)\n");
       printf("char\t\t%zd\t%zd\n", sizeof(char), 8*sizeof(char));
       printf("short\t\t%zd\t%zd\n", sizeof(short), 8*sizeof(short));
       printf("int\t\t%zd\t%zd\n", sizeof(int), 8*sizeof(int));
       printf("long\t\t%zd\t%zd\n", sizeof(long), 8*sizeof(long));
       printf("long long\t%zd\t%zd\n", sizeof(long long), 8*sizeof(long long));
       printf("float\t\t%zd\t%zd\n", sizeof(float), 8*sizeof(float));
       printf("double\t\t%zd\t%zd\n", sizeof(double), 8*sizeof(double));
       printf("long double\t%zd\t%zd\n", sizeof(long double), 8*sizeof(long double));
       return 0;
}

* 仕様(C99)では,size_t型の出力には z を付すとなっている。[C99, 7.19.6.1]
[例]%zd%zo

実行結果(奥山研究室の場合)

Type            Byte(s) Bit(s)
char            1       8
short           2       16
int             4       32
long            4       32
long long       8       64
float           4       32
double          8       64
long double     8       64

char型のサイズが1byte,short が 16-bit,intlong が同じで 32-bit になっています。また,float が 32-bit,double はその倍の 64-bit です。但し,プログラムの移植性を考えた場合,int が16-bit,long が32-bit と考えた方が良いでしょう。

 扱える桁数も上のサイズに応じて決まります。整数の桁数を確認してみましょう。

/* Example 4.9 */

#include <stdio.h>
#include <limits.h>

int main(void)
{
       printf("TYPE\t\tMIN\t\t\tMAX\n");
       printf("char\t\t%d\t\t\t%d\n", CHAR_MIN, CHAR_MAX);
       printf("short\t\t%d\t\t\t%d\n", SHRT_MIN, SHRT_MAX);
       printf("int\t\t%d\t\t%d\n", INT_MIN, INT_MAX);
       printf("long\t\t%ld\t\t%ld\n", LONG_MIN, LONG_MAX);
       printf("long long\t%lld\t%lld\n", LLONG_MIN, LLONG_MAX);
       return 0;
}

出力結果(奥山研究室の場合)

TYPE            MIN                     MAX
char            -128                    127
short           -32768                  32767
int             -2147483648             2147483647
long            -2147483648             2147483647
long long       -9223372036854775808    9223372036854775807

CHAR_MINCHAR_MAX などは,ヘッダ limits.h で定義されている値です。また,%lldlllong long 用の変換指定子です。「3.1. 型指定子」で見た最小値(MIN)と異なります。扱える最大値,最小値は処理系依存な訳です。

[C言語目次へ]

[この項を1頁で読む (19)]

付録 文字定数,整数定数,浮動小数点定数

 コンパイラが認識する字句の一つである「定数」の内,列挙定数空ポインタ定数を除くものです。

■文字定数(Character Constants)[C99, 6.4.4.4]

'文字'int型。

文字 に指定できるのは,シングル・クォーテーション ',バックスラッシュ \,そして改行を除く文字です。バックスラッシュはエスケープに使うため,使用できません。次は,エスケープ文字です。

\aビープ音(警報)
\bバック・スペース(後退)
\fフィード(書式送り)
\n改行
\rキャリッジ・リターン(復帰)
\t水平タブ
\v垂直タブ
\'シングル・クォーテーション '
\"ダブル・クォーテーション "
\??
\\バックスラッシュ \
\8進数八進法での ASCIIコード番号 8進数 の文字。unsigned char型の範囲内であること。
\x16進数 十六進法での ASCIIコード番号 16進数 の文字。unsigned char型の範囲内であること。

■整数定数(Integer Constants)[C99, 6.4.4.1]

十進法0 以外から始まる数字。
八進法0 から始まる数字。使えるのは 0 から 7 までの数字。
十六進法0x から始まる数字。10は a (A),11は b (B),以下15の f (F) まで。

整数定数は,次の表の中でその値が収まる最初の型となる。

接尾語(suffix)十進法八進法,十六進法
なしint
long
long long
int
unsigned int
long
unsigned long
long long
unsigned long long
l 又は Llong
long long
long
unsigned long
long long
unsigned long long
ll 又は LL long longlong long
unsigned long long
u 又は Uunsigned int
unsigned long
unsigned long long
uluLUlUL unsigned long
unsigned long long
ulluLLUllULL unsigned long long

■浮動小数点定数(Floating Constants)[C99, 6.4.4.2]

整数.整数E+整数suffix

少数部 整数.整数 を十六進法で示す場合には,EP にする。EP は小文字 ep でも良い。また,+ 部分は - も使える。suffix を付さない場合はdouble型,F 又は f を付した場合は float型,L 又は l を付した場合は long double型となる。

[C言語目次へ]

[この項を1頁で読む (20)]

5. 単純代入演算子 =

 初期化子が付された宣言を初期化(Initialization)といいます。また,変数の値を記憶させる宣言をその変数の定義と言います。[C99, 6.7, 5] 初期化は変数の値をメモリ領域に記憶させるので定義です。これまで見た宣言はすべて初期化であり,したがって,定義となります。

 これに対し,初期化せずにとりあえず変数を宣言し,後に値を記憶させることもできます。

/* Example 5.1 */

#include <stdio.h>

int main(void)
{
       int m;
       m = 1;
       printf("m = %d\n", m);
       return 0;
}

宣言

int m;

には初期化子がありません。この段階でメモリには,ゴミのようなものが記憶されています。初期化されていないので,m を参照するとその「ゴミ」を使用することになります。次の

m = 1;

m = 1 は「代入式」(Assignment Expression)と呼ばれる「式」(Expression)です。この式が実行されると,メモリに 1 が記憶されます。そして,セミコロン ; で「文」(Statement)となり,文法上完結します。

* m = 1;宣言ではなく,である。

 一般に,メモリに記憶されている値が変化するなど,プログラムの実行環境を変化させることを副作用(Side Effect)と呼びます。演算子の多くは副作用を起こします。文 m = 1;= は「単純代入演算子」と呼ばれ,副作用を起こす代表的な演算子です。

■単純代入演算子(Simple Assignment Operator)
変数 = の値を 変数 に代入。
/* Example 5.2 */

#include <stdio.h>

int main(void)
{
       int m = 1, n, p;

       p = (n = m);
       printf("m = %d, n = %d, p = %d\n", m, n, p);

       return 0;
}

実行結果です。

m = 1, n = 1, p = 1

n = mnm の値が代入されます。(n = m)n = m の値,すなわち,1 です。代入式自体,値を持ちます。それは左辺側の変数の値です。[C99, 6.5.16, 3] p = (n = m) でそれを p に代入します。

Q 左辺値(lvalue)
元来,代入演算子の左辺側にとれるものを「左辺値」と呼んでいた。仕様(C99)では,オブジェクト型,あるいは void 以外の不完全型の式を左辺値という。[C99, 6.3.2.1, 1] 代入演算子の左辺側にとれる変数は,仕様(C99)に基づくと,変更可能な左辺値(modifiable lvalue)と呼ばれているものになる。[C99, 6.15.6, 2] これには不完全型は含まれない。[C99, 6.3.2.1, 1] 後に「9. 配列」で見る
  char str[] = "language";
str[] は不完全型であるので,初期化(宣言)に使う = は代入演算子ではないことが理解できる。ちなみに,式の値を「右辺値」(rvalue)という。

[C言語目次へ]

[この項を1頁で読む (21)]

6. 型変換(Conversion)

 代入式自体,値をもつことを見ました。ということは,代入式自体,型をもつはずです。何故ならば,C言語では,扱うデータのすべてがそれ固有の型をもつからです。仕様(C99)によると,

となっています。そこで疑問になるのが,異なる型の間の代入は,どのような変換が行われるかです。というのは「4.5. 変数のサイズ」で見たように,例えば,unsigned char255 までなのに対し,unsigned long int の最大値は 4294967295 ですから,unsigned long int型変数を unsigned char型へ代入すれば,値が収まらないことが起こりえます。

 実は,異なる型の間の変換は,予め決まったルールに従います。

■型変換(Conversion)

これらのルールは,単純代入演算子だけでなく,後に見る「キャスト演算子」も含め,ある型が他の型に変換されるときに適用されます。

/* Example 6.1 */

#include <stdio.h>

int main(void)
{
       int n = 500;
       unsigned char c = 0;

       c = n;
       printf("%u (%o, %x)\n", c, c, c);

       return 0;
}

この例は,int型変数を unsigned char型へ変換したものです。変換指定子 %uunsigned 用です。実行結果は次の通りです。

244 (364, f4)

unsigned char型の最大値は 255 なのでそれに 1 を足した 256 を 500 から引いた値となっています。

[C言語目次へ]

[この項を1頁で読む (22)]

7. 式(Expressions)

 演算子が作用する変数をオペランド(Operand)と言います。1つのオペランドをとる演算子を「単項演算子」(Unary Operator),2つのオペランドをとる演算子を「二項演算子」(Binary Operator),3つのオペランドをとる演算子を「三項演算子」と言います。これまで見た演算子の内,sizeof演算子は単項演算子,単純代入演算子は二項演算子です。

 値の計算を指定したり,オブジェクトや関数自体を示したり,副作用を起こす演算子とオペランドの組を(Expression)と言います。sizeof(int) も式,また,代入式 m = p も式,また

int x = 99;

x が他で使用される場合,x 自体式です。

 代入式で見たように,式自体,値を持ち,したがって,式は型を持つことになります。更に,代入式で見たことは,とったオペランドの型が異なる場合,右辺側の値を左辺側の型に変換してしまうことです。ここで見る演算子には,そのような「暗黙の型変換」(Implicit Conversion)を行うものを含みます。

[C言語目次へ]

[この項を1頁で読む (23)]

7.1. 整数拡張と通常の算術型変換

 ここで見る演算子の一部は,各オペランドに対し次のいずれかの型変換を施すため,式の値の型が変換後の型となります。

■整数拡張(Integer Promotions)[C99, 6.3.1.4, 2]

int型で元の型(例えば,char型)のすべての値を表すことができる場合,int型に変換。できない場合は,unsigned int に変換。

■通常の算術型変換(Usual Arithmetic Conversion)[C99, 6.3.1.8]

[C言語目次へ]

[この項を1頁で読む (24)]

7.2. 四則演算子 +, -, *, /, %

 四則演算については,多くのプログラミング言語で使用されているものと同じです。ただ,文字を ASCIIコード番号で扱っているため,文字についても四則演算ができます。

■加減演算子(Additive Operators)[C99, 6.5.6]
+加算
-減算
■乗除演算子(Multiplicative Operators) [C99, 6.5.5]
* 乗算
/ 除算:第2変数が 0 の場合,未定義の動作となる。整数型同士の場合,少数部は切り捨てられる。
% 剰余:第2変数が 0 の場合,未定義の動作となる。

整数型同士で a/b が計算可能な場合,

(a/b)*b + a%b

a に等しくなります。(確認してみください。)

/* Example 7.1 */

#include <stdio.h>

int main(void)
{
       unsigned char c = 't';

       printf("%c (%d)\n", c, c);

       c = c + 'A' - 'a';
       printf("%c (%d)\n", c, c);

       return 0;
}

実行結果です。

t (116)
T (84)

丸括弧内は,ASCIIコード番号です。c + 'A' - 'a' は小文字から大文字への変換式です。(但し,変数 c の値が小文字の場合。付言すると,標準ライブラリ ctype.h に小文字を大文字に変換する関数 toupper と,大文字を小文字に変換する関数 tolower があります。また,ctype.h には小文字であることをテストする islower関数,大文字であることをテストする isupper関数などもあります。)

 式 c + 'A' - 'a'int型,そして,unsigned char型へ変換されます。(この例の場合,unsigned charint とすれば,型変換は一切行われない。)

/* Example 7.2 */

#include <stdio.h>

int main(void)
{
       long long l = 111111111LL, m = 0LL;

       m = l*l;
       printf("%lld\n", m);
       m = m % 11;
       printf("%lld\n", m);

       return 0;
}

実行結果です。

12345678987654321
1
[C言語目次へ]

[この項を1頁で読む (25)]

7.3. キャスト演算子 ()

 すべての式が値をもち,したがって,型を持ちます。その型を意図的に異なる型に変換することができます。これを「明示的な型変換」(Explicit Conversion)と言います。明示的な型変換を行うための演算子が「キャスト演算子」です。この演算子を付したそのときに限り,型が指定した型に変換されます。

■キャスト演算子 (Cast Operator)

()
/* Example 7.3 */

#include <stdio.h>

int main(void)
{
       int n = 500;
       printf("%u\n", (unsigned char)n);
       return 0;
}

実行すると 244 と出力されます。「6. 型変換」で見た型変換が適用され,そこでの Example 6.1 と同じ結果となります。

/* Example 7.4 */

#include <stdio.h>

int main(void)
{
       int a = 10, b = 3;
       float x;

       x = a/b;
       printf("(1) x = %f\n", x);

       x = (float)a/b;
       printf("(2) x = %f\n", x);

       return 0;
}

実行結果です。

(1) x = 3.000000
(2) x = 3.333333

変数 abint 型なので割算をしても少数部が切り捨てられます。それが出力 (1) です。これに対し,変数 afloat 型に型変換することで,少数部まで計算されたのが出力 (2) です。「7.2. 四則演算子」で見たように,割算には「通常の算術型変換」が適用されるため,式 a/b において a あるいは b の一方が float 型であれば,すべてが float 型に変換され,少数部も計算されることとなります。

[C言語目次へ]

[この項を1頁で読む (26)]

7.4. 関係演算子 <<=>=>;等価演算子 ==!=

 大小関係,等号の判定です。数学でいう「二項関係」です。真の場合には 1,偽の場合には 0int型で返します。この意味で,関係演算子や等価演算子はブール型(Boolean)です。

■関係演算子(Relational Operators)
x > yx > y
x >= yx ≥ y
x < yx < y
x <= yx ≤ y
■等価演算子(Equality Operators)
x == yx = y
x != yx ≠ y
/* Example 7.5 */

#include <stdio.h>

int main(void)
{
       int x = 2, y = 3;
       int d = 0;

       printf("x = %d, y = %d\n", x, y);
       d = (x > y);
       printf("x > y\t%d\n", d);
       d = (x >= y);
       printf("x >= y\t%d\n", d);
       d = (x < y);
       printf("x < y\t%d\n", d);
       d = (x <= y);
       printf("x <= y\t%d\n", d);
       d = (x == y);
       printf("x == y\t%d\n", d);
       d = (x != y);
       printf("x != y\t%d\n", d);

       return 0;
}

実行結果です。

x = 2, y = 3
x > y   0
x >= y  0
x < y   1
x <= y  1
x == y  0
x != y  1
[C言語目次へ]

[この項を1頁で読む (27)]

7.5. 論理演算子 &&||!

 論理演算子は,関係演算子同様,真の場合には 1,偽の場合には 0int型で返します。

■論理演算子(Logical Operators)
x&&y 論理AND演算子
(Logical AND Operator)
論理積 (AND,∧):一方が 0 の場合には 0 を,両方とも 0 以外ならば 1 を返す。
x||y論理OR演算子
(Logical OR Operator)
論理和 (OR,∨):一方が 0 以外ならば 1 を,両方とも 0 ならば 0 を返す。
!x論理否定演算子
(Logical Negation Operator)
否定 (NOT,¬):これは (x == 0) と同じ。x0 ならば 1 を,0 以外ならば 0 を返す。

論理積については,第1オペランドが 0 ならば,第2オペランドを評価しません。論理和については,第1オペランドが 0 に等しくなければ,第2オペランドを評価しません。

/* Example 7.6 */

#include <stdio.h>

int main(void)
{
       int x = 0, y = 1;

       printf("(1) !(!x && !y)\t= %d\n", !(!x && !y));
       printf("(2) x||y\t= %d\n", x||y);

       return 0;
}

実行結果です。

(1) !(!x && !y) = 1
(2) x||y        = 1

(1) と (2) は同じになります。「ド・モルガンの法則」です。

/* Example 7.7 */

#include <stdio.h>

int main(void)
{
       char c = '\0';
       printf("!'\\0' = %d\n", !c);
       return 0;
}

実行結果です。

!'\0' = 1

ナル文字 '\0' は ASCIIコード番号 0 の文字であり,それは 0 に等しいです。

[C言語目次へ]

[この項を1頁で読む (28)]

7.6. 増分演算子 ++,減分演算子 --

 B言語から受け継いだ省略形の一つに,和算,減算に対するものがあります。

■増分演算子(インクリメント・オペレータ, Increment Operator)
++xx を参照前に 1 を加算
x++x を参照後に 1 を加算
■減分演算子(デクリメント・オペレータ, Decrement Operator)
--xx を参照前に 1 を減算
x--x を参照後に 1 を減算

変更可能な左辺値(Modifiable Lvalue)とは,配列型とconst-修飾型(いずれも後出)を除くオブジェクト型の式を指します。「4. 実数型オブジェクト」で見た変数は,すべて「変更可能な左辺値」です。

/* Example 7.8 */

#include <stdio.h>

int main(void)
{
       int m = 0, n = 5;

       m = n++;
       printf("m = %d, n = %d\n", m, n);

       n = 5;
       m = ++n;
       printf("m = %d, n = %d\n", m, n);

       return 0;
}

この例は,++xx++ の差を示したものです。実行結果は次の通りです。

m = 5, n = 6
m = 6, n = 6

n++ は,m に代入後1増えます。一方,++nn が1増えた後に m に代入されます。

 増分,減分演算子は,for文などの繰り返し文に使われます。例えば,

int i;
for(i = 0; i < 10; i++) 

の場合,繰り返しの初期条件が i = 0,繰り返す条件が i < 10 であり,これが真の場合(7.4. 関係演算子を参照) を実行し,その後,条件変更 i++ を行います。条件変更 i++++ は増分演算子です。

/* Example 7.9 */

#include <stdio.h>

int main(void)
{
       int sum = 0;
       int i;

       for (i = 1; i <= 10; i++)
              sum = sum + i;

       printf("sum = %d\n", sum);

       return 0;
}

ソース内の

sum = sum + i;

は「式文」に属す文であり,for文はこの文を条件 i <= 10 が真である限り実行します。

 実行結果です。

sum = 55

1から10までの和です。

[C言語目次へ]

[この項を1頁で読む (29)]

7.7. ビット演算子 <<>>&|^~

 これまで,数値や文字(ASCIIコード番号)の比較,算術について見てきました。ここでは,コンピュータならではの二進法での比較や算術を見ます。

 メモリにデータが記憶されるときの1単位は1byte です。1byte は 8-bit,そして1つのビットに 01 が入ります。例えば,unsigned char型の場合は 8-bit ですが,これは 01 からなる8個のデータで整数を表現することになります。

 01 からなる値と言えば,二進法です。例えば,十進法での 15 は,

1×23 + 1×22 + 1×21 + 1×20 = 15

ですので,二進法に直すと 1111 となります。これを 8-bit のデータで表せば,

00001111

となります。

 さて,これを 1-bit 分左にシフトさせ,空いた部分に 1 を入れると,

00011111

となります。これは十進法での 31 であり,元の数に 16 を加えた大きさです。逆に,元の状態から右へ 1-bit 分シフトさせ,空いた部分に 0 を入れると,

00000111

すなわち,十進法で 7 になり,元の数から 8 を引いた数になります。

■ビット演算子(Bitwise Operators)
x<<nビット単位の左シフト演算子
(Bitwise Shift Operator)
n ビット分左シフト。シフトで空いたビットには0が代入される。xunsigned の場合,x×2n に等しい。
x>>nビット単位の右シフト演算子
(Bitwise Shift Operator)
n ビット分右シフト。シフトで空いたビットには0が代入される。xunsigned の場合,x/2n に等しい。
~xビット単位の補数演算子
(Bitwise Complement Operator)
各ビットについて0と1を反転。unsigned の場合,最大値の補数となる。
x&yビット単位の論理AND演算子
(Bitwise AND Operator)
ビット毎の論理積。
x^yビット単位の排他論理OR演算子
(Bitwise Exclusive OR Operator)
ビット毎の排他論理和。両者が1のビットに0を。
x|yビット単位の論理OR演算子
(Bitwise Inclusive OR Operator)
ビット毎の論理和。ビット毎に1か0かを見て,xy に1があればそのビットに1。すなわち,両者が0のビットに0。
/* Example 7.10 */

#include <stdio.h>

int main(void)
{
       unsigned short x = 0, y = ~0;
       int n;

       printf("%u (%x) [%hu, %hx]\n", x, x, ~x, ~x);

       for(n = 0; x != y; n++) {
              x = x | (1<<n);
              printf("%u (%x) [%hu, %hx]\n", x, x, ~x, ~x);
       }

       return 0;
}

この例は,0 であった x をその補数 y = ~0unsigned short の最大値)に等しくない限り,2進法で右から 1 を次から次へと入れていったものです。すなわち,

0000000000000000
0000000000000001
0000000000000011
0000000000000111
0000000000001111
...

という変化を見たものです。short へは整数拡張が施されるため,補数の出力に対する変換指定子に short型での出力を意味する h を付しています。

 実行結果です。

0 (0) [65535, ffff]
1 (1) [65534, fffe]
3 (3) [65532, fffc]
7 (7) [65528, fff8]
15 (f) [65520, fff0]
31 (1f) [65504, ffe0]
63 (3f) [65472, ffc0]
127 (7f) [65408, ff80]
255 (ff) [65280, ff00]
511 (1ff) [65024, fe00]
1023 (3ff) [64512, fc00]
2047 (7ff) [63488, f800]
4095 (fff) [61440, f000]
8191 (1fff) [57344, e000]
16383 (3fff) [49152, c000]
32767 (7fff) [32768, 8000]
65535 (ffff) [0, 0]
[C言語目次へ]

[この項を1頁で読む (30)]

7.8. 複合代入演算子 op=

 二項演算子の一部について,次のような簡略化が可能です。例えば,

x = x + y;

x += y;

とする簡略化です。このような他の演算を含む代入演算子を「複合代入演算子」と言います。

■複合代入演算子(Compound Assignment Operators)

op= 

op に適用可能な演算子

 + - * / % >> << & ^ |

単純代入演算子を使った x = x op y と複合代入演算子を使った x op= y の差は,後者では x が1回しか評価されないことです。

[C言語目次へ]

[この項を1頁で読む (31)]

7.9. 条件演算子 ? :

 C言語には B言語から引き継いだ「省力化」「簡略化」があります。増分演算子 x++ や複合代入演算子 x += y などもそうですが,if文を書かなくとも条件判断する「条件演算子」も又そうです。

■条件演算子(Conditional Operator)

式1 ? 式2 : 式3

式10 でない(真である)場合には,式2 を返し,0(偽)のときには 式3 を返します。後に学ぶ if文を使うと,

if(式1) {
     式2
} else {
     式3
}

を実行し,値を返していることになります。典型的な例は,2つの変数の最大値を求めるものです。

x > y ? x : y

第1オペランドは算術型でなければなりませんが,第2,第3オペランドについては双方が算術型のときだけでなく,双方が同じ構造体型でも許されます。(第2,第3オペランドについては,C99, 6.5.15 を参照のこと。)双方が算術型の場合には,通常の算術型変換によって式の値(右辺値)の型が決まります。

[C言語目次へ]

[この項を1頁で読む (32)]

8. 関数(Function)

 ソースファイルに書き込む字句は,

のまとりとなることを確認し,ここまで実数型変数の宣言と式,そして若干の文を見ました。ここでは,関数について見ます。

[C言語目次へ]

[この項を1頁で読む (33)]

8.1. 関数の定義

 数学の関数と同様,受け取る独立変数と排出する従属変数があります。独立変数を仮引数(パラメータ,parameter)と呼び,従属変数の値を戻り値,返り値,或いは返却値(return value)と呼びます。

 C言語では,すべての変数がデータの型を持っています。したがって,引数や返却値にも型があります。関数を定義する際,それらを指定する必要があります。

■関数の定義

返却値型 識別子(仮引数の型 仮引数,...)
{ /* 本体 */ }

例えば,これまで使用してきた

int main(void)
{ /* ... */ }

の場合,引数をとらず,返却値として int型変数を返す関数 main の定義となります。次の場合,

double f(char x, int y)
{ /* ... */ }

仮引数に char型と int型の2つの独立変数をとり,double型の従属変数を返す関数 f の定義となります。

[C言語目次へ]

[この項を1頁で読む (34)]

8.2. return

 返却値の指定には return文を使います。

return

return ;

return文に出会うと,関数内の処理はそこで止まり,呼び出し元に処理は戻されます。return文は複数存在しても構いません。[C99, 6.8.6.4, 2] return文を使う際には,返却値型が void のときには return を指定しないこと,逆に返却値型が void 以外の場合には return を指定する必要があることに注意します。[C99, 6.8.6.4, 1]

 例えば,

double f(int x, int y)
{
       // 処理
       return;
}

というのは駄目です。返却値型が double ですから,return に何かしらの式を指定します。また,

void f(int x, int y)
{
       return x + y;
}

というのも駄目です。返却値型が void だからです。

 return に指定する には,返却値型と同じ型の値を出す式を入れます。もし返却値型と異なる型の変数や値を指定した場合,返却値型に変換されます。[C99, 6.8.6.4, 3] 例えば,1 を入れたとします。返却値型が int ならば,int型の 1 を返します。返却値型が double の場合,double型の 1 を返します。

 異なる型の変数を return に指定することはできますが,返す変数の型と従属変数の型を一致させるように心掛けることを勧めます。例えば,次のケースを見ましょう。

int f (void)
{
       char x = 'a';
       return x;
}

返却値型を int としたにもかからず,return には char x = 'a'; と定義した x を指定しています。つまり,従属変数の型と return で指定した変数の型が一致していません。しかしながら,xint 型変数へ変換されるため,エラーはでません。そうであっても,返却値型には char を指定し,return で指定する変数の型と一致させる習慣を先ずは身に付けることを勧めます。

Q main関数の仮引数と返却値
仕様(C99)によると,main 関数は,次のいずれかに限る。(但し,後者については,それと同値の形を許す。後者については,後出。)
   int main(void) { /* ... */ }
   int main(int argc, char *argv[]) { /* ... */ }
従属変数が int 型ということは,返却値に何か整数を指定するのでしょうか。main 関数の戻りについて,国際規格の C99では次のように述べています。[C99 5.1.2.2.3]
If the return type of the main function is a type compatible with int, a return from the initial call to the main function is equivalent with the exit function with the value returned by the main function as its argument; reaching the } that terminates the main function returns a value of 0.
先ず,exit 関数ですが,これはヘッダファイル stdlib.h に入っている標準ライブラリ関数です。exit 関数は,ヘッダファイル stdlib.h に入っている標準ライブラリ関数 atexit 関数で指定した関数を実行し,バッファされているデータをフラッシュしてプログラムを正常終了させます。exit 関数の引数に 0 或いは EXIT_SUCCESS を指定すると,処理系に依存した形で成功終了を戻します。
 さて,main 関数からの戻りですが,もし戻り値の型が int 型ならば,戻り値として指定しようとしている整数をその exit 関数に代入したのと同値であり,return を入れずに } に至ると,main 関数は 0 を返すとなっています。C99によると main 関数は,return を使わずとも 0 を戻す特殊な関数です。これは,プログラムの実行部分という性格を持っているからでしょう。

[C言語目次へ]

[この項を1頁で読む (35)]

8.3. プロトタイプ宣言

 関数は,呼び出せば,動くように思いがちですが,どのような関数なのかを宣言せずに呼び出すのは,やや乱暴かもしれません。そこで,呼び出す前に関数の原型を示す宣言が用意されています。

■関数原型宣言(関数プロトタイプ宣言,Function Prototype Declaration)

返却値型 識別子(仮引数の型,...);
返却値型 識別子(仮引数の型 仮引数名,...);

ここで,識別子(仮引数の型,...) 部分が「3.2. 宣言子」で触れた「関数宣言子」です。プロトタイプが示された時点から,その関数の呼び出しは,すべて同じ関数であると認識されます。[C99, 6.9.1, 7] プロトタイプ宣言がない前に関数を呼び出すと,コンパイル時に warning が出ます。[C99, Annex I, 2]

 例えば,

double f(char x, int y)
{ /* ... */ }

という関数の定義をせずに,関数 f を呼び出すのであれば,プロトタイプ宣言を予め行います。

double f(char, int);

プロトタイプとは,返却値型と仮引数の型が何なのかを示したものなので,仮引数の識別子はあってもなくても同じです。

[C言語目次へ]

[この項を1頁で読む (36)]

8.4. 関数の呼出し

 関数の呼出し(Function Call)はプロトタイプ宣言に従った形で行います。

double f(char, int); // プロトタイプ宣言
char x; int y; double z;

// 処理

z = f(x, y); // 関数 f の呼出し

関数呼出しは「式」です。この例の1つ目は宣言(プロトタイプ宣言),最後は文(式文)です。

/* Example 8.1 */

#include <stdio.h>

int max(int, int);

int main(void)
{
       int a = 5, b = 9;
       printf("%d\n", max(a, b));
       return 0;
}

int max(int x, int y)
{
       return (x > y ? x : y);
}

関数 max のプロトタイプ宣言をし,mainmax を定義しています。

max(x, y) = (x > y ? x : y)

が成り立つので,実行すると 9 という出力を得ます。

[C言語目次へ]

[この項を1頁で読む (37)]

8.5. 規定の実引数拡張

 引数を一切とらないのであれば,

double f(void);

のように,仮引数の型に void を指定したプロトタイプ宣言を行います。[C99, 6.7.5.3, 10]

 これに対し,仮引数の型を全く指定しない宣言

double f();

は「引数をとらない」という意味にはなりません。これは,引数の型が不明という意味になります。このような場合に関数呼出しをすると,不明な箇所に代入された変数のすべてに整数拡張が施され,浮動小数点型の引数は double型に変換されます。この暗黙の型変換を規定の実引数拡張(Default Argument Promotions)と言います。[C99, 6.5.2.2, 6] 関数の定義は,この型変換に符合させる必要があります。

double f(int x, int y)
{
       // 処理
}
[C言語目次へ]

[この項を1頁で読む (38)]

9. 配列(Array)

 C言語で扱えるデータの内,実数型と関数型を見ました。残りの複素数型,配列型,ポインタ型,構造体型の内,ここでは配列型を見ます。

 これまで見た宣言は,単一の変数(オブジェクト),あるいは関数の属性を指定するものでした。「配列」とは,変数の列から構成されるデータです。数学に例えると「集合 (set)」や「数列 (sequence)」,あるいは「ベクトル (vector)」です。

[C言語目次へ]

[この項を1頁で読む (39)]

9.1. 配列宣言子

 配列型(array type)は,要素数(サイズ)と要素の型(要素型,element type)の2つで特徴化されるデータ型です。要素型は型指定子で指定し,要素数(サイズ)は宣言子に配列宣言子を用いて指定します。

■配列宣言子(Array Declarator)

識別子[サイズ]

例えば,

double x[5];

といった宣言の場合,要素型は double,配列宣言子は x[5] です。この結果,要素数 5 の「double の配列(array of double)」あるいは「double型配列」が出来上がります。個別要素は,x[0] から x[4] までの5個であり,すべて double型のオブジェクトです。

 配列型は要素型からの派生型であり,後に学ぶ構造体型と同じように「集成体型」(集合体型,aggregate type)に属します(3.3 参照)。集成体型の初期化には,{ } で囲んだ初期化子を使います[C11(旧C99)§6.7.9, 16]。 例えば,次の宣言の場合,

double x[5] = {1.5, -0.2, 4.7};

{1.5, -0.2, 4.7} が初期化子であり,x[0] = 1.5x[1] = -0.2x[2] = 4.7,そして残りの x[3]x[4]0 と初期化されます。初期化子に要素数よりも少なく値を指定すると,残りには 0 が入ります[C11(旧C99)§6.7.9, 10 & 21]。

/* Example 9.1 */

#include <stdio.h>

int main(void)
{
       int m[10] = {3, 7, 10};
       int i;

       printf("sizeof(int) = %td\n"
              "sizeof(m)   = %td\n\n", sizeof(int), sizeof(m));

       for(i = 0; i < 10; i++)
              printf("m[%d] = %d\n", i, m[i]);

       return 0;
}

要素数 10 の int型配列です。配列宣言子は m[10],初期化子が {3, 7, 10} です。最初の要素が m[0],2つ目の要素が m[1],以下 m[9] までの10個の int型変数 m[i]i = 0, 1,..., 9)が連なる配列です。実行すると,

sizeof(int) = 4
sizeof(m)   = 40

m[0] = 3
m[1] = 7
m[2] = 10
m[3] = 0
m[4] = 0
m[5] = 0
m[6] = 0
m[7] = 0
m[8] = 0
m[9] = 0

となります。初期化において要素数よりも少なく値を指定すると,仕様(旧C99,現C11)通り,残りには 0 が入っていることも確認できます。

 配列はデータを一括処理するための工夫です。例えば,次のようなデータをも扱えます。

項目     A   B   C
データ1   1   2   3
データ2  10  11  12

この場合,2×3のデータになります。このようなデータを処理するためのものが「多次元配列」です。

■多次元配列の宣言

型 配列名[サイズ1][サイズ2];
/* Example 9.2 */

#include <stdio.h>

int main(void)
{
       int n[2][3] = {
                     {1, 2, 3},
                     {10, 11, 12}
       };
       int i, j, sum;

       // 行の和
       for(i = 0; i < 2; i++)
       {
              sum = 0;
              for(j = 0; j < 3; j++)
              {
                     sum += n[i][j];
                     printf("%d\t", n[i][j]);
              }
              printf("| %d\n", sum);
       }
       printf("------------------------+\n");
       // 列の和
       for(j = 0; j < 3; j++)
       {
              sum = 0;
              for(i = 0; i < 2; i++)
              {
                     sum += n[i][j];
              }
              printf("%d\t", sum);
       }
       printf("\n");

       return 0;
}

これは,n[0] = {1,2,3}n[1] = {10,11,12} という2つの配列(2次元配列)を作ります。個別要素は,n[0][0] = 1, n[0][1] = 2, n[0][2] = 3 といった具合です。

 実行結果です。

1       2       3       | 6
10      11      12      | 33
------------------------+
11      13      15

各行,各列の和が求まっています。

[C言語目次へ]

[この項を1頁で読む (40)]

9.2. 文字列と char型配列

 「char の配列」を使えば,文字列を扱うことができます。配列型を含む集成体型の初期化には { } を使うことを 9.1 で見ましたが,char型配列に限り,初期化子に 2.1 で見た文字列リテラルも使用できます[C11(旧C99)§6.7.9, 14]。 例えば,"ABCDE" という文字列を扱うには,

char str[8] = {'A', 'B', 'C', 'D', 'E'};

といった文字定数(付録4.A)を使った初期化や,ASCIIコードより,

char str[8] = {65, 66, 67, 68, 69};
char str[8] = {0x41, 0x42, 0x43, 0x44, 0x45};

といった整数定数(付録4.A)を使った初期化がありますが,

char str[8] = "ABCDE";

のように文字列リテラルを使った初期化も可能です。文字定数は文字列リテラルに使えるので,

char str[8] = "\x41\x42\x43\x44\x45";

も又,文字列 ABCDE を意味します。

/* Example 9.3 */

#include <stdio.h>

int main(void)
{
       char str[] = "language";
       size_t i;

       printf("sizeof(str) = %zd\n\n", sizeof(str));
       printf("\t%%c\t%%d\t%%x\n");
       for(i = 0UL; i < sizeof(str); i++)
              printf("str[%ld]\t%c\t%d\t%x\n", i, str[i], str[i], str[i]);
       return 0;
}

実行結果です。

sizeof(str) = 9

        %c      %d      %x
str[0]  l       108     6c
str[1]  a       97      61
str[2]  n       110     6e
str[3]  g       103     67
str[4]  u       117     75
str[5]  a       97      61
str[6]  g       103     67
str[7]  e       101     65
str[8]          0       0

先ず,配列宣言子 str[] 自体は要素数の指定がないため,3.3 で見た不完全型ですが[C11(旧C99)§6.2.5, 22],初期化されれば不完全型にはなりません[C11(旧C99)§6.7.9, 22]。その証拠として,「4.5. 変数のサイズ」で見た sizeof演算子で配列 str[] のサイズを調べると,

sizeof(str) = 9

と出ます。すなわち,値を記憶するためのメモリ領域の大きさが確定しています。

 初期化に用いた文字数は language の8文字です。しかしながら,sizeof(str) = 9 なので,メモリは9バイト,すなわち,9文字分のメモリ領域が割り当てられています。実は,文字列の末尾を表すためにナル文字(null文字,空文字,null character;4.A 参照 )が最後の9文字目 str[8] に代入されているのです[C11(旧C99)§6.7.9, 14]。 上の出力結果でも,str[8]0 が入っているのが確認できます。

 上では,配列のサイズを指定せずに初期化しました。もし配列の要素数を指定するのであれば,ナル文字が入るようにサイズを大きくとります。

char str[9] = "language";

配列の要素数より初期化での指定数が少なければ,残りに 0 が入るのは 9.1 で見た通りです。

[C言語目次へ]

[この項を1頁で読む (41)]

9.3. 文字列の操作

 初期化子 {定数}char型配列の場合には文字列リテラルも)を使った初期化,例えば,

char str[8] = "ABCD";
char x[] = {0x41, 0x42, 0x43};
int n[10] = {89, 67, 93};

は可能ですが,単純代入演算子にこれらを使うことはできません。例えば,

char str[8];
str[8] = "ABCD";   // 文法エラー

の場合,2つ目は宣言でなく,単純代入演算子を使った式(代入式)を実行させる文(式文)です。左辺側 str[8] は単なる char型ですが,文字列リテラルは配列型です。異なるものを代入しようとしているため,エラーが出ます。文字列リテラルや {89, 100} は,宣言における初期化子には使えますが,単純代入演算子のオペランドには使えません。

 単純代入演算子を使った代入は,個別要素には可能です。例えば,

char str[8];
str[0] = 'A';

の場合,第1要素 str[0]'A' が代入されます。

 配列は要素型のオブジェクトが要素数分連なったデータなので,要素毎の代入となります。したがって,文字列を配列に「代入する」には,配列の各要素にその文字列の一文字ずつを代入してゆく作業となります。これを「コピー」と言います。文字列をコピーする関数はライブラリ string.h に幾つかあります。

#include <string.h>
strcpy(str1, str2);
strncpy(str1, str2, size);
memcpy(str1, str2, size);
memset(str, c, size);

memset を除き,いずれも str2str1 へコピーします。size はコピーする文字数を指定します。memset は文字 csize分コピーします。また,次の関数は str2str1 の末尾に連結します。

#include <string.h>
strcat(str1, str2);
strncat(str1, str2, size);
/* Example 9.4 */

#include <stdio.h>
#include <string.h>

int main(void)
{
       char str[] = "ABCD";
       char buf[20];

       memset(buf, 0, sizeof(buf)); // memset(buf, '\0', 20) と同じ。
       strcpy(buf, str);
       printf("%s\n", buf);
       strcat(buf, "EFGH");
       printf("%s\n", buf);

       return 0;
}

変換指定子 %s は文字列を出力します。(正確な用法は後程を示します。)実行結果です。

ABCD
ABCDEFGH

配列 buf に文字列 ABCD がコピーされ,文字列 EFGH が末尾に連結されています。

Q バッファ・オーバーフロー
当初のメモリ領域を超えてメモリにデータが書き込まれることを「バッファ・オーバーフロー」(Buffer Overflow)とか「バッファ・オーバーラン」(Buffer Overrun)という。例えば,
    char buf[4];
    strcpy(buf, "ABCDEFG");
とすると,buf には7文字がコピーされ,3バイト分オーバーフローする。オーバーフローした部分はそのままメモリに書き込まれ,実行すると segmentation fault となる。main関数以外の関数内でバッファ・オーバーフローが起こる場合,main関数へのリターンアドレスを書き換えることが可能となり,そこへシェルコマンドの機械語(シェルコード)を送ることでそのシェルコマンドを実行させることができる。バッファ・オーバーフローを起こさせないためには,strcpy の代わりに
    memcpy(str1, str2, sizeof(str1)-1);
    strncpy(str1, str2, sizeof(str1)-1);

また,strcat の代わりに
    strncat(str1, str2, sizeof(str1)-strlen(str1)-1);
とすると良い。strlen関数はライブラリ string.h で定義されている関数で,文字数を size_t型で返す。

[C言語目次へ]

[この項を1頁で読む (42)]

10. ポインタ(Pointer)

 C言語で扱えるデータの内,実数型,関数型,配列型の扱い方を見ました。残りの複素数型,ポインタ型,構造体型の内,ここではポインタ型を見ます。

 ポインタ型データとは,アドレスのことです。「アドレス」とはメモリ箇所の番地です。メモリの1単位は1バイト,そして,それが連なってメモリは構成されています。例えば,次のような具合です。

アドレス | 8257 | 8258 | 8259 | 8260 | 8261 |
値    | 't'  | 'e'  | 's'  | 't'  | '\0' |

アドレス 8257 のメモリ箇所に記憶されている値は,文字定数で表すと 't' という意味です。ポインタ型データは,値側ではなく,アドレス側のデータのことです。そして,ポインタ型データを扱う変数を,単にポインタと言います。

 アドレス自体は十六進法で表すと 0x7fff53c8db84 のように固定的に存在しますが,C言語ではデータの型によって確保するメモリ領域が異なるため,記憶するデータの型によってポインタも区別します。記憶するデータ(上の表の値側)の型がT の場合,アドレスをT へのポインタ型データであると考えます。型T被参照型(referenced type)と言い,ポインタ型は被参照型からの派生型となります。

[C言語目次へ]

[この項を1頁で読む (43)]

10.1. 配列型とメモリ領域

 配列型は,要素数(サイズ)と要素型の2つで特徴化されるデータであることを 9.1 で見ました。この特徴は,メモリ上では,次のような意味を持ちます。すなわち,「要素型をもつオブジェクトが連続してメモリに記憶されるデータ」という意味です。更に,これに次の文法が加わります。

ここで「第1要素を指し示すポインタ(pointer that points to the initial element)」とは,例えば,宣言

char str[8] = "ABCDEFG"

の場合,str 自体が,配列の第1要素 'A' を記憶しているアドレスを指し示す char へのポインタになっているという意味です。

/* Example 10.1 */

#include <stdio.h>

int main(void)
{
       char str[] = "ABCDEFG";
       printf("str[0] = '%c' [%p]\n", str[0], str);
       return 0;
}

printf関数内の変換指定子 %p はポインタの(処理系依存の)出力です。ここでは,上の文法に従い,str が第1要素を指し示す char へのポインタになっているため,配列の第1要素のアドレスが出力されます。

 実行結果です。

str[0] = 'A' [0xbffffa18]

第1要素のアドレスは 0xbffffa18 です。(十六進法の読み方は整数定数を参照。)char型は1バイトなので,次のようにメモリに値が記憶されていることになります。

+------------+------------+------------+------------+
| 0xbffffa18 | 0xbffffa19 | 0xbffffa1a | 0xbffffa1b | アドレス
+------------+------------+------------+------------+
|    'A'     |    'B'     |    'C'     |    'D'     | 値
+------------+------------+------------+------------+

+------------+------------+------------+------------+
| 0xbffffa1c | 0xbffffa1d | 0xbffffa1e | 0xbffffa1f | アドレス
+------------+------------+------------+------------+
|    'E'     |    'F'     |    'G'     |    '\0'    | 値
+------------+------------+------------+------------+

十六進法の整数定数で表せば,ASCIIコードより,

+------------+------------+------------+------------+
| 0xbffffa18 | 0xbffffa19 | 0xbffffa1a | 0xbffffa1b | アドレス
+------------+------------+------------+------------+
|    0x41    |    0x42    |    0x43    |    0x44    | 値
+------------+------------+------------+------------+

+------------+------------+------------+------------+
| 0xbffffa1c | 0xbffffa1d | 0xbffffa1e | 0xbffffa1f | アドレス
+------------+------------+------------+------------+
|    0x45    |    0x46    |    0x47    |    0x00    | 値
+------------+------------+------------+------------+

配列は要素型(この場合 char型)のオブジェクトが連続して記憶されるデータです。

/* Example 10.2 */

#include <stdio.h>

int main(void)
{
       unsigned int m[5] = {3, 7, 10};
       printf("m[0] = %d [%p]\n", m[0], m);
       return 0;
}

実行結果です。

m[0] = 3 [0xbffffa78]

奥山研究室では sizeof(int) = 4 ですので,次のように値がメモリに記憶されていることになります。

+------------+------------+------------+------------+
| 0xbffffa78 | 0xbffffa79 | 0xbffffa7a | 0xbffffa7b | アドレス
+------------+------------+------------+------------+
|    0x00    |    0x00    |    0x00    |    0x03    | 値
+------------+------------+------------+------------+

+------------+------------+------------+------------+
| 0xbffffa7c | 0xbffffa7d | 0xbffffa7e | 0xbffffa7f | アドレス
+------------+------------+------------+------------+
|    0x00    |    0x00    |    0x00    |    0x07    | 値
+------------+------------+------------+------------+

+------------+------------+------------+------------+
| 0xbffffa80 | 0xbffffa81 | 0xbffffa82 | 0xbffffa83 | アドレス
+------------+------------+------------+------------+
|    0x00    |    0x00    |    0x00    |    0x0a    | 値
+------------+------------+------------+------------+

+------------+------------+------------+------------+
| 0xbffffa84 | 0xbffffa85 | 0xbffffa86 | 0xbffffa87 | アドレス
+------------+------------+------------+------------+
|    0x00    |    0x00    |    0x00    |    0x00    | 値
+------------+------------+------------+------------+

+------------+------------+------------+------------+
| 0xbffffa88 | 0xbffffa89 | 0xbffffa8a | 0xbffffa8b | アドレス
+------------+------------+------------+------------+
|    0x00    |    0x00    |    0x00    |    0x00    | 値
+------------+------------+------------+------------+

配列は要素型(この場合 int型)のオブジェクトが連続して記憶されるデータです。

[C言語目次へ]

[この項を1頁で読む (44)]

10.2. 間接演算子 *

 配列は要素型のオブジェクトが連続してメモリに記憶されるデータであり,配列型は式で使うと第1要素のアドレスに変換されることを見ました。ということは,配列型を式で使えば第1要素の値が入っている場所に直接アクセスできるという意味です。それならば,そのアドレスのメモリ箇所に入っている値にもアクセスできるはずです。

     +------------+
     | 0xbffffa18 |
     |    str     | →→
     +------------+    ↓ 間接参照
     |    'A'     |    ↓
     |   str[0]   | ←←
     +------------+

     char str[] = "ABCDEFG"; の場合

アドレスからそのメモリ箇所に記憶される値を参照することを「間接参照」(indirection)と言います。間接参照するための演算子が「間接演算子」です。

■間接演算子(Indirection Operator)

*オペランド

オペランドが「T へのポインタ」型のアドレスの場合,間接演算子はそのアドレスのメモリ箇所に記憶されている値をT 型で戻します。

/* Example 10.3 */

#include <stdio.h>

int main(void)
{
       char str[] = "ABCDEFG";
       printf("str[0] = '%c' [%p]\n", *str, str);
       return 0;
}

これは,Example 10.1 において,間接演算子を使って配列の第1要素の値を出力させる例です。実行結果です。

str[0] = 'A' [0xbffffa18]

str 自体がアドレス 0xbffffa18 を指し,そのメモリ箇所に記憶されている値 'A' を間接演算子で求めた訳です。

/* Example 10.4 */

#include <stdio.h>

int main(void)
{
       unsigned int m[5] = {3, 7, 10};
       printf("m[0] = %d [%p]\n", *m, m);
       return 0;
}

こちらは Example 10.2 において,m[0]*m に置き換えて配列の第1要素の値を出力させるものです。実行結果です。

m[0] = 3 [0xbffffa78]
+------------+------------+------------+------------+
| 0xbffffa78 | 0xbffffa79 | 0xbffffa7a | 0xbffffa7b | アドレス
|     m      |            |            |            |
+------------+------------+------------+------------+
|    0x00    |    0x00    |    0x00    |    0x03    | 値
|<--------------------- m[0] ---------------------->|
+------------+------------+------------+------------+

間接参照 *m → m[0]

mintへのポインタ型なので,*mint型です。奥山研究室では sizeof(int) = 4 ですので,それは m[0] に等しいです。

[C言語目次へ]

[この項を1頁で読む (45)]

10.3. ポインタ宣言子 char *p

 配列は要素型のオブジェクトが連続してメモリに記憶されるデータであり,配列型は式で使うと第1要素のアドレスに変換され,そのアドレスのメモリ箇所に入っている値に直接アクセスできる方法(間接参照)があることも見ました。ポインタ型データ,すなわち,アドレスについて理解したことでしょう。ここでは,ポインタ型データを扱うための変数,「ポインタ」の導入方法を見ます。

 ポインタを導入するためには,宣言子にポインタ宣言子を用います。

■ポインタ宣言子(Pointer Declarator)

*識別子

宣言の文法に従うと,型指定子T に対し,

T *識別子 = 初期化子;

と宣言しなければなりません。この宣言よって得られる変数 識別子T へのポインタ型データに対するポインタです。型は「T へのポインタ」であり,それは

T *

となります。初期化子には,T へのポインタ型データを代入します。

/* Example 10.5 */

#include <stdio.h>

int main(void)
{
       char str[] = "ABCDEFG";
       char *p = str;
       printf("str[0] = '%c' [%p]\n", *p, p);
       return 0;
}

これは Example 10.3 をポインタ p を用いて書き直したものです。ポインタ pchar *型,すなわち,charへのポインタ型です。str配列の第1要素を指し示す char へのポインタです。したがって,ポインタ pstr に初期化されることになります。すなわち,宣言

char *p = str;

は,char * 部分が型,p = str 部分が初期化であり,したがって,char *型変数の初期化になります。

 一方,printf関数

printf("'%c' [%p]\n", *p, p);

の中の *pp の間接参照です。すなわち,*p*間接演算子です。また,p = str なので,printf関数内の pstr の値となります。

 かくして,実行結果は,

str[0] = 'A' [0xbffffa18]

となり,Example 10.3 の結果と同じになります。(但し,機械によってはアドレスは異なるかもしれません。)

[C言語目次へ]

[この項を1頁で読む (46)]

10.4. ポインタを含む和算

 ポインタを含む和算は,ポインタと整数型の間のみに許されます。ポインタ同士の和算は許されません。[C99, 6.5.6, 2](c.f. 加算演算子)また,p が配列の第i要素を指し示すポインタであるとすると,

p + n

は第 (i + n) 要素を指し示すポインタとなります。[C99, 6.5.6, 8]

/* Example 10.7 */

#include <stdio.h>

int main(void)
{
       char str[] = "ABCDEFG";
       char *p;
       for(p = str; *p != '\0'; p++)
              printf("'%c' %x [%p]\n", *p, *p, p);
       return 0;
}

これは,Example 10.5 において,ポインタの和算を利用して,配列 str[] の各要素の値(ASCII文字と十六進法)とアドレスを出力させたものです。実行結果です。

'A' 41 [0xbffffa98]
'B' 42 [0xbffffa99]
'C' 43 [0xbffffa9a]
'D' 44 [0xbffffa9b]
'E' 45 [0xbffffa9c]
'F' 46 [0xbffffa9d]
'G' 47 [0xbffffa9e]

for文における繰り返しの初期条件が p = str ということで,Example 10.5 のときのようにポインタ pstr に設定します。繰り返しの条件 *p != '\0' は「ナル文字でないならば」という意味になります。そして,繰り返す場合の条件変更 p++p1 増加させます。ポインタ p に整数 1 を加えること(ポインタと整数の和算)は許されます。(str左辺値ではないため,str++不可です。)

 ナル文字で繰り返し制御から抜けるので,メモリには次のように値が記憶されていることになります。(10.1 でのアドレスと異なるのは,アドレスは実行環境によって異なるため。)

+------------+------------+------------+------------+
| 0xbffffa98 | 0xbffffa99 | 0xbffffa9a | 0xbffffa9b | アドレス
+------------+------------+------------+------------+
|    'A'     |    'B'     |    'C'     |    'D'     | 値
+------------+------------+------------+------------+

+------------+------------+------------+------------+
| 0xbffffa9c | 0xbffffa9d | 0xbffffa9e | 0xbffffa9f | アドレス
+------------+------------+------------+------------+
|    'E'     |    'F'     |    'G'     |    '\0'    | 値
+------------+------------+------------+------------+

Q 文字列の末尾を探す方法
上の例では,
  for(p = str; *p != '\0'; p++)
によって文字列 ABCDEFG の末尾の(ナル文字 '\0' が入っている)アドレスまでポインタ p が増加しました。すなわち,文字列の末尾を探した訳です。文字列の長さ無関係に文字列の末尾を探す方法は,このようにすればできる訳ですが,ただ単に文字列の末尾を探したいのであれば,論理演算空文より次のようにすれば良いです。
  for(p = str; *p; p++);
付言すると,str が配列の第1要素を指し示すポインタであり,それが左辺値ではないという文法は,配列とポインタが異なる概念であることを意味している。C言語の先祖 B言語,そして B言語の先祖 BCPL ではポインタと配列は同じものであったが,C言語がそれら先祖から大きく飛躍したのは,型の導入と,そして配列とポインタの分離であった。詳しくは C言語の作者 Dennis M. Ritchie による C言語の歴史に関する著作 The Development of the C Language を参照。

[C言語目次へ]

[この項を1頁で読む (47)]

10.5. ポインタを含む減算演算と ptrdiff_t 型変数

 ポインタを含む減算演算には,次のルールがあります。[C99, 6.5.6, 3]

但し,ポインタ間の差は,ライブラリ stddef.h で定義された ptrdiff_t型変数となります。ポインタ間の差はそれら2つのポインタが同一の配列を指し示す場合に許されたもので,その結果はその配列上の要素番号の差になります。[C99, 6.5.6, 9]

/* Example 10.8 */

#include <stdio.h>

int main(void)
{
       char str[] = "ABCDEFG";
       char *p;
       for(p = str; *p != '\0'; p++)
              printf("str[%td] '%c' %x [%p]\n", p - str, *p, *p, p);
       return 0;
}

* 仕様(C99)では,ptrdiff_t型の出力には t を付すとなっています。[C99, 7.19.6.1]
[例]%td%to

これは,Example 10.7 において,ポインタ同士の減算を利用して,配列の要素番号を出力させたものです。実行結果です。

str[0] 'A' 41 [0xbffffa98]
str[1] 'B' 42 [0xbffffa99]
str[2] 'C' 43 [0xbffffa9a]
str[3] 'D' 44 [0xbffffa9b]
str[4] 'E' 45 [0xbffffa9c]
str[5] 'F' 46 [0xbffffa9d]
str[6] 'G' 47 [0xbffffa9e]

ポインタ同士の差(減算式)p - str が配列の要素番号となっていることが理解できます。

[C言語目次へ]

[この項を1頁で読む (48)]

10.6. 添字演算子(Subscript Operator)

 p が配列の第 i 要素を指し示すポインタのとき,

p + n

は第(i + n)要素を指し示すポインタになるということは,間接演算子の文法から,配列の要素を意味する添字演算子 [] は,次の文法を持つこととなります。[C99, 6.5.1, 2]

x[i] == (*(x + i))

例えば,Example 10.8 の場合,メモリに記憶されている値とアドレス,そして,配列型 str とその間接参照の関係は次のようになります。

+------------+------------+------------+------------+
| 0xbffffa98 | 0xbffffa99 | 0xbffffa9a | 0xbffffa9b | アドレス
|    str     |  str + 1   |  str + 2   |  str + 3   |  →→
+------------+------------+------------+------------+     ↓ 間接参照
|    'A'     |    'B'     |    'C'     |    'D'     | 値   ↓ *(str + i)
|   str[0]   |   str[1]   |   str[2]   |   str[3]   |  ←←
+------------+------------+------------+------------+

+------------+------------+------------+------------+
| 0xbffffa9c | 0xbffffa9d | 0xbffffa9e | 0xbffffa9f | アドレス
|  str + 4   |  str + 5   |  str + 6   |  str + 7   |  →→
+------------+------------+------------+------------+     ↓ 間接参照
|    'E'     |    'F'     |    'G'     |    '\0'    | 値   ↓ *(str + i)
|   str[4]   |   str[5]   |   str[6]   |   str[7]   |  ←←
+------------+------------+------------+------------+

したがって,Example 10.8 をポインタ p を使わずに書き換えると,次のようになります。

/* Example 10.9 */

#include <stdio.h>

int main(void)
{
       char str[] = "ABCDEFG";
       int i;
       for(i = 0; str[i] != '\0'; i++)
              printf("str[%d] '%c' %x [%p]\n", i, *(str + i), *(str + i), str + i);
       return 0;
}

上の添字演算子の文法から,*(str + i)str[i] とすることも可能です。

 ポインタ p を使わずとも,同じ目的が達成可能です。それでは,ポインタを利用する利点は何でしょうか。ポインタを利用せずに Example 10.9 のように要素番号を意味する i を使った場合,i を計算し,str から i 番目のアドレスに移動し,そして,そのアドレスのメモリ箇所に記憶されている値を呼び出すことになります。というのは,配列は要素型のオブジェクトが連続してメモリに記憶されたデータだからです。最初からアドレスにアクセスできていれば,i を計算し,str から i 番目のアドレスに移動する必要はありません。一方,ポインタは最初からあるアドレスを指しています。ポインタの利点は,処理の高速化にあるのです。

[C言語目次へ]

[この項を1頁で読む (49)]

10.7. アドレス演算子 &

 これまで配列を中心にポインタの利用方法を見て来ました。ここでは配列以外のオブジェクトについて,ポインタ型データの取得方法とポインタの利用の仕方を見てみます。

 配列の場合,式に使うと第1要素を指し示すポインタになります。すなわち,配列型は式で使えば直接,ポインタ型データ,すなわち,アドレスを取得できます。これに対し,配列以外のオブジェクト,例えば,

int x = 1;

などの場合,アドレスをどのように取得できるのでしょうか。というのは,アドレスが取得できなければ,アドレスを変数にしたポインタは利用できないからです。ポインタ型データ,すなわち,アドレスを取得するための演算子が「アドレス演算子」です。

■アドレス演算子 (Address Operator)

&オペランド

アドレス演算子はオペランドのアドレスを返します。オペランドの型がT のとき,アドレス演算子は T *型(T へのポインタ型)としてアドレスを返します。

/* Example 10.10 */

#include <stdio.h>

int main(void)
{
       int x = 2, y = 3;
       int *p;    // int *型変数(intへのポインタ)の宣言

       printf("x = %d [%p]\n", x, &x);
       printf("y = %d [%p]\n", y, &y);

       p = &x;
       printf("p = %p\n", p);
       *p = 10;   // * は間接演算子

       p = &y;
       printf("p = %p\n", p);
       *p = 10;   // * は間接演算子

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

       return 0;
}

実行結果です。

x = 2 [0xbffffa98]
y = 3 [0xbffffa9c]
p = 0xbffffa98
p = 0xbffffa9c
x = 10
y = 10

x は最初 2y3 です。それを 10 に変えています。

 先ず,式文

p = &x;

でポインタ px のアドレス 0xbffffa98 に設定します。これは3つ目の出力でも確認できます。そこで,間接参照を利用して,すなわち,

x == *&x == *p
     +------------+
アドレス | 0xbffffa98 | &x →
     +------------+     ↓ *&x = *p
   値 |     x      | ← ←
     +------------+

となることを利用して,アドレス 0xbffffa98 のメモリに記憶されている値を 10 に変えます。それが式文

*p = 10;

です。(*p = 10 は代入式。=単純代入演算子。)同じ操作を y についても行います。

 ポインタ p に代入しているのはアドレス &x&y であることを見れば,ポインタがアドレス上の変数であることが再確認できるかと思います。

[C言語目次へ]

[この項を1頁で読む (50)]

10.8. NULLポインタ (空ポインタ)

 char 型には '\0''A' など,int 型には10進数表記で 0 や16進数表記で 0x00 などの「定数」がありました(付録4.A)。ポインタ(char* 型や int* 型など)には,つぎの「定数」が用意されています。

[ノート]ISO規格C11(旧C99)の原文は,つぎの通りです。
[§6.3.2.3, 3] An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
[§6.3.2.3, fn.66] The macro NULL is defined in <stddef.h> (and other headers) as a null pointer constant.
[§7.19, 3] NULL ... expands to an implementation-defined null pointer constant[.]
"a" や "an" とあるように,空ポインタ定数は,一つだけではありません。
その中でも,定数 NULL の定義は,処理系依存になっています。試しにヘッダ stddef.h を開いて,マクロ NULL を確認してみましょう。
[参考]奥山研究室のコンパイラ gcc では マクロ NULL(void*)0 と定義されていましたが,他のコンパイラで同じように定義されているとは限りません。

これらの「定数」を使って,つぎの「空ポインタ」が出来上がります。

[ノート]ISO規格C11(旧C99)の原文は,つぎの通りです。
[§6.3.2.3, 3] If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
[§6.3.2.3, 4] Conversion of a null pointer to another pointer type yields a null pointer of that type. Any two null pointers shall compare equal.

/* Example 10.11 */

#include <stdio.h>

int main(void)
{
       void *p = 0x0;                     // 右辺は空ポインタ定数なので,p は空ポインタ
       int *q = 0x0;                      // 整数定数式 0x0 が空ポインタ定数か否かを(1)や(8)(9)で確認
       int n = 0;


       /* A */
       printf("(A) p [%p]\n", p);         // %p の出力は処理系依存;後出 13.1 参照

       if(p == NULL)                      // (1) NULL は処理系依存の空ポインタ定数
                                          //     if文については,後出 14.3 参照
              printf("OK, integer constant expression 0x0 "
                     "is a null pointer constant in your computer.\n");
       else
              printf("* WARNING\n"
                     "* Integer constant expression 0x0 may not be "
                     "with \"the value 0\" in your computer.\n"
                     "* Or, NULL may be incompatible with ISO:C11.\n");


       /* B */
       p = (void *)0;                     // (2) 右辺は空ポインタ定数なので,p は空ポインタ
                                          //     (void *)0 が空ポインタ定数か否かを(3)で確認
       printf("(B) p [%p]\n", p);

       if(p == NULL)                      // (3)
              printf("OK, (void *)0 is a null pointer constant in your computer.\n");
       else
              printf("* WARNING\n"
                     "* Integer constant expression 0 may not be "
                     "with \"the value 0\" in your computer.\n"
                     "* Or, NULL may be incompatible with ISO:C11.\n");


       /* C */
       printf("(C) p [%p]\n"
              "    q [%p]\n", p, q);

       if(q == (int *)p)                  // (4)
              printf("OK, any two null pointers shall compare equal "
                     "[C11 \u00A76.3.2.3, 4; \u00A76.5.9, 6].\n");
       else
              printf("* WARNING\n"
                     "* Either integer constant expression 0x0 or 0 may not be "
                     "with \"the value 0\" in your computer.\n");


       /* D */
       q = &n;                            // (5) 右辺は空ポインタ定数でもなければ,空ポインタでもないので,
                                          //     q は空ポインタではない
       printf("(D) q [%p]\n", q);

       if(q != NULL)                      // (6)
              printf("OK, q is not a null pointer.\n");
       else
              printf("* WARNING\n"
                     "* NULL may be broken.\n");


       /* E */
       p = 0x0;                           // (7)

       printf("(E) p [%p]\n"
              "    q [%p]\n", p, q);

       if(q != p)                         // (8)
              printf("OK, q is not a null pointer.\n");
       else
              printf("* WARNING\n"
                     "* Integer constant expression 0x0 may not be "
                     "with \"the value 0\" in your computer.\n");

       
       /* F */
       printf("(F) q [%p]\n", q);

       if(q != 0x0)                      // (9)
              printf("OK, q is not a null pointer.\n");
       else
              printf("* WARNING\n"
                     "* Integer constant expression 0x0 may not be "
                     "with \"the value 0\" in your computer.\n");


       return 0;
}

奥山研究室では gcc -Wall で no errors & no warnings,つぎの実行結果を得ます。
* UTF未対応のターミナルでの実行の場合には,§ が \u00A7 と出力されたり,文字化けするかもしれません(その場合は,上記プログラムの中の \u00A7 を削除してからコンパイルしてください)。Macユーザの方は,Terminal.app にて実行してみてください。

(A) p [0x0]
OK, integer constant expression 0x0 is a null pointer constant in your computer.
(B) p [0x0]
OK, (void *)0 is a null pointer constant in your computer.
(C) p [0x0]
    q [0x0]
OK, any two null pointers shall compare equal [C11 §6.3.2.3, 4; §6.5.9, 6].
(D) q [0x7fff5142bb8c]
OK, q is not a null pointer.
(E) p [0x0]
    q [0x7fff5142bb8c]
OK, q is not a null pointer.
(F) q [0x7fff5142bb8c]
OK, q is not a null pointer.

Example 10.11の(2)(5)(7)には,単純代入演算子 = が使われています。ポインタに対する文法は,つぎの通りです。

なお,void へのポインタには,つぎの変換法則があります。

上記(1)(3)(4)(6)(8)(9)の if文内の等価演算子 ==!= に対するポインタの使用については,つぎの文法があります。

3つ目の文法での空ポインタ定数(NULLポインタ定数)は,もう一方のオペランドのポインタ型に変換されます[C11(旧C99)§6.5.9, 5]。よって,空ポインタとして,もう一方のオペランドのポインタと比較されます。

等価演算子 == においてポインタ同士が「等しい」と判定される,すなわち,演算式が 1 を返すのは,つぎのケースです。

[ノート]ISO規格C11(旧C99)の原文は,つぎの通りです。
[§6.5.9, 6] Two pointers compare equal if and only if both are null pointers, both are pointers to the same object (including a pointer to an object and a subobject at its beginning) or function, both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.

最後に,関係演算子上の文法を再述 (7.4)しておきます。

[C言語目次へ]

[この項を1頁で読む (51)]

10.9. ポインタへのポインタ:char **x

 ポインタはそれ自体変数であり,int *char * が型(タイプ)を意味し,

char *p;

といった宣言(型 変数名;)によってポインタの値を格納するためのアドレスも確定します。すなわち,ポインタにも値(アドレス番号)を格納するためのアドレスが割り振られている訳です。ポインタのアドレスを変数にもつポインタが「ポインタへのポインタ」です。

/* Example 10.12 */

#include <stdio.h>

int main(void)
{
       char x[] = "testing";
       char *p;
       char **z;

       printf(" x = %p\n", x);

       p = x;
       printf(" p = %p, &p = %p\n", p, &p);

       z = &p;
       printf("*z = %p,  z = %p\n", *z, z);

       return 0;
}

この例では,z がポインタへのポインタです。z にポインタ p のアドレスを代入している様子から見ても,ポインタのアドレスを変数にもつ変数であることが理解できるかと思います。実行結果は次の通りです。

 x = 0xbffffa68
 p = 0xbffffa68, &p = 0xbffffa70
*z = 0xbffffa68,  z = 0xbffffa70

z = &p ですので,アドレス演算子間接演算子の文法より,*z == p です。

[C言語目次へ]

[この項を1頁で読む (52)]

10.10. ポインタの配列:char *x[]

 int * や char * 自体が型を意味するので,例えば,

char *x[7];

という宣言は char *型変数の配列,すなわち,ポインタの配列,「ポインタ配列」となります。この例の場合,合計7個のポインタ x[i] (i = 0, 1,...,6) が出来上がります。

 添字演算子で見たように,x + i は第i要素のアドレスです。しかし,x + i はポインタ x[i] のアドレスですので,x + i を代入できるのは「ポインタへのポインタ」となります。例えば,次のように宣言すれば,

char *x[7];
char **z;

ポインタへのポインタ zx + i を代入することができます。

/* 例 */
z = x + 2;
/* Example 10.13 */

#include <stdio.h>

int main(void)
{
       char *x[10] = {
              "char",
              "short",
              "int",
              "long",
              "float",
              "double"
       };

       char *p;
       char **z;

       for (p = *x; *p != '\0'; p++)
              printf("%c\t(%d)\t[%p]\n", *p, *p, p);

       for (z = x; *z != NULL; z++)
              printf("%s\t[%p]\n", *z, *z);

       return 0;
}

実行結果です。

c       (99)    [0x1f04]
h       (104)   [0x1f05]
a       (97)    [0x1f06]
r       (114)   [0x1f07]
char    [0x1f04]
short   [0x1f0c]
int     [0x1f14]
long    [0x1f18]
float   [0x1f20]
double  [0x1f28]

丸括弧内はその文字の ASCIIコード番号,角括弧内はアドレスです。x第1要素のアドレスであり,したがって,間接参照すると *x == x[0] となります。但し,x[0]charへのポインタです。最初の for 文ではポインタ p をそのポインタ x[0] に設定し,ナル文字でない限り処理を繰り返すようにしています。それが出力結果の最初の4行となります。2つ目の for 文では「ポインタのポインタ」z をポインタ x[0] のアドレス x == &x[0] に設定し,ポインタの配列 x[i] において NULLポインタでない限り処理を繰り返すものです。それが残りの出力結果となります。

[C言語目次へ]

[この項を1頁で読む (53)]

10.11. ポインタの関数 f(char *x), f(int *x)

 関数にアドレスを渡すことができます。すなわち,アドレスを受け取る関数を作成することができます。

/* Example 10.14 */

#include <stdio.h>

void f(int *);

int main(void)
{
       int x = 2, y = 3;

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

       return 0;
}

void f(int *p)    // int *型変数(intへのポインタ)
{
       *p = 10;   // * は間接演算子
}

これは Example 10.10 において,各変数を 10 に変更する部分を関数 f にまとめたものです。実行すると,

x = 10
y = 10

という出力結果を得ます。関数 f に渡すのはアドレス &x&y,そして受け取る側の仮引数はポインタ p ということで,「アドレス → ポインタ」という連繋になっています。

 アドレスを渡すと関数 f 内でも xy の値を変更することができます。アドレスを渡さない場合は変更できません。次の例でそれが確認できます。

/* Example 10.15 */

#include <stdio.h>

void f(int);

int main(void)
{
       int x = 2;
       f(x);
       printf("(3) x = %d\n", x);
       return 0;
}

void f(int s)
{
       printf("(1) s = %d\n", s);
       s = 10;
       printf("(2) s = %d\n", s);
}

実行結果です。

(1) s = 2
(2) s = 10
(3) x = 2

関数 fx を渡したので,最初 s2 です。その後 10 に変わっています。しかし,x の値は 2 のままです。x の値を変更するには,それが記憶されているメモリ領域にアクセスできなければならない訳です。

/* Example 10.16 */

#include <stdio.h>

void f(char *);

int main(void)
{
       char str[] = "ABCDEFG";
       f(str);
       return 0;
}

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

Example 10.8 のプリント部分を少し変更したものを関数 f にしたものです。また,変換指定子 %s はこれまでも使用してきましたが,その意味は指定されたアドレスのメモリ箇所に記憶されている値からナル文字の直前までを出力するというものです。実行結果です。

[0xbffffa98] 'A' ABCDEFG
[0xbffffa99] 'B' BCDEFG
[0xbffffa9a] 'C' CDEFG
[0xbffffa9b] 'D' DEFG
[0xbffffa9c] 'E' EFG
[0xbffffa9d] 'F' FG
[0xbffffa9e] 'G' G

値を変更したりプリントしたりと,様々な作業や役割を関数に持たせることが可能になります。

[C言語目次へ]

[この項を1頁で読む (54)]

10.12. ポインタを返す関数 char *f()

 今度は逆に,アドレスを返す関数,「ポインタ関数」を作成してみましょう。

/* Example 10.17 */

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

char *f(void);

int main(void)
{
       char *y;

       y = f();
       printf("(2) %s [%p]\n", y, y);

       exit(0);
}

char *f(void)
{
       char str[8] = "testing";
       char *x;

       x = malloc(strlen(str) + 1);
       if(x == NULL) { exit(1); }

       strncpy(x, str, strlen(str));
       printf("(1) %s [%p]\n", x, x);

       return x;
}

ソース内の malloc は,ライブラリ stdlib.h で定義されている関数で,引数に指定したバイト数分のメモリを割り当て,そのポインタを返します。メモリの割り当てに失敗した場合には,NULLポインタを返します。(man 3 malloc で確認して下さい。)実行結果は次の通りです。

(1) testing [0x442b0]
(2) testing [0x442b0]

角括弧内はアドレスです。関数 f 内のポインタ x が指しているアドレス(出力 1)と main関数内のポインタ y が指しているアドレス(出力 2)が同じであることが確認できます。関数 f がアドレスを返している様子が分かるかと思います。

 アドレスを返すためのポイントは,f( ) 自体がポインタとなるよう,ポインタ宣言 (char *f) していること,そして,それが返す変数もポインタ宣言(char *x)していることです。(返却値型 char *return で返す型 char * を一致させるようにしましょう。f( ) 自体がポインタなのですから,return には返却値型と同じ型のポインタを指定します。)そして,main 関数で受け取る y もポインタであり,それにポインタ f( ) を渡します。要約すると,xf( )y がポインタであり,xf( )y という連携がすべて同じ型のポインタ間になっています。

 malloc によって割り当てたメモリ領域は,解放するまでメモリは消費されています。例えば,上の場合で main関数における printf の実行後にメモリが不必要になるのであれば,

free(y);

とすれば,malloc で割り当てたメモリ領域は解放されます。割り当ての場合,変数の寿命は割り当ててから解放するまでの期間となります。

Q メモリ・リーク(Memory Leak)
 プログラムが常に動作しているような場合,malloc で割り当てたメモリ領域を free で適切に解放しないと,メモリ消費が増えてゆく「メモリリーク」が起こる。メモリリークが起こり,メモリを消費尽くすと,システムがダウンする。

[C言語目次へ]

[この項を1頁で読む (55)]

10.13. 関数へのポインタ (*f)()

 関数型については,次の文法があります。

この文法は,配列と同様に,特別に用意されたものです。(アドレス演算子を使用せずともアドレスを取得できるのは,配列と関数のみである。)例えば,

double f(char, int);

があったとき,関数指示子(式)f は関数 f のアドレスとなります。

 関数のアドレスを受け取ることのできるポインタが「関数へのポインタ」です。例えば,上の関数 f のアドレスを受け取るには,

double (*pf)();

といった具合に宣言したポインタ pf になります。これは doubleを返す関数へのポインタです。

/* Example 10.18 */

#include <stdio.h>

void f(void);

int main(void)
{
       void (*pf)(void) = f;
       pf();
       return 0;
}

void f(void)
{
       printf("Hello World!\n");
}

実行結果です。

Hello World!

pfvoid型関数へのポインタです。それを f に初期化しています。関数呼出しは pf() ですが,これは f() と同値です。すなわち,pf == f です。

[C言語目次へ]

[この項を1頁で読む (56)]

11. 構造体(Structure)

 C言語で扱えるデータの内,実数型,関数型,配列型,ポインタ型を見ました。ここでは構造体型を見ます。

 「構造体」とは,複数の変数を1つのカテゴリー,ユニットにしたものです。例えば,文献リストの作成を考えてみましょう。1つの文献に対し,著者,タイトル,発行年,出典元(論文の場合にはジャーナル名,本の場合には出版社)があります。すると,1つの文献に対し,変数が次のようになるでしょう。

char author[40];             /* 著者 */
char title[80];              /* タイトル */
int  year;                   /* 発行年 */
char resource[120];          /* 出典元 */

「1つの文献」自体がこれらの変数を持つことになります。「構造体」とは,複数の構成要素からなるユニットです。

[C言語目次へ]

[この項を1頁で読む (57)]

11.1. 構造体型オブジェクト

 構造体型オブジェクトを作成するには,型指定子

struct タグ {struct-宣言}
struct タグ

のいずれかを使用します。struct-宣言 には,構造体タグ 型がもつ「メンバ」のリストを指定します。例えば,文献リストの場合,次のように宣言すれば,

/* Example 11.1 */

struct ref {
       char author[40];        /* 著者 */
       char title[80];         /* タイトル */
       int  year;              /* 発行年 */
       char resource[120];     /* 出典元 */
};

構造体ref型が charの配列 authortitleresource,そして int型変数 year を「メンバ」に持つ「文献」(reference)を意味します。

* タグ,或いは列挙体メンバを含む場合は,宣言子はオプションである。このため,上の宣言は文法上間違いではない。

 しかしながら,上の場合,型指定子のみの宣言で宣言子がないため,オブジェクト(変数)は未だありません。オブジェクトを意味する識別子の導入が必要です。例えば,

/* Example 11.2 */

struct ref {
       char author[40];        /* 著者 */
       char title[80];         /* タイトル */
       int  year;              /* 発行年 */
       char resource[120];     /* 出典元 */
};

struct ref paper, book;

とすれば,識別子 paperbook は構造体ref型オブジェクトであり,paper がある「論文」,book がある「本」だとすれば,それらは著者(author),タイトル(title),発行年(year),そして出版元(resource)をメンバに持ちます。

 宣言の文法に従えば,構造体ref型とオブジェクトを同時に指定することもできます。

/* Example 11.3 */

struct ref {
       char author[40];        /* 著者 */
       char title[80];         /* タイトル */
       int  year;              /* 発行年 */
       char resource[120];     /* 出典元 */
} paper, book;

Example 11.2 と同じ意味になります。

 構造体型は集成体型(集合体型)であるため,初期化は配列と同じように波括弧 { } の初期化子を使います。メンバの個数より少なく指定すると,残りは各メンバの型に合わせ 0 がメモリに記憶されます。[C99, 6.7.8, 21]

/* Example 11.4 */

struct ref {
       char author[40];        /* 著者 */
       char title[80];         /* タイトル */
       int  year;              /* 発行年 */
       char resource[120];     /* 出典元 */
} paper = {
       "J. von Neumann",
       "Zur Theorie der Gesellschaftsspiele",
       1928,
       "Mathematische Annalen"
};

struct ref book = {
       "J. von Neumann and O. Morgenstern",
       "Theory of Games and Economic Behavior",
       1944,
       "Princeton University Press"
};
[C言語目次へ]

[この項を1頁で読む (58)]

11.2. メンバ演算子 .

 構造体型オブジェクトはメンバを持ちます。メンバを参照するための演算子は「メンバ演算子」と「ポインタ演算子」の2つがあります。ここでは,メンバ演算子を見ます。

■メンバ演算子(Member Operator)

構造体型オブジェクト.メンバ名
/* Example 11.5 */

#include <stdio.h>

struct ref {
       char author[40];        /* 著者 */
       char title[80];         /* タイトル */
       int  year;              /* 発行年 */
       char resource[120];     /* 出典元 */
};

int main(void)
{
       struct ref paper = {
              "J. von Neumann",
              "Zur Theorie der Gesellschaftsspiele",
              1928,
              "Mathematische Annalen"
       };

       printf("[AU] %s\n", paper.author);
       printf("[TI] %s\n", paper.title);
       printf("[YR] %d\n", paper.year);
       printf("[SO] %s\n", paper.resource);

       return 0;
}

実行結果です。

[AU] J. von Neumann
[TI] Zur Theorie der Gesellschaftsspiele
[YR] 1928
[SO] Mathematische Annalen

paper.author が構造体ref型オブジェクト paper のメンバ author を参照します。それは要素数 40charの配列です。paper.author は配列型です。また,paper.year が構造体ref型オブジェクト paper のメンバ year を参照します。メンバ yearint型なので,paper.yearint型となります。

/* Example 11.6 */

#include <stdio.h>

struct s { char c; double r[3]; } x = {'A', {1.5, -3.0, 2.0}};

int main(void)
{
       printf("c\tr[0]\t\tr[1]\t\t4[2]\n");
       printf("%c\t%f\t%f\t%f\n", x.c, x.r[0], x.r[1], x.r[2]);
       return 0;
}

実行結果です。

c       r[0]            r[1]            4[2]
A       1.500000        -3.000000       2.000000
[C言語目次へ]

[この項を1頁で読む (59)]

11.3. 構造体配列

 文献リストの例に戻りましょう。文献リストは,通常,幾つもの文献が連なることでリストとなります。これを構造体を用いずに作成する場合,

char author[100][60];          /* 著者 */
char title[100][80];           /* タイトル */
int  year[100];                /* 発行年 */
char resource[100][120];       /* 出典元 */

とすることで,100件の文献からなる文献リストとなります。構造体が便利なところは,このような宣言をせずとも,構造体の中身を変えずに,オブジェクトを配列にすることで,リストを作成できることです。

/* Example 11.7 */

#include <stdio.h>
#include <string.h>

struct ref {
       char author[40];        /* 著者 */
       char title[80];         /* タイトル */
       int  year;              /* 発行年 */
       char resource[120];     /* 出典元 */
} paper[100], book[100];

int main(void)
{
       memcpy(paper[0].author,
              "J. von Neumann", sizeof(paper[0].author)-1);
       memcpy(paper[0].title,
              "Zur Theorie der Gesellschaftsspiele", sizeof(paper[0].title)-1);
       paper[0].year = 1928;
       memcpy(paper[0].resource,
              "Mathematische Annalen", sizeof(paper[0].resource)-1);

       printf("[AU] %s\n", paper[0].author);
       printf("[TI] %s\n", paper[0].title);
       printf("[YR] %d\n", paper[0].year);
       printf("[SO] %s\n", paper[0].resource);

       return 0;
}

構造体 ref 自体に変化はありません。1つの文献に対し,それを構成する要素のみを指定しているのみです。その代わり,100件の文献が扱えるように,構造体の宣言において配列を指定しています。それが論文リスト paper[100] や本のリスト book[100] な訳です。構造体配列の個別要素は paper[i] であり,それがもつメンバは paper[i].author と言った具合になります。実行結果は次の通りです。

[AU] J. von Neumann
[TI] Zur Theorie der Gesellschaftsspiele
[YR] 1928
[SO] Mathematische Annalen
/* Example 11.8 */

#include <stdio.h>

struct s {
       int n;
       char str[12];
} x[100] = {
       {1, "first"},
       {2, "second"},
       {3, "third"}
};

int main(void)
{
       int i;
       for (i = 0; x[i].n != 0; i++)
              printf("x[%d] %d\t%s\n", i, x[i].n, x[i].str);
       return 0;
}

実行結果です。(丸括弧内はその文字の ASCIIコード番号です。)

x[0] 1  first
x[1] 2  second
x[2] 3  third

配列の初期化ルールを利用して,x[i].n0 のところで繰り返しから抜けるようにしています。

[C言語目次へ]

[この項を1頁で読む (60)]

11.4. 構造体の構造体

 構造体は,様々な要素から構成されるものを1つのユニットにし,理解しやくされた工夫です。そこで,ユニットを幾つも作り,それらのユニットからなるユニットも考えられます。それが「構造体の構造体」です。

 例えば,文献リストの場合,分野別に分けて作成する方法が考えられます。分野の名称を付してリストを作成するのであれば,文献リストは次の変数を持つことになります。

char   class[32];        /* 分野 (classification) */
struct ref paper[100];   /* この分野の論文リスト */

これら2つの要素をもつユニットも作成できる訳です。

/* Example 11.9 */

#include <stdio.h>
#include <string.h>

struct ref {
       char author[40];        /* 著者 */
       char title[80];         /* タイトル */
       int  year;              /* 発行年 */
       char resource[120];     /* 出典元 */
};

struct list {
       char   class[32];       /* 分野 31字以内 */
       struct ref paper[100];
} list;

int main(void)
{
       memcpy(list.class, "Game Theory", sizeof(list.class)-1);
       memcpy(list.paper[0].author,
              "J. von Neumann", sizeof(list.paper[0].author)-1);
       memcpy(list.paper[0].title,
              "Zur Theorie der Gesellschaftsspiele",
              sizeof(list.paper[0].title)-1);
       memcpy(list.paper[0].resource,
              "Mathematische Annalen", sizeof(list.paper[0].resource)-1);
       list.paper[0].year = 1928;

       printf("[AU] %s\n", list.paper[0].author);
       printf("[TI] %s\n", list.paper[0].title);
       printf("[YR] %d\n", list.paper[0].year);
       printf("[SO] %s\n", list.paper[0].resource);
       printf("[FI] %s\n", list.class);

       return 0;
}

構造体 listchar型配列 class と構造体ref型配列 paper から構成され,構造体list型オブジェクト list が文献リスト全体を意味することとなります。

/* Example 11.10 */

#include <stdio.h>

struct s {
       char c;
       char str[12];
};

struct st {
       int n;
       struct s x;
} y[20] = {
       {1, {'a', "first"}},
       {2, {'b', "second"}},
       {3, {'c', "third"}}
};

int main(void)
{
       int i;
       for (i = 0; y[i].n != 0; i++)
              printf("y[%d] %d %c %s\n", i, y[i].n, y[i].x.c, y[i].x.str);
       return 0;
}

Example 11.10 の実行結果です。

y[0] 1 a first
y[1] 2 b second
y[2] 3 c third
[C言語目次へ]

[この項を1頁で読む (61)]

11.5. ポインタ演算子 ->

 構造体メンバを参照する演算子には既に見た「メンバ演算子」と,もう一つ「ポインタ演算子」があります。ここでは「ポインタ演算子」を見ます。

 ポインタ演算子は,構造体へのポインタよりメンバを参照するものです。次の宣言

struct s {
       int n;
       char str[12];
} x[20], *p;

におけるオブジェクト x は構造体 s 型配列,そして p は構造体 s 型へのポインタです。これに対し,式文

p = x;

が実行されると,ポインタ p に配列 x[20] の第1要素のアドレスが代入されます。(配列型の文法を参照。)間接参照すると *p = x[0] ですが,操作対象の値は x[0] ではなく,そのメンバ x[0].nx[0].str です。したがって,間接演算子を使った値の参照ができません。そこで,ポインタからメンバを直接参照できる仕組みが必要です。それが「ポインタ演算子」です。

■ポインタ演算子(Pointer Operator)

ポインタ -> メンバ名

例えば,上のケースを使えば,p -> n == x[0].n が成り立ちます。したがって,

p -> n = 4;

とすれば,x[0].n4 が代入されます。すなわち,式文

x[0].n = 4;

を実行したことと同じことになります。一般的には,構造体変数 x とメンバ m,そしてポインタ p == &x があったとき,

p -> m == x.m

が成り立つことになります。

/* Example 11.11 */

#include <stdio.h>
#include <string.h>

struct s {
       char n;
       char str[12];
} x[20] = {
       {1, "first"},
       {2, "second"},
       {3, "third"}
};

int main(void)
{
       struct s *p;

       for (p = x; p -> n != 0; p++)
              printf("x[%td] %d %s\n", p - x, p -> n, p -> str);

       p -> n = 4;
       memcpy(p -> str, "fourth", sizeof(p -> str) - 1);
       printf("x[%td] %d %s\n", p - x, p -> n, p -> str);

       return 0;
}

この例の for文は「配列の末尾を探す」の構造体配列版です。繰り返しの条件 p -> n != 0 は,ポインタ p が現在指しているアドレス(配列 x[i] のアドレス)のメンバ n の値が 0 でなければという意味です。また printf内の p - x は「ポインタ同士の差」です。実行すると,次のように出力されます。

x[0] 1 first
x[1] 2 second
x[2] 3 third
x[3] 4 fourth

x[3] に指定した値や文字列が代入されています。

[C言語目次へ]

[この項を1頁で読む (62)]

11.6. 自己参照型構造体

 構造体のメンバに自己へのポインタが入っている構造体を「自己参照型構造体」(自己参照構造体,Self-Referential Structure)と言います。例えば,次のようなケースです。

struct s {
       int n;
       struct s *p;
};

このような構造体に対し,

struct s x, *a;

と宣言すると,構造体変数 xint型変数 n と 構造体 s 型へのポインタ p の2つのメンバを持ちます。したがって,

a = &x;

とした場合,

a -> p

x.p への参照となります。そして,

x.p -> n

x.n を参照することになります。

/* Example 11.12 */

#include <stdio.h>

struct s {
       int n;
       struct s *p;
} x, *a;

int main(void)
{
       a = &x;
       a -> n = 1;
       printf("(1) x.n = %d\t[%p]\n", x.n, &x.n);

       printf("(2) x.p    \t[%p] -> ", x.p);
       a -> p = a;
       printf("[%p]\n", x.p);

       x.p -> n = 2;
       printf("(3) x.n = %d\t[%p]\n", x.n, &x.n);

       return 0;
}

実行結果です。

(1) x.n = 1     [0x20a8]
(2) x.p         [0x0] -> [0x20a8]
(3) x.n = 2     [0x20a8]

角括弧内はアドレスです。NULLポインタであった x.pa -> p = ax のアドレスを指定し(出力 2),ポインタ演算子でメンバ n に値 2 を代入しています。この結果,x.n の値が 1 から 2 に変化している様子が出力 (1) と出力 (3) の比較で分かります。

 自己参照型である利点は,次に見る「リスト型データ構造」や応用編でみる「二分木」の方で活用されます。

[C言語目次へ]

[この項を1頁で読む (63)]

11.7. 自己参照構造体の応用:リスト

 次の構造体を考えましょう。

struct s {
       int n;
       char str[12];
       struct s *p;
};

これは int型変数 nchar型配列 str,そして,自己へのポインタ p をメンバにもつ自己参照型構造体です。それら3つのメンバからなるユニットを「データ」と呼ぶことにしましょう。今,データが次のようにあるとします。(角括弧内はアドレスです。)

データ1            データ2            データ3            データ4
[0x442b0]           [0x442d0]           [0x442f0]           [0x44310]
n = 0               n = 1               n = 2               n = 3
str = if            str = while         str = int           str = char
p = 0x0             p = 0x0             p = 0x0             p = 0x0

現在,どのデータのポインタ p も NULLポインタです。それらを次のようにしてみましょう。データ1のポインタを p = 0x442d0,データ2のポインタを p = 0x442f0,そしてデータ3のポインタを p = 0x44310 としてみます。すると,上の4つのデータは,次のようになります。

データ1            データ2            データ3            データ4
[0x442b0]           [0x442d0]           [0x442f0]           [0x44310]
n = 0               n = 1               n = 2               n = 3
str = if            str = while         str = int           str = char
p = 0x442d0         p = 0x442f0         p = 0x44310         p = 0x0

このようにすると,データ1を先頭に,ポインタ p によって4つのデータが繋がることになります。このような構造をしたデータを「リスト構造」といい,「連結リスト(linked list)」とか「つなぎリスト」と呼ばれています。

/* Example 11.13 */

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

struct list {
       int n;
       char str[12];
       struct list *next;
};

struct list *createData(int, char *);

int main(void)
{
       int i;
       char *keywords[10] = {
              "\0",
              "if",
              "while",
              "for",
              "do",
              "switch",
              "case",
              "break",
              "int",
              "char"
       };

       struct list *head, *new, *data;

       head = createData(0, keywords[0]);
       data = head;
       for (i = 1; i < 10; i++)
       {
              new = createData(i, keywords[i]);
              data -> next = new;
              data = new;
       }

       printf("[Address]\tn\tstr\tnext\n");

       for (data = head; data != NULL; data = data -> next)
              printf("[%p]\t%d\t%s\t%p\n", data, data -> n, data -> str, data -> next);

       exit(0);
}

struct list *createData(int num, char *word)
{
       struct list *data;

       data = (struct list *)malloc(sizeof(struct list));
       if(data == NULL) { exit(1); }

       data -> n = num;
       memcpy(data->str, word, sizeof(data->str)-1);
       data -> next = NULL;

       return data;
}

ポインタ head は,リストの先頭のアドレスを指すために用意したポインタです。また,ポインタ new は新しいデータを指すためのポインタ,そして,ポインタ data は現在のデータのアドレスを指すために用意したポインタです。関数 createData は「ポインタの関数」と「ポインタ関数」の複合型で,この場合,構造体 list型のポインタが従属変数,構造体のメンバ nstr へ代入するための変数が独立変数(仮引数)となっています。

 先ず,リストの先頭となるデータを作ります。そのために,ポインタ head に関数 createData の値を代入します。createData関数は,データを確保するためのメモリ領域を確保し,確保したデータのメンバ nstr,そして next に値を代入し,そのデータを返します。今回,ポインタ以外の構造体変数は,全く用意していません。したがって,ポインタが指すアドレスのメモリ領域を確保する必要があります。そのために,「10.12. ポインタを返す関数」で見た malloc を使っています。

 このようにしてリストの先頭データを確定させた後は,for文による繰り返し処理を使ってリストを作成しています。新しいデータを作成し,現在のデータからその新しいデータへリンクさせ,そして,現在のデータをその新しいデータに書き直す処理の繰り返しです。

 2つ目の for文は,データを示すポインタが head のアドレスのときに繰り返しが始まり(data = head),データが空でない(NULLポインタでない)限り繰り返しを続け,繰り返すとポインタの値を次のデータに移動させるというものです。実行結果は次の通りです。

[Address]       n       str     next
[0x442b0]       0               0x442d0
[0x442d0]       1       if      0x442f0
[0x442f0]       2       while   0x44310
[0x44310]       3       for     0x44330
[0x44330]       4       do      0x44350
[0x44350]       5       switch  0x44370
[0x44370]       6       case    0x44390
[0x44390]       7       break   0x443b0
[0x443b0]       8       int     0x443d0
[0x443d0]       9       char    0x0

ポインタ next が次のデータのアドレスを指している様子が見て取れます。

 この結果を見ると「結局,配列が出来上がるのか,然らば最初から配列を使えば良いのでは」といった疑問がでることでしょう。連結リストの利点は,配列のサイズを指定する必要がなく,メモリ消費を最小に済ませることにあります。例えば,

struct list {
       int n;
       char str[12];
} x[100];

として,構造体変数に配列を指定すると,最初から大きなメモリ領域をとることになります。これに対し,連結リストは,リストの長さが不明な場合にも利用でき,必要なメモリのみを消費します。

[C言語目次へ]

[この項を1頁で読む (64)]

12. 型定義 typedef

 C言語の特徴であるデータを区別するをプログラマが分かりやすい名称に定義し直すことができます。そのために用意されたキーワード(予約語)が typedef です。

■型定義(Type Definition)

typedef T my_type;

識別子my_type が型T を意味することになります。

* キーワード typedef は,便宜上,記憶域クラス指定子に含まれている。[C99, 6.7.1, 3] この結果,上は「式文」ではなく「宣言」に分類される。

次の場合,BOOLEANint を意味します。

typedef int BOOLEAN;
BOOLEAN true = 1;
BOOLEAN false = 0;

truefalseint型です。次の WEEK は要素数 7intの配列を意味します。

typedef int WEEK[7];
WEEK Jan1st = {4, 5, 6, 7, 8, 9, 10};

オブジェクト Jan1st は要素数 7int型配列です。

typedef struct list {
       int n;
       char str[20];
       struct list *next;
} LIST;

LIST *head;

LISTstruct list型を意味し,したがって,ポインタ headstruct list型へのポインタです。

[C言語目次へ]

[この項を1頁で読む (65)]

13. 標準入力,標準出力(Standard I/O)

 標準入出力,標準エラー出力,ファイル入出力のために用意されたライブラリ(標準ヘッダ,Standard Header)が stdio.h です。ファイル操作(例えば,ファイルオープン,ファイルクローズ,リネーム,削除など)を行う関数も用意されていますが,ここでは,標準出力,標準エラー出力,標準入力の方法を見ます。

*「標準入出力」「標準エラー出力」自体については「UNIX入門」を参照ください。

[C言語目次へ]

[この項を1頁で読む (66)]

13.1. 標準出力と標準エラー出力:put系関数,printf系関数

 ライブラリ stdio.h で出力用に用意された関数は数多くあります。次はその一部です。

関数(プロトタイプ:2段目)出力されるもの出力先
fputc(文字定数, ストリーム)
int fputc(int c, FILE *stream);
文字定数
c はASCIIコード)
ストリーム
putc(文字定数, ストリーム)
int putc(int c, FILE *stream);
文字定数ストリーム
putchar(文字定数)
int putchar(int c);
文字定数標準出力
fputs(文字列リテラル, ストリーム)
int fputs(const char * restrict s, FILE * restrict stream);
文字列リテラルストリーム
puts(文字列リテラル)
int puts(const char *s);
文字列リテラル標準出力
fprintf(ストリーム, フォーマット, 出力元)
int fprintf(FILE * restrict stream, const char * restrict format,...);
フォーマットストリーム
printf(フォーマット, 出力元)
int printf(const char * restrict format,...);
フォーマット標準出力
snprintf(char型配列, 文字数, フォーマット, 出力元)
int snprintf(char * restrict s, size_t n, const char * restrict format,...);
フォーマット
但し,
 文字数 - 1
文字まで。
char型配列
sprintf(char型配列, フォーマット, 出力元)
int sprintf(char * restrict s, const char * restrict format,...);
フォーマットchar型配列

* プロトタイプ内の constrestrict は後に学びます。また,FILE はライブラリ stdio.htypedef によって型定義された「型」です。

ストリーム とは出力先です。ライブラリ stdio.h には次の三つが用意されています。[C99, 7.19.1, 3]

stdout標準出力
stderr標準エラー出力
stdin標準入力

例えば,printf は,fprintfストリームstdout を指定したのと同じです。同じことは,putcputchar についても言えます。[C99, 7.19.6.3, 7.19.7.9]

put出力先
ストリーム標準出力
出力文字fputcputcputchar
文字列fputsputs
printf出力先
ストリーム標準出力文字列
出力フォーマットfprintfprintfsnprintf
sprintf

大きくは put系と printf系に分けられ,前者は指定された文字,あるいは文字列を出力しますが,後者はフォーマットを出力します。「フォーマット」とは,変換指定子を含む文字列のことです。変換指定子は出力元の変数の出力スタイルを指定します。

■変換指定子(conversion specifier)[C99, 7.19.6.1]

/* Example 13.1 */

#include <stdio.h>

int main(void)
{
       char str[] = "ABCDEF";
       char *p;

       for(p = str; *p; p++)
              printf("%td\t%s\t%c\t(%u, %o, %x)\t[%p]\n",
                     p - str, p, *p, *p, *p, *p, p);

       return 0;
}

実行結果です。(丸括弧内は ASCIIコード番号,角括弧内はアドレスです。)

0       ABCDEF  A       (65, 101, 41)   [0xbffffa98]
1       BCDEF   B       (66, 102, 42)   [0xbffffa99]
2       CDEF    C       (67, 103, 43)   [0xbffffa9a]
3       DEF     D       (68, 104, 44)   [0xbffffa9b]
4       EF      E       (69, 105, 45)   [0xbffffa9c]
5       F       F       (70, 106, 46)   [0xbffffa9d]
/* Example 13.2 */

#include <stdio.h>

int main(void)
{
       fprintf(stdout, "This is a test.\n");
       fprintf(stderr, "Error!\n");
       return 0;
}

標準エラー出力されているか否かを実験してみます。(太字部分が入力した箇所です。)

% ./a.out
This is a test.
Error!
% ./a.out > tmp.txt
Error!
% cat tmp.txt
This is a test.
%

2回目の実行でファイル tmp.txt へ出力をリダイレクトすると,エラーメッセージのみが出力されています。そして,tmp.txt には標準出力部分のみが書き込まれています。

[C言語目次へ]

[この項を1頁で読む (67)]

13.2. 標準入力:argc, argv, scanf, fscanf

 次は,標準入力です。標準入力には,コマンドラインからの入力と相手にたずねる場合の2通りがあります。

■コマンドラインからの入力

int main(int argc, char *argv[]) { /* 本体 */ }

変数 argc には,コマンドラインに入力(標準入力)された文字列の個数(argument count)が入ります。また,argv[argc] は NULLポインタ(空ポインタ)であり,argc0 より大きい場合,argv[0] にはプログラム名が入ります。[C99, 5.1.2.2.1](argv は「ポインタの配列」です。)例えば,

% ./a.out this is a test.

と実行した場合,argc = 5 となり,argv[0] には ./a.outargv[1] には this,そして以下順番に argv[2]isargv[3]aargv[4]test. が入り,そして argv[5] = NULL となります。

/* Example 13.3 */

#include <stdio.h>

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

       return 0;
}

実行例です。(太字部分が入力箇所です。)

% ./a.out this is a test.
argv[0] = ./a.out
argv[1] = this
argv[2] = is
argv[3] = a
argv[4] = test.

■相手にたずねる場合

ライブラリ stdio.h には入力用に用意された関数も多数あります。次はそのごく一部です。

関数(プロトタイプ:2段目)入力元
fscanf(ストリーム, フォーマット, 記憶先のアドレス)
int fscanff(FILE * restrict stream, const char * restrict format,...);
ストリーム
scanf(フォーマット, 記憶先のアドレス)
int scanf(const char * restrict format,...);
標準入力
sscanf(char型配列, フォーマット, 記憶先のアドレス)
int sscanf(char * restrict s, const char * restrict format,...);
char型配列

これらは printf関数系の入力版です。返却値は記憶された個数です。フォーマット 内に指定する変換指定子は,double型変数の読み込みの場合に限り %lf を使用することを除き,printf で見た変換指定子が使えます。その他には,UNIXにおける正規表現と同じ変換指定子もあります。

■その他の変換指定子
nddn 桁の読み込み,または出力。df などにも適用可。printf系,scanf系の双方。
.nff で小数点 n 桁の出力。printf系のみ。
[a-n]文字 a から n に適合する場合scanf系のみ。
[^a-n]文字 a から n に適合しない場合scanf系のみ。
*適合するものをスキップscanf系のみ。
/* Example 13.4 */

#include <stdio.h>

int main(void)
{
       int n = 0, s = 0;
       double x = 0.0;
       char str[12] = "\x00";

       s = scanf("%4d%lf%11s", &n, &x, str);
       printf("n = %d, x = %f, str = %s (%d)\n", n, x, str, s);

       return 0;
}

%4d は数字4桁までを n に格納します。%lfdouble型データへの格納です。%11s は文字列のうち11文字までを str へ格納します。これは sizeof(str)12 なので,バッファ・オーバーフローを起こさないためです。

 実行例です。丸括弧内の数値は scanf の戻り値です。(太字部分が入力箇所です。)

% ./a.out
123456ABC
n = 1234, x = 56.000000, str = ABC (3)
% ./a.out
123.456ABC
n = 123, x = 0.456000, str = ABC (3)
% ./a.out
12 3.456 ABC
n = 12, x = 3.456000, str = ABC (3)

付言すると,「10.1. 配列型とメモリ領域」で見たように,配列型(式)str は配列の第1要素を指し示すポインタであり,scanf の第2変数以降はすべてアドレスを指定しています。

/* Example 13.5 */

#include <stdio.h>
#include <string.h>

int main(void)
{
       int s = 0;
       char x[4], y[4], z[4];

       memset(x, 0, sizeof(x));
       memset(y, 0, sizeof(x));
       memset(z, 0, sizeof(x));

       s = fscanf(stdin, "%3[a-z]%*[a-z]%3[A-Z]%*[A-Z]%3[0-9]", x, y, z);
       printf("x = %s, y = %s, z = %s (%d)\n", x, y, z, s);

       return 0;
}

この例では,配列 xyz が3文字までのため,4文字目以降の該当文字列を切り捨てる(次の配列に読み込まれない)ように %*[ ] で余分な文字列をスキップします。しかしながら,もし該当の文字列が3文字以内の場合,残りは捨てられます。

% ./a.out
opqrstuVWXYZ98765
x = opq, y = VWX, z = 987 (3)
% ./a.out
abcdeABC0123
x = abc, y = ABC, z =  (2)
[C言語目次へ]

[この項を1頁で読む (68)]

14. 文(Statements)

 ソースファイルに書き込む字句(トークン,Token)は,

まとりとなります。この内,整数型や浮動小数点型の実数型配列型ポインタ型構造体型関数型宣言,各種演算子と,そして,関数の定義を見ました。ここでは「文」について見ます。

 「文」には次の種類があります。

ここでは,既に見た returnを除く文の文法を見ます。

[C言語目次へ]

[この項を1頁で読む (69)]

14.1. 複合文

 「複合文」(Compound Statement)とは「ブロック」{ } のことです。

/* Example 14.1 */

#include <stdio.h>

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

これは Example 7.9複合代入演算子を使って書き直したものです。このソースには2つの複合文があります。一つは main関数の定義に使われているブロック { }

int main(void)
{

}

そして for文に使われているブロック { } です。

for(i = 0; i <= 10; i++)
{

}

関数の定義に使うブロック { } は「複合文」です。[C99, 6.9.1]

[C言語目次へ]

[この項を1頁で読む (70)]

14.2. 式文

 「式文」(Expression Statement)とは,

;

のことです。特に, がない式文を「空文」(ナル文,NULL文,Null Statement)と言います。「空文」は何も実行しません。[C99, 6.8.3, 3]

void f(int x);  // 宣言
int x = 2;      // 宣言
;               // 空文
f(x);           // 式文
x = 3;          // 式文

1つ目はプロトタイプ宣言,2つ目はオブジェクト x の宣言,4つ目は関数呼出し(式)を実行する文,5つ目は代入式 x = 3 を実行する文。

[C言語目次へ]

[この項を1頁で読む (71)]

14.3. if

 選択文(Selection Statement)には if文と switch文があり,いずれも条件分岐します。ここでは if文を見ます。

if文(if Statement)

if () 
if ()  else 

条件 が真であるとき,続く を実行します。else がある場合には条件 が偽のときに else の後の を実行します。 とは,複合文式文if文などの「文」のことです。

 関係演算子論理演算子より,条件 が真であるとは0 以外のときとなります。

/* Example 14.2 */

#include <stdio.h>

int main(void)
{
       int x = 0;
       printf("Input an integer: ");
       scanf("%d", &x);
       if(x)
              printf("Ok, x = %d.\n", x);
       else
       {
              printf("Input a nonzero integer: ");
              scanf("%d", &x);
              printf("Well, x = %d.\n", x);
       }
       return 0;
}

この例では式 x が真(0 以外)であれば,続く式文を実行し,偽の場合には else の後の複合文を実行します。次は実行例です。(太字部分が入力箇所です。)

% ./a.out
Input an integer: 0
Input a nonzero integer: 7
Well, x = 7.
% ./a.out
Input an integer: 9
Ok, x = 9.
[C言語目次へ]

[この項を1頁で読む (72)]

14.4. switch

 選択文の内,もう一つの switch文を見ましょう。

switch文(switch Statement)

switch ()
{
case 整数定数式1 : 
case 整数定数式2 : 
/* ... */
default: 
}

casedefault は「ラベル」(Label)と呼ばれ,

ラベル 式 : 

は「ラベル付き文」(名札付き文,ラベル文,Labeled Statement)となります。

 switch文は,条件 とマッチする 整数定数式x がある caseラベル文を実行し,defaultラベル文を実行します。ケースマッチがなく,defaultラベル文もない場合,何も実行しません。

/* Example 14.3 */

#include <stdio.h>

int main(void)
{
       int x = 0;
       printf("Input an integer: ");
       scanf("%d", &x);
       switch(x)
       {
       case 0:
              printf("Input a nonzero integer: ");
              scanf("%d", &x);
              printf("Well, x = %d.\n", x);
              break;
       default:
              printf("Ok, x = %d.\n", x);
       }
       return 0;
}

この例は,Example 14.2if文を switch文に書き換えたものです。ケースマッチがあったときに defaultラベル文を実行させないためには breakを入れます。

 次の例は,while文や for文の知識を必要としますが,switch文の応用の代表的なものなので,ここで示しておきます。UNIX のコマンドに見られるオプションを組み入れたプログラムです。(難しいようでしたら,for文まで読んだ後,再度,トライしてみてください。)

/* Example 14.4 */

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

int main(int argc, char *argv[])
{
       int c;
       char *s;
       int lflag = 0;
       int rflag = 0;

       while ((c = getopt(argc, argv, "lr")) != -1)
              switch((char)c) {
              case 'l':
                     lflag = 1;
                     break;
              case 'r':
                     rflag = 1;
                     break;
              default:
                     fprintf(stderr, "usage: command [-lr] [keyword]\n");
                     exit(1);
              }

       argv += optind;

       if (!argv[0])
              exit(1);

       s = argv[0];

       if (lflag) {
              printf("- l mode -\n");
              for(; *s; s++)
                     printf("%c (%d)\n", *s, *s);
       }

       if (rflag) {
              printf("- r mode -\n");
              for(; *s; s++);
              --s;
              for(; *s; s--)
                     printf("%c (%d)\n", *s, *s);
       }

       exit(0);
}

関数 getopt はヘッダファイル unistd.h に入っているものです。実行時にオプションを付すと,それを読み込み,第3変数に示した文字がない場合にはエラーを標準エラー出力として返します。変数 optind は実行時に付されたオプションの数に1を加えた数値です。

 実行結果です。(太字部分が入力箇所です。)

% ./a.out
% ./a.out -z
./a.out: illegal option -- z
usage: command [-lr] [keyword]
% ./a.out -l abc
- l mode -
a (97)
b (98)
c (99)
% ./a.out -r abc
- r mode -
c (99)
b (98)
a (97)
% ./a.out -lr abc
- l mode -
a (97)
b (98)
c (99)
- r mode -
c (99)
b (98)
a (97)
% ./a.out -r -l abc
- l mode -
a (97)
b (98)
c (99)
- r mode -
c (99)
b (98)
a (97)
% 

[ノート]getopt のより詳しい説明は,マニュアルを参照して下さい。

% man 3 getopt
man: Formatting manual page...
GETOPT(3)               System Library Functions Manual              GETOPT(3)

NAME
     getopt - get option character from command line argument list

SYNOPSIS
     #include <unistd.h>

     extern char *optarg;
     extern int optind;
     extern int optopt;
     extern int opterr;
     extern int optreset;

     int
     getopt(int argc, char * const *argv, const char *optstring);

DESCRIPTION
     The getopt() function incrementally parses a command line argument list
     argv and returns the next known option character.  An option character is
     known if it has been specified in the string of accepted option charac-
     ters, optstring.

     The option string optstring may contain the following elements: individ-
     ual characters, and characters followed by a colon to indicate an option
     argument is to follow.  For example, an option string "x" recognizes an
     option ``-x'', and an option string "x:" recognizes an option and argu-
     ment ``-x argument''.  It does not matter to getopt() if a following
     argument has leading white space.
/tmp/man.000854 (23%)
[C言語目次へ]

[この項を1頁で読む (73)]

14.5. while

 次は「繰り返し文」(Iteration Statement)を見ましょう。繰り返し文には while文,do文,そして for文があります。ここでは,while文を見ます。

while文(while Statement)

while () 

while文は,条件 が真のとき, を実行します。ifでも確認したように,条件 が真とは 0 でないときとなります。 とは,複合文,空文を含む式文ifなどの「」です。

/* Example 14.5 */

#include <stdio.h>

int main(void)
{
       int sum = 0, x[6] = {0}, *p = x;
       printf("Input integers: ");
       scanf("%d%d%d%d%d", x, x + 1, x + 2, x + 3, x + 4);
       while(*p)
              sum += *p++;
       printf("sum = %d\n", sum);
       return 0;
}

これは,配列 x[6] に5つの数字を格納し,その和を求めるものです。scanf では5つの数字を読み込むので,x[5] = *(x + 5) は必ず 0 となります。while文は *p が真のとき式文 sum += *p++; を実行します。x + 5 までポインタ p が増加すると,繰り返し処理から抜けます。次は実行例です。(太字部分が入力箇所です。)

% ./a.out
Input integers: 1 2 3 4 5 6 7 8 9 10
sum = 15

5までの和となります。

[C言語目次へ]

[この項を1頁で読む (74)]

14.6. do

 次は,繰り返し文の中の do文を見ます。

do文(do Statement)

do  while ();

do文は, を実行してから を評価します。whileとは, の実行と条件 の評価が逆です。

/* Example 14.6 */

#include <stdio.h>

int main(void)
{
       int sum = 0, x[6] = {0}, *p = x;
       printf("Input integers: ");
       scanf("%d%d%d%d%d", x, x + 1, x + 2, x + 3, x + 4);
       do
              sum += *p;
       while(*++p);
       printf("sum = %d\n", sum);
       return 0;
}

これは Example 14.5while文を do文で書き直したものです。

[C言語目次へ]

[この項を1頁で読む (75)]

14.7. for

 3種類の繰り返し文の中の最後,for文を見ましょう。

for文(for Statement)

for ( 式1 ; 式2 ; 式3 ) 

for文は,式1 を最初に評価し,式2 が真であれば を実行後 式3 を評価します。式1 が繰り返しの初期条件,式2 が繰り返す条件,そして 式3 が条件変更と解釈できます。while文で書き直すと,

式1;
while(式2)
{
     
     式3;
}

となります。

/* Example 14.7 */

#include <stdio.h>

int main(void)
{
       int sum = 0, x[6] = {0}, *p = NULL;
       printf("Input integers: ");
       scanf("%d%d%d%d%d", x, x + 1, x + 2, x + 3, x + 4);
       for(p = x; *p; p++)
               sum += *p;
       printf("sum = %d\n", sum);
       return 0;
}

これは Example 14.5while文を for文で書き直したものです。実行結果は同じになります。

 次の例は,文字列 str[] を文字列 key[] で分割し,それをポインタの配列 *s[] に格納するものです。

/* Example 14.8 */

#include <stdio.h>
#include <string.h>

int main(void)
{
       char str[] = "ABCD\tabc\t123";
       char key[] = "\t";
       char *s[10];
       char **z;

       z = s;
       *z = strtok(str, key);
       ++z;
       for ( ; (*z = strtok(NULL, key)); z++);

       for (z = s; *z; z++)
              printf("%s\n", *z);

       return 0;
}

実行結果は次の通りです。文字列 ABCD\tabc\t123 がタブ \t で分割されている様子が分かるかと思います。

ABCD
abc
123

ここでは,NULLポインタで for 文の繰り返しから抜けます。strtok 関数はヘッダファイル string.h に入っている関数で,第1変数の文字列から第2変数の文字列が現れるまでの文字列を返します。もしこの処理に失敗すると NULLポインタを返します。成功すれば,残りの文字列から探索します。strtok 関数のソースファイル(原物)は,練習問題で取り上げますので,そこで動作を確認してください。

 次の例は 3 から 100 の間の自然数から素数を抽出するプログラムです。3 からの数を順番に素数であるものを int 型配列 p に格納します。for文と論理演算子を組み合わせて素数で割り切れる数をスキップします。また,100 の中から 2 を除く素数を抽出するので配列 p のサイズは 50 としています。素数 2 は抽出しないことを前提にしている訳です。(2 も抽出するプログラムは読者自身で考えてください。また,理論的には unsigned long long を使って 264 までの素数を計算できますが,メモリ消費や処理に無駄のあるプログラムです。)

/* Example 14.9 */

#include <stdio.h>

int main(void)
{
       int n = 0;
       int p[50] = {2};
       int *v = NULL;

       for (n = 3; n <= 100; n++)
       {
              for (v = p; *v && (n % *v); v++);
              if (!*v && v - p < 50)
                     *v = n;
       }

       for (v = p; *v; v++)
              printf("%d ", *v);

       putchar('\n');
       printf("number of prime numbers = %td\n", v - p);

       return 0;
}

実行結果です。

2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 
number of prime numbers = 25
[C言語目次へ]

[この項を1頁で読む (76)]

14.8. goto

 最後に「分岐文」(Jump Statement)を見ましょう。分岐文(ジャンプ文)には continue文,break文,goto文,そして return文があります。ここでは,goto文を見ます。

goto文(goto Statement)

goto ラベル;

goto文は,同一の関数内にあるラベル文

ラベル : 

へ処理を分岐(ジャンプ)させます。

/* Example 14.10 */

#include <stdio.h>

int main(void)
{
       unsigned char x = 0;

call:
       printf("Ok, input a character: ");
       scanf("%c", &x);

       if(x < 32 || x > 126)
              goto call;

       printf("x = %c (%d)\n", x, x);

       return 0;
}

入力が ASCIIコード 32 から 126 までの文字以外の場合,再入力画面がでます。実行例です。

% ./a.out 
Ok, input a character: (リターンキー)
Ok, input a character: A
x = A (65)
[C言語目次へ]

[この項を1頁で読む (77)]

14.9. continue文と break

 分岐文(ジャンプ文)の内,continue文と break文を見ましょう。

continue文(continue Statement)

continue ;

continue文は,while文,do文,for文の「繰り返し文」において,繰り返し対象の の末尾にジャンプさせます。goto文を使った右側と同じです。

while(x)              while(x)
{                     {
       // 処理               // 処理
       continue;             goto cont;
       // 処理               // 処理
                      cont: ;
}                     }

すなわち,繰り返し処理において,continue 以下を飛ばすこととなります。

break文(break Statement)

break ;

break文は,while文,do文,for文の「繰り返し文」と switch文において,処理を止めます。すなわち,繰り返し処理から抜けます。

 次の例は,2の倍数でないものをスキップし,スキップしなかった数値が3の倍数ならばループから抜けるものです。(すなわち,2と3の倍数であったとき,繰り返し処理から抜けます。)

/* Example 14.11 */

#include <stdio.h>

int main(void)
{
       int x[10] = {8, 5, 14, 10, 9, 12, 16, 7, 2, 22};
       int flag2 = 0, flag3 = 0;
       int *p = NULL;

       for (p = x; *p; p++) {
              flag2 = *p % 2;
              if (flag2)
                     continue;

              printf("x[%td] = %d\n", p - x, *p);

              flag3 = *p % 3;
              if (!flag3)
                     break;
       }

       ++p;
       printf("Process terminates at the %tdth element.\n", p - x);
       return 0;
}

実行結果です。

x[0] = 8
x[2] = 14
x[3] = 10
x[5] = 12
Process terminates at the 6th element.
[C言語目次へ]

[この項を1頁で読む (78)]

15. プリプロセッサ (#directive) とマクロ

 ソースファイルに書き込む字句(トークン,Token)は,

まとりとなります。この内,整数型や浮動小数点型の実数型配列型ポインタ型構造体型関数型宣言,各種演算子と,そして,関数の定義を見ました。ここでは「プリプロセッサ」について見ます。

 前処理(プリプロセッシング,Preprocessing)とは,コンパイラがアセンブリ言語に翻訳する前に,ソースファイルの一部を条件的にスキップしたり,他のソースファイル(ヘッダファイル)を読み込んだり,マクロを置き換えるための処理で,「プリプロセッサ」(前処理指令,プリプロセッシング・ディレクティブ,Preprocessing Directive)はそのための命令を指します。

 プリプロセッサは,字句要素の中の区切り子 # で始まり,改行で終わります。プリプロセッサで定義された定数や関数をマクロ(Macro)と呼びます。

■プリプロセッサ
#include <file>備え付けのヘッダファイル file の読み込み。/usr/include 内のファイル。
#include "myheader"自らが作成したヘッダファイル myheader の読み込み。
#if 条件if 文の始まり。#endif とペア。
#endifif 文の終わり。#if とペア。
#elseif 文における分岐。
#elif 条件if 文における分岐。
#define マクロマクロ を定義。
#undef マクロ#define マクロ で定義された マクロ を無効にする。
#ifdef マクロマクロ が定義されていれば」という意味。#if defined と同じ。
#ifndef マクロマクロ が定義されていなければ」という意味。#if !defined と同じ。

ヘッダファイルの読み込みについては,コンパイル時に I オプションを付してヘッダのあるディレクトリを追加することができます。

cc -I /path/to/directory1/ -I /path/to/directory2/ ...

この場合,自らが作成したヘッダファイルのあるディレクトリを追加すれば,#include "myheader" ではなく #include <myheader> としても,myheader を読み込めます。

/* Example 15.1 */

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

#ifndef PI
#define PI 3.14159
#endif

#ifndef E
#define E  2.71828
#endif

int main(void)
{
       printf("PI = %f\n", PI);
       printf("E = %f\n", E);
       printf("max{PI,E} = %f\n", MAX(PI,E));
       printf("min{PI,E} = %f\n", -MAX(-PI,-E));

       return 0;
}

2つ目の #include で独自に作成したヘッダファイル def.h を読み込んでいます。その def.h のソースは次の通りです。

/* def.h */

#include <math.h>

#define PI 4*atan(1.0)
#define E  exp(1.0)
#define MAX(x,y) (x>y?x:y)

def.h の1つ目の #include でヘッダファイル math.h を読み込んでいます。ヘッダファイル math.h には,関数 expatan が入っています。最初の #define では円周率を意味するマクロ PI4*atan(1.0) と定義しています。2つ目の #define では,マクロ E を自然対数の底と定義しています。3つ目の #define では MAX というマクロを定義しています。2つの変数の最大値を出す関数です。def.h で定義された PIE が定義されていない場合を想定して,元のソースファイルには #ifndef で再定義しています。実行結果は,次の通りです。

PI = 3.141593
E = 2.718282
max{PI,E} = 3.141593
min{PI,E} = 2.718282

def.h で定義された定数や関数は,正しく読み込まれているのが確認できます。(ソースを分割せずに,def.h の中身を元のソースの main 関数の上に入れ,#ifndef 部分を削除しても良いです。ここでは,例証のために分割しました。上のようなヘッダファイルの利用法とは別に,プログラムをモジュール化して行く段階で作成するヘッダファイルもあります。これについては「16. プログラムの分割とリンク」を参照して下さい。)

 上では,PIEMAX というマクロを定義しましたが,マクロはコンパイラによって既に定義済みのものがあります。それらを変更することはできません。

■定義済みマクロ[C99, 6.10.8, 1の一部]
__LINE__     行番号。
__FILE__ソースファイル名。
__DATE__ソースファイルの変更日。mmm dd yyyy の形式で出力。
__TIME__ソースファイルの変更時間。hh:mm:ss の形式で出力。
__STDC__C99準拠のコンパイラならば 1 を出力。
/* Example 15.2 */

#include <stdio.h>

int main(void)
{
       printf("LINE = %d\n", __LINE__);
       printf("FILE = %s\n", __FILE__);
       printf("DATE = %s\n", __DATE__);
       printf("TIME = %s\n", __TIME__);
       printf("STDC = %d\n", __STDC__);

       return 0;
}

実行結果です。(機械に依存する部分があります。)

LINE = 7
FILE = ex15.2.c
DATE = 09/24/02
TIME = 13:47:17
STDC = 1
[C言語目次へ]

[この項を1頁で読む (79)]

16. プログラムの分割とリンク

 マクロ集やコンパイル条件集としてのヘッダファイルを作成する方法は見ました。ここでは「モジュール化」という立場からのプログラムの分割を見ます。

 プログラムを作成する上で,機能や目的,役割別に関数を作成することはプログラムを構造化する上で必要不可欠です。しかし,関数の数が増加するにつれ,ソースファイルは次第に変更箇所を探すのも難しくなる程膨張します。そこで,一つの役割を主題にした関数群を別のソースファイルにまとめることが必要になってきます。そのようなソースファイルの分割を「モジュール化」と呼んでいます。

■プログラムの分割:例

分割前のプログラムが次のようになっていたとしましょう。

/* Example 16.0 */

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

void myPrint(char *);

int main (void)
{
       char str[8] = "test";
       myPrint(str);
       exit(0);
}

void myPrint(char *x)
{
       printf("This is a %s.\n", x);
}

このプログラムを,メインの処理部分(例えば,main.c というファイル)とプリント関連部分(例えば,myPrint.c というファイル)に分けてみます。

/* main.c */

#include <stdlib.h>

void myPrint(char *);

int main (void)
{
       char str[8] = "test";
       myPrint(str);
       exit(0);
}
/* myPrint.c */

#include <stdio.h>

void myPrint(char *x)
{
       printf("This is a %s.\n", x);
}

このような分割で main.c の役割と myPrint.c の役割分担が可能となり,ソースのメインテナンスが容易になります。実際,各々のソースファイルをコンパイルすることができます。

% cc -c main.c
% cc -c myPrint.c

このコンパイルによってオブジェクト・ファイル main.omyPrint.o が出来上がります。出来上がったオブジェクト・ファイルから実行ファイルを作ってみます。

% cc main.o myPrint.o

この作業によって,main.c に使われている myPrint関数は myPrint.c で定義されている myPrint関数になります。これを「リンク」(Link)と言っています。ここでは,関数だけでなく,オブジェクト(変数)についてもリンクさせるためのプログラミングを見ます。

* UNIX系 OS では,オブジェクトファイル間のリンクを行っているのはコンパイラではなく ld である。man ld を参照。

[C言語目次へ]

[この項を1頁で読む (80)]

16.1. 有効範囲(スコープ)

 変数名,関数名,ラベル,タグ,構造体や列挙体のメンバといった「識別子」(Identifiers)は,プログラム内の宣言された場所によって,プログラム内で使用可能な範囲が異なります。使用可能な範囲を有効範囲(スコープ,Scope)と言い,使用可能なときその識別子は可視(visible)であると言います。

■有効範囲(Scope)[C99, 6.2.1]

あるスコープの中に他のスコープがあると,前者は後者の外部有効範囲(アウター・スコープ,outer scope),後者は前者の内部有効範囲(インナー・スコープ,inner scope)となります。外部側で宣言された変数は内部側でも有効(可視)ですが,内部側にも同一名で変数宣言がある場合には,内部側では内部で宣言した方が有効となり,外部側で宣言した方は内部側では隠れた状態(hidden)となります。(内部側の変数が外部側の変数を隠すという。)

/* Example 16.1 */ // ファイル・スコープの始まり

void f(int x);     // プロトタイプ・スコープ: ) まで
double y;

int main(void)
{                  // ブロック・スコープ1の始まり
       int n;
       {           // ブロック・スコープ2の始まり
              char c;
              double y;

       }           // ブロック・スコープ2の終わり

}                  // ファイル・スコープとブロック・スコープ1の終わり

ブロック・スコープ1はファイル・スコープの内部,ブロック・スコープ2はファイル・スコープとブロック・スコープ1の内部になります。f,1つ目の ymain はファイル・スコープ,x はプロトタイプ・スコープ,n はブロック・スコープ1,c と2つ目の y はブロック・スコープ2の有効範囲を持ちます。c はブロック・スコープ2の外部では可視ではありません。同様のことは n についても言えます。n はブロック・スコープ1の外部では可視ではありません。一方,2つ目の y は1つ目の y をブロック・スコープ2の範囲で隠します。

[C言語目次へ]

[この項を1頁で読む (81)]

16.2. 外部定義

 すべての関数の外側での宣言を外部宣言(External Declaration)と言い,外部宣言によるオブジェクトや関数の定義を外部定義(External Eefinition)と言います。[C99, 6.9, 1 & 4]

既に見た関数の定義は外部定義です。また,ファイル・スコープをもつオブジェクト初期化は,そのオブジェクトの外部定義です。[C99, 6.9.2, 1]

/* Example 16.2 */

#include <stdio.h>

int n = 1;

int main(void)
{
       int m = 2;
       printf("n = %d, m = %d\n", n, m);
       return 0;
}

最初の変数 n の宣言は,外部定義です。この変数はファイル・スコープを持つため,このソースファイル内では可視です。2つ目の変数 mブロック・スコープを持つので,外部定義ではありません。main関数の本体,複合文内が有効範囲です。

 実行結果です。

n = 1, m = 2

* 外部宣言されたオブジェクトを「外部変数」と呼ぶ本もある。

[C言語目次へ]

[この項を1頁で読む (82)]

16.3. リンケージ(結合)

 プログラム中に同じオブジェクト名や関数名で複数の宣言があるとき,それらが同一のオブジェクト,或いは関数として参照されることをリンケージあるいは結合と言います。

■結合(リンケージ,Linkage)[C99, 6.2.2]

リンケージは,有効範囲と記憶域クラスで決まります。

記憶域クラスデータ有効範囲リンケージ
指定なしオブジェクトファイル・スコープ外部結合
ブロック・スコープなし
関数ファイル・スコープexternと同じ。
ブロック・スコープ
staticオブジェクトファイル・スコープ内部結合
ブロック・スコープなし
関数ファイル・スコープ内部結合
externオブジェクト,
関数
ファイル・スコープ可視な宣言済みの同一識別子がリンケージをもつ場合,それと同じリンケージを持たせる。可視な宣言済みの同一識別子がない,或いは存在してもそれがリンケージを持たない場合,外部結合を持つこととなる。
ブロック・スコープ
* 「ファイル・スコープ」,「ブロック・スコープ」,「可視」については「16.1. 変数の有効範囲」を参照。

extern を付さないブロック・スコープのオブジェクトは,リンケージなし(無結合,No Linkage)となります。[C99, 6.2.2, 6]

■リンケージがある場合の初期化ルール

/* Example 16.3 */

char c = 'A';
static double pi = 3.14;

int main(void)
{
       extern double x;
       char c;
       static long l;
}

最初の char型変数 c はファイル・スコープを持ち,記憶域クラスの指定がありません。したがって,外部結合を持ちます。double型変数 pi はファイル・スコープを持ち,記憶域クラスには static を指定しています。したがって,内部結合をもちます。外部結合はないので,宣言されているソースファイルのみのリンケージとなります。変数 x は,宣言済みの同一変数がないので外部結合を持ちます。これには外部定義が必要です。また,ブロック・スコープを持つので初期化はできません。main関数内の変数 c は,3行目で宣言している変数 c を隠すので,それとは異なる無結合変数として認識されます。同一と認識させるには,変数の宣言を削除するか,3行目の変数 c がリンケージを持っているので extern を付せば良いです。変数 l は記憶域クラスに static を付していますが,ブロック・スコープをもつため,リンケージをもちません。

[C言語目次へ]

[この項を1頁で読む (83)]

16.4. オブジェクトの寿命:静的記憶域期間と自動記憶域期間

 変数が値を記憶するためのメモリ領域を確保している期間をその変数の「寿命」(lifetime)と言います。変数の寿命は,プログラム内でメモリの割り当て(allocate)をしない限り,次の2つとなります。[C99, 6.2.4]

■記憶域期間(Storage Duration)

今後は,自動記憶域期間をもつ変数を「自動変数」,静的記憶域期間をもつ変数を「静的変数」と略称することとします。

 初期化は,記憶域期間によってルールが異なります。

/* Example 16.4 */

#include <stdio.h>

int n;
static double r = 3.14;

int main(void)
{
       printf("n = %d\n", n);
       printf("r = %f\n", r);
 
       {
              extern double r;
              int n = 2;
              static long l = 1234567890L;

              printf("n = %d\n", n);
              printf("r = %f\n", r);
              printf("l = %ld\n", l);
       }

       printf("n = %d\n", n);
       printf("r = %f\n", r);
       printf("l = %ld\n", l); /* コンパイル・エラー */

       return 0;
}

最初の変数 n外部結合をもつので静的変数です。上の場合 0 に初期化されます。変数 r内部結合をもつので静的変数となり,3.14 と初期化されます。次に main関数の中にあるブロック { } 内ですが,

extern double r;

の変数 r は,6行目で宣言した内部結合をもつ静的変数 r同じリンケージ(内部結合)を持つこととなります。したがって,ブロック・スコープを持ちながらリンケージも持つので初期化はできません。初期化は,その外部定義(6行目)に従います。一方,

int n = 2;

は,5行目で 0 と初期化された外部結合をもつ変数 n隠す(hide)ため,これとは別個のプロック・スコープをもつ int型自動変数 n を定義することとなり,2 と初期化されます。また,

static long l = 1234567890L;

は,long型変数 l1234567890 と初期化しています。この変数は,リンケージは持ちません。しかし,static を付しているので静的変数です。静的変数ですがブロック・スコープを持つので,その有効範囲外では使用できません。ソース内の

printf("l = %ld\n", l); /* コンパイル・エラー */

部分を削除せずにコンパイルすると,`l' undeclared といった具合に「宣言されていません」というエラーが出ます。エラーが出ないように修正したものをコンパイルし,実行ファイルを実行すると,次の出力を得ます。

n = 0
r = 3.140000
n = 2
r = 3.140000
l = 1234567890
n = 0
r = 3.140000

上の変数 l のようなプロック・スコープをもつ静的変数をその有効範囲外で参照するにはどうすれば良いのか,あるいは,そもそもそのような変数をいつ使用するのか,といった疑問を持ったことでしょう。次はその例です。

/* Example 16.5 */

#include <stdio.h>

char *f (void);

int main (void)
{
       char *y;
       y = f();
       printf("%s [%p]\n", y, y);
       return 0;
}

char *f (void)
{
       static char str[8] = "testing";
       char *x = str;
       return x;
}

これは malloc でメモリを割り当てる Example 10.17static を使って静的に割り当てたケースです。ポインタを渡せば,有効範囲外でも参照することができます。

仕様書(C99)理解度 ○×クイズ
1. static を付せば,結合をもつ。
2. static を付せば,内部結合をもつ。
3. extern を付せば,結合をもつ。
4. extern を付せば,外部結合をもつ。
5. 変数の寿命を静的記憶域期間にするには,static を付さなければならない。
6. 外部宣言された変数の寿命は,静的記憶域期間となる。

答え:1 × 2 × 3 ○ 4 × 5 × 6 ○

* C99 を作成していた人達によると,スコープリンケージ記憶域期間は,伝統的に混乱していた領域であったという。(ISO/IEC JTC1/SC22/WG14, A draft rationale for the C99 standard (N897), p.30.)

[C言語目次へ]

[この項を1頁で読む (84)]

16.5. ソースファイルの分割と外部参照の解消

 以上で,プログラムを分割し,関数やオブジェクトをリンクさせるためのプログラミング技術の知識は終わりです。ここでは簡単なプログラム分割を行い,各ソースファイルで個別に定義された関数やオブジェクトをリンクさせてみます。

 分割前のプログラムは最初に見た例 Example 16.0 にオブジェクトを加えた単純なものです。

/* Example 16.6 */

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

void myPrint(char *);
char head[] = "Welcome!";

int main (void)
{
       char str[8] = "test";
       myPrint(str);
       exit(0);
}

void myPrint(char *x)
{
       printf("%s\n", head);
       printf("This is a %s.\n", x);
}

プログラムの分割は,以前同様,メインの処理部分(main.c というファイル)とプリント関連部分(myPrint.c というファイル)への二分割です。

/* main.c */

#include <stdlib.h>

void myPrint(char *);
char head[] = "Welcome!";

int main (void)
{
       char str[8] = "test";
       myPrint(str);
       exit(0);
}
/* myPrint.c */

#include <stdio.h>

void myPrint(char *x)
{
       printf("%s\n", head);
       printf("This is a %s.\n", x);
}

しかしながら,myPrint関数は,main.c で宣言されている文字列 head を必要とします。実際,myPrint.c のみコンパイルしてみると,

cc -c myPrint.c
myPrint.c: In function `myPrint':
myPrint.c:7: `head' undeclared (first use in this function)
myPrint.c:7: (Each undeclared identifier is reported only once
myPrint.c:7: for each function it appears in.)

という具合に head が宣言されていないというエラーが出ます。そこで利用するのが「リンケージ」です。この場合,myPrint.c が外部参照するオブジェクトなので,extern を使います。myPrint.c のプリプロセッサ(#include <stdio.h>)の次に,次を書き加えてみます。

extern char head[];

これで上のエラーメッセージが出なくなるだけなく,main.cheadmain.c に残しておくことが可能となります。

 myPrint.c に書き加える方法は,確かに有効です。しかしながら,今後作成しうるモジュールでも変数 head を利用するかもしれません。そこで,main.c 固有の変数や関数をまとめたヘッダファイルを用意するのが良いかもしれません。

/* main.h */

extern char head[];

このようにしたら,myPrint.c は次のようにすれば良いこととなります。

/* myPrint.c */

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

void myPrint(char *x)
{
       printf("%s\n", head);
       printf("This is a %s.\n", x);
}

myPrint.c 内独自の関数やオブジェクトが書かれているヘッダファイルも同様に作成しておくと便利かもしれません。

/* myPrint.h */

extern void myPrint(char *);

このようにしたら,main.c は次のようにすれば良いこととなります。

/* main.c */

#include <stdlib.h>
#include "myPrint.h"

char head[] = "Welcome!";

int main (void)
{
       char str[8] = "test";
       myPrint(str);
       exit(0);
}

これでソースの分割は完了です。プログラムは,main.cmain.hmyPrint.c,そして myPrint.h の4つのファイルから構成されることとなった訳です。各ソースファイルの依存性も明確になったと思います。

■コンパイル

各ソースをコンパイルしてみましょう。

cc -c main.c
cc -c myPrint.c

成功すればオブジェクトファイル main.omyPrint.o が出来上がります。成功したらそれらをリンクさせます。

cc main.o myPrint.o

これで実行ファイルが出来上がります。

 main.c あるいは myPrint.c を書き換えた場合には,書き換えたソースのみを再コンパイルします。というのは,一度生成したオブジェクトファイルは,常時,利用可能だからです。(Makefile を利用すると,書き換えを自動検知してくれます。次の「付録 Makefile」を。)

[C言語目次へ]

[この項を1頁で読む (85)]

付録 Makefile

 Makefile を使った makeコマンドによるコンパイルには,幾つかの利点があります。

ここでは,これらの利点を中心に,Makefile の作成方法を見ます。(Makefile の作成方法のすべてを知りたい方はマニュアルを参照ください。)

makeコマンド

make [options] [target]

ここでの options とは makeコマンドのオプションです。オプション一覧は,次のようすれば得られます。

% make -h

また,target とは,Makefile に記されたものです。その一般形式は次の通りです。

target: prerequisites
       command

これを「ルール」と呼びます。makeコマンドは,Makefile を探し,そこに記述されている targetcommand を実行します。command に指定するのは,UNIXのコマンドです。(Bシェルがそのまま使えます。)

 例えば,Makefile が次のようになっていたとしましょう。

# Makefile

sample: sample.c
       cc $^ -o $@

このルールの場合,targetsample で,それは $@ で参照できます。また,prerequisitessample.c で,それは $^ で参照できます。そして,commandcc $^ -o $@ です。

% make

あるいは

% make sample

とすれば,コマンド cc sample.c -o sample が実行されます。

■デフォルトの設定

Makefile がない場合に,

% make sample

とすれば,どうなるのでしょうか。実は,target $@ には sample が,prerequisites $^ には sample.c が代入され,デフォルトの設定での command を実行します。デフォルトの設定を確認しましょう。

% make -p
# GNU Make version 3.79, by Richard Stallman and Roland McGrath.
# Built for powerpc-apple-darwin7.0
# Copyright (C) 1988, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99
#       Free Software Foundation, Inc.
...
# default
LINK.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# default
CC = cc
...
# default
RM = rm -f
...
# Not a target:
.c:
#  Implicit rule search has not been done.
#  Modification time never checked.
#  File has not been updated.
#  commands to execute (built-in):
        $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
...

LINK.cCC を「変数」と呼びます。変数は $() で参照することができます。例えば,

CC = cc

は変数 CC をコマンド cc に設定し,したがって,

LINK.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)

$(CC) には cc が代入されます。コンパイルの対象となったファイルは sample.c でしたので,built-in での実行が

$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@

です。デフォルトの設定では変数 CFLAGSCPPFLAGSLOADLIBES などが空なので,これは

cc $^ -o $@

という意味になります。したがって,Makefile が存在しないときに

% make sample

とすると,

cc     sample.c   -o sample

というコマンドが実行されます。

■設定の変更

上の CFLAGSCPPFLAGS などの変数は,make コマンドが予め意味を持った変数として設定しているものです。例えば,CFLAGScc のオプションを意味しています。そこで,次を書き込んだテキストファイルを Makefile として保存し,makeしてみましょう。

# Makefile

CFLAGS = -O -Wall

実行例です。

% make sample
cc -O -Wall    sample.c   -o sample
%

Makefile に書き込んだ cc のオプションが付いているのが確認できます。

* "CPP" と言えば C++ ですが,make のマニュアルでは CPPFLAGS はコンパイラへの追加的オプション用であるとなっています。

■変数とファイルの依存性

デフォルトの設定にある変数以外の変数を作成することができます。次の例はソースファイルの分割(前頁)で見たプログラムに対する Makefile です。

# Makefile

CFLAGS = -O -Wall
CPPFLAGS = -c
OBJECTS = main.o myPrint.o

sample: $(OBJECTS)
        $(CC) $(CFLAGS) $^ -o $@

main.o: main.c myPrint.h
        $(CC) $(CFLAGS) $(CPPFLAGS) $< -o $@
myPrint.o: myPrint.c main.h
        $(CC) $(CFLAGS) $(CPPFLAGS) $< -o $@

clean:
        $(RM) $(OBJECTS)

CPPFLAGSRM はデフォルトの設定にあった変数です。デフォルトの設定にない変数は,OBJECTS です。これにはオブジェクトファイルが指定してあります。各オブジェクトに対するルールがターゲット clean の前に記してあります。$<prerequisites の一つ目を参照します。

% make
cc -O -Wall -c main.c -o main.o
cc -O -Wall -c myPrint.c -o myPrint.o
cc -O -Wall main.o myPrint.o -o sample
% ./sample
Welcome!
This is a test.

上の場合,main.cmyPrint.cmain.hmyPrint.h のいずれかが書き直されたとき,必要な部分のみを再コンパイルし,実行ファイルを生成してくれます。特に,myPrint.c はヘッダ main.h を読む込む設定になっているので,

myPrint.o: myPrint.c main.h

とすることで,myPrint.cmain.h に依存していることを教えることができます。したがって,main.h のみを書き換えると,myPrint.c のみを再コンパイルし,実行ファイルを生成し直します。

 上では,ターゲットに clean があります。

% make clean
rm -f main.o myPrint.o

ターゲットを指定することで,そのコマンドが実行されることは,prerequisites がなくとも同じです。

[C言語目次へ]

[この項を1頁で読む (86)]

17. 型修飾子(Type Qualifiers)

 宣言には型指定子宣言子(但し,タグ或いは列挙体メンバがある場合を除く)が必須です。必須ではないものの,データの扱いに特定の属性を付与するもののとして「型修飾子」があります。型修飾子として使用されるキーワード(予約語)は,constvolatile,そして restrict の3つです。

 型修飾子xxx が付された型をxxx 修飾型(xxx -qualified type)と呼びます。例えば,

const int c = 1;

といった場合,オブジェクト c は const修飾型(const-qualified type)であり,その型は const int です。また,

const int m[8];

の場合,const修飾型 const int の配列(各要素が const修飾型)であり,配列型(式)m 自体は const修飾型ではありません。[C99, 6.7.3, 8] ポインタの場合,例えば,

const int *p;
int *const cp;

のポインタ p は const修飾型 const int へのポインタでその型は const int * ですが,ポインタ cp は非修飾型 int へのポインタであり,ポインタ cp 自体が const修飾型となります。[C99, 6.7.5.1]

[C言語目次へ]

[この項を1頁で読む (87)]

17.1. const

 const修飾型は次の文法を持ちます。

数学の言葉を使えば「定数」ということです。処理系によっては(volatile を付さない)const修飾型は読み込み専用の記憶域領域(read-only region of storage)に割り当てられることがあります。

/* Example 17.1
 *
 * コンパイルエラーとなる例
 *
 * pi を 3.14159 に変更不可能としたにもかかわらず,x と y の和を代入。
 *
 */

int main(void)
{
       double x = 1.0, y = 2.0;
       const double pi = 3.14159;
       pi = x + y;

       return 0;
}

この例は const修飾型オブジェクト pi単純代入演算子の左側オペランドに指定したため,コンパイルエラーとなります。

/* Example 17.2 */

#include <stdio.h>       // printf
#include <string.h>      // strncat, strncpy, strlen

void myPrint(const char *, size_t);

int main (void)
{
       char str[8] = "ABCDE";
       char x[16] = "0123";

       myPrint(x, sizeof(x));
       strncat(x, str, sizeof(x) - strlen(x) - 1);
       myPrint(x, sizeof(x));
       strncpy(x, str, sizeof(x) - 1);
       myPrint(x, sizeof(x));

       return 0;
}

void myPrint(const char *str, size_t size)
{
       const char *s;

       printf("str: SIZE = %zd, LENGTH = %zd\n", size, strlen(str));
       for(s = str; *s != '\0'; s++)
              printf("[%p]\tx[%td]\t%c\t(%d)\t%s\n", s, s - str, *s, *s, s);
}

この例はポインタに const を利用したケースです。関数 myPrint 内で受け取った配列 str が変更できないようにします。関数 myPrint 内のポインタ strsconst char へのポインタであり,ポインタ自体は const修飾型ではありません。(ソース内のその他は既出です。)

 実行結果です。

str: SIZE = 16, LENGTH = 4
[0xbffffa88]    x[0]    0       (48)    0123
[0xbffffa89]    x[1]    1       (49)    123
[0xbffffa8a]    x[2]    2       (50)    23
[0xbffffa8b]    x[3]    3       (51)    3
str: SIZE = 16, LENGTH = 9
[0xbffffa88]    x[0]    0       (48)    0123ABCDE
[0xbffffa89]    x[1]    1       (49)    123ABCDE
[0xbffffa8a]    x[2]    2       (50)    23ABCDE
[0xbffffa8b]    x[3]    3       (51)    3ABCDE
[0xbffffa8c]    x[4]    A       (65)    ABCDE
[0xbffffa8d]    x[5]    B       (66)    BCDE
[0xbffffa8e]    x[6]    C       (67)    CDE
[0xbffffa8f]    x[7]    D       (68)    DE
[0xbffffa90]    x[8]    E       (69)    E
str: SIZE = 16, LENGTH = 5
[0xbffffa88]    x[0]    A       (65)    ABCDE
[0xbffffa89]    x[1]    B       (66)    BCDE
[0xbffffa8a]    x[2]    C       (67)    CDE
[0xbffffa8b]    x[3]    D       (68)    DE
[0xbffffa8c]    x[4]    E       (69)    E
[C言語目次へ]

[この項を1頁で読む (88)]

17.2. volatile

 volatile修飾型は,次の文法を持ちます。

プログラムの実行とは無関係に値が変化するデータという意味なのですが,仕様書(C99)だけでは良く分からないので,冗長にならない程度に説明を付します。

 volatile は該当の変数に処理の最適化をしないようにコンパイラに知らせるためのものです。コンパイラは処理の最適化を試み,一部の変数を CPU のレジスタに割り当てたり,文を削除することがあります。次の例では,register を付していないにもかかわらず,変数 i が CPU のレジスタ(r30)に割り当てられています。

* 逆アセンブル disassemble の結果を読むにはアセンブリ言語の知識が必要です。マニュアルは各CPUの生産元からダウンロード可能です。また,同じような最適化をすべてのコンパイラが行うとは限りません。

/* test1.c */

#include <stdio.h>

int main(void)
{
       int i;
       for(i = 0; i < 5; i++)
              printf("%d\n", i);

       return 0;
}
% cc -O -Wall test1.c
% gdb a.out
GNU gdb 5.1-20020125 (Apple version gdb-213) (Wed Apr  3 04:16:48 GMT 2002)
Copyright 2002 Free Software Foundation, Inc.
...
(gdb) disassemble main
...
0x1e94 <main+20>:       mflr    r31
0x1e98 <main+24>:       li      r30,0
0x1e9c <main+28>:       addis   r3,r31,0
0x1ea0 <main+32>:       addi    r3,r3,216
0x1ea4 <main+36>:       mr      r4,r30
0x1ea8 <main+40>:       bl      0x1fdc <dyld_stub_printf>
0x1eac <main+44>:       addi    r30,r30,1
0x1eb0 <main+48>:       cmpwi   r30,4
0x1eb4 <main+52>:       ble+    0x1e9c <main+28>
0x1eb8 <main+56>:       li      r3,0
0x1ebc <main+60>:       lwz     r0,88(r1)
0x1ec0 <main+64>:       addi    r1,r1,80
0x1ec4 <main+68>:       mtlr    r0
0x1ec8 <main+72>:       lmw     r30,-8(r1)
0x1ecc <main+76>:       blr

次の例では,最適化によって while文全体が削除されています。(i0 なため,繰り返しの条件はつねに偽。コンパイラは while文の処理が無駄であると考えたため削除。)

/* test2.c */

#include <stdio.h>

int main(void)
{
       int i = 0;
       while(i) {
              printf("%d\n", i);
              i = 0;
       }
       printf("%d\n", i);
       return 0;
}
% cc -O -Wall test2.c
% gdb a.out
GNU gdb 5.1-20020125 (Apple version gdb-213) (Wed Apr  3 04:16:48 GMT 2002)
Copyright 2002 Free Software Foundation, Inc.
...
(gdb) disassemble main
...
0x1ea4 <main+20>:       mflr    r31
0x1ea8 <main+24>:       addis   r3,r31,0
0x1eac <main+28>:       addi    r3,r3,200
0x1eb0 <main+32>:       li      r4,0
0x1eb4 <main+36>:       bl      0x1fdc <dyld_stub_printf>
0x1eb8 <main+40>:       li      r3,0
0x1ebc <main+44>:       lwz     r0,88(r1)
0x1ec0 <main+48>:       addi    r1,r1,80
0x1ec4 <main+52>:       mtlr    r0
0x1ec8 <main+56>:       lwz     r31,-4(r1)
0x1ecc <main+60>:       blr

しかしながら,このような最適化は,上のようなプログラムでは,特段,問題はありません。むしろ歓迎でしょう。それではいつ volatile を使うのでしょうか。

 仕様(C99)で volatile を付さない場合に未定義の動作となるのは次の2つです。

* これらが仕様に含まれた経緯は ISO/IEC JTC1/SC22/WG14, A draft rationale for the C99 standard (N897) が参考になります。

シグナルによる割り込み(interrupt)は,プログラムの進行を遮断するため,変数の値が記憶されているか否か不明になります。volatile を付すと,読み込み時点で値が変化することを許します。(通常は,代入演算子などでプログラムの中で値が変更され保存される。volatile を付すと,参照するだけで値が変化することをコンパイラは考慮する。)volatile は,プログラムの外で値が変化する変数のための型修飾子とも言えます。例えば,次のようなケースです。

マウス,プリンタ,モデムなど,デバイスのプログラムをする方には必需品です。

■非局所ジャンプ(Nonlocal Jump):setjmplongjmp [C99, 7.13]

#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

setjmplongjmp で使われる環境 env を保存します。型 jmp_buf はヘッダ setjmp.h で定義されており,保存する内容を確認できます。setjmpifswitch の選択文(selection statement)と whilefor などの繰返し文(iteration statement)のみに使えます。longjmpsetjmp で保存された環境 env を,setjmp が呼ばれたところで復元します。setjmp は,longjmp から戻った場合には val を,そうでない場合には 0 を返します。

/* Example 17.3 */

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

void f(void);

static jmp_buf env;

int main(void)
{
       int val = -1;
       int x;     // volatile が必要

       x = 3;
       if((val = setjmp(env)) != 0) {
              printf("val = %d (x = %d)\n", val, x);
              exit(0);
       }

       x = 5;
       printf("val = %d (x = %d)\n", val, x);
       f();

       exit(0);
}

void f(void)
{
       longjmp(env, 1);
}

実行結果です。

val = 0 (x = 5)
val = 1 (x = 3)

valsetjmp が最初に呼ばれた時点で 0 となり,longjmp が呼ばれたところで 1 となります。envsetjmp が実行された時点の環境,例えば,x = 3 が保存されており,longjmp が実行された時点での x = 5 という環境は保存されていません。longjmp が実行された時点での x の値を setjmp にジャンプした後に参照するには,

volatile int x;

としなければなりません。volatile を付さない場合,longjmp から setjmp にジャンプ後, x が3なのか5なのか,動作は未定義です。

signal関数と割り込み(Interrupt)[C99, 7.14]

#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);

シグナル sig を受け取ると,シグナルハンドラ func を実行します。シグナルハンドラには,次の2つが最初から用意されています。

SIG_DFL   規定の処理
SIG_IGN   シグナルを無視

また,シグナル sig は仕様(C99)に掲載されている

SIGFPE    floating-point exception
SIGILL    illegal instruction
SIGSEGV   segmentation violation

以外にも多数あります。各シグナルに対する SIG_DFL での処理も含め,man 3 signal で確認してください。

/* Example 17.4 */

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>   // sleep

void sigHandler(int);

int main(void)
{
       int i;

       signal(SIGINT, sigHandler);

       for(i = 1; i < 60; i++) {
              printf("sleeping...(%d)\n", i);
              sleep(1);
       }
       exit(0);
}

void sigHandler(int sig)
{
       if(sig == SIGINT)
              fprintf(stderr, "\nSIGINT recieved.\n");
       else
              fprintf(stderr, "\nUnpredictable signal recieved.\n");

       exit(0);
}

実行例です。「コントロール+C」でシグナル SIGINT がおきます。

% ./a.out
sleeping...(1)
sleeping...(2)
sleeping...(3)
sleeping...(4)
sleeping...(5)
sleeping...(6)
^C
SIGINT recieved.

シグナルハンドラ sigHandler が参照する変数は volatile修飾型である必要があります。

[C言語目次へ]

[この項を1頁で読む (89)]

17.3. restrict

 restrict は C99 より導入された新しい型修飾子です。その文法は次の通りです。

ポインタとオブジェクトへのアクセスが1対1対応になるということです。これは malloc によって返却されたポインタとオブジェクトの関係と同じように,他のオブジェクトとポインタに1対1対応を持たせるというものです。

 restrict修飾型の応用は,標準ライブラリ関数に見られます。

int fprintf(FILE * restrict stream, const char * restrict format,...);
int printf(const char * restrict format,...);
int snprintf(char * restrict s, size_t n, const char * restrict format,...);

* 標準出力標準入力を参照。

[C言語目次へ]

[この項を1頁で読む (90)]

18. 応用編

 C99 のすべてを網羅できている訳ではありませんが,大方の文法は理解できたことでしょう。ここでは,理解を深めるために応用を試みます。

[C言語目次へ]

[この項を1頁で読む (91)]

18.1. ライブラリ関数への応用

 ここでは,次の標準ライブラリ関数の原本を見ます。

* Copyright は原本に従います。但し,一部変更しています。

/* Example 18.1 */

#include <stdio.h>
#include <assert.h>

int my_strcmp (const char *, const char *);

int main(void)
{
       int n;
       char x[] = "test", y[] = "testing";

       n = my_strcmp(x, y);
       if (n)
              printf("Distinct!\n");
       else
              printf("Match!\n");

       return 0;
}

int
my_strcmp(s1, s2)
       const char *s1, *s2;
{
       assert (s1 != NULL);
       assert (s2 != NULL);

       while (*s1 == *s2++)
              if (*s1++ == 0)
                     return (0);
       return (*(const unsigned char *)s1 - *(const unsigned char *)--s2);
}

実行すると

Distinct!

と出力されます。strcmp 関数は文字列を比較します。ソース内の assert 関数は,ヘッダファイル assert.h に入っている関数で,引数が 0 のときエラーを標準エラー出力し,処理をやめます。(const unsigned char *)()キャスト演算子です。

/* Example 18.2 */

#include <stdio.h>
#include <assert.h>

size_t my_strlen (const char *);

int main(void)
{
       char x[] = "testing";
       printf("Number of letters = %zd\n", my_strlen(x));
       return 0;
}

size_t
my_strlen(str)
       const char *str;
{
       const char *s;

       assert(str != NULL);

       for (s = str; *s; ++s);
       return(s - str);
}

実行結果です。

Number of letters = 7

strlen関数は文字列の数を数えます。返却値型が size_tですが,実際は ptrdiff_tで返しています。

/* Example 18.3 */

#include <stdio.h>
#include <assert.h>

char *my_strcpy (char *, const char *);

int main(void)
{
       char x[10] = "testing";
       char y[10];
       char *z;

       z = my_strcpy(y, x);
       printf("Copied: %s (z: %s)\n", y, z);
       return 0;
}

char *
my_strcpy(to, from)
       char *to;
       const char *from;
{
       char *save = to;

       assert(to != NULL);
       assert(from != NULL);

       for (; (*to = *from) != '\0'; ++from, ++to);
       return(save);
}

実行結果です。

Copied: testing (z: testing)

strcpy 関数は文字列をコピーする関数で「9.3. 文字列の操作」で使用しました。ソースを読むと,第2変数から第1変数の大きさを無視してメモリに書き込む様子が見て取れると思います。次は,strrchr 関数です。

/* Example 18.4 */

#include <stdio.h>
#include <assert.h>

char *my_strrchr (const char *, int);

int main(void)
{
       char str[] = "C@langauge@testing";
       char key = '@';
       char *y;

       y = my_strrchr(str, key);
       printf("Last segmented = %s\n", y);
       return 0;
}

char *
my_strrchr(p, ch)
       const char *p;
       int ch;
{
       char *save;

       assert(p != NULL);

       for (save = NULL;; ++p) {
              if (*p == ch) {
                     save = (char *)p;
              }
              if (!*p)
                     return(save);
       }
}

実行結果です。

Last segmented = @testing

strrchr 関数は,第1変数の文字列の中で第2変数で指定した文字が最後に現れたポインタを返します。

[C言語目次へ]

[この項を1頁で読む (92)]

18.2. 自己参照構造体の応用:二分木

 自己参照構造体を使った二分木による単語の並べ替えと出現数を計算するプログラムを書いたのは R. Haight であり,C のコンパイラを最初に作成した Dennis M. Ritchie によるマニュアル C Reference Manual (1975) に掲載された C のプログラム例です。

 「二分木(binary tree)」とは,連結リストにおいて「データ」と呼んだものを「ノード」と読み直し,各ノードが2つのポインタをもち,一つが左,もう一つが右を意味する形でデータが繋がったものです。ここでは,二分木を使ったソート(並べ替え)を見ます。

 次の数字を降順に並び替える場合を考えましょう。

78, 85, 79, 98, 87, 90, 95, 99, 97, 92

先ず,出発点のノードに 0 と打ちます。そのアドレス番号が 0x442b0 であったとしましょう。枝を2つ這わせ各ノードに NULLポインタを入れます。そこで,78 を読み込み,ノード 0 から出発します。78 は 0 より大きいので,左の枝に分岐し,NULLポインタに至ったノードに 78 と打ち,そこから2つの枝を這わせ各ノードに NULLポインタを入れます。ノード 78 のアドレスが角括弧内の数値で書いてあります。

[0x442b0]  0 .---------. [0x0]
             |
             |
             |
[0x442c0] 78 .---------. [0x0]
             |
             |
             |
       [0x0] .

2つ目の 85 ですが,0 より大きいので左,78 より大きいので左です。すると,NULLポインタに至るので,そのノードに 85 と入れ,そこから2つの枝を這わせ各ノードに NULLポインタを入れます。NULLポインタでないときは,ノード番号と比較し,それより大きい場合には左,小さい場合には右へ進み,NULLポインタならばノードに数値を入れ,そこから NULLポインタを2つ打つという訳です。このようにして行くと,次のような木が出来上がります。

[0x442b0]  0 .---------. [0x0]
             |
             |
             |
[0x442c0] 78 .---------. [0x0]
             |
             |
             |        79 [0x442e0]
[0x442d0] 85 .---------.---------. [0x0]
             |         |
             |         |
             |         |
             |   [0x0] .
             |               87 [0x44300]
[0x442f0] 98 .-----------------.---------. [0x0]
             |                 |
             |                 |
             |                 |
             |    [0x44310] 90 .---------. [0x0]
             |                 |
             |                 |
             |                 |              92 [0x44350]
             |    [0x44320] 95 .---------------.---------. [0x0]
             |                 |               |
             |                 |               |
             |                 |               |
             |                 |               . [0x0]
             |                 |
             |    [0x44340] 97 .---------. [0x0]
             |                 |
             |                 |
             |                 |
             |                 . [0x0]
[0x44330] 99 .---------. [0x0]
             |
             |
             |
       [0x0] .

出来上がった木を左奥から読み上げると,数値が降順に並びます。

/* Example 18.5 */

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

struct tnode {
       int n;
       struct tnode *left, *right;
};

struct tnode *createNode (int);
struct tnode *insertNode (struct tnode *, int);
void printTree (struct tnode *);

int main(void)
{
       int i;
       int x[10] = {78, 85, 79, 98, 87, 90, 95, 99, 97, 92};
       struct tnode *top;

       top = createNode(0);

       for (i = 0; i < 10; i++)
              top = insertNode(top, x[i]);

       printTree(top);
       exit(0);
}

struct tnode *createNode (int num)
{
       struct tnode *node;
       if ((node = malloc(sizeof(struct tnode))) == NULL) {
              fprintf(stderr, "malloc\n");
              exit(1);
       }
       node -> n = num;
       node -> left = NULL;
       node -> right = NULL;
       return node;
}

struct tnode *insertNode (struct tnode *node, int num)
{
       if (!node) {
              node = createNode(num);
              return node;
       }

       if (num > node -> n)
              node -> left = insertNode(node -> left, num);

       if (num < node -> n)
              node -> right = insertNode(node -> right, num);

       return node;
}

void printTree (struct tnode *node)
{
       struct tnode *tmp;
       tmp = node;
       while (tmp) {
              printTree(tmp -> left);
              printf("n = %d\t[%p] -> [%p, %p]\n",
                     tmp -> n, tmp, tmp -> left, tmp -> right);
              tmp = tmp -> right;
       }
}

実行結果です。角括弧内はアドレスです。

n = 99  [0x44330] -> [0x0, 0x0]
n = 98  [0x442f0] -> [0x44330, 0x44300]
n = 97  [0x44340] -> [0x0, 0x0]
n = 95  [0x44320] -> [0x44340, 0x44350]
n = 92  [0x44350] -> [0x0, 0x0]
n = 90  [0x44310] -> [0x44320, 0x0]
n = 87  [0x44300] -> [0x44310, 0x0]
n = 85  [0x442d0] -> [0x442f0, 0x442e0]
n = 79  [0x442e0] -> [0x0, 0x0]
n = 78  [0x442c0] -> [0x442d0, 0x0]
n = 0   [0x442b0] -> [0x442c0, 0x0]
[C言語目次へ]

[この項を1頁で読む (93)]

18.3. Tools

 道具箱です。


■環境変数

 以下のようにすると,環境変数が得られます。

/* Example 18.6 */

#include <stdio.h>

int main(void)
{
       extern char **environ;
       char **z;

       for (z = environ; *z; z++)
              printf("%s\n", *z);

       return 0;
}

environ については man 7 environ で確認してください。(environ は C99 の範囲外です。)

 これを次のように書き直し,コンパイル後の実行ファイルを CGI として動かせば,httpd の環境変数一覧が得られます。

/* Example 18.7 */

#include <stdio.h>

int main(void)
{
       extern char **environ;
       char **z;

       printf("Content-Type: text/html\n\n");
       puts(  "<body>\n"
              "<pre>");

       for (z = environ; *z; z++)
              printf("%s\n", *z);

       puts(  "</pre>\n"
              "</body>");

       return 0;
}

特定の環境変数の値を得るには,stdlib.h にある getenv関数を利用します。(こちらは C99 の範囲内です。)

/* Example 18.8 */

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

int main(void)
{
       char *env;
 
       printf("Content-Type: text/html\n\n");
       puts(  "<body>\n"
              "<pre>");

       if((env = getenv("PATH")))
              printf("(1) %s\n", env);

       if((env = getenv("REQUEST_METHOD")))
              printf("(2) %s\n", env);

       puts(  "</pre>\n"
              "</body>");

       return 0;
}

■文字列の分割

 string.h にある strtok関数を利用した文字列の分割は,既に見た通りです。(forExample 14.8 を参照。)ただ,strtok は,元の文字列を分割後の文字列に変更してしまうため,元の文字列の再利用が不可能になります。そこで,次のような方法があります。

/* Example 18.9 */

#include <stdio.h>

int main(void)
{
       char str[] = "Taro\t89\tA";
       char name[16] = "\0";
       int point = 0;
       char grade[4] = "\0";

       sscanf(str, "%15[^\t]\t%d\t%3[^\t]", name, &point, grade);
       printf("%s, %d, %s\n", name, point, grade);

       return 0;
}

文字列 Taro\t89\tA がタブ \t で分割され,次のような出力を得ます。

Taro, 89, A

[応用]chomp

 chomp とは,文字列の末尾にある \n を取り去る Perl の関数のことです。これは文字列の分割と同様 sscanf が使えます。

/* Example 18.10 */

#include <stdio.h>

int main(void)
{
       char str1[] = "testing\n";
       char str2[8] = "\0";

       sscanf(str1, "%7[^\n]", str2);
       printf("\"%s\" chomped.\n", str2);

       return 0;
}

■文字列の連結

 ある文字列を他の文字列の末尾に連結するのは「9.3. 文字列の操作」で見たように,string.h にある strcat関数,もしくは strncat関数が使えます。(後者は,連結する文字数が指定できますので,バッファ・オーバーフローを確実に回避できます。)

 末尾への連結ではなく,連結の位置を任意に指定するには,sprintf関数が利用できます。しかし,sprintf関数も又,バッファ・オーバーフローが起きないように注意が必要です。次の例は,バッファ・オーバーフローを避けるために snprintf関数を利用し,文字列 str と数字 num から新たな文字列 dir を作成するものです。

/* Example 18.11 */

#include <stdio.h>

int main(void)
{
       char dir[32] = "\x00";
       char str[] = "ABCD";
       int num = 123;

       snprintf(dir, sizeof(dir), "/path/to/home/data%d%s.txt", num, str);
       printf("%s\n", dir);

       return 0;
}

sizeof(dir)-1 個の文字が dir にコピーされ,最後にナル文字が入ります。[C99, 7.19.6.5]

 実行結果です。

/path/to/home/data123ABCD.txt

* CGI で利用する場合には,strnum 部分を QUERY_STRING や POSTデータから直接読み込まないでください。


itoa

 itoa とは stdlib.h にある atoi関数の逆の意味です。すなわち,数字を文字列として格納するものです。ただ,itoa は存在しません。そこで,snprintf関数を使います。

/* Example 18.12 */

#include <stdio.h>

int main(void)
{
       int n = 123456;
       char str[4] = "000";

       printf("%s\n", str);
       snprintf(str, sizeof(str), "%d", n);
       printf("%s\n", str);

       return 0;
}

実行結果です。

000
123

■時間

 1900年1月からの秒数を取得するには,ヘッダ time.h で定義されている関数 time を使います。取得した秒数を DDD MMM dd hh:mm:ss YYYY の形にするには関数 ctime を使えば良いです。

/* Example 18.13 */

#include <stdio.h>
#include <time.h>     /* time_t, time(), ctime() */

int main(void)
{
       time_t timer;

       timer = time(NULL);
       printf("%ld, %s", timer, ctime(&timer));

       return 0;
}

time_t とはヘッダ time.h で定義されている時間を示す型です。実行すると,次のような出力を得ます。

1049039826, Mon Mar 31 00:57:06 2003

取得した時間 time_t timer の年,月,分,秒,週などの個別データを取得するには,ヘッダ time.h で定義されている構造体 tm を利用します。構造体 tm は次のメンバを持ちます。

int tm_sec;      /* seconds (0 - 60) */
int tm_min;      /* minutes (0 - 59) */
int tm_hour;     /* hours (0 - 23) */
int tm_mday;     /* day of month (1 - 31) */
int tm_mon;      /* month of year (0 - 11) */
int tm_year;     /* year - 1900 */
int tm_wday;     /* day of week (Sunday = 0) */
int tm_yday;     /* day of year (0 - 365) */
int tm_isdst;    /* is summer time in effect? */
char *tm_zone;   /* abbreviation of timezone name */
long tm_gmtoff;  /* offset from UTC in seconds */

次の例は,取得した時間 time_t timertime.h にある localtime関数

struct tm *localtime(const time_t *timer);

を利用して構造体 tm 型ポインタ t に渡し,構造体 tm の各メンバの値を取得するものです。

/* Example 18.14 */

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

int main(void)
{
       time_t timer;
       struct tm *t;

       timer = time(NULL);
       printf("%ld, %s", timer, ctime(&timer));

       t = localtime(&timer);
       printf("days since Sunday = %d\n", t->tm_wday);
       printf("year = %d\n", t->tm_year + 1900);
       printf("month = %d\n", t->tm_mon + 1);
       printf("day of the month = %d\n", t->tm_mday);
       printf("hours since midnight = %d\n", t->tm_hour);
       printf("minutes after the hour = %d\n", t->tm_min);
       printf("seconds after the minute = %d\n", t->tm_sec);
       printf("days since January 1st = %d\n", t->tm_yday + 1);
       printf("Daylight Saving Time = %d\n", t->tm_isdst);

       return 0;
}

実行結果です。

1049039826, Mon Mar 31 00:57:06 2003
days since Sunday = 1
year = 2003
month = 3
day of the month = 31
hours since midnight = 0
minutes after the hour = 57
seconds after the minute = 6
days since January 1st = 90
Daylight Saving Time = 0

逆に,ある特定の日付けを time_t型変数(1900年1月からの秒数)に変換することも可能です。それには time.h にある mktime関数を利用します。次の例は,日付け my_time と現在の日付け(1年以内)の日数の差を求めるものです。

/* Example 18.15 */

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

int main(void)
{
       time_t time1, time2, timer;
       struct tm *t, my_time;

       time1 = time(NULL);
       printf("Current: %ld, %s", time1, ctime(&time1));

       my_time.tm_year = 2003 - 1900;
       my_time.tm_mon = 2;
       my_time.tm_mday = 28;
       my_time.tm_hour = 22;
       my_time.tm_min = 15;
       my_time.tm_sec = 0;
       my_time.tm_isdst = 0;

       if ((time2 = mktime(&my_time)) != -1)
       {
              printf("my_time: %ld, %s", time2, ctime(&time2));

              timer = time1 - time2;
              t = localtime(&timer);
              printf("%d days %d hours %d minutes %d seconds\n",
                     t->tm_yday, t->tm_hour, t->tm_min, t->tm_sec);
       }

       return 0;
}

実行結果です。

Current: 1049041988, Mon Mar 31 01:33:08 2003
my_time: 1048857300, Fri Mar 28 22:15:00 2003
2 days 12 hours 18 minutes 8 seconds

■ファイル情報の取得

 ファイルやディレクトリの情報取得には,ヘッダ sys/stat.h で定義されている関数

int stat(const char *path, struct stat *sb);

と構造体 stat が利用できます。(低位の取得方法を知りたい方は,コマンド ls のソースファイルでも。また,stat関数は C99 の範囲外です。)関数 stat の第1変数はファイル或いはディレクトリのパス,第2変数はファイル情報を確保するための構造体変数のアドレスです。構造体 stat のメンバについては man 2 stat で確認してください。

 次は,ファイル情報取得のサンプルプログラムです。サイズ,最後のアクセス,変更時刻,そして最後のアクセスと変更時刻の差がプリントされます。

/* Example 18.16 */

#include <sys/types.h>      /* stat(), struct stat      */
#include <sys/stat.h>       /* stat(), struct stat      */
#include <stdio.h>          /* fprintf(), printf()      */
#include <errno.h>          /* errno                    */
#include <string.h>         /* strerror()               */
#include <stdlib.h>         /* exit()                   */
#include <time.h>           /* time_t, ctime()          */

int main(void)
{
       struct stat filestat;
       char path[] = "/path/to/file";
       time_t dtime;

       if(stat(path, &filestat) == -1) {
             fprintf(stderr, "* Error (%d) [stat: %s]\n", errno, strerror(errno));
             exit(errno);
       }

       printf("Size: %ld\n", (long)filestat.st_size);
       printf("Last accessed: %ld, %s", filestat.st_atime, ctime(&filestat.st_atime));
       printf("Last modified: %ld, %s", filestat.st_mtime, ctime(&filestat.st_mtime));

       dtime = filestat.st_atime - filestat.st_mtime;
       printf("%ld\n", dtime);

       exit(0);
}

実行例です。

Size: 86555
Last accessed: 1049485323, Sat Apr  5 04:42:03 2003
Last modified: 1049428803, Fri Apr  4 13:00:03 2003
56520

エラー番号 errno とその意味 strerror() については,存在しないファイルを path に指定してみると良いでしょう。ちなみに,より簡単に標準エラー出力にエラーを出す方法は,stdio.h にある perror関数を利用することです。

perror("Error");
perror(NULL);

■ファイル・ロック

 ファイルをロックするには,BSD系では sys/file.h にある flock関数が,他の UNIX系OS では fcntl.h にある lockf関数が使えます。ここでは,奥山研究室のサーバーでの実験を元に,flock関数について紹介します。(但し,flock関数は C99 の範囲外です。)

 flock関数は次の形をしています。(man 2 flock で確認してください。)

int flock(int fd, int operation);

第1変数には fopen関数などでオープンに成功したファイルのファイル・ディスクリプタを,第2変数にはロックの種別を指定します。ロックの種類は次の通りです。

LOCK_SH   1    /* shared lock */
LOCK_EX   2    /* exclusive lock */
LOCK_NB   4    /* don't block when locking */
LOCK_UN   8    /* unlock */

読み込むだけであれば LOCK_SH を,書き込むならば排他ロックである LOCK_EX を指定します。いずれの場合にしても,LOCK_NB を付すと,他によってロックされている際には flock関数は -1 を返します。

 次は,相手にロックされている時は書き込みをせずにファイルを閉じるプログラムです。実行ファイルを異なる名称で2つ作成し,それらを同時に動かすと期待通りに動作することが確認できます。

/* Example 18.17 */

#include <stdio.h>          /* fprintf(), printf(), fileno(), FILE     */
#include <sys/file.h>       /* flock()     */
#include <string.h>         /* strerror()  */
#include <errno.h>          /* errno       */
#include <stdlib.h>         /* exit()      */

void myWrite(int, char *);

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

       for(n = 1; n <= 20000; n++)
              myWrite(n, argv[0]);

       exit(0);
}

void myWrite(int n, char *myname)
{
       FILE* fptr;

       if((fptr = fopen("/path/to/you_want_to_write", "a")) == NULL) {
              fprintf(stderr, "*** %s (%d): %s\n", __FILE__, errno, strerror(errno));
              exit(1);
       }
       if(flock(fileno(fptr), LOCK_EX | LOCK_NB) == -1) {
              printf("%s: Oops. Locked! (%d)\n", myname, n);
              if(fptr) { fclose(fptr); }
              return;
       }
       fprintf(fptr, "%s (%d)\n", myname, n);
       printf("%s: Yes, I could write! (%d)\n", myname, n);
       fclose(fptr);
}
[C言語目次へ]

[この項を1頁で読む (94)]

[練習問題]

  1. 次は,UNIX のシェルコマンド echo のソースファイルである。解読せよ。(Copyright は,UC Berkeley にあります。改良したものも含め,再配布するときは,copyright を必ず保持してください。)
    /* $NetBSD: echo.c,v 1.9 2001/07/29 22:36:11 wiz Exp $     */
    
    /*
     * Copyright (c) 1989, 1993
     *     The Regents of the University of California.  All rights reserved.
     *
     * Redistribution and use in source and binary forms, with or without
     * modification, are permitted provided that the following conditions
     * are met:
     * 1. Redistributions of source code must retain the above copyright
     *    notice, this list of conditions and the following disclaimer.
     * 2. Redistributions in binary form must reproduce the above copyright
     *    notice, this list of conditions and the following disclaimer in the
     *    documentation and/or other materials provided with the distribution.
     * 3. All advertising materials mentioning features or use of this software
     *    must display the following acknowledgement:
     *     This product includes software developed by the University of
     *     California, Berkeley and its contributors.
     * 4. Neither the name of the University nor the names of its contributors
     *    may be used to endorse or promote products derived from this software
     *    without specific prior written permission.
     *
     * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
     * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
     * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
     * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
     * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
     * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
     * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
     * SUCH DAMAGE.
     */
    
    #include <sys/cdefs.h>
    #ifndef lint
    __COPYRIGHT(
    "@(#) Copyright (c) 1989, 1993\n\
         The Regents of the University of California.  All rights reserved.\n");
    #endif /* not lint */
    
    #ifndef lint
    #if 0
    static char sccsid[] = "@(#)echo.c     8.1 (Berkeley) 5/31/93";
    #else
    __RCSID("$NetBSD: echo.c,v 1.9 2001/07/29 22:36:11 wiz Exp $");
    #endif
    #endif /* not lint */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int, char *[]);
    
    /* ARGSUSED */
    int
    main(int argc, char *argv[])
    {
           int nflag;
    
           /* This utility may NOT do getopt(3) option parsing. */
           if (*++argv && !strcmp(*argv, "-n")) {
                  ++argv;
                  nflag = 1;
           }
           else
                  nflag = 0;
    
           while (*argv) {
                  (void)printf("%s", *argv);
                  if (*++argv)
                         (void)putchar(' ');
           }
           if (nflag == 0)
                  (void)putchar('\n');
           exit(0);
           /* NOTREACHED */
    }
    
  2. 次は,ヘッダファイル string.h にある strtok 関数のソースファイルである。解読せよ。但し,ソース中の _DIAGASSERT は,assert と置き換えてよい。(Copyright は,UC Berkeley にあります。改良したものも含め,再配布するときは,copyright を必ず保持してください。)
    /*     $NetBSD: strtok.c,v 1.10 1999/09/20 04:39:49 lukem Exp $     */
    
    /*
     * Copyright (c) 1988, 1993
     *     The Regents of the University of California.  All rights reserved.
     *
     * Redistribution and use in source and binary forms, with or without
     * modification, are permitted provided that the following conditions
     * are met:
     * 1. Redistributions of source code must retain the above copyright
     *    notice, this list of conditions and the following disclaimer.
     * 2. Redistributions in binary form must reproduce the above copyright
     *    notice, this list of conditions and the following disclaimer in the
     *    documentation and/or other materials provided with the distribution.
     * 3. All advertising materials mentioning features or use of this software
     *    must display the following acknowledgement:
     *     This product includes software developed by the University of
     *     California, Berkeley and its contributors.
     * 4. Neither the name of the University nor the names of its contributors
     *    may be used to endorse or promote products derived from this software
     *    without specific prior written permission.
     *
     * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
     * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
     * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
     * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
     * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
     * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
     * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
     * SUCH DAMAGE.
     */
    
    #include <sys/cdefs.h>
    #if defined(LIBC_SCCS) && !defined(lint)
    #if 0
    static char sccsid[] = "@(#)strtok.c     8.1 (Berkeley) 6/4/93";
    #else
    __RCSID("$NetBSD: strtok.c,v 1.10 1999/09/20 04:39:49 lukem Exp $");
    #endif
    #endif /* LIBC_SCCS and not lint */
    
    #include <assert.h>
    #include <string.h>
    
    char *
    strtok(s, delim)
           char *s;
           const char *delim;
    {
           const char *spanp;
           int c, sc;
           char *tok;
           static char *last;
    
           /* s may be NULL */
           _DIAGASSERT(delim != NULL);
    
           if (s == NULL && (s = last) == NULL)
                  return (NULL);
    
           /*
            * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
            */
    cont:
           c = *s++;
           for (spanp = delim; (sc = *spanp++) != 0;) {
                  if (c == sc)
                         goto cont;
           }
    
           if (c == 0) {              /* no non-delimiter characters */
                  last = NULL;
                  return (NULL);
           }
           tok = s - 1;
    
           /*
            * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
            * Note that delim must have one NUL; we stop if we see that, too.
            */
           for (;;) {
                  c = *s++;
                  spanp = delim;
                  do {
                         if ((sc = *spanp++) == c) {
                                if (c == 0)
                                       s = NULL;
                                else
                                       s[-1] = 0;
                                last = s;
                                return (tok);
                         }
                  } while (sc != 0);
           }
           /* NOTREACHED */
    }
    
  3. 次は,UNIX コマンド tee のソースファイルである。解読せよ。(Copyright は,UC Berkeley にあります。改良したものも含め,再配布するときは,copyright を必ず保持してください。)
    /*       $NetBSD: tee.c,v 1.6 1997/10/20 00:37:11 lukem Exp $       */
    
    /*
     * Copyright (c) 1988, 1993
     *       The Regents of the University of California.  All rights reserved.
     *
     * Redistribution and use in source and binary forms, with or without
     * modification, are permitted provided that the following conditions
     * are met:
     * 1. Redistributions of source code must retain the above copyright
     *    notice, this list of conditions and the following disclaimer.
     * 2. Redistributions in binary form must reproduce the above copyright
     *    notice, this list of conditions and the following disclaimer in the
     *    documentation and/or other materials provided with the distribution.
     * 3. All advertising materials mentioning features or use of this software
     *    must display the following acknowledgement:
     *       This product includes software developed by the University of
     *       California, Berkeley and its contributors.
     * 4. Neither the name of the University nor the names of its contributors
     *    may be used to endorse or promote products derived from this software
     *    without specific prior written permission.
     *
     * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
     * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
     * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
     * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
     * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
     * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
     * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
     * SUCH DAMAGE.
     */
    
    #include <sys/cdefs.h>
    #ifndef lint
    __COPYRIGHT("@(#) Copyright (c) 1988, 1993\n\
           The Regents of the University of California.  All rights reserved.\n");
    #endif /* not lint */
    
    #ifndef lint
    #if 0
    static char sccsid[] = "@(#)tee.c       8.1 (Berkeley) 6/6/93";
    #endif
    __RCSID("$NetBSD: tee.c,v 1.6 1997/10/20 00:37:11 lukem Exp $");
    #endif
    
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <signal.h>
    #include <errno.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <locale.h>
    #include <err.h>
    
    typedef struct _list {
           struct _list *next;
           int fd;
           char *name;
    } LIST;
    LIST *head;
    
    void       add __P((int, char *));
    int       main __P((int, char **));
    
    int
    main(argc, argv)
           int argc;
           char *argv[];
    {
           LIST *p;
           int n, fd, rval, wval;
           char *bp;
           int append, ch, exitval;
           char *buf;
    #define       BSIZE (8 * 1024)
    
           setlocale(LC_ALL, "");
    
           append = 0;
           while ((ch = getopt(argc, argv, "ai")) != -1)
                  switch((char)ch) {
                  case 'a':
                         append = 1;
                         break;
                  case 'i':
                         (void)signal(SIGINT, SIG_IGN);
                         break;
                  case '?':
                  default:
                         (void)fprintf(stderr, "usage: tee [-ai] [file ...]\n");
                         exit(1);
                  }
           argv += optind;
           argc -= optind;
    
           if ((buf = malloc((size_t)BSIZE)) == NULL)
                  err(1, "malloc");
    
           add(STDOUT_FILENO, "stdout");
    
           for (exitval = 0; *argv; ++argv)
                  if ((fd = open(*argv, append ? O_WRONLY|O_CREAT|O_APPEND :
                      O_WRONLY|O_CREAT|O_TRUNC, DEFFILEMODE)) < 0) {
                         warn("%s", *argv);
                         exitval = 1;
                  } else
                         add(fd, *argv);
    
           while ((rval = read(STDIN_FILENO, buf, BSIZE)) > 0)
                  for (p = head; p; p = p->next) {
                         n = rval;
                         bp = buf;
                         do {
                                if ((wval = write(p->fd, bp, n)) == -1) {
                                       warn("%s", p->name);
                                       exitval = 1;
                                       break;
                                }
                                bp += wval;
                         } while (n -= wval);
                  }
           if (rval < 0) {
                  warn("read");
                  exitval = 1;
           }
    
           for (p = head; p; p = p->next) {
                  if (close(p->fd) == -1) {
                         warn("%s", p->name);
                         exitval = 1;
                  }
           }
    
           exit(exitval);
    }
    
    void
    add(fd, name)
           int fd;
           char *name;
    {
           LIST *p;
    
           if ((p = malloc((size_t)sizeof(LIST))) == NULL)
                  err(1, "malloc");
           p->fd = fd;
           p->name = name;
           p->next = head;
           head = p;
    }
    
[C言語目次へ]

[この項を1頁で読む (95)]

付録 ASCIIチャート(ASCIIコード表)

 UNIX (Mac OS X を含む) ユーザは,ターミナルより man ascii で確認可能。但し,国際規格は ISO/IEC 6429 と ISO/IEC 10646-1 で規定されている。

十進法八進法十六進法文字
0000NUL (null)
1101SOH (start of heading)
2202STX (start of text)
3303ETX (end of text)
4404EOT (end of transmission)
5505ENQ (enquiry)
6606ACK (acknowledgement)
7707BEL (bell)
81008BS (backspace)
91109HT (character tabulation)
10120aLF (line feed)
11130bVT (line tabulation)
12140cFF (form feed)
13150dCR (carriage return)
14160eSO (shift-out)
15170fSI (shift-in)
162010DLE (data link escape)
172111DC1 (device control one)
182212DC2 (device control two)
192313DC3 (device control three)
202414DC4 (device control four)
212515NAK (negative acknowledgement)
222616SYN (synchronous idle)
232717ETB (end of transmission block)
243018CAN (cancel)
253119EM (end of medium)
26321aSUB (substitute)
27331bESC (escape)
28341cIS4 (file separator)
29351dIS3 (group separator)
30361eIS2 (recode separator)
31371fIS1 (unit separator)
324020SP (space)
334121!
344222"
354323#
364424$
374525%
384626&
394727'
405028(
415129)
42522a*
43532b+
44542c,
45552d-
46562e.
47572f/
4860300
4961311
5062322
5163333
5264344
5365355
5466366
5567377
5670388
5771399
58723a:
59733b;
60743c<
61753d=
62763e>
63773f?
十進法八進法十六進法文字
6410040@
6510141A
6610242B
6710343C
6810444D
6910545E
7010646F
7110747G
7211048H
7311149I
741124aJ
751134bK
761144cL
771154dM
781164eN
791174fO
8012050P
8112151Q
8212252R
8312353S
8412454T
8512555U
8612656V
8712757W
8813058X
8913159Y
901325aZ
911335b[
921345c\
931355d]
941365e^
951375f_
9614060`
9714161a
9814262b
9914363c
10014464d
10114565e
10214666f
10314767g
10415068h
10515169i
1061526aj
1071536bk
1081546cl
1091556dm
1101566en
1111576fo
11216070p
11316171q
11416272r
11516373s
11616474t
11716575u
11816676v
11916777w
12017078x
12117179y
1221727az
1231737b{
1241747c|
1251757d}
1261767e~
1271777fDEL (delete)
[C言語目次へ]