wiki:HowTo/CTutorial

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

とりあえず使ってみよう

蘊蓄はこれぐらいにして、とりあえず C 言語で何かプログラムを書き、それを動かしてみることにしましょう。

コンパイラを用意する

C 言語で書いたプログラムを動かすには、コンパイラが必要です。 UNIX ライクな環境では GCC を用いるのが一般的です。 Windows 環境の場合、恐らく最もポピュラーなのは Microsoft Visualo Studio の Express 版ですが、 MinGW や Cygwin などの UNIX ライクな環境を提供するプロジェクトも存在します。

  • Windows で Visual Studio の Express 版を利用する場合

Microsoft Visual Studio Express のサイトから、 Visual C++ 2010 Express をダウンロードし、インストールします。 C++ となっていますが、 C 言語も扱えるのでまったく問題ありません。

Visual Studio は統合開発環境 (IDE) になっているので、変数名などを補完してくれる便利なテキストエディタや、ファイルをまたいだ検索機能、グラフィカルなデバッガなどもセットでついてきます。非常に便利です。

  • Windows で Cygwin や MinGW を利用する場合

Microsoft 的なものを好きになれない人や、クロスプラットフォームでの開発を前提にしており GCC に慣れておきたい人などは、 Cygwin や MinGW の導入を検討してみるのも良いでしょう。

Cygwin を導入する場合は、 Cygwin Installation and Information のサイトでインストーラをダウンロードし、実行して下さい。デフォルトの構成だといろいろと歯抜けなので、構成が表示されたらツリーのトップをクリックして「Install」とし、全部インストールするようにした方がよいでしょう。ただし、インストールに非常に時間がかかりますが…。

MinGW を導入する場合は、 SourceForge.net の MinGW ダウンロードサイトにて、「Download Now!」ボタンをクリック (または「Automated MinGW Installer」→「mingw-get-inst」→「mingw-get-inst-yyyymmdd」→「mingw-get-inst-yyyymmdd.exe」の順に選択) してインストーラをダウンロードし、実行して下さい。デフォルトの構成では C コンパイラのみの構成の GCC のみがインストールされます。 MinGW の GCC は Windows のコマンドプロンプトからでも扱うことはできますが、他の言語のコンパイラや UNIX ライクなターミナルが欲しい場合は、構成ツリーが表示されたところで該当するコンポーネントにチェックを入れて下さい。

  • Linux や *BSD、 Solaris、 Mac OS X などの UNIX ライクな OS を利用する場合

多くの UNIX 風環境においては、 GCC などのコンパイラが最初からインストールされています。まずはターミナルから cc コマンドか gcc コマンドを、 --version などのオプションをつけて入力してみて下さい。

コマンドが見当たらないようであれば、 GCC をインストールしましょう。 Linux の場合、使用しているディストリビューションがサポートする方法に従ってインストールして下さい。例えば RedHat や Fedora 、 CentOS などの場合は Yum を、 Debian や Ubuntu など (それから何故か Vine も) の場合は apt-get や aptutil を (特に Ubuntu の場合はよりグラフィカルなパッケージ管理ユーティティが利用可能であるはず) それぞれ利用します。 Slackware 系の場合はディストリビューションのポータルサイトからリンクされているミラーに直接アクセスし、書庫ファイルをダウンロードして展開し、説明が書かれたテキストファイル (多分 INSTALL ファイル) の指示に従ってインストールを行って下さい。

*BSD の場合は、新しいバージョンの GCC が欲しいのであれば、 port を利用して下さい。… port が利用できると言うことは、恐らく GCC などのコンパイラも既にインストールされているのでしょう。 Gentoo Linux の場合も同様ですね。

Mac OS X の場合は、Xcode をダウンロードし、インストールして下さい。 Xcode は Visual Studio と同様の統合開発環境 (IDE) ですが、これをインストールすることで GCC も一緒にインストールされます。

それ以外の OS の場合は、 GNU プロジェクトの Installing GCC: Binaries のページからリンクを辿ってダウンロード、インストールを行って下さい。

テキストエディタ

テキストエディタは各自好きなものをご利用下さい。

Windows で Visual Studio を利用するのであれば、 Visual Studio のエディタをそのまま利用すればよいでしょうFootNote(自動補完機能のために動作が重くなるのを嫌がって別のエディタを併用する人もいますが…少なくともチュートリアルレベルであれば要らぬ心配です。)

Windows だが Visual Studio は使わない、という方は、この辺から適当なのを選んで入れて使って下さい。…といっても選択肢が多すぎるので、有名どころをいくつか挙げておきましょう。

UNIX ライクな環境であればやはり Emacs や Vim が人気でしょうが、 GNOME の gedit や KDE の KWrite なんかでも ok です。

Mac OS X の場合、 Xcode を利用するのも良いのですが、チュートリアルレベルで使っているとデフォルトで勝手に Hello World プログラムを作ってしまうのがウザイ、という方は、別途テキストエディタをインストールした方がよいかも知れません。定番どころはやはり mi (旧称ミミカキエディット) でしょうか。

それから、1つ重要な注意事項なのですが、最近の gnomeFootNote(Linux などでおなじみのデスクトップ環境ですね。) や Mac OS X などでは、デフォルトの日本語キーボード設定において、円記号 '¥' のキーを押すと、バックスラッシュ '\'FootNote(これも円記号に見えますか? 日本語版 Windows などで一般的なフォントを用いて閲覧しているとそう見えるかも知れませんが、これは紛れもなく U+005C の半角バックスラッシュです。) ではなく、本物の円記号 '¥' がタイプされる場合があるようです。その場合、そのプログラムをそのままコンパイルして実行すると、意図しない動作をすることがあるので注意して下さい。

gnome の場合は「キーボードの設定」にて選択しているキーボードレイアウトを確認し、キーボードの右上にある「¥」キーと、右下にある「\」キーとで入力される文字が異なることを確認して下さい。

Mac OS X の場合は、ことえりの設定で「¥」キーを押したときに入力される文字を選択することができます。画面右上のことえりアイコンをクリックし、コンテキストメニューの「環境設定を表示」を選んでダイアログを開き、「入力文字」タブの「JIS キーボードの ¥ キーで入力する文字」コンボボックスにて「\ (バックスラッシュ)」を選択して下さい。詳しくはこちらのサイトなどを参考にして下さい。

コンパイルと実行

Hello World プログラムは既に例示しましたが、

#include <stdio.h>

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

このプログラムをコンパイルして、実際に動かしてみましょう。

  • Windows で Visual Studio を利用する場合

Visual Studio を起動し、「新規プロジェクトを作成」で「コンソールアプリケーション」を選択し、適当なプロジェクト名でプロジェクトを作成しましょう。

次に、ソリューションエクスプローラにて、プロジェクト名を右クリックし、「追加」→「新しい項目…」を選択して、「空の C コードファイル」を選択し、ファイル名を「test.c」として「追加」ボタンをクリックします。

空のテキストファイルがエディタに表示されるので、上記のサンプルプログラムをコピー&ペーストし、 Ctrl + F5 キーを押すと、ファイルが保存され、コンパイルが行われ、そしてプログラムが実行されます。

  • GCC を利用する場合

テキストエディタを用いて上記のサンプルプログラムをコピー&ペーストし、ファイル名「test.c」で保存します。

次に、ターミナル (コマンドプロンプト、またはシェルなどのコンソール) にて、 cd コマンドで test.c を保存したディレクトリに移動してから、以下のコマンドを実行します。

$ gcc test.c

すると、同じディレクトリに a.out というファイルが生成されます。そこで、

$ ./a.out

とタイプすると、プログラムを実行できます。

実行ファイルのファイル名を指定したい場合は、 -o オプションを指定します。

$ gcc -o test test.c
$ ./test

Windows の場合、余計に拡張子 .exe がついた名前のファイルが生成されます。

標準ライブラリを使ってみる

言語をしっかり習得するには文法を学び、そこに通じるさまざまな思想や観念を学ぶことが必要ですが、そうしたものを一通り学んでまともに動くプログラムを組み上げるまでの道のりは長く、よっぽど好きでもない限り退屈なものでしょう。

そこで、とりあえず動くものを作ってしまう最も手っ取り早い方法として、他の言語では普通に用意されていそうな命令の代わりになる関数がたくさん盛り込まれている標準ライブラリから学んでしまうことにしましょう。

文字を表示する

コンソール上に文字を表示する関数の類は、入出力ライブラリとしてまとめられています。入出力ライブラリを使用するには、プログラムの先頭に stdio.h ファイルを読み込ませる必要があります。

#include <stdio.h>

番号記号 "#" で始まる行をプリプロセッサ行といいます。プリプロセッサとはその名の通り「前処理」を行うツールで、 C コンパイラコマンドがコンパイルを行う前に行う仕事ですFootNote(思い出しましょう。 C コンパイラの仕事は、 1 に前処理、 2 にコンパイル、 3 にアセンブル、最後にリンク、でしたね。)。プリプロセッサは include という命令を、「その位置に指定されたファイルの内容を挿入する」と解釈します。

ヘッダファイルを読み込むことにより、ライブラリに用意されている関数を呼び出す準備ができました。それでは、文字を表示する関数を一つ一つ試してみましょう。

#include <stdio.h>

int main()
{
    puts("Hello, World!");
    return 0;
}

puts() 関数は、文字列を表示します。行末は改行されます。実は、 Hello World プログラムは、よく知られている printf() 関数を用いるより、 puts() 関数を用いた方がすっきり書けます。

なお、プログラムは必ず main() 関数の定義内容として記述します。 C 言語では、プログラムは必ず main() 関数から始まるのです。

#include <stdio.h>

int main()
{
    int x = 4, y;
    char formula[] = "y = x^2 + 2x + 1";
    printf("式: %s\nx = 4 のとき、 y = %d\n", formula, x^2 + 2 * x + 1);
    return 0;
}

みんな大好き printf() 関数は、書式を指定して文字列を表示する関数です。1つ目の引数は書式指定文字列で、この中に 2つ目以降の引数の値 (を文字列に変換したもの) を埋め込んで作った文字列を表示します。

2つ目以降の引数の値が埋め込まれる場所は、書式指定文字列中で "%s" とか "%d" などと書かれた場所です。埋め込まれる順番は、左からの出現順です。埋め込みたい値の種類に応じて、以下の記号から選択して使用します。

  • %s ... 文字列を埋め込む。対応する引数には char 型の配列を指定しなければならない。
  • %d ... 10進数の整数値を埋め込む。対応する引数には int 型などの整数値を指定する。
  • %h ... 16進数の整数値を埋め込む。a ~ f は小文字で表示される。対応する引数には int 型などの整数値を指定する。
  • %f ... 実数値を小数点表記で埋め込む。対応する引数には double 型などの実数値を指定する。
  • %g ... 実数値を埋め込む。通常は小数点表記だが、値の大きさによっては指数表記で表示される。対応する引数には double 型などの実数値を指定する。
  • %% ... % 記号そのものを表示する。

また、 printf() 関数は puts() 関数とは違って、行末は改行されません。改行したい場合は、改行したい場所で "\n" を挿入します。 "\n" 自体はエスケープシーケンスと呼ばれるもので、 C 言語で文字リテラルを記述する場面では共通して利用できます (つまり、 printf() 関数のみならず puts() 関数やその他の関数でも利用可能です)。エスケープシーケンスは、 "\n" の他に、以下のようなものがあります。

  • \t ... タブ文字。
  • \a ... ベル文字 (アラート)FootNote(ブザーが鳴ります。;))
  • \r ... 復帰。改行せずに行の先頭に戻る。
  • \' ... シングルクォーテーション "'"。
  • \" ... ダブルクォーテーション '"'。

  • ... バックスラッシュ (円記号) "\"。
#include <stdio.h>

int main()
{
    int i;
    char text[] = "Hello, World!";
    for (i = 0; text[i] != '\0'; i++) {
        if (i > 0) {
            putchar(',');
            putchar(' ');
        }
        putchar(text[i]);
    }
    putchar('\n');
    return 0;
}

putchar() 関数は、文字を一文字だけ表示する関数です。puts() 関数や printf() 関数のように文字列を表示するのではなく、一文字だけ表示します。従って、 putchar() 関数の引数には、 char 型の配列ではなく、 char 型の (単一の) 値を指定します。

ところで、リテラルの書き方ですが、ダブルクォーテーション '"' で括るリテラルと、シングルクォーテーション "'" で括るリテラルは、全くの別物です。ダブルクォーテーションで括った場合は文字列リテラルとなりますので、 char 型の配列として扱われますが、シングルクォーテーションで括った場合は文字リテラルとなり、 char 型の (単一の) 値として扱われます。

    char text[] = "Hello, World!";  /* 文字列なので、配列として扱われる */
    char letter = ',';              /* 単一の値として扱われる */

それから、 "\0" はヌル文字といって、 C 言語では文字列の終端を表す値として利用されています。実は、

    char text[] = "Hello, World!";

と書くのは、

    char text[] = { 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33, 0 };

と書くのと同じ意味で、文字列リテラルで char 型の配列を初期化した場合、末尾にヌル文字が挿入されるというルールになっています。

ユーザーに文字を入力させる

次は、プログラムの実行中に、ユーザーにキーボードで文字を入力させる関数を見ていきましょう。これらも入出力ライブラリに含まれている関数です。

#include <stdio.h>

int main()
{
    char text[BUFSIZ];
    printf("何か入力してください。 >> ");
    gets(text);
    printf("あなたは今、「%s」と入力しましたね。\n", text);
    return 0;
}

gets() 関数は、ユーザーに文字列を 1行だけ入力させる関数です。ユーザーがキーボードを用いて文字列を入力し、 Enter キー (または Return キー) を押して改行するまで、処理を返しません。入力した文字列は、 gets() 関数の引数に渡した char 型の配列に、ヌル文字で終端した文字列として格納されます。なお、ユーザーが Enter キーを押すことで、コンソール上では改行されますが、この改行は char 型の配列には含まれません。

なお、上記で配列 text を宣言する際、添え字に "BUFSIZ" と書いていますが、これは入出力ライブラリにおいて定義されている定数値のマクロで、その内容は、そのプログラムを実行する環境において最適だと思われるバッファーのサイズです。つまり、コンソールから 1行を入力するんであれば、配列の大きさはこのぐらい確保しておくのが目安だよ、という値が入っていますFootNote(本当に目安なので (多分 512 とか、そんな値)、あまり当てにできるものではありませんが…。)

…ここまで読んで、聡明な方であれば、「あれ? じゃあ BUFSIZ よりも大きいサイズの文字列を入力された場合、どうなるんだ?」と思うかも知れません。どうなるかというと、gets() 関数はそんな事情などお構いなしに、配列が存在する場所に、行全体を格納しようとします。その結果、本来配列として確保したわけではないメモリー領域にまで文字列が書き込まれてしまいます。つまり、メモリーが破壊されてしまいます

実は、 gets() 関数は最近の C 言語標準規格 (C89 や C99、 UNIX 関連だと POSIX.1-2001.LSB など) では非推奨とされており、 Linux のマニュアルページにおいては「絶対に使用してはならない」とさえ書かれている、代表的なセキュリティーホールの原因だったりします。 C 言語の世界ではこの手のメモリー管理に関する罠が少なくないので、注意が必要です。ちなみにさらに新しい UNIX 関連の規格 (POSIX.1-2008 など) では存在自体がなかったことにされていたりもするようなので、環境によっては上記のサンプルはコンパイルすら出来なかったかも知れません。

#include <stdio.h>

int main()
{
    char text[BUFSIZ];
    int i;
    printf("何か入力してください。 >> ");
    fgets(text, BUFSIZ, stdin);
    
    /* 改行文字を潰す */
    for (i = 0; i < BUFSIZ && text[i] != '\0'; i++) {
        if (text[i] == '\n') {
            text[i] = '\0';
            break;
        }
    }
    
    printf("あなたは今、「%s」と入力しましたね。\n", text);
    return 0;
}

gets() 関数の代用として最も一般的に使われているのは、おそらく fgets() 関数 でしょう。この関数は、ファイルからテキストを 1行読み込む関数ですが、ユーザーからの入力を読み込むのにも使うことができますFootNote(というより、ファイルから何かを読み込む関数はすべて、ユーザーからの入力を読み込むこともできます。同様に、ファイルに何かを書き込む関数はすべて、コンソールに何かを表示することもできます。)。ただし、いくつかの点で gets() 関数とは動きが異なるので、注意が必要です。

    fgets(text, BUFSIZ, stdin);

まず、 fgets() 関数は、 2つ目の引数に、文字列を受け取る配列の大きさを指定する必要があります。裏を返せば、ここでサイズを指定するからこそ、この関数は gets() 関数よりも安全であると言えます。

説明を分かりやすくするために、 BUFSIZ を使うのをやめて、配列のサイズを普通の数字に置き換えてみましょう。

    char text[100];
    fgets(text, 100, stdin);

このとき、 fgets() 関数は、ユーザーから入力された文字列が (改行を含めて) 配列の要素 99個分に収まらない場合には、配列の要素 99個分までを配列 text にコピーし、残りは読み込まなかったことにしますFootNote(その場合、もう一度 fgets() 関数を呼ぶと、残りの文字列を読み込むことになります。)。いずれにせよ、配列に読み込まれた文字列の末尾には、文字列の終端を表すヌル文字 '\0' が書き込まれ、そのまま文字列として扱えるようになります。

それから、 3つ目の引数に指定している謎のキーワード stdin は、ファイルポインタと呼ばれるものです。ここには本来、 fopen() という関数を用いてファイルを開いた際に得られる値を指定しますが、ユーザーからの入力を読み込む場合には、この stdin というキーワードを代わりに指定します。この stdin というのも、入出力ライブラリ stdio.h の中で定義されている値です。

    /* 改行文字を潰す */
    for (i = 0; i < BUFSIZ && text[i] != '\0'; i++) {
        if (text[i] == '\n') {
            text[i] = '\0';
            break;
        }
    }

もう一つ大事な性質として知っておかなければならないのは、 fgets() 関数は受け取った文字列に含まれる改行文字をそのまま残すということです。このプログラムでは改行文字を残しておきたくはなかったので、上記のように、ループを回して配列の中を先頭から順に辿り、改行文字 '\n' を見つけたら、それをヌル文字 '\0' に置き換える、ということをやっています。ヌル文字に置き換えてしまえば、そこが文字列の終端になるので、改行文字はなかったことにされる、という寸法です。

なお、ユーザーからの入力を 1行読み込むために使用できる関数は、他にもいくつかありますので、各自で調べて、しっくり来るものを探してみるのも良いかも知れません。例えば GNU C ライブラリや POSIX.1-2008 に準拠したライブラリを用いた環境であれば getline() 関数が使えますFootNote(GCC を用いているのであれば普通は使える…と言いたいところなのですが、残念ながら MinGW は GNU C ライブラリを用いていないためか、 getline() 関数は使えないようです…。)し、 Microsoft Visual Studio をお使いであれば gets_s() 関数を使うという手もあります。

#include <stdio.h>

int main()
{
    int a, b;
    puts("a + b を求めるプログラム");
    printf("a = ? >> ");
    scanf("%d", &a);
    printf("b = ? >> ");
    scanf("%d", &b);
    
    printf("a + b = %d\n", a + b);
    
    return 0;
}

さて次に、ユーザーに文章ではなく数字を入力させ、それを数値として扱いたい場合にはどうしようか、という問題です。いくつかのアプローチが考えられますが、恐らく最も手軽なのは、上記のサンプルで使用している scanf() 関数 を用いることでしょう。 printf() 関数が書式を指定して表示を行うのとは逆に、 scanf() 関数は指定した書式でのみ入力を受け付けて、そこから値を拾い上げるというものです。

ただ、 scanf() 関数の書式指定はクセが強いので、あまりあてにしない方が良いかも知れません。以下のプログラムを見てみましょう。

#include <stdio.h>

int main()
{
    int a = 0, b = 0;
    printf("a,b >> ");
    scanf("%d,%d", &a, &b); /* ←注目!! */
    printf("a + b = %d\n", a + b);
    return 0;
}

scanf() 関数で指定した書式指定は "%d,%d" となっていますね。この場合、入力する側もこの通り、2つの数字をカンマ "," で挟んで入力する必要があります。ちょっと、実行して、いろいろ試してみましょう。

$ gcc -o hoge hoge.c
$ ./hoge
a,b >> 1,3
a + b = 4
$ ./hoge
a,b >> 1 3
a + b = 1
$ ./hoge
a,b >> 1, 3
a + b = 4
$ ./hoge
a,b >> 1 , 3
a + b = 1
$ ./hoge
a,b >> 1,3,5
a + b = 4
$ ./hoge
a,b >> ,1,3
a + b = 0
$ ./hoge
a,b >> 1
a + b = 1
$

カンマの後ろに空白が入るのは許容されるようですが、カンマの手前に空白が入ると、 2つ目の数字は無視されてしまうようです。それから、カンマで挟んで 3つ数字を入力しようとした場合は頑張って最初の 2つを読み取ってくれますが、いきなりカンマで始まるようなケースでは 1つも数字を読み取ってくれません。さらに、数字を 1つだけ入力した場合は、 2つ目の入力を待たずに終了してしまうようです。

それでは、書式指定を "%d,%d" ではなく、 "%d %d" と、空白で挟んだ場合はどうでしょうか。

#include <stdio.h>

int main()
{
    int a = 0, b = 0;
    printf("a b >> ");
    scanf("%d %d", &a, &b); /* カンマではなくスペースで挟む */
    printf("a + b = %d\n", a + b);
    return 0;
}

実行してみましょう。

$ gcc -o hoge hoge.c
$ ./hoge
a b >> 1 3
a + b = 4
$ ./hoge
a b >> 1,3
a + b = 1
$ ./hoge
a b >> 1 , 3
a + b = 1
$ ./hoge
a b >> 1 3 5
a + b = 4
$ ./hoge
a b >>  1 3
a + b = 4
$ ./hoge
a b >> 1
3
a + b = 4
$ ./hoge
a b >>

1
3
a + b = 4
$

2つの数字の間に空白以外の文字が入ると、 2つ目以降の数字は無視されてしまうようですね。数字を 3つ入力した場合も最初の 2つまで受け付けてくれるのは、概ね想像通りでしょう。しかし、最初に空白を入れた場合でも問題なく 2つの数字を読み取ってくれたり、数字を 1つだけ入力しても 2つ目の数字の入力を待ってくれたり、あるいは何も入力せずにひたすら Enter を押しても数字の入力を待ち続けてくれたりする辺りは、ちょっと意外だったのではないでしょうか。

scanf() 関数は整数以外に、実数や文字列を受け取ることもできます。以下のプログラムを見てみましょう。

#include <stdio.h>

int main()
{
    char name[BUFSIZ] = "", format[BUFSIZ];
    int age = 0;
    float tall = 0, weight = 0;
    printf("名前 >> ");
    (void) sprintf(format, "%%%ds", BUFSIZ - 1);    /* BUFSIZ == 512 の場合、 "%511s" となる。 */
    scanf(format, name);    /* 文字列を受け取る; name は配列なので & は不要 */
    printf("年齢 >> ");
    scanf("%d", &age);
    printf("身長(cm), 体重(kg) >> ");
    scanf("%f,%f", &tall, &weight);
    
    printf("%s、%d歳。身長%.1fcm、体重%.1fkg。\n", name, age, tall, weight);
    
    return 0;
}

とりあえず、実行する様子を見てみましょう。

$ gcc -o hoge hoge.c
$ ./hoge
名前 >> 村山俊之
年齢 >> 32
身長(cm), 体重(kg) >> 161.0, 69.4
村山俊之、32歳。身長161.0cm、体重69.4kg。
$ 

名前、年齢、身長、体重を問われ、指示に従って入力すると、入力した通りの内容を表示する、というプログラムです。ここでは、名前として文字列を、身長や体重として実数値を入力しています。

名前の入力については、ちょっと解りづらいプログラム内容になっていますね。

    printf("名前 >> ");
    (void) sprintf(format, "%%%ds", BUFSIZ - 1);    /* BUFSIZ == 512 の場合、 "%511s" となる。 */
    scanf(format, name);    /* 文字列を受け取る; name は配列なので & は不要 */

2行目の sprintf() 関数 は、 3行目の scanf() 関数に渡す書式指定文字列を作っています。もしも、定数 BUFSIZ の値が 512 だった場合、この部分は以下のように記述するのと同じ意味になります。

    printf("名前 >> ");
    scanf("%511s", name);

文字列を取得する場合の書式指定は "%s" ですが、 "%s" だけだと何文字でも受け取って良いことになってしまい、配列 name に確保した領域よりも長い文字列が入力されると、メモリーが破壊されてしまいます。そこで、受け付ける文字列の長さをあらかじめ制限する必要があります。 "%511s" とやると、 char 型配列の要素 511 個分までの文字列を入力し、残りは捨てる、という動作になり、とりあえず安全です。

FootNote

Last modified 14 years ago Last modified on Sep 28, 2010, 1:08:13 AM
Note: See TracWiki for help on using the wiki.