kentsu.dat

何かその時の興味でいろいろする人。最近はScala使ってる。アルゴリズムと自然言語処理、深層学習が大好き。

C言語 やさしい入門

この前、プログラミング言語Cという
C言語の開発者が書いている本を買ったので
それで勉強したことをまとめていく

1.第一章 やさしい入門

この章では細かい点を抜きにしてCを手短に紹介している

1.1 手始めに

まず避けては通れない
「hello,world」の出力をしてみよ
というもの

これは、Cを以前少しだけやっていたのでわかる

#include<stdio.h>

//int,return 0;は無くてもいい
int main(){
    printf("hello,world\n");
    return 0;
}


こうなる
これをコンパイルするには

cc か gccを使う(linux環境では)

個人的にはgccを使うけど、別にどっちを使ってもいい
そして、何もオプションを付けないならa.outという実行可能ファイルができるので
それを実行すれば、「hello,world」と表示される

まずはこの世界一有名なプログラムから重要な部分を解説していく


まず一行目の

#include<stdio.h>

これをおまじないとまとめてしまうヌルい入門書やサイトがあるが
そうごまかさないで意味を理解する

includeとはなにかヘッダーやファイルを参照するという意味で
今回であれば、stdio.hという標準入出力に関するヘッダーファイルについての情報をこのファイルに含めるという明示的な宣言である
これが無いとprintfなどが使えない

javaでいうところのimportと同じ意味合い

次に

int main(){
    printf("hello,world\n");
    return 0;
}

このmain関数
Cではmain関数から実行されるため、必ずこの関数を実装しないと行けない
mainの前のintは戻り値の型で、戻り値が無い場合voidや、省略が可能。
return は戻り値で、ここで関数の先頭(今回ならmainの前)で宣言している型と戻り値の型が一致している必要がある

また戻り値の型を宣言しているのにreturnしないのもだめ

今回のhello,worldプログラムでは別に戻り値を出す必要はないので
こうも書ける

#include<stdio.h>

main(){
    printf("hello,world\n");
}

この方が短くなる
そしてこの場合大して必要ないreturn文も省略できる

ここまでのプログラムでも今後書くコードでも言えるのが
Cのプログラムは関数と変数で大部分が構成される

そして関数には処理となる計算などの文
それを支える変数が大抵実装される

また関数の他にこの後出てくる構造体なんかもよく出る

1.2変数と算術式

次のように
0 -17
20 -6
40 4
60 15
80 26
100 37
といった℃=(5/9)(F-32)で出せる
華氏の温度、摂氏の温度の対応を出力するプログラムから
C言語をもう少し掘り下げてみる

#include<stdio.h>

main(){
	int fahr,celsius;
	int lower,upper,step;

	lower = 0;//温度表の下限
	upper = 100;//温度表の上限
	step = 20;//刻み

	fahr = lower;

	while(fahr <= upper){
		celsius = 5 * (fahr-32)/9;
		printf("%d\t%d\n",fahr,celsius);
		fahr = fahr + step;
	}
}

やっぱりCのプログラムはmain関数に書く(今は)
ここでは変数、算術式、コメント、宣言なんかが使われている

まずココでの変数とその宣言は

int fahr,celsius;
int lower,upper,step;

これで、すべてint型で宣言されている
同じ型であれば,でつないで宣言できる
そしてCで使うすべての変数は一般的に使う前に普通は実行可能文の前に宣言して置かなければならない

データ型に関しては後述。

コメントは//で書くのはお馴染み。


あと今回はループ文も使用されていて

while(fahr <= upper){
     //処理
}

この記述。
今回使用したwhile文では以下のように動く


まず()の中の条件文をテストする

これが真であれば{}の中の処理が実行される

最初に戻る

逆に()内の条件に対して偽になればループを抜ける


次に算術式
今回で言うならこれがわかりやすい

celsius = 5 * (fahr-32)/9;

そういえばなぜ先に今回は5をかけていたかというと
整数の割り算では切り捨てが行われるため、5/9は0となってしまうため
すべての結果が0になってしまう
そのため先に5倍したものを9で割っている


そして、一番気になるprintfの中にある\tと%d

まず\tはエスケープシーケンスの一種でタブスペースを取るもの
エスケープシーケンスであれば\nの改行も同じエスケープシーケンスの一種

そして%d
これは%記法といわれ、それぞれ対応する引数を代入できる特殊な記法でCではよく使う

printf("%d\t%d\n",fahr,celsius);

今回のprintfでは一個目の%dがfahr,二個目がcelsiusの値を指している

そして大事なこととして
printfはC言語の一部ではない
C自体には入力も出力も定義されておらず、printfは有用な関数に過ぎないということ
ココではC自体に重点を置くため、printfやscanfには深く触れない触れない


そしてさっきのプログラムには少し問題がある
それは、表示が左揃えで少し見にくいこと
なので少なくとも100までの表示である今回のプログラムであるなら
%3dと桁数をdの前に記述することで右揃えにすることができる

printf("%3d\t%3d\n",fahr,celsius);

実は更に重要な問題があり、最初の-17は-17℃ではなく正確には-17.8℃であるということ
それを修正するのにはちょっとした手直しが必要になる

#include<stdio.h>

main(){
	float fahr,celsius;
	int lower,upper,step;

	lower = 0;//温度表の下限
	upper = 100;//温度表の上限
	step = 20;//刻み

	fahr = lower;

	while(fahr <= upper){
		celsius = (5.0/9.0) * (fahr-32.0);
		printf("%3.0f\t%3.1f\n",fahr,celsius);
		fahr = fahr + step;
	}
}

これでいい
具体的にいうならfloatで宣言しなおして、%記法の部分をdからf(浮動小数点)に直し
整数の部分で結果に影響しそうな部分を浮動小数点に変更した
実際には32は32.0に変更する必要はない
なぜなら暗黙の型変換が適応され、32.0と解釈されるから
でも、可読性を考えて浮動小数点表記にした

これでも十分な感じはあるがこのプログラムは次の単元でもっと簡単に書ける様になる

1.3 for文

while文の他にループでfor文というものがある
一般に回数がわかっていたりするときに使われることが多い
(whileは回数不明な時)

といっても実際にはどちらでも実装できる
というわけで、for文で実装してみた

#include<stdio.h>

main(){
	int fahr;
	for(fahr = 0; fahr <= 100; fahr += 20){
		printf("%3d\t%3.2f\n",fahr,(5.0/9.0)*(fahr-32));
	}
}

今回の場合、上限と大体の計算回数がわかってるため
for文を使用することで、変数宣言を一回にし
更にデータの種類を1種類にすることができた

またこれを踏まえて、for文とはなにか
例でわかるようにfor文はwhile文を一般化したもので役割は3つある

まず変数の初期化
例であげれば

fahr = 0;

の部分である。

次にループ本体に入る前に一回だけ実行される

fahr <= 100;

というループを制御する条件文
これに真であれば、ループを開始し
偽であればループに入らず次の処理を実行する

そして

fahr += 20

という大抵はインクリメントのステップ
これには大体の演算処理が適応出来る

これらを踏まえて、while文とfor文はどちらを使ってもいいが
大抵for文の方がコンパクトにまとまるのでfor文を使うことをおすすめする

そして本にはこの後演習問題が掲載されていたのでやってみる

演習

これまでループ文を用いて書いてきたプログラムの出力結果を逆順になるように修正せよ

これはすごく簡単でこの手の問題はfor文を使うことで書ける

#include<stdio.h>

main(){
	int fahr;
	for(fahr = 100; 0 <= fahr; fahr -= 20){
		printf("%3d\t%3.2f\n",fahr,(5.0/9.0)*(fahr-32));
	}
}

これでいい
変数初期化を上限に変更し、範囲を0以上に変え
一回のループごとにループ変数の値を20引いていく

1.4 記号定数

温度換算の話はこれでは終わらない
さっきのfor文のプログラムに記号定数の要素を追加していくと更にわかりやすくなる
というのも、今この記事で上限は何で、一回ごとのループ変数は何でと
解説しているからそれぞれの数字の意味、変数の値の意味を理解しているが
実際はそう簡単ではなく、数カ月後の自分自身や
あるいは誰か他人が見ることも考えられる、そうなるとああいった数値は意味不明だ

そういっためんどくさい理解を防ぐために記号定数を使用する
記号定数の定義は以下の様に行う

#define 記号名 値

これを用いて1.3でfor文を使用したプログラムをわかりやすく変更してみる

#include<stdio.h>

#define LOWER 0   //下限
#define UPPER 100 //上限
#define STEP 20   //ステップサイズ

main(){
	int fahr;
	for(fahr = LOWER; fahr <= UPPER; fahr += STEP){
		printf("%3d\t%3.2f\n",fahr,(5.0/9.0)*(fahr-32));
	}
}

この様に、極力意味不明な数値を記号定数defineで定義することにより
プログラムで使用している数値の意味合いがわかりやすくなる

また記号名を大文字で定義しているが、これは小文字の変数名と混同しないようにするためで小文字でも定義できる

なので、大抵定数は大文字で定義するのが通例である

1.5 文字入出力

次にプログラムで文字を扱うことを考えてみる
ここで初めてプログラミングをする人にとっては一気に難易度があがるのでしっかり解説していきたい

まず文字をどう扱うか
この章では文字をキーボードからの入力により取得しそれを出力する。
という形式を標準ライブラリを使用しつつ取る。

標準ライブラリで文字を扱うことは非常に簡単で
入出力がどこで発生し、どこで出力されるかによらず文字はストリーム(流れ)として扱われる
これらのテキストストリームは行に分割された文字の継起である
各行は、0またはそれ以上の個数の文字の列とその最後につく改行記号によって形成される

標準ライブラリでこれらを実現するにはまずgetchar()という関数が使える
これは一度に1文字キーボード入力を受け取り、受け取った文字を返す関数なので以下のコードは

c = getchar();

cにキーボード入力から受け取った1文字を格納する

出力するにはputcharが使用でき、これは変数に格納された文字を出力する
上のコードでcの値を出力したいなら

putchar(c);

というコードでいい

1.5.1 ファイルの複写

getchar,putcharがあれば、入出力に関してあまり知らなくても役立つプログラムを書くことができる
もっとも単純なプログラムは、入力を出力へ一度に1文字ずつ複写するプログラムである

言語で仕組みを表すなら


1文字取ってくる
while 文字がファイルの終わりでない
読み込んだばかりの文字を出力
新しい文字を取ってくる

といった様に書ける
これをCのプログラムで表現するなら

#include<stdio.h>
main(){
	int c;//文字は整数値
	c = getchar();
	while(c != EOF){
		putchar(c);
		c = getchar();
	}
}

!=は「等しくない」ことであり
EOF(end of file)はファイルの終わりにつくものである

C言語では文字は整数値として扱われるので、整数値を格納出来るデータ型なら大抵格納できる
文字データを格納するのにはchar型を使用するのが一般的だがココでint型を使用するのには微妙だが重要な理由がある

その問題となるのが文字の終わりと正しいデータをどう区別するかという課題である
これを解決するのには、どの値とも混同しないで区別可能な値をgetcharが返す必要がありそれを担うのがEOFである
なので、上記のコードで整数型変数cはプログラムが受け入れる任意の文字に、EOFを付加するだけの十分なサイズを保持している必要があるので
この場合はcharではなくintを使用した

またEOFはstdio.hで定義されている整数であり、それがどのcharとも等しくない値であればその値を具体的に決める必要はない
それに加え記号定数を使用することで、プログラムでどのような値を使用しようと影響はしないことが保証される

そして複写を行うプログラムは経験をある程度つんだ人ならもう少し短く書ける

#include<stdio.h>
main(){
	int c;//文字は整数値
	while((c = getchar()) != EOF){
		putchar(c);
	}
}
1.5.2 文字、行数、単語のカウント

ここらのカウント系はすべてまとめて行う

まずは文字カウントから
これはすごく簡単で、getchar関数が1文字取得(実際には文字列でもいけるがココでは1文字ということにする)するという性質を利用する

#include<stdio.h>
main(){
	int c;//文字は整数値
	int count = 0;//文字カウント用の変数
	while((c = getchar()) != EOF){
		putchar(c);
		count++;
	}
	printf("%d\n",count);
}

これでいい
新しくカウント用の変数を初期化した状態で宣言しそれをインクリメントしていくことでカウントしている

ただこれでは少し問題がある
intが機種によってサイズが異なるため最悪16bitとして扱われた場合最大値が32767であり
比較的小さい入力でオーバーフローを起こす可能性がある
それを防ぐために少なくとも32bitのlongに変更するべきだろう

次に行数のカウント
これは少し考えるかもしれないが実は簡単
まず入力の性質を考える、テキストストリームは行であるから必ず文末に改行の記号が入るので
それをカウントすればいい

#include<stdio.h>
main(){
	int c;//文字は整数値
	int count = 0;//文字カウント用の変数
	while((c = getchar()) != EOF){
		putchar(c);
		if(c == '\n')count++;
	}
	printf("%d",count);
}

これでいい
cに含まれる改行の内部表現を見つけることで行と判断しカウントしていく
この時、シングルクォーテーションで囲まれたものを文字定数といい
それらは数値に変換される、そのため==が適応出来る
ダブルクォーテーションで囲むと、文字列扱いになるのでNG

次に単語をカウントしていく
この時、まず行単位で考え探索が単語の内か外かどちらにあるか判断する

例えば、
hello worldとあったとしよう
それをプログラミング言語的に解釈すると

hello(空白)world(改行)
となる
そのため、単語の外に出た回数をカウントは単語数と一致する
このとき単語の外に出たか判断するのは、改行、タブ、スペースの3種類である

コードはこうなる

#include<stdio.h>

#define IN 1//単語の内
#define OUT 0//単語の外
main(){

	int c;
	int count_word = 0;
	int state = OUT;//はじめは単語の外と仮定

	while((c = getchar()) != EOF){
		//空白、タブ、改行を探索する
		if(c == ' ' || c == '\t' || c == '\n'){
			state = OUT;
		}else if(state == OUT){
			state = IN;
			count_word++;
		}
	}

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

}

この様にgetchar,putchar以外にも
文字定数やif文を使用すると、以前と比べ有用なプログラムを書くことが出来る

1.6 配列

1.5で作成したプログラムを更に改良してみる
具体的には配列を使用して、単語の数、受け取った値に含まれる数字(0~9)の個数を表示してみようというもの
単語の数はさっきのプログラムをそのまま使える
数字のカウントは、サイズが10の配列を生成して添字に対応させる

#include<stdio.h>

#define IN 1//単語の内
#define OUT 0//単語の外
#define NUMBER_SIZE 10//数える数字(0~9)

main(){

	int c;
	int count_word = 0;
	int state = OUT;//はじめは単語の外と仮定
	int numbers[NUMBER_SIZE], i;

	//配列の値を初期化する
	for(i = 0; i < NUMBER_SIZE; i++){
		numbers[i] = 0;
	}

	while((c = getchar()) != EOF){
		//空白、タブ、改行を探索する
		if(c == ' ' || c == '\t' || c == '\n'){
			state = OUT;
		}else if(state == OUT){
			state = IN;
			count_word++;
		}

		//数字の探索
		if(c >= '0' && c <= '9'){
			numbers[c-'0']++;
		}

	}

	//それぞれの数字の個数を表示
	for(i = 0; i < NUMBER_SIZE; i++){
		printf("%d:%d\n",i,numbers[i]);
	}

	printf("words:%d\n",count_word);

}

今回は数字の種類までカウントさせるため
10個の変数より、サイズが10のint型の配列にカウントした値を格納するほうが賢い

C言語の配列は扱いにくく、奥が深いけど入門編ではここまでしか扱わない
このあとじっくりと解説していく
ただ、プログラミング経験者やポインタで挫折した人に覚えてほしいことがある
それは、配列のアドレスは固定されたサイズのメモリ領域を確保するということです


あとこれは少し入門から外れますが、string.hのmemsetという関数を使用すればもう少しコードが短くなります

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

#define IN 1//単語の内
#define OUT 0//単語の外
#define NUMBER_SIZE 10//数える数字(0~9)

main(){

	int c;
	int count_word = 0;
	int state = OUT;//はじめは単語の外と仮定
	int numbers[NUMBER_SIZE], i;

	//配列の値を初期化する
	memset(numbers,0,sizeof(numbers));

	while((c = getchar()) != EOF){
		//空白、タブ、改行を探索する
		if(c == ' ' || c == '\t' || c == '\n'){
			state = OUT;
		}else if(state == OUT){
			state = IN;
			count_word++;
		}

		//数字の探索
		if(c >= '0' && c <= '9'){
			numbers[c-'0']++;
		}

	}

	//それぞれの数字の個数を表示
	for(i = 0; i < NUMBER_SIZE; i++){
		printf("%d:%d\n",i,numbers[i]);
	}

	printf("words:%d\n",count_word);

}

1.7関数

最初に言ったようにC言語のプログラムは関数と変数で構成されているといっても過言ではないほど関数と変数を多用する

今まで標準ライブラリのコードを使ってきたが、この章では実際に関数を自作して学習していく

まずC言語で関数とはある特定の処理をまとめた一つの固まりで、C言語開発者いわく
よく設計された関数とは何をしているかが明確なのではなく、何ができるか明確なものと言われます。
またよくプログラムの中で、一回しか呼ばれない関数などがありますがこれはプログラムを見やすくする為に作成されたもので
プログラム全体の可読性を上げる為に関数を作る場合もあります

それらを踏まえて少し関数を自作してみます
まず、powerという名前である正の整数と指数を受け取りべき乗を計算、計算結果を返すような関数を作成します

そこで気をつけるべきことは、main関数よりしたに独自の関数を追加しないことです
これは一部例外を除いてエラーになります

その一部例外とは、プロトタイプ宣言をした時です
プロトタイプ宣言とは引数のデータ型、関数名、引数と関数の処理を除いた事をmain関数の前に記述することで
その関数をmain関数のしたに書いてもエラーにはならず
また記述が簡単なので可読性も上がります

というわけでこれからはプロトタイプ宣言をして関数を実装していくことにします

#include<stdio.h>

//プロトタイプ宣言
int power(int base,int index);


int main(){
	int n = 2;
	int i;
	for(i = 1; i <= 10; i++){
		printf("result:%d\n",power(n,i));//2のi乗を計算し結果を表示
	}
        return 0;
}


int power(int base,int index){
	int result = 1;
	int i;
	for(i = 0; i < index; i++){
		result *= base;
	}

	return result;
}

まず見てわかるように、4行目がプロトタイプ宣言でこれが重要
後は結果を計算し、returnするだけ
ただ指数計算は計算結果が大きくなりやすいので戻り値をlongにしてもいいかもしれない

これがそこそこ簡単な自作関数
関数はどんな順序であらわれても良く、ひとつのソースファイルでも複数に分割されていてもいい
つまり、呼び出す順番も定義する順番も関係なく
またincludeさえされていれば、複数のソースファイルから呼び出せるというわけ

そして関数について少し追加で解説
power関数の引数、これはどの関数について言えることだが
引数は純粋にその関数のものであるから他の関数から参照できない

そして文末のreturn
これは本来
return 式;
という形をとるが、必ずしも意味のある値を返す必要はなく
何も返さない場合制御が呼び出し側に戻る
なのでvoid、つまり戻り値が無いときにreturn ;と書いてもいい

また今回はmain関数にreturn 0;を追加したが、今まで説明したことを考えると
これでmain関数を抜け制御が呼び出し側に移るが、この場合呼び出し側はプログラムが実行される環境であり、0は正常終了、それ以外の値は異常終了といった意味合いを持つ

なので今後はこれをmain関数につけることにする
ちなみに戻り値の型は無くてもよい

1.8 引数 値による呼び出し

C言語では引数はすべて値で渡される
これは呼び出された関数には一時変数が生成されそれによって値が渡されていることを示す

なので、引数の値を関数内で変更するなどといったことはできず
それをやるにはその変数のアドレスを渡す必要がある

ただ配列については話が別で配列を引数として渡すと、アドレスが渡されるので変更可能となる

1.9 文字と配列

というわけで1.8の流れで配列を扱う
多分一番多用するのが文字の配列、C言語には文字列が無いため文字の配列でそれを実装することがしばしばある

というわけで1群の行を読み込み、一番長い行を表示するようなプログラムを書いてみる
これを擬似コードにしてみるとこう

while(まだ行がある){
if 以前の行より長い
それ格納
その行の長さも格納
}
print 一番長い行


となる。
これに必要なのは行を読み込むgetlineという関数と格納するときに複製するcopyという関数
getlineという関数では行を読み込むが、行の終わりを示す文字
終端文字が必要になってくることも考慮する必要がある

copyは結果を一時的に安全な場所に保持しておくために使用する

#include<stdio.h>

#define MAXLINE 1000//1000行を最大とする

//プロトタイプ宣言
int getLine(char line[],int maxline);
void copy(char to[],char from[]);

int main(){
	int len;
	int max;
	char line[MAXLINE];
	char longest[MAXLINE];

	max = 0;
	while(0 < (len = getLine(line,MAXLINE))){
		if(max < len){
			max = len;
			copy(longest,line);
		}
	}
	//行が存在するなら
	if(0 < max){
		printf("%s\n",longest);
	}
}

int getLine(char line[],int maxline){
	int c;
	int i;

	for(i = 0; i < maxline-1 && (c = getchar()) != EOF && c != '\n'; i++){
		line[i] = c;
	}

	if(line[i] == '\n'){
		i++;
	}

	line[i] = '\0';//終端文字
	
	return i;
}

void copy(char to[],char from[]){
	int i;

	i = 0;
	//終端文字までコピー
	while((to[i] = from[i]) != '\n'){
		i++;
	}
}


このように文字列は文字の配列として扱うことができ
配列を引数で渡すと、アドレスが渡されるため関数内での処理が適応されるため
正確な結果が表示される
詳しくが後述

まとめ

これで変数のスコープを残して(単純に書くのがめんどくさかった)ほぼ入門編は終わり
これから今回解説した部分を掘り下げていく