C言語のプログラムソースを作ったあと,「コンパイラ」と呼ばれるプログラムを用いて,プログラムソースを実行可能なプログラムへ変換する必要があります.
コンパイラには gcc
, clang
などがあります.以下の説明では gcc
を使っています.
プログラムソースのファイル名が SRC.c の場合,以下のようにして gcc
コマンドを実行すると,a.out
という名前のファイルとしてプログラムが作成さ
れます.
$ gcc SRC.c
ここで a.out
というファイル名は,出力ファイル名が指定されない場合の既定値であり,-o
オプションを用いてコンパイル時に変更できます.例えば,
myprog
という名前のプログラムとするなら,以下のように端末へ入力します.
$ gcc -o myprog SRC.c
コンパイルして作成したプログラムは,そのプログラムのファイルが存在するディレクトリにおいて以下のように端末へ入力することで実行できます (ファイル名が a.out
の場合).
$ ./a.out
プログラムのファイル名が myprog
の場合は,端末へ以下のように入力します.
$ ./myprog
コンパイラにはさまざまなオプションを指定できます.以下に一部を抜粋します.
オプション | 効果 |
---|---|
-g, -g2, -g3 | コンパイルして作られるファイルにデバッグ用データを埋め込みます.-g3 により,もっとも詳細なデータが埋め込まれます.gdb などでデバッグする際にお勧めです |
-W, -Wall | 警告メッセージを表示します.「C言語の文法としては間違っていないけれど,バグじゃありませんか?」という箇所を教えてくれます. つねに両方 (-W -Wall ) を指定しておくことを推奨します |
-lLIB | 関数をとってくるライブラリファイルを追加します.C言語の標準ライブラリに存在しない関数 (pow , sqrt など) を使う場合,そのような関数を含んでいるライブラリファイル (libm.a, libm.so など) から関数を引っ張ってくる際に指定します.-l と LIB のあいだにスペースを入れない点に注意しましょう |
-O, -O2, -O3 | プログラムを高速化します.-O3 を指定した場合の結果が指定しない場合の結果と異なることが多いため,本実験では -O3 は使わないようにしましょう |
pow
, sqrt
などの数学関数を使う場合,プログラムソース内に #include <math.h>
と記述することに加えて,コンパイル時に -lm
というオプションの指定が必要です.
多数のプログラムソースから構成されるそこそこ大きなプログラムを作る場合は,make
, cmake
といったプログラムを使うと作業効率が向上します.詳細は各自で調べてみましょう.
ときにプログラムが無限ループに陥ったり,データが多すぎて処理に時間がかかり過ぎる場合があります.このような場合,端末上で [Ctrl] キーを押しながら
[c] キー を押す (いわゆる Ctrl + c
) により,プログラムの実行を強制終了できます.
Ctrl + z
でもプログラムの実行を停止できますが,こちらはプログラムをサスペンド (一時停止) するだけでプロセスは残っています.fg
コマンドにより
実行を再開できますし,bg
コマンドによりプログラムをバックグラウンドで走らせることも可能です.
よくトラブルになるキーとして Ctrl + s
があります.これを押下すると,端末への表示がロックされ,プログラムが終了したように見えますが,同時に端末
へのキー入力を受け付けなくなります.この場合,Ctrl + q
を押下するとロックが解除されます.
C言語の「関数」とは,プログラムにおけるひとかたまりの機能を実現するための部品といえます.
これまであまり意識していなかったかもしれませんが,printf
や scanf
は立派な関数です.
また,main
関数も,プログラム実行後,最初に実行される特別な関数です.
こういった基本的かつ重要な機能のための関数は,C言語の仕様としてあらかじめ用意されており,プログラマがそのような関数を個別に作成する必要はありません.
どのような関数が用意されているかは「C言語 標準ライブラリ関数」といったキーワードを用いて検索し調べてみましょう.
標準ライブラリ関数が提供する機能以外にも,数値演算を行うプログラムでは「べき乗」や「平方根」を算出する機能が必要になるかもしれません.このような
機能は,標準ライブラリとは異なる数学ライブラリというファイル内に,それぞれ pow
, sqrt
という関数として用意されています.このような関数を利用
するためには,プログラムソースに #include <math.h>
と記述することにくわえて,コンパイル時に -lm
というオプションを追加する必要があります.
ほかにも,例えば任意精度の数値演算を可能とする GMP というライブラリが存在するのですが,このライブラリを使うためには
#include <gmp.h>
と記述し,かつコンパイルオプションとして -lgmp
を追加します.なお,このような指定が正しくても,コンパイルしている環境 (パソ
コン) 内にそのライブラリパッケージがインストールされていないと,コンパイルに失敗します.
実際にC言語でプログラムを作っていく場合,機能ごとに 自作の関数 を作成/使用するというのが一般的です.
これまでは main
関数内にすべての処理を書いていたかもしれませんが,それでどうにかなるのはせいぜい 200 行程度のプログラムソースでしょう.長くな
るとプログラムソースを読むだけでも大変であり,間違い探し (デバッグ) はさらに難しくなります.
研究や仕事でプログラムを作る段になると,ソースが数百行のプログラムというのは非常に小さい部類に入り,実際には少なくとも数千行のプログラムを作るこ とになると思います.このとき関数を使えないと,手も足も出ないでしょう.
一連の処理を関数として切り出せば,
main
関数の処理) は,関数を順に呼び出すだけであり,プログラムの全体像を把握することが容易になります.
例えば,5,000行のプログラムソース全体を一時に把握するのは大変ですが,10個程度の関数に分割すれば,一時に把握する必要があるのは500行程度になります. デバッグなどの際は,5,000行全体を追う必要はなくて,そのとき処理中である関数のソースである500行程度を相手にすれば十分となります.
関数への分割は面倒だと思うかもしれませんが,これができないと卒業研究や開発現場では話になりません.また,関数へうまく分割できない場合,プログラム を理解できていない可能性が高いです.このような場合,苦労して関数へ分割することを通してプログラムの理解が進むでしょう.
関数の作り方は,慣れないうちは段階的に行うのがよいと思います.
main
関数から,関数としてまとめる一連のコードを選びます.main
関数から,その外側へ移すことになります.main
関数内のもとの箇所ではその関数をたんに呼び出すようにします.main
関数における関数の呼び出しを,実引数を与えて戻り値を使用するよう,変更します.例えば以下のプログラムソースを考えます.
#include <stdio.h>
int main(void)
{
int a, b, c:
a = 2;
b = 3;
c = a + b;
printf("a=%d b=%d c=%d\n", a, b, c);
return 0;
}
ここで,関数化する必要のないコードですが,足し算をしている c = a + b
という処理を add
という関数で行うこととします.
そのため,a, b, c
という変数を大域変数とし,大域変数を処理する void add(void)
という関数を定義し,もとのコードをこの関数で置き換えます.
#include <stdio.h>
int a, b, c; /* main 関数内から変数の宣言・定義を移動 */
void add(void)
{
c = a + b; /* 処理結果を大域変数へ代入 */
}
int main(void)
{
a = 2; /* 変数 a, b, c の宣言・定義がなくなっている */
b = 3;
add(); /* この呼び出し後,大域変数 c の値が変更されている */
printf("a=%d b=%d c=%d\n", a, b, c);
return 0;
}
大域変数を使わないように add
関数を改良し,かつ呼び出し方を変更します.
#include <stdio.h>
int add(int a, int b) /* 必要なデータを引数として与える */
{
int c;
c = a + b;
return c; /* 処理結果を返り値とする */
}
int main(void)
{
int a, b, c: /* 変数の宣言・定義が復活 */
a = 2;
b = 3;
c = add(a, b); /* どの変数を用いてどの変数の中身を設定するのか,わかりやすくなった */
printf("a=%d b=%d c=%d\n", a, b, c);
return 0;
}
同じように使える場合が多いですが,同じではありません.
array
という名前の配列が存在すれば,その配列の先頭アドレスを保持する
array
という読取り専用のポインタが存在するかのように扱われます.ポインタに対しては,*
演算子や[]
演算子によるメモリの中身へのアクセスが可能です.配列名も「読出し専用ポインタ」のように扱える場合,それらの演
算子を用いた配列の中身へのアクセスに使用できます.
int main(void)
{
int array[] = {1, 2};
int* int_p = array; /* 配列 array の先頭アドレスを int_p へ代入 */
printf("%d %d\n", array[0], array[1]);
printf("%d %d\n", *array, *(array+1));
printf("%d %d\n", int_p[0], int_p[1]);
printf("%d %d\n", *int_p, *(int_p+1));
return 0;
}
また,関数の仮引数として配列を記述すると,実際にはポインタに変換して処理されます.例えば,以下の関数 func_1
を考えます.
void func_1(int an_array[])
{
printf("%d\n", an_array[0]);
}
この関数は,結局のところ以下の関数として処理されます.
void func_1(int *an_array)
{
printf("%d\n", an_array[0]);
}
しかし配列はポインタとは異なります.以下のプログラムを見てみましょう.
int main(void)
{
int array[100];
int* int_p = array; /* 配列 array の先頭アドレスを int_p へ代入 */
printf("%lu %lu\n", sizeof(array), sizeof(int_p));
return 0;
}
このプログラムの,手元の環境での実行結果はつぎのようになりました.
400 8
この結果は,メモリ領域に占めるデータサイズが,配列では400バイト (int型変数1つで4バイト x 100個) であることに対し,ポインタでは8バイトであること を表します.
ポインタが占めるデータサイズは,プログラムが扱えるアドレス空間のサイズに依存します.8ビット = 1バイトですので,手元の環境 (計算機) で64ビットCPU を使っていれば,64ビット/8ビット = 8バイト,となります.32ビットCPUなら4バイトです.
文字列 (char型の配列) をトークンへ分割するための,標準ライブラリ関数です.トークンを区切るデリミタを任意に (スペースやタブ以外に) 指定できるため,
strtok
関数により文字列の柔軟な処理が可能になります.
strtok
関数を使用する際の要点は以下になります.
string.h
ヘッダファイルをインクルードする.NULL
を指定する.\0
で上書きされる).NULL
を返す.strtok
関数の使用例として,例えば以下が考えられます.
#include <stdio.h>
#include <string.h>
int main(void)
{
char str[] = "hello, I am fine. Thank you"; /* 処理対象の文字列 */
char *str_head, *ptr;
int str_size, i;
str_size = strlen(str);
str_head = str;
while ((ptr = strtok(str_head, " ,."))) { /* 「 」「,」「.」をデリミタとする */
printf("token=%s\n", ptr);
str_head = NULL; /* 2回目以降は第1引数が NULL になる */
}
for (i=0; i < str_size; i++) {
printf("str[%d]=[%c]\n", i, str[i]);
}
return 0;
}
手元の環境での,このプログラムの実行結果は以下になりました.
token=hello
token=I
token=am
token=fine
token=Thank
token=you
str[0]=[h]
str[1]=[e]
str[2]=[l]
str[3]=[l]
str[4]=[o]
str[5]=[]
str[6]=[ ]
str[7]=[I]
str[8]=[]
str[9]=[a]
str[10]=[m]
str[11]=[]
str[12]=[f]
str[13]=[i]
str[14]=[n]
str[15]=[e]
str[16]=[]
str[17]=[ ]
str[18]=[T]
str[19]=[h]
str[20]=[a]
str[21]=[n]
str[22]=[k]
str[23]=[]
str[24]=[y]
str[25]=[o]
str[26]=[u]
strtok
関数は,関数内部に情報を保持しており,この情報は strtok
関数を呼び出すたびに原則として変更されます.よって,ある文字列A を strtok
関
数で処理している最中に,べつの文字列B を strtok
関数で処理しようとすると,内部の情報が壊れて適切に動きません.この問題を回避できる,strtok_r
という再帰呼出しの可能な関数が存在します.
strtok_r
関数を含め,標準ライブラリ関数の使用例や確実な情報を調べたい場合は,マニュアルを参照するようにしましょう.手元の Unix 環境にマニュア
ルがインストールされていれば,man 3 strtok
と端末へ入力することで,標準ライブラリ関数 strtok のマニュアルを読むことができます.
標準入力 (stdin
) から整数や浮動小数点数,文字列を読み取ることを考えます.
標準入力からデータを読み取ることは,端的には端末上にキーボードから文字を入力することに相当します.
この際,scanf
関数を使うと,以下のような問題が発生することがあります.
scanf
関数で想定した様式に従った入力でなかった場合,標準入力の中身の一部だけが処理され,残りがバッファに残っているという状態になる.
この状態にはさまざまなケースが考えられ,つぎにどう処理が進むかを予測することが難しい.scanf
関数を用いると,データはスペースやタブ,改行で分割されてから処理されます.)fgets
関数は,アドレス (S),文字数 (SIZE),ファイルポインタ (STREAM) を引数として受け取り,たかだか SIZE-1 だけの文字を STREAM から
読み取り,S へ読み取ったデータを格納する関数です.ここで,読み取った文字列の末尾に \0
を追加するため,SIZE よりも1つ少ない数の文字が読み取
られる点に注意しましょう.
標準入力からのデータを,fgets
関数を用いて文字数の上限を決めて取り出し,その配列に対して strtok
関数や sscanf
関数などを用いて処理すること
で,上述の問題を回避できます.
fgets
関数と似た関数として gets
関数が存在しますが,この関数は読み取る文字数を指定できないという致命的欠陥を有するため,使ってはいけませ
ん.
fgets
関数の使用例として,つぎのプログラムを考えてみます.
#include <stdio.h>
int main(void)
{
char str[10];
fgets(str, sizeof(str), stdin);
printf("str=%s\n", str);
return 0;
}
このプログラムの実行例を以下に示します.
$ ./a.out
123456789abcd
str=123456789
この実行例から,10文字目 (a) 以降が読み取られていないことを確認できます.
scanf
関数の使用が問題になる例として,つぎのプログラムを考えてみます.
#include <stdio.h>
int main(void)
{
int i;
char whole[1024];
char alpha[4] = "";
char beta[4] = "";
char gamma[4] = "";
while (fgets(whole, sizeof(whole), stdin)) {
i = -1;
*beta = '\0';
if (2 != sscanf(whole, "%d %s", &i, beta)) {
break;
}
printf("i=%d beta=%s\n", i, beta);
}
printf("alpha=%s beta=%s gamma=%s\n", alpha, beta, gamma);
return 0;
}
手元の環境で,「1 dd」と入力してから,Enter, Ctrl-d と押下した場合の実行結果は以下になりました.
$ ./a.out
1 dd
i=1 beta=dd
alpha= beta=dd gamma=
この場合は適切に動いています.
「1 abcdefg」と入力してから,Enter, Ctrl-d と押下した場合の実行結果は以下になりました.
$ ./a.out
1 abcdefg
i=1 beta=abcdefg
alpha= beta=abcdefg gamma=efg
文字配列 beta
が,本来許される3文字を超えて「abcdefg」という文字を有していることになっています.
また,本来空であるべき文字配列 gamma
に「efg」という文字列が入っていることを確認できます.
beta
の中身の出力にあたっては,beta
の先頭アドレスから読みはじめて,その配列内に \0
が存在せず beta
を超えて,beta
の後ろにあるgamma
のメモリ領域へ入り,結局 gamma[3] にある \0
をもってして beta
の文字列が終端した,と処理されています.
なお,scanf
関数において文字を読み取る際,読み取る (\0
を除いた) 最大文字数が M として所与であれば,「%s」ではなく「%Ms」とフォーマット
を指定することで,上の例のような問題を回避できます.
上の例では文字配列のサイズが4 (最大文字数が3) であることから,「%s」を「%3s」とした以下のプログラムを考えます.
#include <stdio.h>
int main(void)
{
int i;
char whole[1024];
char alpha[4] = "";
char beta[4] = "";
char gamma[4] = "";
while (fgets(whole, sizeof(whole), stdin)) {
i = -1;
*beta = '\0';
if (2 != sscanf(whole, "%d %3s", &i, beta)) {
break;
}
printf("i=%d beta=%s\n", i, beta);
}
printf("alpha=%s beta=%s gamma=%s\n", alpha, beta, gamma);
return 0;
}
このプログラムを実行し,「1 abcdefg」と入力してから,Enter, Ctrl-d と押下した場合の実行結果は以下になりました.
$ ./a.out
1 abcdefg
i=1 beta=abc
alpha= beta=abc gamma=
この結果から,beta
, gamma
の中身が適切であることを確認できます.
関数の再帰呼出し (recursive call) とは,ある関数 f
の中で再び同じ f
を呼び出すことをいいます.
例えば,「N の階乗 (N !)」を求める関数について考えましょう.階乗の計算式は「N ! = N * (N-1) !」であり,「N-1 の階乗」を求めておいてそれに N をか ければ「N の階乗」を求めることができます.つまり,「階乗を求める処理」のなかに再び「階乗を求める処理」 が登場します.
この計算式を,1 の階乗は 1 (1 ! = 1) であることに注意して関数 f
として実現すると,つぎのようになります.
int f(int n)
{
int y;
if (1 == n) {
y = 1;
} else {
y = n * f(n - 1);
}
return y;
}
この関数 f
を main
関数から,つぎのようにして呼び出したとします.(このプログラムでは「3 !」を z に代入し,出力しています.)
#include <stdio.h>
int f(int); /* 関数 f のプロトタイプ宣言.関数 f の定義は省略 */
int main(void)
{
int z;
z = f(3);
printf("z=%d\n", z);
return 0;
}
この場合,つぎのように関数 f
が呼び出されていきます.
関数 f
が 1 を実引数として呼び出されると,それ以上再帰呼出しは行われません.その後,つぎのように関数 f
は,呼び出し元へ値を返してゆき,最終
的に main
関数まで戻ります.
結局,上述のプログラムを実行すると以下のようになります.
$ ./a.out
z=6
「==」や「!=」と間違えて「=」演算子を使ってしまった,という誤りを発見しやすくするためです.
左辺が変数ですと,定数を変数へ代入するものとしてコンパイルできてしまいます.一方,左辺が定数なら,「定数には代入できません」とコンパイラに怒られて誤りに気づくことができます.
malloc
や calloc
って何ですか?malloc
や calloc
は,変数として使用するメモリ領域を確保するための関数です.
プログラムでは,おおまかにいって以下の3種類の変数が使われます.
ここで,「スタック領域」「データ領域」「ヒープ領域」というのは,プログラムが走るアドレス空間における用途別の領域名です.
malloc
は,バイト数を引数にとり,そのサイズのメモリ領域をヒープ領域において「使用中」とし,そのメモリ領域の先頭アドレスを返します.例えば,
int *pa;
pa = malloc(sizeof(int) * 4);
と書けば,pa
というポインタはあたかも長さ4のint型配列のように扱えます.
ここで sizeof
というのは,以下のいずれかを返す演算子です.
sizeof(int)
, sizeof(int*)
などとして使用する場合の返り値です.int d, array[40]
といった変数が存在するとき,sizeof(d)
, sizeof(array)
などとして使用する場
合の返り値です.もちろん,長さ4のint型配列ならば,
int pa[4];
と書けば使えます.しかし,このような記述では,pa
の長さは4で固定であり,プログラム実行時のデータに基づいて変更できません.
データ数が変更になれば再コンパイルが必要になり,例えば「1〜10 の平均値を計算する」「1〜100 の平均値を計算する」場合に,それぞれ異なるプログラム
を作って使用するという無駄なことになります.
malloc
で「使用中」とされたメモリ領域は,それ以降 malloc
や calloc
を使って確保されることはありません.malloc
を何度も呼び出していると,
ヒープ領域を使い果たしてプログラムは異常終了します.
malloc
や calloc
で確保したメモリ領域が不要になった場合,free
という関数を用いて「使用中」というラベルを取り除きます.適宜 free
を呼び出
さないと,もう二度と使わないデータを保持し続けて使えるメモリ領域が減っていくというメモリリークが発生します.
なお,malloc
はメモリ領域を確保するだけであり,そのメモリ領域に何らかのデータが書き込まれていても何もしません.
例えば,
int *pa;
pa = malloc(sizeof(int));
*pa += 1;
などと書いた場合,*pa
の中身が「1」であることを期待するかもしれませんが,その中身は不定です.「1」かもしれませせんし,「-1」かもしれません.
malloc
で確保した領域は,多くの場合明示的な初期化が必要です.
上の例でいえば,
int *pa;
pa = malloc(sizeof(int));
*pa = 0;
*pa += 1;
と書けば,*pa
の中身は必ず「1」となります.
このような初期化を自動的に行ってくれるのが calloc
です.calloc
は,
calloc(nmemb, size)
というように2つの引数をとり,size
である要素が nmemb
個連なるメモリ領域を確保し (全体のバイト数は nmemb ✕ size),動的に長さの変化する配列を用意し,かつその要素をすべて 0 (ポインタ配列なら NULL
) にするといった処理を短く記述できます.
引数をとる関数は,呼び出されたときに,
処理を開始します.
引数がint型やchar型なら,それぞれ4バイト,1バイトのメモリ領域を確保・設定するだけなので,実引数を仮引数へコピーする処理コストは小さいです.
しかし,引数が構造体なら,その処理コストが大きくなることがあります.例えば,長さ 1000 の int型配列をメンバとしてもつ構造体を引数とすると,関数呼 出しの際に毎回 4000バイトのメモリ処理が生じます.
このように処理コストが大きくなることを避けるためにポインタを用います.ポインタなら,構造体のメンバがいくら大きかろうと,ポインタ1つ分 (たかだか8 バイト) のメモリ処理で済みます.
なお,関数への引数をポインタとして 与えない 場合,実引数と仮引数は (中身が同じ) 異なる変数であるため,関数内で仮引数をどう変更しても実引数に 影響はありません.一方,関数への引数をポインタとして 与える 場合,実引数と仮引数は異なる変数なのですが,中身のアドレスは同じであり,関数内で 仮引数を経由して行ったデータの変更は実引数にも現れる点に注意しましょう.
構造体ポインタは多用されるため,構造体ポインタからそのメンバへのアクセスを短く記述するための ->
演算子が用意されています.
p
という構造体ポインタが存在し,かつその値が適切に設定されていれば,p
は何らかの構造体データの先頭アドレスを保持しているはずです.このとき,
構造体データ自体は *
という演算子を用いて *p
として使用でき,その構造体データのメンバには .
という演算子を用いてアクセスできます.
例えば,以下のように構造体 foo
が定義されているとします.
struct foo {
int elem_A;
char elem_B;
};
また,以下のように構造体データおよびポインタの値が設定されているとします.
struct foo data_1 = { 1, 'a' };
struct foo *foo_p = &data_1;
このとき,data_1.elem_A
と (*foo_p).elem_A
は同じであり,同様に data_1.elem_B
と (*foo_p).elem_B
も同じです.
->
演算子を用いれば,(*foo_p).elem_A
を foo_p->elem_A
と短く記述できます.elem_B
の場合,foo_p->elem_B
となります.