wiki:HowTo/CTutorial

Version 10 (modified by 村山 俊之, 14 years ago) ( diff )

--

C 言語チュートリアル

C 言語って何?

UNIX システム発祥の、割と歴史が古いプログラミング言語です。古い言語でありながら、今でも OS や各種アプリケーションの開発に広く使われています。

構造化言語

C 言語は構造化言語です。これはどういう意味かというと、「○○しなさい」という命令について、その「○○」の処理内容を別の場所で具体的に記述し、その中に書かれている「××しなさい」という命令についても、「××」の処理内容がさらに別の場所で具体的に記述される、ということを繰り返すことで、処理内容をツリー構造として表現できる、ということです。

このスタイルの利点は、処理内容のおおざっぱなシナリオをそのままプログラムとして書き表すことができることです。例えば、お店の POS レジでお会計を済ませた後の、レジ端末おける処理について考えてみましょう。必要な処理として考えられるのは、以下のようなシナリオです。

  1. 今回の会計で得た売り上げを、会計システムに通知し、お店の売り上げとして計上する。
  2. 今回販売した商品の品番と個数を、在庫管理システムに通知し、在庫情報を更新する。
  3. 今回の会計の担当者と販売した商品の情報を、従業員管理システムに通知し、従業員の販売成績として記録する。

この処理全体を行う関数を onSelled() とし、 1 の処理を行う関数を notifyProceeds() 、 2 の処理を行う関数を notifyStockConsumption() 、 3 の処理を行う関数を notifyEmployeesSale() とすると、 onSelled() 関数は以下のように書くことができます。

/**
@brief  レジでの販売会計後に呼ばれる関数
@param  commodity_info  商品の情報データへのポインタ
@param  sum             売上金額
@param  uid             担当者の従業員 ID
*/
void onSelled(const item_info_t *commodity_info, long sum, uid_t uid)
{
    notifyProceeds(sum);
    notifyStockConsumption(commodity_info);
    notifyEmployeesSale(uid, commodity_info);
}

notifyProceeds() などの関数が実際にどんな処理を行うかは、それぞれの関数における定義の中でさらに詳細に記述されることになります。

コンパイラ言語

C 言語は、同時にコンパイラ言語でもあります。コンパイラの定義も種々ありますが、人が直感的に理解しやすい高級言語で書かれたプログラムやデータを、コンピュータや OS、アプリケーションなどの「処理系」が理解できる情報に変換することをコンパイルと呼び、そのコンパイルを行うプログラムのことをコンパイラと呼びます。 C コンパイラの場合、 C 言語で書かれたプログラムを、アセンブリ言語で書かれたプログラムに変換しますFootNote(最終的には実行ファイルに変換されるのですが、一般的な C コンパイラコマンドは、「前処理」を行うプリプロセッサ、 C 言語をアセンブリ言語に変換するコンパイラと、アセンブリ言語をバイナリデータに変換するアセンブラ、バイナリデータの名前を解決して実行可能なファイルに変換するリンカ、の 4つのプログラムを組み合わせたものです。)

プログラム・ソースを読み込みながら逐次実行するインタプリタ言語 (BASIC など) と比べると、コンパイラ言語はその特徴として、文法的に完結していることがわかりやすいルールになっている、ということが上げられます。 C 言語の場合、関数などの処理の単位はブレース "{" ~ "}" で括られたブロックとして表現されます。開きブレースと閉じブレースの数が合わなければ、当然文法エラーと見なされ、コンパイルを行う時点でエラーとしてはじかれます。

また、 C 言語は変数や関数の宣言や定義にうるさい言語でもあります。定義されていない名前の関数を呼び出そうとしたり、宣言されていない変数を使用しようとするようなプログラムも、やはりコンパイル時にエラーになります。これらの性質は、多くのスクリプト言語が、定義されていない関数の呼び出しは実行時にエラーになったり、宣言されていない変数への代入は許されていたりするのとは対照的です。

型の扱い

C 言語そのものには、文字列を扱う型が存在しません。基本的に、整数値と実数値しか扱えません。

しかし、文字列を扱う方法は存在します。 C 言語は配列をサポートしているので、整数値の羅列を配列に納め、それを文字列として扱うことが可能です。

初歩のサンプルプログラムとしてよく用いられる Hello World プログラムを以下に示します。このプログラムは、単に画面に "Hello World!" と表示するだけのものです。

#include <stdio.h>

int main()
{
    printf("Hello World!\n");
    return 0;
}

文字列リテラルがあるので、「文字列扱えるじゃないか! 嘘をつくな!!」とお怒りかも知れませんが、このプログラムは以下のように書き換えることも可能ですFootNote("ターミナルが ASCII 文字セット (をサブセットにする文字セットすべて) で動作している必要はありますが、そうでない端末を探す方が難しいでしょう…。")

#include <stdio.h>

int main()
{
    char text[] = { 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 10, 0 };
    printf(text);
    return 0;
}

配列 text は単なる数値の羅列の筈なのに、関数 printf() に渡すとそれが文字列として表示される。これは、コンピュータにおける文字列データが本質的には文字に割り当てられた番号 (文字コード) の羅列に過ぎないことを表しています。

"char" というのは整数を表す型の一種で、文字コードを扱うのに最適な精度の整数を扱うものです。上記のサンプルのように、行頭に型名を書き、その後ろに変数名を書くことで、使用する変数を宣言します。 C 言語では、変数はどこかであらかじめ宣言しなければ使うことができません。また、宣言する際に変数の型を決定する必要があり、一度宣言した変数の型を変更することはできません。上記のサンプルの場合、 text は char 型の配列として宣言されたので、 char 型の配列以外の種類のデータを持つことはできないのです。

なお、 C 言語は配列の他に、データ構造を表現する構造体共用体、名前付きの列挙値を表現する列挙型、変数や関数が存在するアドレス値を扱うポインタをサポートしています。

アセンブリ言語との関係

C 言語のことを、アセンブリ言語の (割と単純な) ラッパーマクロであるという人もいます。実際の所、 C 言語のプログラム中に、アセンブリ言語のプログラムを埋め込むことができる処理系もあります。例えばコンパイラに GCC を使用する場合、以下のように __asm__ キーワードを用いたインライン構文により、アセンブリ言語のプログラムを埋め込むことができます。

#include <stdio.h>

int main()
{
    int a = 10;
    int b;
    
    __asm__(
        "movl %1, %%eax\n\t"
        "addl $100, %%eax\n\t"
        "movl %%eax, %0"
        :"=r"(a)
        :"r"(a)
        :"%eax"
    );
    
    b = a * 20;
    
    printf("b = %d\n", b);
    
    return 0;
}

このプログラムを実行すると、以下のように表示されます。 b の値は 200 ではなく、 2200 になっています。

b = 2200

何故そうなるかというと、埋め込んだアセンブリ言語のプログラム中で、変数 a に 100 を加算しているからです。

逆に、このプログラムをアセンブリ言語に変換すると、以下のようになりますFootNote("アセンブリ言語プログラムの生成に利用した環境は、一般的な Intel CPU を搭載した Windows XP パソコンと、 MinGW 版 GCC 4.5.0 です。 gcc -O2 -S hoge.c として生成しています。")

	.file	"hoge.c"
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "b = %d\12\0"
	.text
	.p2align 2,,3
.globl _main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$16, %esp
	call	___main
	movl	$10, %edx
/APP
 # 8 "hoge.c" 1
	movl %edx, %eax
	addl $100, %eax
	movl %eax, %edx
 # 0 "" 2
/NO_APP
	leal	(%edx,%edx,4), %eax
	sall	$2, %eax
	movl	%eax, 4(%esp)
	movl	$LC0, (%esp)
	call	_printf
	xorl	%eax, %eax
	leave
	ret
	.def	_printf;	.scl	2;	.type	32;	.endef

"/APP" と書かれた行から "/NO_APP" と書かれた行までの間の部分が、インライン構文によって埋め込んだアセンブリ言語の部分で、その前後が C 言語で書かれたプログラムをアセンブリ言語に変換したものです。特に、 C 言語のプログラムにおける以下の計算式

    b = a * 20;

が、アセンブリ言語では以下のような記述に変換されていますが、

	leal	(%edx,%edx,4), %eax
	sall	$2, %eax

これは、変数 a の値に、変数 a の値を 4回足したもの (従って、 a の 5倍の値に相当) を用意し、その値をビットシフト演算によって左に 2bits 移動する、という内容です。左方向へのビットシフト演算は 1bit 移動する毎に整数値は 2倍になりますので、結果として a の 5倍の 4倍、即ち 20倍の値を得ることができる、というわけです。

このアセンブリ言語を生成した環境である 80x86 系の CPU には、整数の掛け算を行うマシン語の命令 imull というのも存在するので、この部分のアセンブリ言語プログラムは以下のようにも書き表せるはずなのですが、

	movl	%edx, %eax
	imull	$20, %eax

観念的に分かりやすいのは C 言語の中だけでよく、変換されたマシン語においては、分かりやすい表現よりも動作効率や実行ファイルサイズの小ささの方が重要になります。 GCC は imull 命令を用いるより leal 命令と sall 命令を用いた方が動作が速くなると判断し、そのようなアセンブリ言語プログラムを生成したのでした。

必要最小限の命令セット

C 言語自体が用意している命令は、大きく分けて以下の 3通りです。

  • 宣言のための命令。以下のようなものがある。
    • 型名 (int, char, float, unsigned など)。変数や関数を宣言するときに用いる。
    • 修飾子。型名を用いて宣言を行う際に、オプションとして指定する。定数である (値が変化しない) ことを宣言する const 、レジスタを用いるよう指示する register 、コンパイラによる最適化を抑制するよう指示する volatile などがある。
    • 構造体を宣言する struct 、共用体を宣言する union 、列挙子を宣言する enum 、型の別名を宣言する typedef など。これらは、 C 言語に最初からは存在しない新たな型を定義するのに用いられる。
  • 処理の流れを制御する命令。以下のようなものがある。
    • 分岐命令 (if - else, switch)。
    • ループ命令 (for, while, do - while)。
    • ループの復帰および離脱 (continue, break)。
    • ジャンプ (goto)。
    • 関数からの復帰 (return)。
  • 演算子。いろいろある。

これ以外のことはすべて、関数によって記述されます。具体的には、データの切り貼りや、入出力などです。

C 言語が用意している命令だけではできないことの多くは、 C 言語の標準規格として定められている標準ライブラリによって提供される関数を用いて実現することができるでしょう。標準ライブラリに精通することは、 C 言語を実用的に使いこなすようになるための早道であると言えるかも知れません。

標準ライブラリでもできないことをするにはどうすればよいかというと、いくつかのアプローチが考えられます。まず、 C 言語で書いたプログラムを実行する環境が、 Unix ライクな OS や Windows などの PC であるなら、それらの OS が提供するシステムコールAPI を調べてみると良いでしょう。また、とある既存のアプリケーションのプラグインツールを開発するようなケースであれば、そのアプリケーションのベンダーが API にアクセスするための開発キット (SDK と呼ばれるもの) を提供しているでしょう。これらはいずれも (基本的には) 機能を利用する為に用意された関数を呼び出せばよいようになっているはずですFootNote(プラグイン開発なんかの場合、既存アプリケーションがプラグインツールの関数を呼び出すルールが定められていて、そのルールの通りにプログラマーが関数を用意すればいい、というケースもあります。)

あるいは、 OS そのものを開発したい場合や、家電製品などの組み込み用マイクロチップで動作する制御プログラムを開発するような場合には、入出力に関するハードウェアの仕様を把握した上で、アセンブリ言語で記述した入出力用の関数を自前で用意する、といったアプローチが現実味を帯びてくるでしょう。

FootNote

Note: See TracWiki for help on using the wiki.