第
6
章C
言語入門6.1
ここでの目的ここではC言語の文法を中心に解説をするが,単にC言語を用いてプログラムが書けるようになること が目的なのではなく, C言語の言語構造を理解し, C言語の特徴である移植性の高いプログラムを書けるよ うにすることが重要である.
またプログラムは,自分自身だけが読むものではなく他人にも読めるように,わかりやすく簡潔に書くべ きである. これらのことを念頭において学ぶことを期待したい.
6.2 C
言語とはプログラム言語 Cは B. Kernighanと D. Ritchieによって1972年に開発された[1]. 元々は, UNIX オペレーティングシステムを記述するために開発された言語で,システムを記述する能力や可搬性に優れる という特徴を持つ. そのため, C言語を理解するには,オペレーティングシステムの知識,ハードウェアの 知識などが必要であり,初学者には敷居の高い言語であることは事実である.
しかしながら,その移植性の高さとシステム記述能力の高さにより, UNIXをはじめとする各種のシステ ム上のアプリケーションは,現在でもCで記述されているものが多い1. C言語の文法は機械にとっては理 解しやすい形式を持っていて,その分だけプログラマにとっては難解な部分も多い. しかし,機械にとって 理解しやすいということは,処理系の記述が易しいということであり,現在ではほとんどすべてのOS上で C言語処理系が存在している.
この章では, ANSI規格のC言語を[2]の内容にそって解説を行う. なお, C言語の標準規格はISO/IEC
9899-1990であり,その規格書はANSI から入手可能である. また, ANSI Cはそのまま JIS X3010-1993
となっているので,日本語訳はJIS ハンドブック [3]で入手可能である. ANSI Cの Rationale(基本概 念)部分は[4, 5]で入手可能である.
6.3 C
言語をはじめる前にC言語によるプログラミングをはじめる前に,後の混乱を避けるための注意をする. 以下の注意書きは, 無用なトラブルを回避するためできる限り守った方がよい.
• 各ファイルには,その中身がわかるような簡潔なファイル名をつけること.
• 各プログラム毎にサブディレクトリを作成することが望ましい.
• test.cなど,既存のプログラム名に.cを付加したファイル名は避けること.
• 不要になったファイルは削除すること.
1最近ではJAVAなどの言語も流行であるが, JAVAはCを元にした仕様を持ち,クラスライブラリによって,システム仕様など を吸収している部分が多く,コンピュータの理解のためにはCの方が望ましいと考えられている.
多くのプログラムソースを書いた時に,それぞれのファイルがどのような内容のものかがわからなくなるこ とが良くある.
また, プログラムソースを見やすくするために, 「字下げ」をきちんとすること. Emacsで XXX.cとい うファイルを編集する際の「字下げ」の方法は各行頭でTabを押すことにより行なう. これだけでEmacs が状況に応じて適当に処理してくれる2.
6.4 C
のプログラムの書き方・実行の方法講義で利用する処理系は gccとよばれるコンパイラである. 実行コードを作成するには以下の手順で行 なう.
1. エディタ(Emacsなど)を利用して,プログラムソースを書く.
2. コンパイラを起動して,実行コードを作成する.
3. 作成した実行コードを実行する.
例えば, Emacs を利用して, hello.cというプログラムソースを作成した時,これをコンパイルするには, 以下のようにする.
% gcc hello.c -o hello
最後の-o helloという部分は,実行コードをhelloという名前で作成することを指示している. もし,-o
helloという部分を省くと,コンパイラはa.outという名前で実行コード3を作成する.
ここで作成したhelloを実行するには
% ./hello
とする. ここで,./をわざわざ指定していることに注意せよ4.
6.4.1 C
のプログラムのコンパイル方法の詳細gccを利用してANSI規格のC言語のプログラムをコンパイルする時には,単に
% gcc hello.c -o hello
とするだけではなく, gccのオプションをより詳しく設定すべきである. 具体的には,
% gcc -Wall -ansi -pedantic-errors -O hello.c -o hello ここで,新しく付け加えたオプションの意味は次の通りである.
-Wall 文法エラーではない「警告」を軽微なレベルまで出力する.
-ansi ANSI規格のCとしてコンパイルする.
-pedantic-errors ANSI規格のC以外のコードをエラーとして扱う.
2これはemacsの“c-mode”の特徴である.
3多くのUNIX上の処理系では,コンパイラが出力する実行コードのデフォールトの名前はa.outになる. これは, Assembra
Outputの略.最近のLINUX, FreeBSDではelfという名前になるものがある.これら実行形式の名前の違いは,実行形式の違い
でもある.
4UNIXではカレントディレクトリはデフォールトではコマンドサーチパスには入っていないし,入れない方が望ましい.
-O オブジェクトコードの最適化を行う.
これらのオプションの詳細については, Section 6.21 を参照せよ.
Exercise 6.4.1 test.cというプログラムを
% gcc test.c -o test として,作成して,
% test
とすると,どのようなことが起こるか. それは何故か?
より高度なプログラムを書く場合には,複数のファイルからなるプログラムを作成することがある. その ような場合には,一度にコンパイルすることはできないので,それぞれのファイルをコンパイルして,リン クと呼ばれる操作で実行コードを作成する.
6.5 C
言語の基本的な注意6.5.1 C
の基本的な構造Cのプログラムは,コメント(注釈)(comment),プリプロセッサ命令(preprosessing),文(state- ment),関数(function)の集まりで構成されている. それぞれの文は,; で終るか,{ }で囲まれた文の集 まりである複文(compound statement)で構成されている. 関数自身もまた文である. 実際には,main という名前の特別な関数がはじめに実行され,そこに記述されている順序にしたがって実行される.
6.5.2 C
で利用できる文字Cでは, 英文字,数字, 空白文字(スペース,タブなど), 記号文字, 改行文字などが利用できる. 記号文 字には特別な意味があることが多いので,注意すること. またCでは,大文字と小文字は区別される. C の コンパイラにおいて,日本語が利用できるといっても,変数名などに日本語が利用できるわけではないので 注意すること. 日本語が利用できるのは,文字列に日本語が利用できるという程度の意味であり,今回利用 する処理系では,このコードはEUCでなければならない.
また,以下のものは全て空白と見なして無視される.
• 空白文字,改行文字, タブ, 改ページ記号,コメント.
行末に\を書くと, 行の連結を表し, 1行として扱われる.
6.5.2.1 行
C言語では「行」という概念は存在しない. 改行文字は空白文字と見なされるが,改行文字の直前に\が ある場合には,行の連結を表し,処理系によって\と改行文字の連続は一つの空白文字に置き換えられる.
6.5.3
コメントコメント(comment)とは,プログラミングの補助となるようソースプログラム中に書かれた注釈部分
のこと. コンパイラは単にこれを無視するので,プログラムには影響を与えない. コメントは,/* ではじ まり,*/で終り, 入れ子にはできない. すなわち,コメントの中にコメントを入れることはできない. また, 文字定数,文字列の中にはコメントを書くことはできない.5
6.5.4
トークントークン (token)とは,空白やコメントによって区切られた文字の列で, コンパイラが認識する最小の
単位である. トークンは以下のいずれかに分類される.
• 演算子 (+ - など)
• デリミタ(区切り子)({ } ( ) ; など)
• 定数(整数,浮動小数点数,文字定数)
• 文字列リテラル
• 識別子
• キーワード
6.5.4.1 定数
C言語における定数(constant)は整数定数,浮動小数点定数, 文字定数,列挙定数に分類される. 整数 定数は,8進数,10進数,16進数による表現が可能である. それぞれを区別するには,以下の規約による.
• 16進数:0x で始まり,後に0〜9, a〜fがいくつか続く. a〜fとxは大文字でも良い.
• 10進数:0以外ではじまり,後に0〜9がいくつか続く.
• 8進数 :0 で始まり,後に0〜7がいくつか続く.
0x10を 0x0010と書いても良い.
また, 整数定数に u または U をつけると, 符号なしの数を表し, l または L をつけると “長い” 整数
(long)を表す.
浮動小数点定数は, 以下の形をしている.
整数部 . 小数部 e 指数 接尾子
“e 指数” 部分は省略することができる. また, 指数の記号eは E を用いても良い. 接尾子は以下のいず れか.
• fまたはF:float型
• 接尾子なし: double型
5コメント文の説明として,「//から始まり,その行末まではコメント文」であると記述してあるものがある. これはANSIの規 格では誤りである. C++のANSI規格ではこれをコメント文としていて, gccはC++のコンパイラでもあるので,-ansiオプショ ンをはずしてコンパイルすると,そのまま通ってしまう.
• lまたはL:long double型
文字定数とは,’で括られた文字の列である. 例えばaという文字を表すには’a’と書く.
以下の特別な文字を表す以外には,文字定数は一文字でなくてはならない.
\n 改行 \r 復帰
\f 改ページ \t 水平タブ
\v 垂直タブ \b 後退
\a ベル \? ?
\’ ’ \" "
\\ \
\ooo 8進数で ooo. 3桁以下 \xhh 16進数でhh. 2桁以下 このような特別な文字のことをエスケープ文字(escape charactor)と呼ぶ.
6.5.4.2 文字列リテラル
文字列定数とも呼ばれ,"で囲まれた文字の列である. 文字列リテラル6(string literal)が記憶域に格納 される時には,末尾に \0が付けられる. また, 隣接する2つ以上の文字列リテラルは連結される. 例えば
"abc" "ABC"は "abcABC"となる.
また,
"abc"
"ABC"
は"abcABC"に連結される.
6.5.4.3 識別子
識別子(identifier)とは,変数,関数などに付けられる名前のことである. ここで与えられた名前にした
がって,コンパイラはそれぞれを区別する.
識別子として使える文字は,英文字, 数字,_であって,数字を先頭にすることはできない. また, C言語 の規約によって, 31文字までは区別され,大文字と小文字は区別される7. 即ち, 32文字目以後が異なるよ うな2つの名前は区別されるとは限らない.
また, C 言語には名前空間,スコープという概念があり,同じ名前を与えても, 違うものを示しているこ とがあるので注意すること. これについては後ほど解説する.
6.5.4.4 キーワード
以下の単語はキーワード(keyword)と呼ばれ,特別な意味を持ち, 識別子としては利用できない.
6「リテラル」(“literal”)とは,「文字通りの」という意味である.
7正確には,
• 内部識別子またはマクロ名においては意味のある先頭の文字数は31文字であり,大文字と小文字が区別される.
• 外部識別子においては意味のある先頭の文字数を6文字に制限して良く,大文字と小文字の区別を無視しても良い.
というのがANSIの規格[3, X3010 6.1.2, p. 1856]である. しかし,最近の処理系で外部識別子が6文字に制限されたり,大文字と 小文字の区別を無視するようなものは見当たらない.
char double int long enum float short unsigned signed void typedef auto register extern
static volatile union struct const sizeof if
else switch case default break return for
while do continue goto
6.5.5
文文 (statement)とは, C言語のプログラムの基本的な単位になるもので,それぞれの文は; によって
終了する. 一つの文が複数行にわたっても良い.
Example 6.5.1 次の各行は全て文である.
int n ;
printf("Hello World.\n") ; x + y ;
a = b ;
;
最後の行は空文と呼ばれる.
また,{,}によって文の集まりを一つの文(複文(compound statement))にすることができる.
Example 6.5.2 次は一つの複文である.
{
a = b ; c = d ; }
6.5.6
式式 (expression)とは,計算を行なう最小単位のことで, それぞれの式は値を持つ. その値は, 次のよう
にして決まる.
• 計算式の場合は,その結果.
• 代入式の場合は,その左辺の値.
• 関数の場合は,その戻り値.
• 比較の場合,真ならば 1,偽ならば0.
式に; をつけることにより,文にできる. それを式文(expression statement)と呼ぶ.
Example 6.5.3 次のようなものは式文である.
a ; /* 値は a */
x + y ; /* 値は x + y */
a = b ; /* 値は代入された a の値 */
printf("Hello World.\n") ; /* 値はこの関数の戻り値 */
a = b = c ; /* 値は代入された a の値 */
a = b, c = d ; /* 値は最後に代入された c の値 */
a < b ; /* 値は真ならば 1, 偽ならば 0 */
6.6
用語6.6.1
処理系とその動作Cにおいて,処理系(implementation)とは,特定の環境中の特定のオプションの下で,特定の実行環境 用のプログラムに翻訳を行うソフトウェア群を指し,処理系依存(implementation-defined behavior) とは, その処理系ごとにどのように振舞いかが規定されているものである. 一方, 不定(または未定義)
(undefined behavior)とは, 同じ処理系であっても, その振舞いが規定されていないものを指す. 特に,
最適化(オプティマイザ (optimizer))の指定によって振舞いが変わることが多い. 未規定 (unspecified behavior)とは,規格がその動作を一切指定しないものを指す. この他に,処理系依存の項目の一部として, 文化圏固有動作 (locale-specific behavior)がある. これは, 処理系そのものに依存するのではなく, 処 理系動作またはプログラム動作中に与えられた文化圏情報(locale)ごとに,処理系が明示的に動作を規定 するものである.
Example 6.6.1 未規定の動作の例としては,関数の実引数の評価順序. 不定(未定義)の動作の例として は, 整数演算のオーバフローに対する動作. 処理系定義の例としては,符号付き整数を右シフトした場合の 最上位ビットの伝播. 文化圏固有動作の例としては, islower 関数が26個の英小文字以外の文字に関し て, 真を返すかどうかがある.
ANSIの規格にしたがった処理系とは, ANSIの規格で規定されているすべての動作を受理しなくてはなら ない.
6.6.2
その他ANSI規格で定められているその他の用語として,バイト,文字,オブジェクトがある. バイト(byte)と は,実行環境中の基本文字集合の任意の要素を保持するために十分な大きさを持つデータ記憶領域の基本単 位と定められ,1バイトのビット数は処理系依存,バイトは連続するビット列からなる. 文字(character) とは,1バイトに収まるビット表現. オブジェクト(object)とは,その内容によって,値を表現できる実行 環境中の記憶領域. ビットフィールド以外のオブジェクトは, 連続する一つ以上のバイトの列からなる. ま た, オブジェクトを参照する場合, オブジェクトは特定の型を持っていると解釈して良い.
6.6.3
コンパイラとインタープリタの違い処理系に関する用語を解説したついでに,コンパイラとインタープリタという用語と,その違いを解説し ておこう.
言語処理系は,対応する言語の文法にしたがって記述されたソースコード(source code)を受取り,ソー スコードに記述された内容にしたがって,次のいずれかの動作を実行する.
1. ソースコードを読み取り, 指定された環境8に対するオブジェクトコード(object code)または, 実 行可能コード(executable code)を出力する.
すなわち,ソースコードの内容を解釈し,その結果となるオブジェクトコードまたは実行可能コード を出力する.
2. 読み取ったソースコードに記述された命令列を,一つの命令を読み取ると,すぐにその命令を実行す る. すなわち,読み取ったソースコードの内容を逐次実行する.
このように,処理系の動作には2種類があり,前者の動作を行う処理系をコンパイラ(compiler),後者の動 作を行う処理系をインタープリタ(interpreter)と呼ぶ.
Example 6.6.2 多くのC言語処理系の場合には,C言語のプログラムソースコード(これは,単なる「テ キストファイル」であり, コンピュータにとっては, 単なる文字のならび以上の意味を持たないデータで ある.)を受取り,そこに記述されたCの文法通りのオブジェクトコードまたは実行可能コードを出力する.
ユーザにとっては,処理系が出力した実行可能コードを実行することにより,C言語で記述されたプログラ ムを動作させることができる.
したがって,gccはコンパイラである.
標準的なC言語処理系はコンパイラであり,一つのソースコードを2度にわたって解釈系に通す.9
Example 6.6.3 標準的なperlはインタープリタであり,入力されたperlスクリプトを逐次実行する. し たがって,入力ソースコードに文法エラーがあれば, 文法エラーの直前までは実行が行われる.
このように,処理系には2種類があり,安易に考えるとインタープリタの方がうれしいように思えるのだが, コンパイラでは複数回の解釈系の実行を通じて,「変数名」などの「シンボル名」の相互参照や,実行可能 コードの最適化(optimization)を効率よく行うことが可能となる.10
6.7
最も簡単なプログラムはじめにいくつかの最も簡単と思われるプログラムを書いてみよう.
6.7.1 Hello World
プログラムを実行すると画面に何かを表示するものである.
Example 6.7.1 もっとも簡単なプログラムの例
8わざわざ「指定された環境」と書いたのには理由がある.一般には,コンパイラは処理系を動作させた環境でのオブジェクトコー ドまたは実行可能コードを出力するが,クロスコンパイラ(cross compiler)と呼ばれる,他の環境でのオブジェクトコードまたは 実行可能コードを出力する処理系も存在する.
9不思議なことに,インタープリタとして動作するC言語処理系も存在する.
10最適化とは,オブジェクトコード中の無駄や意味のない部分などを,より高速化が可能なようにする操作である.
/* Program 1 *
* Hello World. を出力する. *
* */
#include <stdio.h>
int main(int argc, char **argv) {
printf("Hello World.\n") ; return 0 ;
}
以下では,このプログラムの内容を説明する. (ただし,コメント,空白行は行数として数えない.)
1行目
#include <stdio.h>
#ではじまる行はプリプロセッサと呼ばれるものによって処理される.
Cコンパイラは, 実際には以下の手順によって実行される.
1. プリプロセッサによる前処理.
2. コンパイラによるオブジェクト・コードの作成.
3. リンカによるオブジェクト・コードとライブラリの結合.
C言語のプログラム中に,#ではじまる行があらわれると,プリプロセッサはその文法にしたがって,コー ドを書き換える. 実際,#include という指示は,これに続くトークンで指示されたファイルを,その位置 に挿入する命令である.
C言語では,原則として全ての関数は, 定義されたり,利用される前に宣言されなくてはならない. そこ で, 標準的な関数(このプログラムではprintf)を使うためには,その宣言が書かれているファイル(こ
こではstdio.h)を挿入することによって,その関数の宣言をする. このような(標準関数の)宣言が書か
れているファイルのことをヘッダ・ファイルと呼ぶ.
また,ヘッダ・ファイルの挿入には
#include <XXXX.h>
#include "XXXX.h"
の2つの書き方がある. コンパイラの実装によって決まる標準的な場所11にあるファイルを挿入するには 前者の方法を使い,カレント・ディレクトリにあるファイルを挿入するには後者の方法を使う.
どのような関数が,どのヘッダ・ファイルで宣言されているかはオンライン・マニュアルを見ればわかる.
2行目
int main(int argc, char **argv)
これはmainという関数の定義である. この関数の本体は3行目の {と6行目の}に囲まれた部分である.
この部分は次の3つの部分に分解される.
11これは,コンパイル時のオプションで変更可能
• int
• main
• (int argc, char **argv)
はじめの intは, この関数の戻り値が int型であることを示す. 関数の戻り値が書かれていない時には, コンパイラはintであると解釈する.
次のmainは関数の識別子である.
ここで使われている(int argc, char **argv)に関しては後に議論するので,取りあえずここでは「お 約束」としておこう. ここには,(存在すれば)その関数の引数が書かれる. 引数をとらない場合にも()
または(void)と書かなくてはならない.
プログラムが開始されるときには,その時点で呼び出される関数の名前はmainでなければならない. す なわち,mainという名前を持つ関数が,プログラム開始時点で最初に呼び出され実行される.
4行目
printf("Hello World.\n") ;
ここで利用されたprintf という関数は,その引数として,文字列リテラルをとり,その文字列リテラルを 標準出力に出力する.12 ここで,最後の;によって,この一行が文になっていることを注意せよ.
本来,この関数には戻り値が存在するが,ここではその戻り値は利用していない.
5行目
return 0 ;
returnという文は次の形でなくてはならない.
return 式 ;
式の部分には,どのような式を書いても良い. ここでは,単に0という式を書いている.
この文は, main関数の戻り値を与えている. main関数が終了した時点でプログラムの終了処理が行わ れ, main関数の戻り値はプログラムを実行したシェルに返される13.
Exercise 6.7.2 Example 6.7.1を真似て,次のような出力を得るプログラムを書け.
各自の学籍番号 (改行) 各自の名前 (改行) 何でも好きなこと (改行)
Exercise 6.7.3 printf という関数の戻り値は,出力した文字数である. mainの戻り値として,出力した 文字数を返すようにExample 6.7.1を変更せよ. ただし,変数を用いてはならない. シェルに戻された戻り 値は, cshの場合は
% echo $status
を実行することで得ることができる。
12ここの解説は本当は正しくない。この関数はもっと多くの引数をとり、最初の引数も文字列リテラルである必要はない.
13main関数が明示的な戻り値を持たない場合には,シェル(ホスト環境)に返される値は不定となる.
6.7.2
値を表示する上の例(Hello World)で,printf関数は「画面に出力する」手続きを行っているものであることがわかっ
た. 次の例では,「値」を出力することを考えてみよう.
Example 6.7.4 定数の値を出力する.
/* Next example */
/* ex02.c */
int main(int argc, char **argv) {
printf("Hello World\n") ;
printf("ここには値1が出力される: %d\n", 1) ; printf("ここには値 1.0 が出力される: %f\n", 1.0) ; printf("ここには文字 x が出力される: %c\n", ’x’) ;
printf("ここには文字 x のコード値が出力される: %d\n", ’x’) ;
printf("ここには文字 x のコード値が16進で出力される: %x\n", ’x’) ; printf("ここには文字 y のコード値が16進で出力される: %x\n", ’y’) ; printf("ここには文字列 abc が出力される: %s\n", "abc") ;
printf("ここには値 1 と 1.0 が出力される: %d, %f\n", 1, 1.0) ; printf("ここには値 3 が出力される: %d\n", 1+2) ;
printf("ここには文字 x と y のコード値の和が16進で出力される: %x\n", ’x’+’y’) ; return 0 ;
}
このように,printf関数は,定数式の値を出力することができる. この時,以下のことに注意をしよう.
• 定数の値が「整数」の場合には, “%d” とした場所に値が(10進で)出力される.
• 定数の値が「整数」の場合には, “%x” とした場所に値が(16進で)出力される.
• 定数の値が「実数」の場合には, “%f” とした場所に値が出力される.
• 定数の値が「文字」の場合には, “%c” とした場所に値(文字)が出力される.
• 定数の値が「文字列」の場合には, “%s”とした場所に値(文字列)が出力される.
• 定数の値と“%d”などの(上のような)対応が取れていない場合には,何が出力されるかはわからない.
• 複数の “%d”や“%f”などを使った場合には,それ以後の「値」の対応する順番で出力が行われる.
• 定数の値が「算術式」である場合には,その算術式の結果の値が出力される.
なお,値が「実数」の場合に“%f”の出力結果の小数点以下の表示桁数は処理系に依存している.
6.8
変数とは変数(variable)とは,識別子によって区別された初期化,代入などが許される記憶領域のことである. C
言語の変数には,多くの型があり,それぞれの型によって,どれだけの記憶領域が確保されるかが異なる. ま た, C言語の変数には記憶クラス,スコープ,寿命,リンケージなどの概念があるが,それらについては関数, 分割コンパイルの後で述べる. ここでは,変数の宣言,型などについて考える.
6.8.1
変数の宣言C言語では全ての変数は使う前に宣言しておかなくてはならない. 宣言は変数の性質を告げるためのも ので,
int step ;
int lower, upper ; float x ;
のように,型の名前と変数(の識別子)の名前のリストからなる.
これらの宣言を色々な場所に書くことで,それらの変数の意味が変わるが,ここでは,次のように,どのブ ロックにも含まれず, すべての手続きの前に書くことにする. (下の例を参照.)
#include <stdio.h>
int step ;
int lower, upper ; float x ;
int main(int argc, char **argv) {
...
}
6.8.2
変数の初期値Cでは変数は,定義されただけでは値は定まらない(と考えた方が良い)14. そのため,(必要なら)そ の変数を使う前に初期化を明示的に行なう必要がある.
変数の初期化の方法には2通りある.
...
int a=0, b ;
int main(int argc, char **argv) {
b = 0 ; ...
}
このように,宣言と同時に初期化することもできる. a の初期化は実行時にただ一度だけ行なわれるが, bの場合は, この文を実行されるたびにbに0が代入される.
C においては, 変数の宣言と定義は異なり, 宣言だけではメモリ領域が確保されない. これに関しては, extern宣言を参照.
14[2, 2.4]によれば,次のように書かれている:外部変数,静的変数はゼロに初期化される. 明示的な初期化式がない自動変数は不
定(ゴミ)の値を持つ. (External and static variables are initialized to zero by default. Automatic variables for which there is no explicit initializer have undefined (i.e., garbage) values.)
Example 6.8.1 変数の初期化及び,値の代入を行ってみる.
#include <stdio.h>
int a=0, b ;
int main(int argc, char **argv) {
printf("a = %d, b = %d\n", a, b) ; b = 1 ;
printf("a = %d, b = %d\n", a, b) ; return 0 ;
}
この時,最初のprintf関数で出力される,変数bに格納された値は「不定」であることに注意.
6.8.3
変数の型Cで定義されている変数の型は以下の通りである.15 変数の型 型の名前
char 文字型
short int 短い整数型shortと書いても良い
int 整数型
long int 長い整数型longと書いても良い
float (単精度)浮動小数点型
double 倍精度浮動小数点型
long double 長い倍精度浮動小数点型
void 何もない型
enum 列挙型
また,char,short int,int,long intに対しては,unsignedを前につけると, それぞれ符号無しの型を 表し,signedをつけるとそれぞれ符号つきの型を表す. 何もつけない時は, short,int,longは符号つき であると解釈される. しかし,charに関しては,どちらになるかは処理系依存である. 例えば,今回使用す るgccの場合は charはsigned charである.
6.8.3.1 const修飾子
変数の型に constという修飾子をつけると, 初期化はできるが, プログラム中で変更のできない定数と して扱うことができる. 例えば,次のように宣言する.
const int a=0 ; float const b=1.0 ;
const宣言をした変数を変更した時の振る舞いは不定である16.
15void,enum,long doubleはANSIの規格ではじめて定義された. Kernighan-Ritchieの初版[1]では定義されていない.
16より正しくは,「const修飾型を持つオブジェクトを,非const修飾型の左辺値を使って変更しようとした場合,その動作は未 定義とする.」というのがANSIの規定.
Remark 6.8.2 このremarkは非常に高度で面倒な内容を含んでいるので,興味のない人は無視してもよ い. また,ここでのプログラム断片は表示を少なくするため, あまりきれいな形にはなっていない.
実はconst宣言をした変数の扱いが非常に厄介で,以下のようなプログラム断片を調べてみよう.
int n ;
const int cn ; cn = n ; n = cn ;
この中で代入が許されるのはどの場合かを考えてみる. 当然 n = cnは許される. しかし, cn = nはgcc の場合に は,assignment of read-only variable ‘cn’という警告が出される. SunのCコンパイラでは,left operand must be modifiable lvalueというエラーとなる.
しかし,次の例はどうだろうか?
char *cp ;
const char *ccp ; ccp = cp ; cp = ccp ;
こちらは ccp = cpが許され,cp = cpp の代入では, gcc ではassignment discards qualifiers from pointer target typeという警告が出される. これでは何を言っているかわからないので, SunのCコンパイラに通してみる と,assignment type mismatch: pointer to char "=" pointer to const charという警告が出る.
まず,cpp = cpが許される理由を考えてみよう. 実は,const char *という型指定は,「const charへのポイン タ」という意味であり,ccp自身をconst宣言しているのではなく,ccpが指し示すオブジェクトがconstと言って いるのである(cf. [2, A.8.6.1]). したがって,次のような例は警告対象となる.
const char *ccp="abc" ;
*ccp =’b’ ; しかし,
char cp[4] ;
const char *ccp="abc" ; ccp = cp ; *cp =’b’ ;
のように,一旦const修飾子がついていないオブジェクトを経由して,const宣言を行ったオブジェクトへのアクセ
スを行うことは,文法上問題は発生しない. しかし,const修飾子は,「読み出し専用」のメモリ領域にオブジェクト を配置して良いことをコンパイラに知らせるという役目も持ち,そのような場合も含めて,この例の結果は不定である と考えるべきである. なお,constポインタを宣言するには,char *const ccpとする. すなわち,
char *const ccp="abc" ;
とすれば,ccp = cpといった代入が許されなくなる.
また,cp = cppが許されない理由は,型の適合性の問題にある. 単純代入が許される条件の一つとして,次の条件が 規定されている. (cf. [3, X3010 6.3.16.1, p. 1890])
• 両オペランドが適合する型の修飾版または非修飾版へのポインタであり,かつ左オペランドで指される型が右オ ペランドで指される型の修飾子をすべて持つ.
cp = cppは右オペランドで指される型の修飾子constを左オペランドで指される型が持たないため,この条件に違
反し,他の単純代入の条件にも合致しないため,文法エラーとなる. さらに,次のような例もある. (cf. [6, p. 48])
int foo (const char **p) {}
int main(int argc, char **argv) { foo(argv) ; }
この例では, gccの警告はpassing arg 1 of ‘foo’ from incompatible pointer typeとなる. これは,関数foo の仮引数 const char **pが「const 修飾された char 型変数へのポインタのポインタ」であり, 実引数argv は
「char型変数へのポインタのポインタ」である. そこで, ANSI規格 6.3.2.2を見てみよう. (cf. [3, X3010 6.3.2.2,
p.1876])そこには,関数呼び出しの制約として,「各実引数は,対応する仮引数の型の非修飾版を持つオブジェクトに
その値を代入できる型を持たなければならない」と書かれている. つまり,引数を渡すと代入が行われ,実引数と仮引 数は代入が許される関係になければならないということである. したがって,
int foo (const char *p) {}
int main(int argc, char **argv) {
char *q ; foo(q) ; }
という例であれば,p = q という代入が行われることに相当し,上で述べた単純代入の規約を満たす. しかし,const char **の例では,仮引数pは「const char *へのポインタ」であり,実引数argvは「char *へのポインタ」で あるため,単純代入の規約を満たさない.
なお,char *を仮引数とする多くの標準ライブラリ関数(例えば,strcpyなど)は,仮引数としてconst char * を宣言している. これは,関数内で明示的に仮引数の指し示す値を変更しないための措置である.
6.8.3.2 変数と記憶領域
変数は宣言と同時に対応する記憶領域が確保される. しかしながら,それぞれの型に対して,どれだけの 記憶領域が確保されるかは処理系依存である.
それぞれの型がどれだけの記憶領域をとるかを調べるには, sizeof 演算子を使う. sizeof 演算子の利 用法は以下の通りである.
sizeof (型) ;
sizeof オブジェクト ;
ここで,その結果として得られる値は,符号なし整数で表現され17,その意味は,char型の何倍の記憶領 域が確保されるかを表す. 即ち,sizeof(char)の結果は処理系によらず1である.
それでは, char型がどれだけの記憶領域を使うかを知るには, どのようにすれば良いのだろうか. それ には, C言語の標準的なヘッダ・ファイルを見れば良い. 実際, limits.hに定義されている CHAR BITと いうマクロ18の値がchar型のビット数である. Sun Sparc StationのCコンパイラの場合,
#define CHAR_BIT 0x8
となっているので,char型は8ビット(1バイト)であることがわかる.19 また,int型の長さは,その計 算機の自然な長さであると定義されている.
それぞれの変数が記憶領域に確保される時,宣言した順序で記憶領域内に確保されるという保証はない.
また,多くの処理系では,int,longはワード境界にアロケートされる.
Example 6.8.3 intが2バイト,longが4バイトの処理系で, char c ;
int n ; long l ; char d ;
と変数を定義した場合,下の図のいずれのメモリ配置をとるかは処理系や最適化に依存する. これ以外の取 り方をする可能性もある.
17正確にはsize t型で表現される.size t型がどの型に対応するかは処理系依存である.
18マクロの意味は後日解説する.
19ANSIの規格書によれば,char型の占めるビット幅を1バイトと定義している.
16 bits c (padding) n l
d (padding)
16 bits
c d
n l
16 bits
c n
n(続き)l d
(a) (b) (c)
この中で(c) のメモリ・アロケーションはアライメント(alignment,境界調整)に適合していない環境が 多いため,ほとんどこのようなアロケーションは行われない.
Example 6.8.4 変数に値を代入する操作とは,変数に対して与えられたメモリに数値を書き込むことに他
ならない. 例えば, (intが16ビットの場合)
int n ; n = 1 ; とすることは,
16 bits
n 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 16 bits
n =⇒ または
16 bits
n 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 8 bits
と値を代入することになる. 下のように上位バイトと下位バイトを入れ替えて数値を表現する処理系(CPU) をbig endian,上のように数値を表現する処理系(CPU)をlittle endianと呼び, 8080, Z80などのIntel
社のCPUはbig endianになっていることが多く, 68000, SPARC などのCPUはlittle endianになって
いる.
6.8.3.3 文字型と整数型
文字型(character type)とはその名の通り,(1)文字を変数として扱う型である. 例えば,
char c ; c = ’a’ ;
とすると, 変数cには aという文字が代入される. 文字型では, その中身を文字の持つコードの値とし て扱う. したがって, 文字型変数の実体は“整数”と思って良い. (しかしながら,char型を文字として扱 う時には,常に正の数値として扱う.)その意味で, charは整数型(integer type)(short,longなども 含む)の一部として考えると都合が良いことが多い.
Example 6.8.5 例えば, ASCII コード体系の処理系で, char c ;
c = ’a’ ;
とすると,cには16進整数値0x61が入り,整数値0x61として計算や比較が行われる. したがって, char c, d ;
c = ’a’ ; d = ’A’ ;
の時,c + dは16進数値0x41 + 0x61=0xA2となる. また, char c ;
c = 0x61 ;
とすると,cには’a’が代入されたこととなる.
それでは,整数型がどれだけの記憶領域を使うかを考えてみよう. Cでは,short,int,longなどの記憶 領域については,全て処理系に依存していると定義されているが,次の関係だけはCの定義にある.
char≤short≤int≤long
ここで, char≤shortという意味は,short型の記憶領域はchar型のそれよりも短くないという意味で ある. また,short,int型は最低16ビット,long型は最低32ビットが保証されている.
実際,それぞれの整数型の長さ(sizeofの返す値)を見てみると,今回利用する処理系では,次のように なる.
32ビットコンパイラの例 64ビットコンパイラの例
short 2 2
int 4 4
long 4 8
これで,char型が1バイトであることを使うと,intは4バイトであることがわかる.
6.8.3.3.1 整数の内部表現 整数型の変数の内部での表現は, Section 2.3.2で述べた表現がとられている ことが多い. ANSI規格では整数型の内部表現の具体的な方法については何も規定していない.
6.8.3.4 浮動小数点型
浮動小数点型 (floating type)についても, Cでは以下のことしか定義されていない.
float≤double≤long double
実際,それぞれの浮動小数点型の長さ(sizeofの返す値)を見てみると,今回利用する処理系では,次の ようになる.
32ビットコンパイラの例 64ビットコンパイラの例
float 4 4
double 8 8
long double 16 16
これで,char型が1バイトであることを使うと,floatは4バイトであることがわかる.
(10を底とする)浮動小数点数とは,以下のような型で表現された数のことである.
(0でない1桁の数).<仮数部> × 10^<指数部>
ここで,任意の 0でない実数は,このような表示が可能であることに注意せよ. (もちろん, その表示は有 限小数と仮定すれば一意的である.)
6.8.3.4.1 浮動小数点数の内部表現 浮動小数点数の変数の内部での表現は, Section 2.3.2で述べた表現 がとられていることが多い. ANSI規格では整数型の内部表現の具体的な方法については何も規定してい ない.
オーバーフロー,アンダーフローをした数の演算,比較等の結果がどうなるかは規格では定められていな い. しかし,それぞれの処理系によって規定されている.
6.8.3.5 列挙定数
列挙定数(enumeration constant)とは,次のようなものである.
enum day {sun, mon, tue, wed, thu, fri, sat} ; この時,eum型の変数はintとして扱われる. 即ち,上の例では, sun <-> 0
mon <-> 1
などというように対応づけが行なわれて処理される.
6.8.4
変換Cのプログラムで演算を行なう時には,数多くの型の変換が行なわれてから演算が実行される.
6.8.4.1 整数への格上げ
汎整数型(char,short,int,long,このような型をintegral typeと呼ぶ.)に対して,演算を行なう時 には,整数への格上げ(Integral Promotion)(または汎整数拡張)と呼ばれる操作が行なわれることが ある.
それは,以下のように定義されている20 char,shortは,符号つきも符号なしも, 整数が使える式で使っ て良い. この時,元の型の全ての値が intで表現できる時には, その値はintに変換される. intで表現 できない時には unsigned int に変換される. charまたはshort型の変数が unsigned int にしか変 換できないという状況は, shortと intが同じバイト幅である時に起こり, この時, unsigned short は unsigned intに変換されるという意味である. long,unsigned longについては規定されていない.
6.8.4.2 符号拡張
Cではcharは符号つきか符号無しかを規定していない. この時,最上位ビットが1であるようなchar をintに変換する時の振舞いは処理系依存である. 例えば,最上位ビットが1として負の数に変換される
(これを符号拡張と呼ぶ)こともあれば, 0として正の数に変換されることもある.
例えば,charが1バイト,intが4バイトのときに,charがsigned charである時には,符号拡張され, intに変換され計算される. この時,前に述べた2進数の表現がとられ,負の数は2の補数表現がとられて いる時,負のsigned char型の変数は,上位ビットに1が埋められ,signed intと扱われる. 一方,char がunsigned charの場合は,常に0が埋められる.
20[1]の定義によれば,「符号なし型はより広い符号なし型に変換される」とされているので注意すること.
したがって,0x7Fを越えるchar型の変数を扱う時には,必ずunsigned charとし,より広い整数への 変換がある時には, 符号なしで受けなくてはいけない21. 例えば, char が signed charの時, char a = 0x80とすると,(int)aは0xFFFFFF80となるが, charがunsignedの時には, 0x80のままである.
6.8.4.3 符号拡張と整数への格上げの演算への影響
符号拡張と整数への格上げは,char型の変数同士の演算の場合に大きな影響をおよぼす. CPUの演算レ ジスタ長よりも短いメモリサイズを持つ変数に対する演算を行う場合, 何らかの形で演算レジスタ長に合 う値(ビットパターン)に変換を行ってから演算を行う必要がある. 符号拡張・整数への格上げは,演算レ ジスタ長に値を合わせる変換と理解して良い.
Example 6.8.6 標準演算レジスタ長が16ビット,1バイトが8ビットである処理系を考えよう. すなわ ち,intは2バイト長である. さらに,charはsigned charであり,符号拡張を行う処理系であるとする.
この時,次の演算結果はどうなるだろうか?
char c=0x70, d = 0x80 ;
if (d < c) printf("d < c\n") ; else printf("d >= c\n") ;
通常であれば,0x70 < 0x80であるので,d < cが成り立つはずである. しかし,この結果はd >= c と なる. これは,charが符号付きであり,比較< の演算で整数への格上げが行われるため, 符号拡張を受け, 比較の段階で2つの演算レジスタに格納されている値は,dに対応するものは,0xFF80,cに対応するもの は0x0070であり, 0xFF80は負の数と判断されるためである.
この結果を正しく判断させるためには, unsigned char c=0x70, d = 0x80 ; if (d < c) printf("d < c\n") ; else printf("d >= c\n") ;
としなければならない. すなわち, 文字型変数を「符号なし」と明示的に指定し,符号拡張の影響を排除 しなければならない.
6.8.4.4 整数への変換
任意の整数が符号つき型に変換される時, その数が新しい型で表現可能ならば, その値は不変になるが, そうでない時の結果は処理系依存である.
6.8.4.5 整数と浮動小数点数
浮動小数点数を汎整数に変換する時には,小数部は無視される. また,結果として得られる整数が目的の 型で表現できない時の振舞いは不定である.
逆に,整数を浮動小数点数に変換する時には, その結果が表現可能な範囲にある時でも,正確に表現がで きない時には,一番近い大きな数か小さな数のどちらかに変換される.
21Cの定義によれば,「標準文字セットのすべての文字は正の値を持つ」となっている.