C言語入門
横田 壽
C言語で苦しんでいる人を対象に、Cプログラミングを学ぶ上で必要な最低限の事柄を収めた。説明を読んで、例題、練習問題をこなして行くうちに、自然にC言語がマスターできるようにしたつもりである。しかし、Cは言語です。皆さんも経験しているように、言語を習得する最良の方法は、“習うより慣れろ”だ。囲んであるプログラムはすべて自分で打ち込んで、実行してみよう。
目次
C言語入門, 1
変数と変数名, 59
代入と入力, 142
値の入れ替え キーボードからの入力 コマンドライン引数 scanfの書式
制御構造, 245
単純な分岐 論理演算子 if文 if-else文 while文 ループからの脱出 Max-Min Switch文 else-if文 うるう年の判定 2次方程式の解 3項演算子
繰り返し文, 466
配列, 547
配列の概念 配列を用いる基準 平均・分散・標準偏差 2次元配列 行列の加算
ポインタの基礎, 658
アドレス ポインタとは アドレス演算子 ポインタ変数 ポインタと配列
関数の作り方, 807
引数のない関数 値呼び出し法 実引数と仮引数 配列の引渡し 参照による呼び出し
構造体と共用体, 901
構造体メンバの参照 構造体のネスト 共用体 構造体へのポインタ 日付と時間
ファイルの概念, 1037
ファイルからの入力 ファイル名を指定した入力 ファイルへの書き込み
まず、Cプログラムの形を見てみよう。
一般的なCのプログラムは次の形をしている。
/*************************************************************************** *Program name:fundamental.c *Author:Hisashi Yokota *Date:Dec 1,2000 *Purpose:introduce how to write C program ****************************************************************************/ 前処理句(例えば#include <stdio.h>)が1行目にくる プロトタイプ宣言(プログラムで使用する関数の型、名前、引数を書く) /*1行あける。/*から*/まではコメントとなる*/ void main(void) ( gcc for win32の場合 int main(void) または単にmain( )と書く) { /*main関数の始まり*/ 宣言部および定義 (これから用いる変数の型宣言をする) メインプログラム (重要な部分で、ここの書き方を学んでいく) } 関数型 関数名(仮引数) { 宣言部および定義 関数のプログラム } |
このテキスト内のプログラムには、紙面の都合上プログラムの最初にくるコメントは付けていないが、自分のプログラムには必ずコメントをつけるようにする。
実際にプログラムを書いて実行してみよう。使っているエディタやコンパイラにより作業ステップが異なるので、ここでは、DJGPPのgccとVisualC++での操作方法を挙げる。
例題1.1 画面への出力
Hello world
初めてのC
と画面に表示してみよう。
実行結果
考え方
C言語にはprintfという画面に表示するための標準関数が用意されている。使い方はprintf(“ “);でダブルクォート”内の文字はそのまま表示される。改行するには\nとダブルクォート内で書く。また、段落を取るには\tと書けばよい。
解答 Cで書くと次のようになる。
/*
hello.c*/ #include
<stdio.h> /* 標準ライブラリの情報を取り込む*/ void main(void) /* 最初のvoidはこの関数は何も返さないことを意味し、voidは引数(他の関数に渡す値)がないことを意味する。voidとは英語で無*/ {
/* mainの文は大括弧で囲む*/ printf(“Hello world\n\t初めてのC\n”); } |
このプログラムをエディタを使って書き、hello.cという名前を付けて保存する。次に、hello.cをコンパイルする。
DJGPPの場合:コマンドプロンプトから次のように打ち込む
gcc hello.c –o hello.exe
Visual c++の場合:Visual c++を立ち上げ、ファイルを開くでhello.cを開き、ビルドの下にあるコンパイルをクリックする。
この操作により、hello.exeという実行ファイルができるので、実行させると画面に
Hello world
初めてのC
と表示されるはずである。
gcc hello.c -oの-oはオブジェクトファイルの生成を意味している。
キャッチ Windowsを用いている場合、メモ帳を使って書くとhello.c.txtと保存されてしまうので対処が必要である。スタート(右クリック)→エクスプローラ→表示→フォルダオプション→ファイルタイプ→新規で拡張子の関連付けを行う |
hello.cの説明
#include <stdio.h>は標準入出力のヘッダーファイルを呼び込めという命令である。このヘッダーファイルは、printfという関数を用いるために必要となる。
void main(void)はmain関数の戻り値と引数が何もないことを表している。
{から}までがmain関数の中身である。
printfとは標準出力(ディスプレイ)へ出力させるための関数であり、ダブルクォート”から”までは、書いたものがそのまま出力される。
printf内の\nは行を変えろということを意味し、\tはタブを取れということを意味する。これらをエスケープシークエンス(escape sequence)といい、
ダブルクォート内からの脱出をいみする。よく使うエスケープシークエンスをあげると、
\nは改行、\tはタブ、\aはベル、\bはバックスペース、\”は”の表示、\’は ’の表示などがある。
キャッチ printfは必ず最後にセミコロン;をつける。 |
練習問題1.1 次のプログラムの説明およびプログラムを読んで、設問に答えよ。
「プログラムの説明」
次のプログラムを実行すると、以下のようになる。
<実行例>
Mainは最初に動く関数。
printfは表示するための関数。
「設問」プログラムの中の□を埋めて、プログラムを完成せよ。ただし、実行例の2行目の先頭文字は、水平タブのあとで表示されるものとする。
#include <stdio.h>
/* 1行あける*/
void main(void)
{
printf(“Mainは最初に動く関数。 printfは表示するための関数。\n”);
}
解答 改行してタブを設ければよいので、\n\tが□に入る。
Cでは数値を実数と整数で区別する。もっと詳しくいうと、1/2と1/2.0ではまったく違う値になる。1/2は0になり、1/2.0は0.5になる。
練習問題1.2 以下の項目で、実数定数で表す必要のあるものはどれか。
解答群
ア 体重の平均を計算する。 イ 人数の合計をカウントする。 ウ 実行回数をカウントアップする。 |
解答 体重の平均は小数点を含むので実数定数
Cでは10進数(decimal)、8進数(octagonal)、16進数(hexagonal)、指数の表記が可能である。8進数の表記には0を数字の前に付け、16進数では0xを付ける。例えば、10進数40を16進数に直すと
40 = 2×16+8
となるので0x28となる。
指数表記では1.2×1015を1.2e15と表す。
練習問題1.3 以下の空欄を埋めよ。
10進数 |
16進数 |
8進数 |
18 |
ア |
イ |
ウ |
0x25 |
エ |
オ |
カ |
012 |
8 |
キ |
ク |
解答 (ア)0x12 (イ)022 (ウ)37 (エ)045 (オ)10(カ)0xa (キ)0x8
(ク)010
18を16進数で表すと
18 = 1×16 + 2
より0x12. 8進数で表すと
18=2×8 + 2
より022.。
0x25を10進数で表すと
2×16+5=37
より37。37を8進数で表すと
37=4×8+5
より045。
012を10進数で表すと
1×8+2=10
より10。10を16進数であらわと0xa。
8を16進数で表すと
8 = 0×16 + 8
より0x8、8進数で表すと
8=1×8+0
より010となる。
表記方法に注意。数値の先頭に「0x」が付くと16進数、「0」が付くと8進数になる。 |
練習問題1.4 以下にC言語の記法で表した数字がある。同じ値のものどうしのグループに分けよ。
.25e2, 0x25, 25, 012, 37, 8, 1, e1, 8.e0
解答 .25e2=.25×1025 =25 0x25=37 012=1.e1 8=8.e0
実数は、「e2」「e1」のように、指数部をつけて表す。「E」でもよい。 |
Cでは変数は値を持った箱と理解すると分かりやすい。箱の名前が変数名で、箱の中身が値と考える。変数名にはなるべく中身が分かりやすい名前を付ける。例えばテストの平均を入れる箱にはtest_aveという名前をつける。ただし、幾つかの制約があるので気をつける。例えば、変数名の最初の文字は英字か下線( _ )でなければならないとかキーワード(予約語)であってはならないなどである。
Cでは変数を入れる箱の型を決める必要がある。もし、変数test_timesがテストの回数を表すとしよう。この場合、テストの回数は1,2,3…という整数なので、整数をしまっておく箱は整数型であると宣言する。例えば、
int test_times;
と書く。また、test_aveがテストの平均だとすると、小数になる可能性があるので、test_aveをしまっておく箱は実数型であると宣言する。例えば、
float test_ave;
と書く。
しかし、テストの回数が2の15乗を超えるようであれば、intではまずい。そのときはlongを用いる。このように、箱に入る中身によってそれに見合った箱を用意できるようになっている。下にCで用いる箱の型を挙げておく。取り合えず、基本型をマスターしておこう。
型 |
基本型 (文字型と整数型にはunsignedがある) |
|
構造を持つ型 |
ポインタ型 |
|||||
|
文字型 |
整数型 |
実数型 |
型なし |
列挙型 |
配列型 |
構造体 |
共用体 |
|
char |
short int long |
float double long double |
void |
enum |
[ ] |
struct |
union |
* |
例題2。1 基本型
char c; /* 文字型符号付*/
int i; /* 整数符号付*/
unsigned int u; /* 整数符号なし*/
long l; /* 長い整数符号付*/
float f; /* 実数符号付*/
double d; /*倍精度実数符号付*/
charはcharacter(文字)の略、int はinteger(整数),floatはfloat(浮動小数点)、voidはvoid(無)のことである。
キャッチ 使っているコンパイラやコンピュータによって整数型のshort , int, long, float,doubleが扱える範囲が異なる。 目安として、パソコンを使っている人はshort,intともに-32768~32767,longで-2147483648~2147483647,floatで4 バイト、doubleで8バイト、long doubleで10バイト。 |
printfの書式制御文字一覧
名称 |
文字 |
意味 |
備考 |
負記号 |
― |
左詰めを指定 |
省略可 |
出力幅1 |
整数 |
文字列の桁数の最小値を指定 |
省略可 |
出力幅2 |
整数 |
浮動小数点の場合には小数点以下の桁数 |
省略可 |
修飾子 |
h |
引数の型d,i,o,u,xを修飾し引数がshort型 |
省略可 |
l |
型d,i,o,u,xを修飾し引数がlong型 |
||
l |
型e,f,gを修飾し引数がdouble型 |
||
L |
型e,f,gを修飾し引数がlong double型 |
||
引数の型 |
d,i,o,x |
10進数、10進数、8進数、16進数 |
省略不可 |
u |
符号なし10進数 |
||
c |
1文字 |
||
s |
文字列および文字配列 |
||
e,f,g |
浮動小数点 |
||
p |
ポインタ値 |
使い方
a=123.e-8;
printf(“%3.10lf”,a);
と書くと0.0000012300と表示される。
練習問題2.1 以下のプログラムの実行結果がどうなるか考えよ。
#include <stdio.h>
void main(void)
{
char c;
int i;
c = ‘A’;
printf(“\nc=%c”,c);
i = (int)c;
printf(“\ni=%d”,i);
printf(“\ni=0x%x”,i);
i= 66;
printf(“\ni=%d”,i);
printf(“\ni=0x%x”,i);
c=(char)i;
printf(“\nc=%c”,c);
}
解答 c=A, i=65, i=0x41, i=66, i=0x42, c=B
文字定数Aを入力するには’A’と書く。また、文字AはASCIコードで値65を表す。
練習問題2.2 以下の変数に宣言を付け加えてプログラムを完成せよ。ただし、複数の解答が考えられるときはいちばん小さいサイズを選ぶ。
#include <stdio.h>
void main(void)
{
ア |
イ |
ウ |
エ |
オ |
カ |
c; /* 文字型符号付*/
i; /* 整数符号付*/
u; /* 整数符号なし*/
l; /* 長い整数符号付*/
f; /* 実数符号付*/
d; /*倍精度実数符号付*/
c = ‘*’;
i = -32768;
u = 65535;
l = -2147483648;
f = 0.1234567;
d = 123.234567;
printf(“%c”,c);
printf(“%d”,i);
printf(“%u”,u);
printf(“%ld”,l);
printf(“%f”,f);
printf(“%lf”,d);
}
解答 (ア)char (イ)int (ウ)unsigned (エ)long (オ)float (カ)double
(ウ)の答えはunsigned intでもよい。
列挙型(enumeration)
ある変数の取り得る範囲を明示的に指定できる型を列挙型という。例えば、
enum Boolean {Female,Male} sex;
と書くとき、Booleanは、識別子であり、Female,Maleは列挙子リスト(enumerator-list)と呼ばれ、sexはこの列挙型を持った列挙変数の名前となる。
a=13;
と書くと、aという変数名がついた箱の中に13という値が記録される。C言語と数学の違うところは、式文の最後に「;」をつけることと、変数の型を意識しなければいけない点である。
キャスト
キャストは、変数もしくは定数の前につけることで型の変換を行う。
(型)変数 (型)定数
例えば、(int)nと書くと、nはその場だけ整数型になる。
型変換を伴う代入の問題点
小さな幅の型から大きな幅の型に代入するのは問題ないが、大きな型から小さな型へ代入する場合は、まったく異なる数値に変化する可能性がある。また、符号付きと符号なしの変数どうしでも値が変わるおそれがある。
練習問題3.1 値の入れ替え
代入を使って、変数aとbの内容を壊さずに、入れ替えるプログラムを完成せよ。ただし、変数aとb以外に作業用として変数wを用いることとする。
#include <stdio.h>
void main(void)
{
int a,b,w;
a=10;
b=20;
printf(“\na=%d”,a);
printf(“\nb=%d”,b);
/*入れ替え*/
(ア) |
(イ) |
(ウ) |
printf(“\na=%d”,a);
printf(“\nb=%d”,b);
}
<実行結果> a=10 b=20 a=20 b=10 |
解答 (ア)w = a; (イ)a = b; (ウ)b = w; |
解説 変数wにaの値を入れる。この段階で変数aにはaの値が入っている。次に、変数aに変数bの値を入れる。最後に変数bにwの値を入れる。この結果aにはbの値、bにはaの値が入ることになる。プログラムの基本なので身に付けよう。
例題3.1 キーボードからの入力
キーボードからの入力(標準入力)を取り込むのに便利なものに、scanfという関数がある。例えば、コマンドプロンプト上で自分の名前を打ち込んだら次の行に”こんにちは誰々さん”と表示するプログラムを考えてみよう。ちょっと難しい。
考え方
キーボードから入力した文字列を保存するには、配列name[ ]を用いる。この段階では配列が何か分からない人が殆どあるが、文字列は配列に入れるんだと理解すればよい。また、取り込むときに、配列名nameの前に&は付けない。詳しい説明は6章および8章を参照のこと では、プログラムを書いてみよう。
解答
#include <stdio.h> void main(void) { char name[8]; /*name[8]はnameという箱に8つの文字を入れる場所を確保*/ printf(“名前はなんですか ?\n”); scanf(“%s”, name); //この場合は&を書かない。 printf(“こんにちは%sさん”,name); } |
この例題では、名前をコマンドラインから入力した。それに対して、次の例題では表示させるものをコマンドライン引数として使う。
C++のmain関数はmain(int argc, char *argv[])と書け、最初のint argcは引数の個数を表し、char *argv[]は引数となる文字列を意味する。第1引数はargv[1]となる。
例題3.2 コマンドライン引数
自分の名前を引数として打ち込んだら次の行に”こんにちは誰々さん”と表示するプログラムを考えてみよう。
実行例
考え方
main(int args, char *argv[])において第1引数、横田は、argv[1]に代入される。では、プログラムを書いてみよう。
解答
#include <stdio.h> void main(int argc, char *argv[]) { printf(“こんにちは%sさん”,argv[1]); } |
argv[0]には、このプログラムのディレクトリが入る。
scanfの書式制御文字一覧
名称 |
文字 |
意味 |
備考 |
負記号 |
― |
左詰めを指定 |
省略可 |
出力幅1 |
整数 |
文字列の桁数の最小値を指定 |
省略可 |
出力幅2 |
整数 |
浮動小数点の場合には小数点以下の桁数 |
省略可 |
修飾子 |
h |
引数の型d,i,o,u,xを修飾し引数がshort型 |
省略不可 |
l |
型d,i,o,u,xを修飾し引数がlong型 |
||
l |
型e,f,gを修飾し引数がdouble型 |
||
L |
型e,f,gを修飾し引数がlong double型 |
||
引数の型 |
d,i,o,x |
10進数、10進数、8進数、16進数 |
省略不可 |
u |
符号なし10進数 |
||
c |
1文字 |
||
s |
文字列および文字配列 |
||
e,f,g |
浮動小数点 |
||
p |
ポインタ値 |
キャッチ scanfを用いるときによく起きるエラーは引数の前に&を忘れることである。ただし、引数が配列の場合は&を付けない。詳しいことはポインタの章で説明する。引数の前の&はアドレスを意味しており、初学者には分かりづらいので、CのバイブルR&Kの本ではgetcharを用いている。1文字づつ文の最後まで読み込むには int c; while(c = getchar( ) != EOF) と書けばよい。EOFはend of fileの略である。 |
練習問題3.2 次のプログラムで、出力時に値が変化する可能性があるのはどれか。
1 #include <stdio.h>
2 void main(void)
3 {
4 int i,j;
5 long l;
6 float f;
7 double d;
8 scanf(“%d”,&i);
9 scanf(“%ld”,&l);
10 scanf(“%f”,&f);
11 scanf(“%lf”,&d);
12 j=1;
13 i=(int)l;
14 l=(long)f;
15 f=(float)l;
16 l=(long)i;
17 i=j;
18 printf(“%d %ld %f”,i,l,f);
19 }
20
解答 long型のlは、13行目でint型のiに強制変換して代入された後、16行目でlong型に戻されるが、13行目で桁数が落ちる可能性がある。float型のfは、14行目でlong型のlに強制変換して代入された後、15行目でfloat型に戻されるが、14行目で小数部分が落ちる可能性がある。
練習問題3.3 次のプログラムを実行した結果を数値で答えよ。
<プログラム>
#include <stdio.h>
void main(void)
{
int a,b;
a=9;
b=2;
printf(“a/bの結果=%d\n”,a/b); /*整数どうしの割り算を行うと、結果は整数で表
される。*/
printf(“a%%bの結果=%d\n”,a%b); /* a%bはaをbで割ったあまりを出す*./
}
解答 a/bの結果=4、a%bの結果=1
a,bがint型のとき、a/bは整数の商を表す。a%bはあまりを表す。
1.単純な分岐 - 等価演算子、関係演算子
単純な分岐とは、条件式に1つの式だけを使った選択構造である。
おもに式としては、等価演算子(=)、関係演算子(< >)が使用される。変数や式が単独で設定されることもある。その場合は、その評価値が0ならば偽、0以外ならば真という判定になる。
例 変数の場合
a = 0;
if(a)
{
printf(“aは0ではありません。\n”);
}
else
{
printf(“aは0です。\n”);
}
<実行結果> aは0です。 |
例 式の場合
a=0,b=1;
if(a*b)
{
printf(“a*bは0ではありません。\n”);
}
else
{
printf(“a*bは0です。\n”);
}
<実行結果> a*bは0です。 |
2.分岐条件の組み合わせ - 論理演算子
分岐の条件は範囲の指定などのときに、複数組み合わせられる。その組み合わせには、主に論理積&&と、論理和||の演算が用いられる。
例 論理積(&&)の場合:xがaからbの間にある場合
a =1;x=2;b=3;
if(a
<=x && x <=b)
{
printf(“xはaとbの間にある。\n”);
}
else
{
printf(“xはaとbの間にない。\n”);
}
<実行結果> xはaとbの間にある。 |
論理和(||)の場合:xがa以下またはb以上にある場合
a=1;b=2;x=3;
if(x
< a|| b < =x)
{
printf(“xはaとbの間にない。\n”);
}
else
{
printf(“xはaとbの間にある。\n”);
}
<実行結果> xはaとbの間にない。 |
if文:もし~ならば~する
このダイアグラムをフローチャートという。フローチャートはJIS規格としてきていされているが、プログラムの作り方の研究が進むにつれて、その欠点が指摘され、現在ではあまり使われていない。フローチャートに変わるものとして、NSチャートやHCPチャートなどが開発されている。とはいってもいまだにJIS規格のフローチャートを用いている場合があるので、多少の解説をしておく。フローチャートとは、アルゴリズムの各段階の処理を箱の中に記述し、各箱を線や矢印で結んで、計算順序を示すものである。
例題4.1 if文の例 2数の比較
2つの数字を読み取って比較し、大きい方を求めるプログラム(if文だけを使って)を作成しよう。
考え方
変数aとbに呼び込んだ値を入れる。つぎに、aとbの値がどちらが大きいかの比較をする。
もしaがbより大きいならばmaxはaである。
もしbがaより大きいならばmaxはbである。となる。では、プログラムを書いてみよう。
#include <stdio.h> void main (void) { int max,get_a, get_b; printf(“2つの整数を入力\n”); scanf(“%d %d”,&get_a,&get_b); if (get_a > get_b) { max = get_a; printf(“大きい方は%d\n”,max); } if (get_b > get_a) { max = get_b; printf(“大きい方は%d\n”,max); } } |
if-else 文:そうでなければ~する
上の例題でget_a > get_bが正しければ、maxはget_a。そうでなければmaxはget_bと直せる。つまりif-else文を用いて書いたほうが分かりやすい。
例題4.2 if-else 文の例 2数の比較
2つの数字を読み取って比較し、大きい方を求めるプログラムを作成しよう。
考え方
もしaがbより大きいならばmaxはaで、そうでなければmaxはbである。
解答
#include <stdio.h> void main (void) { int max,get_a, get_b; printf(“2つの整数を入力\n”); scanf(“%d %d”,&get_a,&get_b); if (get_a > get_b) { max = get_a; printf(“大きい方は%d\n”,max); } else { max = get_b; printf(“大きい方は%d\n”,max); } } |
while文:~の間~する
例題4.3 素数判定
与えられた数が素数かどうか判定するプログラムを作成せよ。
考え方
素数とは1とその数自身でしか割ることができない数である。そこで、与えられた数をnとし、nを2から順にn-1までの数で割っていき、割り切れなければ素数であることが示せる。i=2から順に割っていくということをプログラムで書くと、iがnより小さい間は実行するとなるのでwhile(i < n)と表わせる。次に、whileループを実行中あまり(remainder)の最小値(min_remainder)を求める。やがて、iがnに到達しwhileループが終了する。その後、あまりの最小値(min_remainder)の値が0か0以外かを調べれば、nが素数かどうかの判定ができる。プログラムに書くと次のようになる。
解答
/*素数判定*/ #include <stdio.h> void main(void) { int get_num,min_remainder=1,remainder,index=2; printf("整数を入力してください\n"); scanf("%d",&get_num); if(get_num==1) { min_remainder=0; } while(index < get_num) { remainder = get_num%index; if(remainder < min_remainder) { min_remainder=0; } index++; } if(min_remainder !=0) { printf("%dは素数です",get_num); } else {
printf("%dは素数ではありません",get_num); } } |
もう少しよく考えてみよう。2で割り切れる数(偶数)は2以外素数ではない。また、素数でない数(合成数)の約数は対になっているので、nの平方根までの数をチェックすればよいことが分かる。この考えを用いて、もう一度プログラムを書いてみよう。
#include <stdio.h> void main(void) { int get_num,index=2,remainder,min_remainder=1; printf("整数を入力してください\n"); scanf("%d",&get_num); if (get_num==1) { min_remainder=0; } if(get_num > 2 && get_num%2 == 0) { min_remainder=0; } while (index*index <= get_num) /* =が必要となる*/ { remainder = get_num%index; if(remainder < min_remainder) { min_remainder=0; } index++; } if(min_remainder != 0) { printf("%dは素数です\n",get_num); } else { printf("%dは素数ではありません\n",get_num); } } |
こういうプログラムを実行させるとき、その都度エグゼファイル(実行ファイル)を立ち上げなければならず、面倒であると感じた人は少なくないだろう。そこで、好きな回数だけ実行させられる方法を示す。
int choice=0;
do
{
choice =1;
printf ("続ける場合は0をやめるときは0以外の数字キーを押してください");
scanf(“%d”,&choice)
}while(choice==0);と入れておく。これにより0を押せば続行できるし、止めたければ0以外のキーを押せばよい。
while,for,do-while文を用いるとき、気をつけなければならないのは、無限ループにおちいりやすいことである。このようなことを避けるために、ループの条件は点ではなく、区間とするべきである。特に、while(1),for(;;)などの無限ループではじめる人がいるが、Ctrl+Cで脱出を図るようなプログラムは絶対に書くべきではない。
さて、ループからどうしても脱出する必要があるときには、if文とbreak文を用いる。例えば、キーボードからデータを入力して、0を入力したら終了したい場合を考えよう。このときは、
while(scanf(“%d”,&data))
{
if (data == 0)
{
break;
}
}
と書けばよい。whileループの条件はscanf(“%d”,&data)であるが、scanfは呼び込みに成功すると1を返し、失敗すると0を返す関数なので、このループは入力がある限り続行される。次に、入力が0のとき、if文の条件が真となるので、次のbreak文に到達し、ここで、whileループから脱出する。
例題4.4 (Max-Min) データをキーボードから入力し、入力したデータの平均、最大値、最小値を求めるプログラムを作成しよう。ただし、0を入力した時点で終了するとする。
考え方
1. 最大値と最小値を見つけるには、まず、1個データを読んで、その値を最大値と最小値にしておく。これを行うプログラムは次のようになる
int max,min,ini;
scanf("%d",&ini);
max = min = ini;
2. 0を入力したら終了しなくてはならないので、while文の中にif文とbreakを
使ってwhileからの脱出をはかる。関数scanf( )はうまくいけば1を返し、失敗する
と0を返す関数なので、次のようになる。
int data
while(scanf("%d",&data))
{
if(data == 0)
{
break;
}
3. 0以外のときは、新しく読み込んだデータと1個目のデータとを比較し、新し
いデータが1個目より大きければ、それをmaxとおき、小さければminとおく。次に
データを読み込んだときに、読み込んだデータとmax,minを比較する。次のようにな
る。
int max,min;
if(data > max)
{
max = data;
}
if(data < min)
{
min = data;
}
4. 平均は読み込んだデータの和sumを読み込んだ回数nで割ってやればよい。ただ
し、sumもnも整数型なので、平均を出すときには実数型に変えてやらなければならな
い。これはキャストと呼ばれるものを使って行う。次のようになる。
sum = sum + data;
n++;
printf("平均は%d",(float)(sum/n));
以上をまとめると次のようなプログラムになる。
#include<stdio.h> |
Switch文について
C言語にはelse-if文の代わりにswitch文とよばれるものがあるが、これはコンパイラーどうしでの移植性が悪いので、使うのは控えよう。switch文で書けるものはすべてelse-if文で書ける。しかし、逆は真ではない。とは、言うものの、switch文で書かれたプログラムを読むこともあると思うので、簡単にswitch文の紹介をする。
switch (式) { case 定数式:文 case 定数式:文 default:文 } |
switch文の動作
「ステップ1」
式の値を求める
「ステップ2」
式の値が
(1) caseの定数式と等しい場合、
そのcaseに続く文にプログラムの制御が移り、実行される。
(2) どのcaseとも等しくないが、defaultが存在するとき、
defaultに続く文にプログラムの制御が移り、実行される。
(3) どのcaseとも等しくなく、defaultが存在しないとき、
プログラムの制御は、switch文の次の文に移る。
各caseには1つ以上の数値を持つ定数あるいは定数式による名札を付ける。Defaultとい名札の付いたcaseは、他のcaseのどれもが満足されなかったときに実行される。
例題4.5 順位の判定
次のプログラムは、入賞者の判別を行うプログラムである。
#include <stdio.h>
void main (void)
{
int rank;
printf(“あなたの順位は?>>> “);
scanf(“%d”,&rank);
switch (rank)
{
case 1: //caseと1の間にはスペースが入る
printf(“優勝者には、海外旅行です\n”);
break;
case 2:
printf(“2位のあなたには、国内旅行です\n”);
break;
case 3:
printf(“3位のあなたには、図書券です\n”);
break;
default:
printf(“タオルをどうぞ\n”);
break;
}
}
else-if文:もし~ならば~し、そうでなければ~し、そうでなければ~し、。。。そうでなければ~する。
例題4.6 うるう年の判定
西暦年数yearを入力し、うるう年かを判定するプログラムを作成せよ。
考え方 うるう年とは400で割り切れる年数。または、4で割り切れ、かつ100で割り切れない年数のことである。
例えば1900年はうるう年ではない。
これをプログラムにするには、if (year % 400 == 0)ならばうるう年。else if (year % 4 == 0 && year % 100 != 0)ならばうるう年。else うる年ではない。ではプログラムを書いてみよう。
解答
#include <stdio.h> void main(void) { int year; printf("西暦年を入力してください\n"); scanf("%d",&year); if(year%400 == 0) { printf("%dはうるう年です",year); } else if(year%4 == 0 && year%100 != 0) { printf("%dはうるう年です",year); } else { printf("%dはうるう年ではありません",year); } } |
if(year%400 == 0)以降は次のように書くこともできる。
if(year%400 == 0 || (year%4 == 0 && year%100 != 0))
やってみよう。
例題4.7 2次方程式の解
ax2 + bx + c = 0の解を求めるプログラムを作成しよう。
考え方
判別式はD = b2 – 4acで与えられ、解の公式より、解はx = (-b ± √D)/2aで与えられる。よって
「係数aが0であり、係数bが0ならば、 (条件1)
解はない (文1)
「係数aが0であり、係数bが0でなければ(条件2)
x = -c/b (文2)
「係数aが0でなく、判別式が正ならば (条件3)
実数解が2つ (文3)
「係数aが0でなく、判別式が0ならば (条件4)
実数解は1つ (文4)
「係数aが0でなく、判別式が負ならば (条件5)
複素数解が2つ (文5)
これらは全て複数のif文を組み合わせて判断を行わなければ解けない。つまりswitch文では書けないということである。
ではプログラムを書いてみよう。
解答
#include <stdio.h> #include <math.h> void main(void) { double a,b,c,D,x1,x2,x3,x4; printf("ax^2+bx+c=0の実数a,b,cを入力してください\n");
scanf("%lf %lf %lf",&a,&b,&c); D = b*b -4*a*c; x1 = (-b + sqrt(D))/(2*a); x2 = (-b - sqrt(D))/(2*a); if(a == 0.0 && b == 0.0) { printf("解はありません\n"); } else if(a == 0.0 && b != 0.0) { x1 = -c/b; printf("解は%lfです\n",x1); } else if (D >0.0) { printf("解は%lfと%fです\n",x1,x2); } else if(D == 0) { printf("解は重解となり%fです\n",x1); } else { x3 = -b/(2*a); x4 = sqrt(-D); printf("解は%lf+i%fと%lf - i%fです\n",x3,x4,x3,x4); } } |
列挙型を用いたif文
enum {Female,Male} sex;
if(条件= = 真)
{
sex = Male;
}
else
{
sex = Female;
}
これで、列挙変数sexはMaleかFemaleしか取らないことを、文法的にきちんと宣言した。
列挙変数は列挙子に明示的に値が与えられなければ、書かれた順序に「0」から始まる順序数(ordinal number)となる。よって、この例ではFemaleは0、Maleは1となるので、条件が真ならば、sexはMaleとなる。
例題4.9列挙型enum {Female,Male}sex;にscanfで読み込んだ値を代入し、出力するプログラムを作成しよう。
解説 列挙型enum {Female,Male}sex;に入力するには、0または1をキーボードから打つ。
出力するには
printf(“%c”,(self_data.sex = = 0)? ‘F’:’M’;)を用いる。これは3項演算子と呼ばれるもので、次のように用いる。
式1?式2:式3
の形式をとり、式1が真ならば、式2の値が式の値となり、偽ならば式3が式の値となる。
解答
#include <stdio.h> #include <string.h> void main(void) { enum {Female,Male}sex; printf(" 性別> \n"); scanf("%d",&sex); printf("\n SEX\n"); printf("%3c\n",(sex==0)? 'F':'M'); } |
loopとは繰り返し文のことである。俗に反復処理と呼ばれていてC言語の根幹をなしている。反復処理にはwhile文、do-while文とfor文がある。
その昔、コンピュータの性能が悪かった時代、また、しっかりした制御構造および反復処理がプログラミング言語に用意されたいなっかた時代、多くの人がgoto文と呼ばれるジャンプを使ってプログラムを書いていた。しかし、1960年代後半goto文によって書かれたプログラムは、他人にとってわけのわからないプログラム(スパゲッティプログラム)になり、諸悪の根源であると言われ始めた。このことより、ソフトウェアも通常の工業製品と同じように生産管理が必要であるという観点から「ソフトウェア工学」が生まれた。このような流れの中から、分かりやすいプログラムを書くための方法論が提案され、その中の1つに「構造化プログラミング」がある。
構造化プログラミングとは、基本的に3つの構造から成り立っている。
1.プログラムは上から下に流れる(順次)
2.制御構造
3.反復処理
構造化定理によると、すべてのアルゴリズムはこの3つの構造だけで記述できることが証明されている。
しかし、日本語の構造のせいなのか、はたまたgoto文世代がプログラミング言語を教えているせいか、日本の学生は構造化プログラミングをマスターするのに多大な時間を必要としているのが現実である。
現在、C言語を学んでいる学生は、上記の1,2,3だけでアルゴリズムを書く練習をして欲しい。
例題5.1 2乗の和
12 + 22 + 32 + 42 + 52 + … + 1002を求めるというプログラムを作成しよう。
考え方
j番目がj2より和はs=s+j2と表せる。これをjが1から100まで行うのに、Cには3種類の繰り返し(loop)が用意されている。while,do while,for の3つである。
次の3つのプログラムはすべて同じ結果を出してくれる。
/*while loop*/ #include <stdio.h> void main(void) { int j=1,s=0; while(j
<=100) { s = s
+ j*j; j+=1; } printf(“%d”,s); } |
/*do while loop*/ #include <stdio.h> void main(void) { int j=1,s=0; do { s = s
+ j*j; j+=1; } while(j <=100); printf(“%d”,s); } |
/*for loop*/ #include <stdio.h> void main(void) { int j,s=0; for(j=1;j<=100;j++){ s = s + j*j; } printf(“%d”,s); } |
一般的に、j番目がある関数f(j)で表せるならば、上の式文s = s + j*j;をs=s+f(j)と書き換えればよい。
j++,j+=1,j=j+1は皆同じものを意味する。
例題5.2 次のような形を表示するプログラムを作成せよ。
考え方 <実行例1>
printf(“*”);または、
char aster=’*’;
printf(“%c”,aster);
で1つのアスタリスクを表示することができる。では*****と表示するにはどうすればよいだろうか。 確かにprintf(“*****”);でもできるが、これではアスタリスクを任意の数だけ表示せよとなると、不可能である。そこで、printf(“%c”,aster);を5回実行すると*****が表示できることに気づけばよい。5回実行させるには、例えばforループを用いるとfor(i=1;i<=5;i++)と書けばよいことを知っている。次に、第1回目には、1つのアスタリスク、第2回目は2つのアスタリスクと増やすにはどうすればよいだろうか。i<=5の5の代わりに、jを用いて、for(j=1;j<=5;j++){ for(i=1;i<=j;i++)}と書けば、外側のループが1回終了する間に内側のループがj回実行されることになる。内側のループが終了したら、改行する。ではプログラムを書いてみよう。
<実行例2> 実行例1との違いはアスタリスクを右詰めで表示しているところである。そこで、アスタリスクの前にブランクを挿入するコマンドを追加すればよい。
解答
#include <stdio.h> void main(void) { int
i,j; for(j=1;j
<= 5;j++) { for(i=1;i<=j;i++) { printf("*”); } printf("\n"); } } |
#include <stdio.h> void main(void) { int
i,j,k; for(j=4;j
>= 0;j--) { for(k=1;k<=j;k++) { printf(“ “); } for(i=1;i<=5-j;i++) { printf("*”); } printf("\n"); } } |
練習問題5.1 次のプログラムの説明およびプログラムを読んで、設問に答えよ。
「プログラムの説明」
標準入出力よりタイトルと数値データを読み取り、「*」の棒グラフを出力するプログラムである。タイトルはそのままエコーバックする。数値は同数の「*」に変換する。入力データのタイトルと数値データは「,」で区切る。
「設問」 プログラム中の括弧を埋めて、プログラムを完成せよ。
#include <stdio.h>
int echo_line(void); /* 関数*/
void print_stars(int size); /* 関数*/
void main(void)
{
int value;
while( ア ){ /* ファイルの最後まで読む*/
scanf(イ,&value);
print_stars(ウ);
}
}
int echo_line(void)
{
int c;
if(scanf(“%c”,&c) == EOF) return (EOF);
for( ; ; エ) { /* 1文字づつ読み込む*/
if( オ ) return( c );
else putchar ( c );
}
}
void print_stars(int size)
{
int i;
for( i = 1; カ ; i++) putchar( ‘*’ );
putchar( キ );
}
解答 関数echo_line( )は文章の最後を読むと、EOFをmain関数に返す。そこで、文書の最後まで読むにはアにecho_line( ) != EOFが入る。イはvalueを読むための書式なので%dが入る。print_star関数の引数は’*’の個数であるからウにはvalueが入る。区切り文字を判定しつつ1文字づつ読むのでエにはc=getchar( )が入る。区切りの判定なのでオにはc==’,’が入る。print_star関数は、数値分「*」を出力するので、カにはi <=sizeが入る。「*」の出力の後に必要なのは、改行の出力であるのでキには’\n’が入る。
キャッチ getcharが1文字づつ読み込むのに対して、putcharは1文字づつ表示する。 getcharとはget characterの意味で、putchar はput charcterの意味である。 |
例題5.3 (素因数分解)整数を読み取って、素因数分解するプログラムを作成せよ。
<処理例>
入力
100
出力
2,2,5,5
考え方 データnを読み取って、2から順に割っていき、ある数mで割れたらn/mを新たなnとおき、商が1になるまで続ける。このとき発生した、mが素因数となる。表示するときに、最後の数字のあとには「,」を付けないようにする。
では、プログラムを書いてみよう。
解答
#include <stdio.h> void main(void) { int m,n; char c = ' '; printf("整数を入力してください。"); scanf("%d",&n); for (m=2;n != 1;m++) { while(n%m == 0) { n = n/m; printf("%c%d",c,m); c = ','; } } } |
先に断っておくが、C言語では、配列は次章で学ぶポインタとの間に強い関係があるので、配列はポインタと一緒に学ぶことを勧める。
プログラムを作るときには、データを変数という箱に入れて、これに変数名という名前を付けて扱った。しかし、この方法では、プログラムを作る上で非常に困難になることがある。
例えば、今、6段の引き出しに科目別の受講生の数が次のように入っていたとする。
段数 |
1 |
2 |
3 |
4 |
5 |
6 |
内容 |
微分学 |
積分学 |
線形代数 |
C言語 |
数値解析 |
応用数学 |
数量 |
95 |
100 |
55 |
80 |
60 |
40 |
ここで、キーボードから引き出しの段数を表す数を入力し、その内容物の数量を表示するプログラムについて考えてみよう。例えば、2と入力した場合には2段目の積分学の受講生の数100を表示し、4と入力した場合には4段目のC言語の受講生の数80を表示する。
そこで、1段目をdrower1、2段目をdrower2、とおくと,、プログラムは
#include <stdio.h> void main(void) { int drower1 = 95; int drower2 = 100; int drower3 = 55; int drower4 = 80; int drower5 = 60; int drower6 = 40; int i; scanf(“%d”,&i); if (i == 1) { printf(“%d”,drower1); } else if (i == 2) { printf(“%d”,drower2); } else if (i == 3) { printf(“%d”,drower3); } else if (i == 4) { printf(“%d”,drower4); } else if (i == 5) { printf(“%d”,drower5); } else { printf(“%d”,drower6); } } |
となる。このプログラムでは、個々の場合を記述せねばならず、複雑で長くなる。もし、これが、大学全体の受講生となると、引出しの数が100や200では足りなくなってしまう。たとえ、これらの変数を個々に定義して表示できたとしても、長くて見通しの悪いプログラムになってしまうだろう。
そこで、このような場合に用いるものに、配列(array)というものがある。配列は「何番目のデータ」という表現ができる変数である。ベクトルや行列を配列を用いて表現することもできる。
1次元配列を用いるためには、変数を配列として定義する必要がある。例えば、
int drower[6];
とすると、drowerという配列を用意し、そこには6個のデータが格納できることを意味する。このように定義すると、drower[0],drower[1],…,drower[5]の合計6個の変数が用意される。これより上のプログラムをもう一度書き直すと
#include <stdio.h> void main(void) { int i; int drower[6]; int choice=0;
drower[1] = 95; drower[2] = 100; drower[3] = 55; drower[4] = 80; drower[5] = 60; drower[6] = 40; do { choice=1; printf("数字を入力してください\n"); scanf("%d",&i); if((i <=0) || (i > 6)) { printf("1から6までの数字を入力してください\n"); } else { printf("%d\n",drower[i-1]); } printf("続けたければ0を入力"); scanf("%d",&choice); } while (choice==0); } |
2つの添え字をもつ配列を2次元配列という。例えば、
int data[3][2] = {1,2,3,4,5,6};
とすると、dataという配列を用意し、そこには3かける2、つまり6個のデータが格納できることを意味する。これは、data[0][0]=1;data[0][1]=2;data[1][0]=3;data[1][1]=4;data[2][0]=5;data[2][1]=6;と代入を行ったのと同じことである。
配列を用いずにプログラムが組めるのであれば、配列を使わない。
配列を用いるのは、データを蓄えておいて、後で再利用する場合や、そのデータの各要素をランダムに参照する場合などである。
練習問題6.1 プログラムの中の括弧および□を埋めて、素因数分解のプログラムを完成せよ。アルゴリズムとしてはもとの数値を小さい素数から順に割っていき、その割った商が1になるまで繰り返せばよい。
#include <stdio.h>
#define N_MAX 32768
int flg[N_MAX];
void main(void)
{
int i,j,n;
printf(“データを入力してください”);
scanf(“%d”,&n);
for(i=2; i <=N_MAX; i++)
flg[i] = 1;
for(i=2; i < N_MAX && n! = 1; i++)
{
if (flg[i]){
while(ア ==0) {
printf(“%5d”,i);
イ |
}
for(j = 2*i;j < N_MAX; j+=i)
flg[j] = 0;
}
}
}
解答 すでに見つけた約数の倍数以外の中から、新しい約数を検出するプログラムである。約数である条件は、余りが0のことである。よってアにはn%iが入りイにはn/=iが入る。
n/=iとはn=n/iのことである。他にもn+=1はn=n+1またはn++と表せる。 |
例題6.1 平均・分散・標準偏差 もう一つ配列の問題を考えてみよう。
次の表は広島工業大学のある科目の期末テストの点数である。データは30人分ある。このデータを入力して、平均値、分散、標準偏差を計算するプログラムを作成してみよう。
60 80 70 85 90 45 60 70 55 60 70 80 70 75 80 95 60 40 50 60 70 80 60 70 50 70 80 90 60 70 |
考え方
これらのデータをdj、j=1,2,…30に格納する。平均mean、分散variance、標準偏差std_devは次の式で表せることに注意する。
この式に基づいて作成したプログラムは次のようになる。
まず、データを格納する配列をdeta[30]とする。データを順に読み込むにはfor(i=0;i<=29;i++)と配列は0から始まることに注意する。また、平方根を求めるためにsqrt関数を用いるので、math.hをインクルードしておく。
解答
#include <stdio.h> #include <math.h> void main(void) { int data[30]; double mean,variance,std_dev; int i,n = 30; double sum; for (i = 0;i < n; i++) { printf("%d番目のdata = ", i+1); scanf("%d",&data[i]); } for (sum = 0.0,i = 0;i < n; i++) { sum = sum + data[i]; } mean = sum/n; for( sum = 0.0,i = 0;i < n;i++) { sum = sum + (data[i] - mean)*(data[i] - mean); } variance = sum/n; std_dev = sqrt(variance);
printf("平均 =%10.3f\n",mean); printf("分散=%10.3f\n",variance); printf("標準偏差 =%10.3f\n",std_dev); } |
2次元配列を用いた行列の計算
2次元配列は、行列計算としての使い道が非常に多い。例えば、n行m列の行列は、次のように定義する。n=4,m=5とすると
float matrix[4][5];
または、マクロ名を用いて
#define LOW 4
#define COLUMN 5
float matrix[LOW][COLOMN];
としたほうがさらによい。
配列では、確保する記憶量が多くなる。2次元配列では、さらに多くの領域を必要とする。例えば、4行5列の2次元配列では20個の要素が必要で、ここでは要素の型がfloat形なので、20×4バイト →80バイトの領域が必要となる。double型にすると、20×8バイト → 160バイトの領域が必要になる。このこのことから、配列の型は、必要とする精度を考慮してなるべく確保する領域の小さい型を選ぶべきである。
例題6.2 行列の加算
次のような4行3列の行列の和を求めるプログラムを作成してみよう。
考え方
行列A、Bは次のように定義できる。
float a[4][3], b[4][3];
または、マクロ名を用いて
#define LOW3
#define COLUMN 4
とも定義できる。後々のことを考えるとマクロ名を用いて定義しておくほうがよい。
まず、行列Aの成分を読み込む。行と列があるので、ダブルループを作る。行ごとに読み込むには、外側のループが行で内側のループが列となる。つまり
for (i=0;i < LOW; i++)
{
for (j=0;j < COLUMN;j++)
{
printf(“A[%d,%d]=”,i+1,j+1);
scanf(“%f”,&a[i][j]);
}
}
と書くと1行目に対して、1列目から4列目までを読み込むことになる。
準備はこのくらいにして、プログラムを書いてみよう。
解答
#include <stdio.h> #define LOW 3 #define COLUMN 4 void main(void) { float a[LOW][COLUMN]; float b[LOW][COLUMN]; float c[LOW][COLUMN]; int i,j;
for (i = 0;i < LOW; i++) { for (j = 0;j < COLUMN;j++) { printf("A[%d][%d]=",i+1,j+1); scanf("%f",&a[i][j]); } } for (i = 0;i < LOW; i++) { for (j = 0;j < COLUMN;j++) { printf("B[%d][%d]=",i+1,j+1); scanf("%f",&b[i][j]); } } for (i = 0;i < LOW; i++) { for (j = 0;j < COLUMN;j++) { c[i][j] = a[i][j] + b[i][j]; } } printf("\nC:\n"); for (i = 0;i < LOW; i++) { for (j = 0;j < COLUMN;j++) { printf("%10.5f",c[i][j]); } printf("\n"); } } |
例題6.3 行列の積
ついでにもう1つの例を示す。これは次の行列の積を求めるプログラムである。
考え方
行列の積はC=ABのとき、Cのij成分は
で求まることに注意する。これは和である。和の求め方は(s=s+f(j))であるので、
C[i][j] = 0;
C[i][j] = C[i][j] + a[i][k]*b[k][j];
でC[i][j]が求まる。では、プログラムを書いてみよう。
解答
#include <stdio.h> #define ROW_A 3 #define COLUMN_A 4 #define ROW_B 4 #define COLUMN_B 5 void main(void) { float a[ROW_A][COLUMN_A]; float b[ROW_B][COLUMN_B]; float c[ROW_A][COLUMN_B]; int i,j,k;
for (i = 0;i < ROW_A; i++) { for (j = 0;j < COLUMN_A;j++) { printf("A[%d][%d]=",i+1,j+1); scanf("%f",&a[i][j]); } } for (i = 0;i < ROW_B; i++) { for (j = 0;j < COLUMN_B;j++) { printf("B[%d][%d]=",i+1,j+1); scanf("%f",&b[i][j]); } } for (i = 0;i < ROW_A; i++) { for (j = 0;j < COLUMN_B;j++) { c[i][j] = 0; for (k=0;k < ROW_B;k++) { c[i][j] = c[i][j] + a[i][k] * b[k][j]; } } } printf("\nC:\n"); for (i = 0;i < ROW_A; i++) { for (j = 0;j < COLUMN_B;j++) { printf("%10.5f",c[i][j]); } printf("\n"); } } |
Cでは、ポインタは最も重要な概念の1つである。しかし、同時に最も難解な部分でもある。
まずは、Cではどのように実行ファイルが作られるのかを知る必要がある。エディタで書いたプログラム(ソースプログラム(source program))をコンピュータの理解できる機械語に翻訳する(コンパイル(compile))。このときできたプログラムをオブジェクト(object)という。このオブジェクトモジュールを連結させることをリンク(link)といい、連結させるプログラムをリンカ(linker)という。こうして実行可能形式ファイル(ロードモジュール(load module))ができる。ロードモジュールはファイルとしてハードディスクなどに格納されているにで主記憶装置(メモリ)に展開しなければならない。この動作をロード(load)といい、OSによって行われる。ロードされたロードモジュールは、その先頭から実行を開始される。一般に、メモリは連続して並んだバイトの列で、ロードモジュールはメモリ上に連続的にロードされる。このバイトの列には、それぞれアドレス(address)、もしくは番地と呼ばれる連続した番号がふってある。CPUは、このアドレスを目印にしてプログラムの実行を制御したり、データの読み込みや書き出しを行う。多くの場合、ロードモジュールは変数や定数などのデータを格納しているデータ部とプログラムのアルゴリズムを記述したコード部から構成されている。さて、今まで型宣言とか定義という言葉を使ってきたが、定義された変数はメモリ上のアドレスに割り当てられている。
ポインタとは、データオブジェクトの配置されているメモリ上のアドレスを保持する変数であると定義できる。データオブジェクトとは変数であると思ってかまわない。そして、変数とはある値を入れておく箱のようなものである。メモリ上のアドレスとは、この箱がどこにあるかを示す情報で、このアドレスを格納する箱がポインタ(pointer)である。つまり、ポインタもアドレスという数値を入れる箱なので変数である。変数といっても、今まで学習してきた変数とは異なる点がある。ポインタ変数はそれが指し示す対象となる変数が存在しなければ、何の意味もない。
また、ポインタも変数なのでアドレスを持っている。
変数定義
int x;
としたとき、変数xが配置されるメモリはコンパイラが勝手に決めてしまう。このアドレスを知るには、アドレス演算子である&(ampersand)を用いて
&x
とすることで、求めることができる。
例題7.1 変数xの配置アドレスを表示するプログラムを作成しよう。
解答
#include <stdio.h> void main(void) { int x; print(“\n Address = %p\n”,&x); } |
キャッチ printf関数の書式指定%pは、ANSI規格で採択されたポインタ表示書式指定であるが、古いコンパイラではサポートされていない。 |
ポインタ変数の定義
&xで得られたアドレスを保持できるポインタ変数pを定義すると
int *p;
となる。変数pの前についているアスタリスク(*)は、ポインタという派生語を作る記号です。これは配列を作るときに出てきた角括弧[ ]と同じようにすべての方に適用できる。では、変数xのアドレスとx変数xの値をポインタ変数を使用して表示してみよう。
例題7.2 変数xの配置アドレスをポインタを用いて表示するプログラムを作成しよう。
解答
#include <stdio.h> void main(void) { int x; int *p; x = 10; p = &x; printf(“\n Address = %p\n”,p); printf(“\n value = %d\n”,*p); } |
実行結果 Address = 0012FF7C
value = 10
これは、CPUがインテル系なのでセグメントアドレスが0012、オフセットアドレスがFF7C。
キャッチ 最後の行を見ると、ポインタ変数pにアスタリスク(*)がついている、このことにより、ポインタ変数pが指し示す内容(この場合、変数x)を間接的に参照することができる。この「*」を間接演算子とよび、定義や宣言に現れるときは、ポインタとしての型を作ることを示し、式の中でポインタ変数の前に現れるときは間接参照を示している。 |
Cではポインタと配列の間に強い関係がある。配列の添え字を使って実行できるような操作は、ポインタでできるし、ポインタを使うほうが一般に高速である。しかし、初心者にとっては分かりにくいといわれている。どうだろうか?
次の宣言
int a[10];
は、大きさが10の配列aを定義するものである。これはa[0],a[1],…a[9]という名の10個連続したオブジェクトからなるブロックと考えることができる。
a[i]と書くと、先頭からi番目の位置の配列要素を参照する。
paが整数へのポインタだとすると、
int *pa;
と宣言され、代入
pa = &a[0];
によってaの0番目の要素を指すようにpaがセットされる。つまりpaはa[0]のアドレスを保持する。これで代入文
x = *pa;
によりa[0]の内容がxにコピーされることになる。
*(pa + 1)はどんなことを意味するのか
paはa[0]を指し示しているので、*(pa+1)はa[1]の内容を参照する。また、pa+1はa[1]のアドレスである。
pa = &a[0]と書くと、paとaは同じ値をもつ。配列の名前はその先頭の要素の位置と同義であるから、pa = &a[0]という代入文は次のようにも書ける。
pa = a;
キャッチ scanf関数で配列を読み込むときに、よくある間違いはscanf(“%s”,&str)と書いてしまうことである。正解はscanf(“%s”,&str[0])かscanf(“%s”,str)である。 |
Cでは関数に対して、引数を値で渡すから、呼ばれた関数のほうで、呼び出した関数内の変数を直接変更することはできない。例えば、ある分類用ルーチンで、swapと呼ばれる関数で2つの要素の入れ替えを行うとする。次のように書いたのでは十分でないというより間違いである。何がいけないのか考えてみる。
#include <stdio.h>
void swap(int,int); /* プロトタイプ宣言*/
void main(void)
{
int a,b;
printf("整数を2つ入力したください\n");
scanf("%d %d",&a ,&b);
if(a < b){
swap(a,b); /* swap関数を呼び出す*/
}
printf("%d %d",a,b);
}
void swap(int x, int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
このプログラムを解析してみる。swap(a,b)は値による呼び出しであるので、swap関数が呼ばれて、変数a,bがx,yに渡される。swap関数でaとbの値の入れ替えが行われるが、呼び出したswap(a,b)のa,bには何の影響も及ぼさない。
正解は次のように書く。呼び出し側のプログラムで変更すべき値のポインタを渡すようにする必要がある。
swap(&a,&b);
演算子&は変数のアドレスを与えるから、&aはaへのポインタである。ポインタ引数を使えば、関数の中でそれを呼んだ関数の中のオブジェクトをアクセスし変更することが可能になる。次に示すように、swap自身内では引数はポインタとして宣言され、実際の被演算数はそのポインタを通して間接的にアクセスされる。
#include <stdio.h> void swap(int *,int *); void main(void) { int a,b; printf("整数を2つ入力したください\n"); scanf("%d %d",&a ,&b); if(a < b){ swap(&a,&b); } printf("%d %d",a,b); } void swap(int *px, int *py) { int temp; temp = *px; *px = *py; *py = temp; } |
練習問題7.1 選択法ソート
<配列を使った問題例>
「プログラムの説明」
1. このプログラムは、入力された5つの数値を昇順に並べ替える。
2. 並べ替えは、内部の配列dで行われる。
3. 並べ替えの順序は、dの先頭から順に最小のものから設定される。
4. 最小の値を設定する方法は、先頭の値とそれ以降の値を比べていき、先頭より小さな値を見つけたら先頭の値とその値を取り替えることである。
5. 値を取り替えるには、関数swapを用いる。関数swapの引数は取り替える配列の2つのアドレスである。
6. 先頭の最小値が設定される。次は、2番目を先頭として、それ以降の最小値を先頭に設定する。
7. 出力時の番号は、3桁の整数で表す。
次の実行例をみて、プログラムの□を埋めて完成せよ。
<実行例>
入力 出力
1バン = 30 1番 = 70 35%
2バン = 10 2番 = 50 25%
3バン = 40 3番 = 40 20%
4バン = 70 4番 = 30 15%
5バン = 50 5番 = 10 5%
計 200 100% |
<プログラム>
#include <stdio.h>
#define N_MAX 5
int d[N_MAX];
void main(void)
{
int i,j,sum;
for(i=0,sum=0;i < N_MAX;i++)
{
printf(“ ア “, i+1);
scanf(“%d”, イ);
sum+=d[i];
}
for(i=0;i < N_MAX-1;i++)
{
for(j=i+1;j < N_MAX;j++)
{
if(ウ)
swap(&d[i],&d[j]);
}
}
printf(“ソート後\n”);
for(i=0;i < N_MAX;i++)
{
printf(“エ番 = %d %2ld%\n”
,オ,カ,キ);
}
printf(“_________________________\n”);
printf(“計%3d 100%\n”,sum);
}
void swap(int *a,int *b) /* *aは&d[i]で得られたアドレスを保持するポインタ*/
{
int w;
w = *a;
*a = *b;
*b = w;
}
&d[i]はd[i]のアドレスを表し、int *aでポインタの定義をし、a = & d[i]でポインタaへ変数d[i]のアドレスを代入する。(ポインタは変数のアドレスを格納する変数)
解答 アには%3dバン= イには&d[i] ウにはd[i] < d[j] エには%3d オにはi+1 カにはd[i] キには(((long)1000*(long)d[i] + 5)/(long)sum)/10が入る。
これまで学んできたキーボードからのデータの入力や処理結果の表示に使ったscanfやprintfの命令は、実は関数といわれるものである。あるまとまった処理をするためにいくつかの命令を組み合わせた小さなプログラムを関数という。C言語には標準関数という便利な多くの関数が用意されている。また、ユーザが処理したい目的に合わせて関数を作ることもできる。この関数をユーザ関数という。
これまでにもすでにswap関数など作成し使ってきたが、ここでは、体系的に関数について紹介し、自分でいろいろな関数を作成する方法を学び、これらの関数を組み合わせて大きなプログラムを組む方法を学ぶ。
例題8.1
学校名、所在地を表示するプログラムを作成しよう。ただし、学校名、所在地を表示するそれぞれの関数を作成して使用する。
考え方
学校名を表示する関数をschool、所在地を表示する関数をaddressとする。これらをmain関数から呼び出して使用するプログラムを書けばよい。
#include <stdio.h> void school(void); /* プロトタイプ宣言 */ void address(void); void main(void) { school(); address(); } /* 関数schoolの定義*/ void school(void) { printf("広島工業大学\n"); } /* 関数addressの定義*/ void address(void) { printf("広島市佐伯区三宅2-1-1\n"); } |
解説 学校名を表示する関数schoolは、schoolが呼ばれたら学校名を表示すればよい。同様にaddressも住所を表示すればよい。これらの関数を呼び出すにはmain関数でschool( )、address( )とすればよい。
このプログラムを実行すると、main関数から呼び出された関数schollに制御が移り、学校名を表示する。次に制御はmain関数に移り、今度は関数addressを呼び出して住所を表示する。
関数school(void)のように( )の中にvoidと書いた関数を引数のない関数という。
上のプログラムは関数を使わず、printfだけでも同じ結果を得ることができる。では、なぜ関数を使ったのかを考えてみよう。例えば、学校名、所在地以外に自分の名前や学生番号などを表示したいとしよう。もしプログラムが関数を使わずprintfで書かれていたとしよう。すると、自分の名前や学生番号を表示するためには、main関数の中身を修正する必要があり、エラーが発生したときにmain関数の中をすべてチェックしなければならなくなる。しかし、上のプログラムのように関数を用いて書いておけば、少なくとも学校名と所在地を表す関数にはエラーがないことは分かっているので、エラーが発生した場合は追加した自分の名前と学生番号を表示する関数だけチェックすればエラー修正ができる。さらに、プログラムを読んだとき、main関数だけで何をしているのか分かり、読みやすいプログラムを作成することができる。
引数のある関数(値呼び出し法)
例題8.2 データを読み込んで和と差を求める
main関数から実数データa,bを関数sumと関数diffに渡して、その和と差を計算して表示するプログラムを作成しよう。
考え方
1.2つの値x,yの和を計算する関数をsum、差を計算する関数をdiffとすると、関数sumとdiffは次のようになる。
float sum(float x, float y) { float s; s = x+y; return (s); } |
float diff(float x, float y) { float d; d = x-y; return (d); } |
2.main関数から読み込んだa,bの値を関数sumに渡す。同様に関数diffに渡す。
3.計算結果はreturn文を使ってmain関数に戻す。この戻される値を戻り値という。この戻り値は実数なので、関数の前にfloatがついている。ではプログラムを書いてみよう。
解答
#include <stdio.h> float sum(float, float); /* プロトタイプ宣言 */ float diff(float, float); void main(void) { float a,b; printf("2つの数字を入力してください\n"); scanf("%f%f",&a,&b); printf("入力した2つの数%fと%fの和は%fです\n",a,b,sum(a,b)); printf("入力した2つの数%fと%fの差は%fです\n",a,b,diff(a,b)); } /* 関数sumの定義*/ float sum(float x,float y) { float s; s = x+y; return (s); } /* 関数diffの定義*/ float diff(float x,float y) { float d; d = x-y; return (d); } |
関数sum(a,b)を呼び出すときに使用する( )の中のa,bを実引数という。また、呼び出される関数sum(float x,float y)の中のx,yを仮引数という。仮引数x,yには実引数の値a,bが渡されて計算に使用され、その結果をreturn文でmain関数に戻す。このとき、実引数と仮引数の型は同じでなければならない。このように実引数の値を仮引数に渡す方法を値呼び出し法(call by value)という。この方法では仮引数の値を実引数に渡すことはできないし、戻せる値は1つである。では、仮引数を実引数に渡すにはどうすればよいのだろうか。その答えは、参照による呼び出し(アドレス渡し)を行うことである。
例題8.3 データを配列に読み込んで平均を求める
配列a[ ]にn個のデータを読み込み、データの平均を求めて表示するプログラムを作成しよう。ただし、データの平均は関数ave_dataで求めるものとする。また、データの個数nも入力するものとする。
考え方
main関数で配列aにデータを入力し、そのデータを平均値を求める関数ave_dataに渡す。配列とポインタとは兄弟のようなものであることを7章で学んだ。つまり、配列aを関数ave_dataに渡すには配列aのアドレスを渡してやればよい。配列aのアドレスは配列名aまたは先頭アドレス&a[0]である。つまり、main関数の中で
ave = ave_data(a,n);
と書き、関数ave_dataを
float ave_data(float x[ ],int n)
{
float s=0,
int i;
for (i = 0;i < n;i++)
{
s = s + x[i];
}
return (s/n);
}
とすれば、main関数の中でave_dataが呼ばれたとき、ave_dataは配列aのアドレスと読み込んだデータの数nを仮引数x[ ]とnに渡す。その後、関数ave_dataは平均を計算し、その値をmain関数に戻す。では、プログラムを書いてみよう。
解答
#include <stdio.h> float ave_data(float * ,int ); #define MAX 10 void main(void) { int i,n; float ave,a[MAX]; printf("データの個数を入力してください\n"); scanf("%d",&n); for(i=0;i<n;i++) { printf("a[%d] =",i); scanf("%f",&a[i]); } ave = ave_data(a,n); printf("%f\n",ave); } float ave_data(float x[],int n) { float s=0; int i; for (i=0;i<n;i++) { s = s+x[i]; } return s/n; } |
引数のある関数(参照による呼び出し)
例題8.4 2つの数a,b を読み込み、main関数からa,bをユーザ関数bin_opに渡して、和、差を計算し、その結果をmainに戻して表示するプログラムを作成しよう。
考え方
例題8.2との違いは1つの関数で和と差を計算させその結果をmainに戻すことである。つまり、ユーザ関数bin_opは和、差を計算し、その結果をmainに戻さなければならない。値呼び出し法では、return文を使って1つの値しか戻すことができない。そこで、main関数の実引数は、変数のアドレスとし、ユーザ関数bin_opの仮引数は渡されたアドレスを格納できるように、ポインタを対応させる。もう少し詳しく説明しよう。
Main関数内で
bin_op(a,b,&sum,&diff);
printf(“%f%f”,sum,diff);
と書き、ユーザ関数を
void bin_op(float x, float y, float *add, float *subtract)
{
*add = x + y;
*subtract = x – y;
}
と書くと、実引数a,b,&sum,&diffが仮引数x,y,*add,*subtractに渡される。その後、x+yの結果がポインタ変数*addの中に格納されているアドレスを番地とする変数sumに代入される。よって、printfでsumの値が表示されることになる。関数bin_opは値を返さないのでvoid bin_op(float x, float y, float *add, float *subtract)となる。では、プログラムを書いてみよう。
解答
#include <stdio.h> void bin_op(float, float, float *, float *); /* プロトタイプ宣言 */ void main(void) { float a,b,sum,diff; printf("2つの数字を入力してください\n"); scanf("%f%f",&a,&b); bin_op(a,b,&sum,&diff); printf("入力した2つの数%fと%fの和は%fです\n",a,b,sum); printf("入力した2つの数%fと%fの差は%fです\n",a,b,diff);
} /* 関数bin_opの定義*/ void bin_op(float x,float y, float *add, float *subtract) { *add = x + y ; *subtract = x - y; } |
C言語は、システム言語として生まれたので、文字列を操作する標準関数が用意されている。
下に挙げる関数を用いるには、ヘッダファイルstring.hをインクルードする必要がある。
関数名 |
機能 |
strlen (string lengthの略) |
文字列の文字の個数を求める |
strcmp (string comparisonの略) |
ある文字列と他の文字列を比較する |
strcpy (string copyの略) |
ある文字列を他の文字列に複写する |
strcat (string concatinationの略) |
ある文字列に他の文字列を連結する |
“hello world”は文字列よばれ各要素が1文字1バイトである配列として保存される。このとき文字列の内部表現では最後にナル文字(null character)’\0’が置かれるので、必要な物理的記憶容量は、実際に書かれた文字の個数よりも1つ多くなる。関数strlen(s)は’\0’を除いた文字列引数sの長さを求めるものである。
/* strlen関数: sの長さを返す*/ int strlen(char s[ ]) { int i;
i = 0; wile (s[i] != 0) ++I; return i; } |
例題8.5 strlen関数を用いて文字数を求める
“hello world”と”ハロー ワールド”の文字数を求めるプログラムを作成しよう。
考え方
文字列str1[ ]に”hello world”を代入し、文字列str2[ ]に"ハロー ワールド"を代入する。次に、関数strlenに引数として配列名str1とstr2を渡し、計算結果を表示すればよい。では、プログラムを書いてみる。
解答
/*文字列の長さの計算*/ #include <stdio.h> #include <string.h> void main(void) { int n1,n2; const char str1[ ] = "hello world"; const char str2[ ] = "ハロー ワールド"; n1 = strlen(str1); n2 = strlen(str2); printf("文字列1の文字数=%d\n",n1); printf("文字列2の文字数=%d\n",n2); } |
自分自身の表現
自分自身を表現するには、名前、性別、生年月日、住所、電話番号、などたくさんの情報が必要となる。これらの情報を基に名簿を作ることを考えてみよう。まず、これらの情報をしまう箱の型について考える。名前は文字配列で32文字、住所も文字配列で80文字、電話番号も文字配列で20文字、生年月日はlong int型でよいだろう。性別は男か女のどちらかなので列挙型とする。
char name[32];
long int birth;
char address[80];
char tel[20];
enum {Female,Male} sex;
となる。
この場合、nameやaddressがまとまって配置されているので、これらの事柄は関連している1つの事柄を表していると推測できる。そこで、このような互いに関連しているデータを1つの型として宣言するのに用いるのが構造体(structure)である。
例題9.1 構造体宣言を用いて、上の事柄を宣言してみよう。
解答
struct person { char name[32]; long int birth; char address[80]; char tel[20]; enum {Female,Male} sex; } ; |
ここで、structは構造体宣言を示すキーワードで、次のpersonはこの構造体の名前で、構造タグと呼ばれている。{と}に囲まれた中は、構造体のメンバと呼ばれ、メンバどうしの名前が重複しなければ、どのような名前でも使える。
次に、
struct person self_data;
とすると、変数self_dataはperson型と定義される。また、列挙型と同じように
struct person { char name[32]; long int birth; char address[80]; char tel[20]; enum {Female,Male} sex; } self_data; |
とすると、宣言と変数self_dataの定義を同時に行うことができる。
構造体メンバの参照は
構造体変数名.メンバ名
で行う。例えば、構造体変数名self_dataのメンバbirthの参照は
self_data.birth
と書く。
例題9.2 19530315をキーボードから入力し、self_data.birthに代入する。
解答
struct person{
lon int birth;
}self_data;
scanf(“%ld”,&self_data.birth);
printf(“%ld”,self_data.birth);;
とすればよい。
例題9.3 名前、誕生日、住所、電話番号、性別を含んだ個人情報をキーボードから入力し、表示するプログラムを作成しよう。
解答 列挙型enum {Female,Male}sex;にscanfで読み込んだ値を代入するには、0または1をキーボードから打つ。
出力するには
printf(“%c”,(self_data.sex = = 0)? ‘F’:’M’;)を用いる。これは3項演算子と呼ばれるもので、次のように用いる。
式1?式2:式3
の形式をとり、式1が真ならば、式2の値が式の値となり、偽ならば式3が式の値となる。
#include <stdio.h> #include <string.h> void main(void) { int choice=0; struct person { char name[32]; long int birth; char address[80]; char tel[20]; enum {Female,Male}sex; } self_data; do { choice=1; printf("名前> 誕生日> 住所> 電話番号> 性別> \n"); scanf("%s %ld %s %s %d",&self_data.name, &self_data.birth, &self_data.address,&self_data.tel, &self_data.sex); printf("\n NAME BIRTHDAY ADDRESS TEL SEX\n"); printf("%-10.10s %11.8ld %-30.30s %-10.10s %3c",self_data.name, self_data.birth, self_data.address, self_data.tel, (self_data.sex==0)? 'F':'M'); printf("\n次のデータを入れますか?yesなら0以外"); scanf("%d",&choice); } while (choice != 0); } |
struct date birthdate;
を上の構造体の中に入れて新たな構造体を作ることができる。
struct person {
char name[32];
struct date birthdate;
char address[80];
char tel[20];
enum {Female,Male} sex;
}self_data ;
このとき、ネストになっているdate構造体のメンバbirthdateの参照は次のようにして行うことができる。
self_data.date.month
一般的に、
構造体変数名.構造体変数メンバ.構造体変数メンバ….メンバ名
でメンバの参照ができる。
共用体(union)
共用体は、構文や参照方法は構造体と同じであるが、構造体と違って各メンバが同じメモリを使用する。つまり、1つの箱を複数の用途に共有出来るようになっている。先ほどのperson型の構造体を共用体で定義してみよう。
union person{
char name[32];
struct date birthdate;
char address[80];
char tel[20];
enum {Female,Male} sex;
}self_data ;
最初のunionは、共用体を示すキーワードで、次のpersonはこの共用体の名前で、共用体タグと呼ばれている。{と}に囲まれた中は、共用体のメンバと呼ばれ、メンバどうしの名前が重複しなければ、どのような名前でも使える。
struct tag{
int x;
double y;
} test;
この構造体へのポインタは次のように定義する。
struct tag *p;
p = &test;
ここで、構造体メンバxのアドレスはどのように求めるのだろうか。
&test.xと書けば、xのアドレスが求まる。
また、構造体へのポインタpを利用して、構造体の各メンバを間接参照することも出来る。
間接参照「*」を思い出すと、p = &testより*pは構造体変数testを指す。よって各メンバは(*p).x,(*p).yで参照できる。ここで、括弧は必要である。なぜなら、*と.の優先順位は.の方が上である。
しかし、このように書くよりもCでは構造体をポインタで間接参照できるように「→」という構造体演算子(structure operator)が用意されている。この演算子を用いると、(*p).xはp→xと記述でき、視覚的にも分かりやすい。ただし、→はマイナス記号と不等号「>」を用いて表す。
例題9.4 構造体のメンバをポインタを用いて参照する。
#include <stdio.h>
void main(void)
{
struct tag{
int x;
double y;
} test;
struct tag *p;
p = &test;
p ->x = 30;
p ->y = 15.3;
printf("x = %d y = %lf\n",p -> x, p->y);
}
日付や時間を知るには<time.h>で宣言されているtime関数とlocaltime関数を用いる。
time関数は、現在のカレンダ時間を返す関数で型time_tはlong型を<time.h>でtypedef宣言している。
localtime関数は構造体struct tmのポインタ型を返す関数であり、構造体変数tmのメンバは次のようになっている。
int tm_sec; /*second after the minute – [0,59] */ int tm_min; /*minute after the hour – [0,59] */ int tm_hour; /*hours after the midnight – [0,23] */ int tm_mday; /*day of the month – [1,31] */ int tm_mon; /*months since January – [0,11] */ int tm_year; /*years since 1900 */ int tm_wday; /*days since Sunday – [0,6] */ int tm_yday; /*days since January 1 – [0,365] */ int tm_isdst; /*daylight savings time flag */ |
例題9.5 プログラムを実行した時間が何時かを返すプログラムを作成しよう。
解答 日付や時間を知るには、まずtime関数でカレンダ時間を求め、この時間をlocaltime関数で読み込み構造体変数tpに代入する。この後、時間が知りたければtp->tm_hourを表示すればよい。
#include <stdio.h> #include <time.h> void main(void) { struct tm *tp; time_t t; t = time(&t); // カレンダ時間をtに代入 tp = localtime(&t); //カレンダ時間をローカル時間に変更 printf("現在の時刻は%dです",tp->tm_hour); // tp->tm_hourで時間を構造体のメンバを参照 } |
例題9.6 何月何日何時何分何秒を表示するプログラムを作成しよう。
解答
#include <stdio.h> #include <time.h> void main(void) { struct tm *tp; time_t t; t = time(&t); tp = localtime(&t); printf("%d月%d日%d時%d分%d秒\n",tp->tm_mon,tp->tm_mday,tp->tm_hour,tp->tm_min,tp->tm_sec); } |
例題9.7 プログラムを実行した時間が午前か午後かを判断して、午前なら午前です、午後なら午後ですと返すプログラムを作成しよう。
考え方 まず、午前か午後かを判断する関数を作る。この関数をapre_midiという名前にする。
apre_midiは次のようになる。
int apre_mid(void) { struct tm *tp; time_t t; if( (t = time(&t)) == -1) { perror(“time function error \n”); exit(1); } tp = localtime(&t); return(tp -> tm_hour < 12); } |
解答
#include <stdio.h> #include <time.h> #include <stdlib.h> int apre_mid(void); void main(void) { if(apre_mid()) { printf("午前です\n"); } else { printf("午後です\n"); } } int apre_mid(void) { struct tm *tp; time_t t; if( (t = time(&t)) == -1) { perror("time function error \n"); exit(1); } tp = localtime(&t); return(tp -> tm_hour < 12); } |
これまでデータの入力はキーボードから行ってきた。しかし、入力データが大量になると、その作業は大変なものとなる。そこで、Cではあらかじめファイルに入っているデータの入出力ができるように、fopen,fscanf,fprintf,fcloseなどの関数が用意されている。
関数名 |
機能 |
一般形 |
fopen |
x.c やy.datなどの外部名を受け取り、ファイルを読み書きするのに必要なポインタを返す。 |
FILE *fp; fp = fopen(name,mode); |
fscanf |
fopenで開いたファイルのデータを1文字列ずつ読み込む |
int s; fscanf(fp,”%d”,&s); |
fprintf |
fopenで開いたファイルのデータへ書き込み |
FILE *fp; int s; fprintf(fp, “%d”,s); |
fclose |
fopenで開いたファイルを閉じる。 |
FILE *fp; fclose(fp); |
feof |
データが空かどうかの判定を行う。空ならば真を返す。 |
FILE *fp; feof(fp); |
fgets |
1行取り込む fgets(fp,80,s)で配列sに1行取り込まれる。 fgetsを1回実行すると、fpidが次の行の先頭に移る。
|
char s[80]; FILE *fp; fgets(fp,80,s) |
fgetc |
1文字取り込む |
FILE *fp; fgtec(fp); |
fseek |
ファイル位置の移動 offsetには先頭からの文字数が入る。 originには0,1,2が入る。0で始め、1で現在地、2で最後 |
FILE *fp; fseek(fp,offset,origin); |
fflush |
ファイルを閉じずに書き込み |
Fflush(fp); |
ftell |
ファイルポジションインディケータ(fpid)の位置を調べる fgetsを1回実行すると、2行目の先頭にfpidが移動。 |
|
rewind |
ファイルの先頭に移る。 |
|
fopen(name,mode)のnameはファイル名を表し、modeにはr,w,a,r+,w+,a+があり、rはread(読み込み)、wはwrite(書き込み)、aはappend(ファイルの終わりに追加書き込み)を表す。また、r+はファイルの読み書き、w+は今までのファイルを破棄して読み書き、a+はファイルの追加・変更を表す。
fopen,fscanf,fprintf,fcloseの使い方
これらの関数を使ってdata.datという名前のファイルの操作をしてみよう。
例題10.1 入力ファイルからデータを読み込む
次のようなデータを持ったファイルdata.datがC:\tempにあるとき、data.datからデータを読み込み、平均と和を求めるプログラムを作成しよう。
データ
150
164
153
174
189
185
168
156
152
174
173
169
考え方
fopenを使ってdata.datファイルを開き、feofでデータが空かチェックし、fscanfを用いて1つずつデータを最後まで読み込む。data.datはC:\tempにあるのでダブルクォートの中では、”C:\\temp\\data.dat”と書く必要がある。いったんファイルを開いてしまえば、fscanfはscanfと同じ使い方になる。平均と合計を計算したら、最後にfcloseでファイルを閉じる。
解答
#include <stdio.h> #include <stdlib.h> void main(void) { int value=1; int sum=0; int count=0; FILE *fpin; if( (fpin=fopen("C:\\temp\\data.dat","r")) == NULL) { printf("ファイルが見つかりません。---data.dat\n"); exit(1); } while( !feof(fpin) ) // 最後まで { fscanf(fpin,"%d",&value); sum = sum + value; count++; } if (count > 0) { printf("合計=%d 平均値=%f",sum,(double)sum/count); } fclose(fpin); } |
読み込みのときは、読み込みであることが後からすぐに分かるように、fpinとしておくと便利である。
例題10.2 ファイル名を指定した入力
プログラムを工夫することによって、データファイルの名前をキーボードから直接入力するプログラムを作成せよ。
解答 fopenで指定したファイル名の要素は、文字配列でも指定できる。例えば、
char FileName[16]:
printf(“ファイル名を入力してください”);
scanf(“%s”,FileName);
とすればよい。ではプログラムを書いてみよう。
#include <stdio.h> #include <stdlib.h> void main(void) { int value=1; int sum=0; int count=0; char FileName[16]; FILE *fpin; printf("ファイル名を入力してください"); scanf("%s",FileName); if( (fpin=fopen("data.dat","r")) == NULL) { printf("ファイルが見つかりません。---data.dat\n"); exit(1); } while( !feof(fpin) ) { fscanf(fpin,"%d",&value); sum = sum + value; count++; } if (count > 0) { printf("合計=%d 平均値=%f",sum,(double)sum/count); } fclose(fpin); } |
ここでは、1つのデータファイルdata.datからデータを読み込むことを行ったが、複数個のデータファイルを読み込むには
FILE *fpin1, *fpin2, *fpin3;
と宣言し、それぞれのファイルをfopenで開けばよい。
ファイルへデータを出力するには、
1.まずout.datという名前のファイルを作成する。書きこみのときはfpoutとしておく。
FILE *fpout;
fpout = fopen(“out.dat”,”w”);
このとき、fopenの第2引数modeは書き込み可能とするため”w”になる。ただし、”w”を指定してfopenを行った場合、out.datが存在するとout.datのデータ内容は失われるので注意すること。つまり、”w”は上書きを意味する。
2.fprintf関数を用いてファイルにデータを出力する。
fprintf(fpout,”%d\n”,value);
3.最後にファイルを閉じる。
fclose(fpout);
例題10.3 次のデータをout.datというファイルを作成し、書き込むプログラムを作成しよう。
データ
150
164
153
174
189
185
168
156
152
174
173
169
考え方
1.まずout.datというファイルを作成。
2.キーボードからのデータをscanfで読み込み、fprintfでout.datに書き込む
解答
#include <stdio.h> #include <stdlib.h> void main(void) { int value=1; FILE *fpout; if( (fpout = fopen("out.dat","w")) == NULL) { fclose(fpout); printf("ファイルが作成できません。---out.dat\n"); exit(1); }
while( value != 0 ) { printf("データを入力してください。終了するには0を入力\n"); scanf("%d",&value); fprintf(fpout,"%d\n",value); } fclose(fpout); } |
ここでは、すでに作成してあるファイルに新しいデータを追加する方法を考える。fopenのmodeをaにすると追加書き込みができるようになる。このとき新しく追加したデータは元のファイルの下に追加されます。
例題10.4 例題10.3で作成したout.datに次のデータを追加してみよう。
データ
160
170
180
175
解答
#include <stdio.h> #include <stdlib.h> void main(void) { int value=1; FILE *fpout; if( (fpout = fopen("out.dat","a")) == NULL) { fclose(fpout); printf("ファイルが作成できません。---out.dat\n"); exit(1); }
while(value != 0) { printf("データを入力してください。終了するには0を入力\n"); scanf("%d",&value); fprintf(fpout,"%d\n",value); } fclose(fpout); } |
ファイルの文字変更(fseekの使いかた)
fseek(fp,offset,origin)でoriginに対してファイルのoffsetの位置に移動。originは0,1,2が入る、0は先頭、1は現在地、3はファイルの最後。offsetには0L,1L,2L,…が入る。0Lで1文字目、2Lで2文字目に移動する。
fseek(fp,0L,0)で開始行の1文字目に移動する。
次のような文字列があるデータファイルをtest.datと名前を付けて保存しておく。
D is defficalt.
1.文字DをCに変える。
#include <stdio.h> #include <stdlib.h> void main(void) { char c; char s[80]; FILE *fpout; if( (fpout = fopen("test.dat","r+")) == NULL) { fclose(fpout); printf("ファイルが作成できません。---test.dat\n"); exit(1); } fgets(s,80,fpout); fseek(fpout,0L,0); c = 'C'; fprintf(fpout,"%c",c); fclose(fpout); } |
で文字DはCに置き換えられる。
2.C is defficaltのeをiに変える。
#include <stdio.h> #include <stdlib.h> void main(void) { int i; int j; char c; char s[80]; FILE *fpout; if( (fpout = fopen("test.dat","r+")) == NULL) { fclose(fpout); printf("ファイルが作成できません。---test.dat\n"); exit(1); } fgets(s,80,fpout); for(i=0;i<80;i++) { if(s[i] == 'e') { j = (long)i; fseek(fpout,j,0); c = 'i'; fprintf(fpout,"%c",c); } } fclose(fpout); } |
3.C is difficaltのスペルミスを見つけて修正する。
fseekとfprintfはペアで用いる。つまり、fseekとfprintfの間には他の操作を行ってはいけない。他の操作を行うとfseekのfpが変わってしまう。
#include <stdio.h> #include <stdlib.h> void main(void) { int i; int j; char ch,ad; char s[80]; FILE *fpout; if( (fpout = fopen("test.dat","r+")) == NULL) { fclose(fpout); printf("ファイルが作成できません。---test.dat\n"); exit(1); } fgets(s,80,fpout); printf("%s",&s[0]); printf("修正したい文字と修正用の文字を入力\n"); scanf("%c %c",&ch,&ad); for(i=0;i<80;i++) { if(s[i] == ch) { j = (long)i; fseek(fpout,j,0); fprintf(fpout,"%c",ad); } } fclose(fpout); } |
ファイルの追加変更(文字列)
文字列を読み込むにはfgets(s,80,fpout);を用いる。
次の文字列を含んだデータファイルtest.datを用意する。このファイルをプロンプトから開き、文字列を表示し修正したい文字列を入力することにより、ファイルが書き直されるプログラムを作成しよう。
They is stupid.
文字列の長さはstrlen(s)で計る。
#include <stdio.h> #include <stdlib.h> #include <string.h> void main(void) { int i; int j; int len,len1,len2; char ch[5],ad[5]; char s[80]; FILE *fpout; if( (fpout = fopen("test.dat","r+")) == NULL) { fclose(fpout); printf("ファイルが作成できません。---test.dat\n"); exit(1); } fgets(s,80,fpout); printf("%s",&s[0]); len=strlen(s); printf("修正したい文字列と修正用の文字列を入力\n"); scanf("%s %s",ch,ad); len1=strlen(ch); len2=strlen(ad); for(i=0;i<80;i++) { if(s[i] == ch[0]) { j = (long)i; fseek(fpout,j,0); fgets(s,40,fpout); fseek(fpout,j+len2-len1,0); fprintf(fpout,"%s",s); fseek(fpout,j,0); fprintf(fpout,"%s",ad); break; } } fclose(fpout); } |
Cにはリダイレクションと呼ばれ、入出力の切り替えを行う機能がある。つまり、キーボードやディスプレイもファイルと考えると、ファイルから読み込むことも、ファイルへの出力もキーボードからの入力やディスプレイへの出力と同じと考えることができる。では、実際にリダイレクションを行ってみる。
1. 次のプログラムをread_data.cというファイル名でコンパイルしよう。
#include <stdio.h> void main(void) { char s[80];
while(gets(s) !=NULL)) { puts(s); } } |
2. 例題10.1で用いた入力ファイルdata.datをリダイレクションを用いて読み込んでみよう。
read_data < data.dat
とプロンプトで実行する。これは、data.datとread_data.exeは同じフォルダにあることを前提にしている。もしdata.datが別の場所にあるときは、パス(path)が必要になる。パスとは目的地までの道順のことである。
例えば、read.datがCドライブのフォルダMy Documents の下のフォルダCEXEにあり、read_dataがCドライブのフォルダDATにあるとする。この場合は次のように入力すればよい。
cd c:\mydocu~1\CEXE
read_data < ..\data.dat
これは相対パスと呼ばれるものである。この他絶対パスを用いると次のようになる。
read_data < C:\DAT\data.dat
3. 例題10.3で行ったファイルへの書き込みをリダイレクションを用いて行ってみよう。書き込みするファイル名はout.txtとする。
read_data > out.txt
この後、type out.txtとプロンプトで入力するとout.txtの内容が見れる。UNIXの場合はcat out.txtと入力。
4. 例題10.4で行ったファイルへの追加書き込みをリダイレクションを用いて行ってみよう。追加書き込みするファイル名はout.txtとする。
read_data >> out.txt
この後、type out.txtとプロンプトで入力するとout.txtの内容が見れる。UNIXの場合はcat out.txtと入力。
5. 読み込みと書き込みをリダイレクションで行ってみよう。
read_data < data.dat > data.out
この操作を行うと、まずdata.datの内容がよき込まれ、次にdata.outに書き込まれる。
練習問題10.1
次のプログラムの説明およびプログラムを読んで、設問に答えよ。
「プログラムの説明」
ソフトウェア開発のテスト工程におけるバグ収束状況を評価するための信頼度成長曲線を印刷するプログラムである。信頼度成長曲線は、テスト項目の消化状況とバグの発生状況との関係をグラフにしたものである。
(1) テスト担当者が日々記録しているテスト項目の消化項目数(その日に終了したテストの項目数)と摘出バグ数(終了した項目において摘出したバグ数)の一覧が、下のバグ記録表である。
バグ記録表
日付 |
担当 |
消化項目数 |
摘出バグ数 |
19960802 |
A |
2 |
40 |
19960803 |
B |
1 |
20 |
19960804 |
A |
3 |
20 |
19960805 |
B |
4 |
10 |
|
|
|
|
19960823 |
A |
10 |
5 |
19960823 |
C |
5 |
0 |
19960825 |
A |
8 |
1 |
19960827 |
C |
12 |
1 |
(2) バグ記録表は、下の図のレコード様式でbugrec.txtというファイルに格納されている。
8けた |
1けた |
1けた |
1けた |
2けた |
1けた |
2けた |
1けた |
日付 |
△ |
担当 |
△ |
消化項目数 |
△ |
摘出バグ数 |
\n |
△ :空白文字
1. 日付は、年月日を8桁の10進数字(YYYYMMDD)で表現しており、先頭の4桁が西暦年を、次の2桁が月を、最後の2桁が日を表している。
2. 担当は、テスト担当を1桁の英字にコード化したものである。
3. 消化項目数および摘出バグ数は、2桁の10進数字である。
4. 各項目は、1個の空白文字で区切られており、レコードの終端は改行文字”\n”である。
5. レコードはテスト担当者ごとに1日のテスト結果を記録したものであり、同一日付でテスト担当者が異なるレコードが2件以上存在することもある。
6. レコードは日付の昇順に整列されている。
7.
ファイルbugrec.txtに記録されているテスト作業日の数は60日以内である。
(3) このプログラムでは、ファイルbugrec.txtを入力し、1日あたりの消化項目数および摘出バぐ数を集計する。さらに、その日までの累積数を計算して、累積消化項目数と累積摘出バグ数の関係をもとめ、折れ線グラフ出力関数prtgraphを用いて信頼度成長曲線を印刷する。
(4) 関数prtgraphの仕様は、次のとおりである。
「機能」 第1引数で指定された個数の点(座標は第2引数の配列内に格納)を折れ線でつないで印刷する。
「書式」
typedef struct{int x;int y;} ZAHYO;
void prtgraph(int dcnt,ZAHYO +tbl, char *title);
「引数」 構造体ZAHYOのメンバx:印刷する点のX座標
構造体ZAHYOのメンバy:印刷する点のY座標
dcnt: 印刷する点の個数
tbl: 点の座標を格納した配列
title: グラフの表題
<プログラム>
#include <stdio.h>
#define STBMAX 61
typedef struct{int x;int y;} ZAHYO;
void prtgraph(int,ZAHYO,char*);
void main(void)
{
ZAHYO s_tbl[STBMAX];
FILE *fp;
long yymmdd, t_yymmdd;
int t_bug, t_chk,I;
int idx=0;
char tanto;
fp = fopen(“bugrec.txt”,”r”);
yymmdd=0;
s_tbl[0].x = s_tbl{0},y = 0;
while(fscanf(fp,”%81d %c %2d %2d\n”,
&t_yymmdd, &tanto, &t_chk, &t_bug) != EOF)
{
if (yymmdd != t_yymmdd)
{
idx++;
yymmdd = t_yymmdd;
s_tbl[idx].x = s_tbl[idx].y = 0;
}
s_