● 実習内容
【C言語】
1.
以下のex13-1.c
を入力してコンパイル及び実行をしてみよう.•
このプログラムは,main
関数の引数argc, argv
の利用法を示している.•
プログラム起動時に% ./a.out 1 2 3 4
Return
として,単にコマンド名だけではなく,「コマンドライン引数」をつけて起動した場合,
argv
にその「コマンドライン引数」が格納される.•
簡単に言うとargv
は「文字列の配列」であり, argv[0]
には「コマンド名そのもの」が格 納される. (上の例のように4つの引数があれば)argv[1]
からargv[4]
にそれぞれの 引数の文字列が格納される.• argc
は引数の個数をあらわす値である.• argv[1]
などは「文字列」であり,たとえそれが「数値をあらわす文字列」であっても,数値そのものを表すわけではない
.
そのため,
(上の例のように)引数として入力された「数 値をあらわす文字列」を「数値」としてint
型変数に代入するにはatoi
関数を用いて「文 字列からint
型の値への変換」を行う必要がある.•
【上級者向けの注意】より正確には,argv
はchar
型へのポインタへのポインタである.argv
が指し示す先は,プログラム起動時に静的に格納されたコマンドライン引数が格納さ れた領域へのポインタが並んだ静的メモリ領域である.
別の宣言方法としてchar *argv[]
という書き方もある. 特に注意すべきこととして,
argv
の場合にはプログラム起動時にO Sが静的領域に文字列を格納して,それを指し示すようにargv
がセットされるのであって,char **p
としても「文字列の配列」が定義できるわけではない.2.
以下のex13-2-1.c, ex13-2-2.c, ex13-2-3
を入力してコンパイルと実行をしてみよう.•
このプログラムのうちex13-2-1.c
とex13-2-2.c
は標準入力から文字列を受け取って, それを出力するプログラムである.• ex13-2-1.c
ではgetchar
関数を用いて,
標準入力から「1文字づつ」読み込みを行って いる.–
このプログラムを実行すると, キーボードからの入力待ちが発生する. そこで適当に キーボードから文字列を入力して,最後にEOF
(キーボード操作としては,Ctrl + D
) を入力すると,入力された文字列が標準出力(画面)に出力される.–
このプログラムでは, 入力された文字列の長さを判定していないため,「バッファオー バーフロー」が発生する可能性がある. つまり,s
は255
文字しか入力できないのに, それを超えたかどうかの判定を行っていない.• ex13-2-2.c
ではfgets
関数を用いて,
標準入力から「1行づつ」読み込みを行っている. –
このプログラムを実行すると, キーボードからの入力待ちが発生する. そこで適当に キーボードから文字列を入力して,Ctrl + D
を入力するごとに(または, 256文字ご とに)入力された文字列が標準出力に出力される.– fgets
はEOF
を検出するとNULL
を返す仕様となっているので,EOF
を入力するとプロ グラムは終了する.
• getchar
を使った場合とfgets
を使った場合の動作の違いを理解しよう. なお,% man getchar
Return
または% man -s 3c getchar
Return
でそれぞれのオンラインマニュアルを読むことができるので,オンラインマニュアルを読ん で,それぞれの関数の意味を調べてみよう.
• ex13-2-3.c
ではfgets
関数を用いて, ファイル名ex13-3.c
から「1行づつ」読み込み を行っている.–
特定のファイルからデータを読み込むためにはfopen
関数を使って「ファイル名」と「ファイルポインタ」(FILEで定義される変数)に結び付きを作らなければならない.
一旦ファイルを開いてしまうと
,
標準入力stdin
と全く同様に利用することができる. –
ファイルの読み込みが終わったらfclose
関数でファイルを閉じることが必要である.–
ファイルを開くためのfopen
関数の呼び出し方法には以下の利用法がある.∗ fopen(FILENAME, "r")
既存のファイルを読み出し専用として開く. ファイルが存 在しなければNULL
を返す.∗ fopen(FILENAME, "w")
ファイルを書き込み用として開く.
既存のファイルに対し ては, その長さを0
にしてから書き込みを行う. 書き込みができない場合にはNULL
を返す.∗ fopen(FILENAME, "a")
ファイルを追加書き込み用として開く. 既存のファイルに対 しては,末尾に書き込みを行うようにセットする. 書き込みができない場合にはNULL
を返す.
この他にもいくつかの方法があるが, その詳細については
fopen
関数のオンラインマ ニュアルを参照すること.3.
以下のex13-3-1.c, ex13-3-2.c
を入力してコンパイル及び実行をしてみよう.•
この2つのプログラムはfprintf
関数を用いて, 文字列を標準出力及び標準エラー出力(ex13-3-1.cの場合)や,特定のファイル(ex13-3-2.cの場合)に出力するプログラムで ある
.
•
通常の状態では「標準出力」と「標準エラー出力」はともに「画面」に結び付けられてい るが,
% ./a.out > /dev/null
Return
として「標準出力」だけを
/dev/null
に結び付けると「標準エラー出力」への出力だけが 画面に表示される. なお,/dev/null
とはUNIX
システムで定義された「ブラックホール」のようなものである.
多くのアプリケーションでは,このようにして「エラーメッセージ」は「標準エラー出力」
に出力していることが多い
.
•
特定のファイルにデータを出力する場合も,fopen
関数とfclose
関数を用いて,ファイル にファイルポインタを結び付ければ,「標準出力」への出力と全く同じ扱いが可能になる.4.
なお,
以上のファイル入出力はgetchar, fgets, fprintf
関数を用いている.
これらの関数で 扱うことが可能なファイルは「テキストファイル」に限られる. テキストファイル以外のファイ ルを扱う場合にはfread, fwrite
関数を用いる必要がある.【課題】 (当然ですが,C言語の標準関数は自由に使ってかまいません. でも,「標準関数と(ほぼ)同等 の関数を作りなさい」という課題でその標準関数を使うのはダメです
.
)なお,
以下のプログラムを作 成する段階での失敗で必要なファイルが誤って消去されても我々は責任を持たない.1.
【初級者向け】コマンドライン引数に与えられたファイル名を持つファイルからその内容を読み取り
,
標準出力に出力するプログラムを書きなさい.2.
【初級者〜中級者向け】標準入力から以下のような形式の1行のデータを読み取り,その中に含まれる整数値を 数値配列に格納するプログラムを書きなさい.
•
入力データは1行のみからなり,最大文字数は255文字とします.•
入力データは一つ以上の空白で区切られた整数値をあらわす文字列であり,
以下の形式を取 ります.<空白文字*><整数値をあらわす文字列><空白文字+><整数値をあらわす文字列>[<空白文
字+><整数値をあらわす文字列>]*<空白文字*>なお,各文字の意味は以下の通りです.
<空白文字> := <空白 (0x20)>|<タブ文字 (0x09)>|<改行文字 (0x0a)>
<符号文字> := +|-
<数字> := 0|1|2|3|4|5|6|7|8|9
<整数値をあらわす文字列> := <符号文字><数字+> | <数字+>
すなわち,「空白文字」は
0x20,
タブ文字0x09,
改行文字0x0a
のいずれかであり,「符号 文字」は+
または-
のいずれかであることをあらわします. <文字*>
は<文字>
の0個以 上の繰り返しをあらわし,<文字+>
は<文字>
の1個以上の繰り返しをあらわします. また,[....]*
は[
と]
の中の0回以上の繰り返しをあらわします.•
入力データに含まれる整数値をあらわす文字列のうち,先頭から最大20個のみを配列に格 納すればよいとします.3.
【初級者〜中級者向け】テキストファイルに対するコピーコマンドとして扱えるプログラムを書きなさい
.
すな わち, コマンドライン引数に2つのファイル名を与え,第一引数のファイルの内容を第 二引数のファイルにコピープログラムを書きなさい. なお,第二引数に与えたファイル がすでに存在する場合には,
その事実を警告して,
キーボードからY
またはy
と入力さ れたときにのみコピーを実行するようにしなさい.4.
【中級者向け】上の「テキストファイルに対するコピーコマンド」を拡張して,「オプション
-f
が与 えられたときには,第二引数に与えたファイルがすでに存在する場合にも警告なしでコ ピーするようにしなさい.
5.
【中級者〜上級者向け】上の「テキストファイルに対するコピーコマンド」を拡張して,一般のファイルに対す るコピーコマンドを作成しなさい
.
この問題は
fread, fwrite
関数を利用する必要があります.6.
【初級者向け】標準入力から入力されたファイルに含まれる「アルファベット」の文字数の頻度を調べ るプログラムを書きなさい. 大文字または小文字の
a
からz
までの文字数を調べるこ とを意味します.7.
【上級者向け】コマンドライン引数の第二引数として与えられたテキストファイルの中から,コマンド ライン引数の第一引数として与えられた文字列を含む行のみを表示するプログラムを書 きなさい.
このコマンドは
UNIX
ではgrep
として知られています.8.
【上級者向け】標準入力から入力されたファイルの文章(英語)の中から,冠詞と前置詞を除く単語の 先頭文字をすべて大文字に変換するプログラムを書きなさい
.
なお,
冠詞と前置詞は以 下のリストにあるものとします. また,単語の区切り文字は英数字以外の文字とします.冠詞
:= a | an | the
前置詞
:= in | on | of | to 9.
【上級者向け】標準入力から与えられたファイル(テキストとは限りません)のデータを以下の規則に したがってテキストファイルに変換するプログラムを書きなさい. また, その逆変換を するプログラムを書きなさい.
【規則】 ファイルのデータを先頭から順に6ビットづつ取り出し,その6ビットのデータを6 桁の2進数として
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
という文字にマッピングします
.
すなわち,
その6ビットのデータが0x00
ならば, A
とし, 0x3F
なら/
とします. このマッピングを行なったデータを,改行文字を除いて,1行あたり 72文字のテキストファイルとして出力しなさい. ただし,最終行は72文字に満たなくて もよいとします. この時,ファイル末尾では24ビットに満たないまとまりが出ることがあ るので,入力ファイルで最後に8ビットが余った場合には,入力データに8ビットの0
をつ け加え,
出力文字から1文字を削って,
その代りに1つの=
を付け加え,
16ビットが余った 場合には,入力データに16ビットの0
をつけ加え,出力文字から2文字を削って,その代 りに2つの=
を付け加えることとします. (このようなことを「パディング」と呼びます.)この符号化を
Base64 encoding
と呼び,電子メールの符号化に利用されています.電子メールで「今日の講義の感想や意見」を送ってください.
★
ex13-1.c
の内容/* ex13-1.c */
/*
コマンドライン引数を読み取る*/
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
int i ;
for(i=0;i<argc;i++) printf("\t%s", argv[i]) ; printf("\n") ;
for(i=1;i<argc;i++) printf("\t%d", atoi(argv[i])) ; printf("\n") ;
return 0 ; }
★
ex13-2-1.c
の内容/* ex13-2-1.c */
/*
標準入力からの文字の読み込み*/
/* getchar()
を用いて1文字づつ読み込む*/
#include <stdio.h>
#define STR_MAX 256
int main(int argc, char **argv) {
char s[STR_MAX] ; int c ;
int i=0 ;
while((c = getchar()) != EOF) { s[i++] = c ; s[i] = 0 ; }
printf("%s\n", s) ; return 0 ;
}
★
ex13-2-2.c
の内容/* ex13-2-2.c */
/*
標準入力からの文字の読み込み*/
/* fgets()
を用いて1行づつ読み込む*/
#include <stdio.h>
#define STR_MAX 256
int main(int argc, char **argv) {
char s[STR_MAX] ;
while(fgets(s, STR_MAX, stdin) != NULL) printf("%s", s) ;
return 0 ;
}
★
ex13-2-3.c
の内容/* ex13-2-2.c */
/*
ファイルからの文字列の読み込み*/
/* fgets()
を用いて1行づつ読み込む*/
#include <stdio.h>
#define STR_MAX 256
int main(int argc, char **argv) {
char s[STR_MAX] ; FILE *in ;
if ((in = fopen("ex13-2-3.c", "r")) != NULL) { while(fgets(s, STR_MAX, in) != NULL)
printf("%s", s) ; fclose(in) ; }
return 0 ; }
★
ex13-3-1.c
の内容/* ex13-3-1.c */
/* fprintf
関数で標準出力と標準エラー出力に出力*/
#include <stdio.h>
int main(int argc, char **argv) {
fprintf(stdout, "This string is printed on STDOUT\n") ; fprintf(stderr, "This string is printed on STDERR\n") ; return 0 ;
}
★
ex13-3-2.c
の内容/* ex13-3-1.c */
/* fprintf
関数でファイルに出力*/
#include <stdio.h>
int main(int argc, char **argv) {
FILE *out ;
if ((out = fopen("output", "w")) != NULL) {
fprintf(out, "This string is printed on FILE\n") ; fclose(out) ;
}
return 0 ;
}
● なぜ
scanf
を使うことは望ましくないかC言語に関する書籍の多くでは,標準入力などからのデータの読み込みに
scanf
関数(または,fscanf
関数)を利用する例が掲載されている. しかし,私(内藤)はこの方法は望ましくないと考えている. 以下 では,
その理由と実例をあげておこう.
★
Example 1
はじめに,次のプログラム
scanf_1.c
を考えてみよう./* scanf_1.c */
/*
空白で区切られた3つの整数値を標準入力から読む*/
#include <stdio.h>
int main(int argc, char **argv) {
int a,b,c ; int n ;
n = scanf("%d %d %d", &a, &b, &c) ; printf("%d %d %d\n", a,b,c) ; printf("n = %d\n", n) ; return 0 ;
}
これは,「標準入力から空白で区切られた3つの整数値を読む」プログラムである. このプログラムに対し て以下の3種類のデータを入力して,その結果を見てみよう.
【入力1】 すべての入力が正しい場合
1 2 3
【出力1】
1 2 3 n = 3
【入力2】 入力のうち2つが正しくない場合
1.0 x 5
【出力2】
1 0 0 n = 1
【入力3】 入力データが足りない場合
1 2
【出力3】
1 2 0 n = 2
これらの結果から次のようなことがわかる.
•
「整数値を期待してるscanf
関数に対して,整数値のみを入力した場合には正しい結果を得るが,整 数値でない文字列を入力した場合には結果が不定となる」ことがわかる.•
期待する入力の個数(この例の場合は“3”)が正しく入力されない限り,
どの入力が「間違っている」のかを知る方法はない.
★
Example 2
そこで,
scanf_1.c
を改良してみよう./* scanf_2.c */
/*
空白で区切られた3つの整数値を標準入力から読む*/
#include <stdio.h>
int main(int argc, char **argv) {
char sa[10], sb[10], sc[10] ; int n ;
n = scanf("%s %s %s", sa, sb, sc) ;
printf("%d %d %d\n", atoi(sa),atoi(sb),atoi(sc)) ; printf("n = %d\n", n) ;
return 0 ; }
これは
scanf
関数では「文字列」として3つのデータを読み込み,それをatoi
関数で整数値に変換している. この場合の(上の3つの入力に対する)出力は次の通りである.
【出力1】
1 2 3 n = 3
【出力2】
1 0 5 n = 3
【出力3】
1 2 0 n = 2
この場合の結果は以下のように読み取ることができる.
• Example 1
とは異なり,「文字列」としては正しく読み取れているので,atoi
関数で「整数値でない文字列」の判定が可能であれば「整数値」を正しく読み取ることができる
.
また,
「どの入力データ が正しいフォーマットでないか」も知ることができる.•
なお,atoi
関数は「整数フォーマットでない文字列」に対しては,値0
を返すため,「本当に0
が入 力されたかどうか」は別個に調べる必要がある.このプログラムを見る限り,入力文字列を
scanf
関数の「文字列フォーマット」で読み取り, それを変換 すれば正しい入力を得ることができるように思える.
しかし,
次のような場合が考えられる.
★
Example 3
/* scanf_3.c */
#include <stdio.h>
#define N 10
int main(int argc, char **argv) {
char a[N], b[N], c[N] ; int n ;
n = scanf("%s %s %s", a, b, c) ; printf("n = %d\n", n) ;
printf("%s %s %s\n", a, b, c) ; printf("%p %p %p\n", a, b, c) ; return 0 ;
}
この例は「最大長さ9」の文字列を3つ読み込むプログラムである. このプログラムに次のような入力を 与え,出力結果を見てみよう.
【入力】
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 01234567890!@#$%^&*()_+=\~
【出力】
n = 3
qrstuvwxyz ^&*()_+=\~ 01234567890!@#$%^&*()_+=\~
ffffffff7ffff6d0 ffffffff7ffff6c0 ffffffff7ffff6b0
本来期待される出力結果(2行目)はABCDEFGHI abcdefghi 012345678
である
.
•
読み込みに成功した個数(n)
は正しく3を返しているにも関わらず,
どれひとつとして正しい入力を 得ることができていない.•
このようなことが起った理由は,scanf
関数では読み込むデータの長さを認識できないため,本来「9 文字」で打ち切らなければならない文字列をそのまま読み込んでしまい,他のオブジェクトのデータ 領域を破壊したことが原因である.
★
Example 4
/* scanf_4.c */
#include <stdio.h>
#define N 10
int main(int argc, char **argv) {
char a[N], b[N], c[N] ; int n ;
n = scanf("%10s %10s %10s", a, b, c) ; printf("n = %d\n", n) ;
printf("%s %s %s\n", a, b, c) ; return 0 ;
}
この例は
scanf_3.c
の問題点(他のオブジェクトのデータ領域の破壊)を避けるために, 読み込み文字数 を制限したものである.
このプログラムにscanf_3.c
と同じ入力を与え,
出力結果を見てみよう.
【出力】
n = 3
ABCDEFGHIJ KLMNOPQRST UVWXYZ
本来期待される出力結果(2行目)はABCDEFGHI abcdefghi 012345678
である.
•
読み込みに成功した個数(n)
は正しく3を返しているにも関わらず,先頭以外は正しい入力を得るこ とができていない.★ 考察と結論
本来
scanf
関数はprintf
関数の「逆操作」であり, 「フォーマット(書式)つき入力」を扱う関数である. したがって,
scanf
を利用する場面としては以下のような状況で用いるのが望ましい.• printf
関数を用いて「フォーマットつき出力」を行ったデータファイルを読み出す場合には,printf
関数で利用した出力フォーマットを入力フォーマットとして
scanf
関数を用いれば,余分な操作なし に入力を扱うことができる.
逆に,一般のテキストデータ入力を扱う場合に
scanf
関数を使うことが望ましくない理由は以下の通りで ある.•
一般の入力データに関しては,(例えば)整数の入力を期待する状況で必ず整数フォーマットの文字 列が入力される保証はない.•
一般の入力データに関しては, 長さN
以下の文字列の入力を期待する状況で必ず長さN
以下の文字 列が入力される保証はない.
•
正しくないフォーマットの入力データを読み取ったときに, それが「不正なフォーマットのデータ」であることを検証する手段がない
.
特にキーボードから「手で」入力を行う場合には, このような不正なフォーマットで入力される場合が多
く,
scanf
関数では,そのエラー処理ができないことが問題となる.★ 正しい入力の扱い
入力データを正しく扱い,いろいろな入力エラーに対処するためには,
fgets
関数などを用いて入力デー タの1行全体を文字列として扱い,その文字列を解析する必要がある.「課題2」は,そのような具体的な文字列解析を行う問題である.
★ 注意
なお
,
規定以外の入力を行ったときのscanf
関数の動作は「不定」である.
つまり, scanf
関数の実装に 依存する. したがって,ここにあげてある出力結果と異なる結果を得る場合もありうる.● 前回の課題の解説
前回の課題の中から3つを選んで「解答例」を解説しましょう.
2つの
int
型の変数の値を入れ替える関数swap(int *, int *)
を作りなさい.
/* prog12-1.c */
/* swap */
#include <stdio.h>
void swap(int *, int *) ; int main(int argc, char **argv) {
int a = 1, b = 2 ;
printf("a = %d, b = %d\n", a,b) ; swap(&a,&b) ;
printf("a = %d, b = %d\n", a,b) ; return 0 ;
}
void swap(int *a, int *b) {
int t ; t = *a ;
*a = *b ;
*b = t ; return ; }
• swap
関数に対してa, b
の値ではなく,それらのアドレスを渡していることに注意してください.•
もう少し巧妙に作ると, 以下のような書き方も可能です. なぜかは考えてみてください. ただしswap(&a,&a)
としては利用できません./* prog12-1-1.c */
/* swap,
ただしswap(&a,&a)
としては使えない*/
#include <stdio.h>
void swap(int *, int *) ; int main(int argc, char **argv) {
int a = 1, b = 2 ;
printf("a = %d, b = %d\n", a,b) ; swap(&a,&b) ;
printf("a = %d, b = %d\n", a,b) ; return 0 ;
}
void swap(int *a, int *b) {
*a ^= *b ; *b ^= *a ; *a ^= *b ; return ;
}
文字列
s
の中で文字c
が一番後ろに出てくる場所を返す関数char *string_r_char(char *s, char c)
を作りなさい. c
が見つからないときにはNULL
を返しなさい./* prog12-4.c */
/* strrchr */
#include <stdio.h>
char *string_r_char(char *, char) ; int main(int argc, char **argv) {
char str[] = "a quick brown fox jumps over the lazy dog." ; char *p ;
printf("%s\n", str) ;
printf("0123456789012345678901234567890123456789012345\n") ; if ((p = string_r_char(str,’o’)) != NULL)
printf("%ld\n", p - str) ; else
printf("Not found\n") ; return 0 ;
}
char *string_r_char(char *s, char c) {
char *p=NULL ; while(*s) {
if (*s == c) p = s ; s++ ;
}
return p ; }
• *s
の中でc
が「最後」に見つかる場所ですから,「見つけた場所」をp = s
で記憶しておきます.*s
を文字列終端まで調べたときのp
が「最後に見つかった場所」となります.•
見つからないときのことを考えてchar *p = NULL
と初期化していることに注意してください.文字列