• 検索結果がありません。

● 実習内容

N/A
N/A
Protected

Academic year: 2021

シェア "● 実習内容"

Copied!
13
0
0

読み込み中.... (全文を見る)

全文

(1)

● 実習内容

【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

を使った場合の動作の違いを理解しよう. なお,

(2)

% 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

関数を用いる必要がある.

(3)

【課題】 (当然ですが,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.

【中級者〜上級者向け】

(4)

上の「テキストファイルに対するコピーコマンド」を拡張して,一般のファイルに対す るコピーコマンドを作成しなさい

.

この問題は

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

と呼び,電子メールの符号化に利用されています.

電子メールで「今日の講義の感想や意見」を送ってください.

(5)

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 ;

}

(6)

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 ;

}

(7)

● なぜ

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”)が正しく入力されない限り,

どの入力が「間違っている」

のかを知る方法はない.

(8)

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

関数の「文字列フォーマット」で読み取り, それを変換 すれば正しい入力を得ることができるように思える

.

しかし

,

次のような場合が考えられる

.

(9)

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 ;

}

(10)

この例は

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

関数の実装に 依存する. したがって,ここにあげてある出力結果と異なる結果を得る場合もありうる.

(11)

● 前回の課題の解説

前回の課題の中から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 ;

}

(12)

文字列

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

と初期化していることに注意してください.

(13)

文字列

s

の中で文字列

t

に含まれる文字のいずれかが一番最初に出てくる場所を返す 関数

char *string_chars(char *s, char *t)

を作りなさい

. t

に含まれる文字が 見つからないときには

NULL

を返しなさい.

/* prog12-5.c */

/* strchars */

#include <stdio.h>

char *string_chars(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_chars(str,"PQRpqr")) != NULL)

printf("%ld\n", p - str) ; else

printf("Not found\n") ; return 0 ;

}

char *string_chars(char *s, char *t) {

char *p ; while(*s) {

p = t ; while(*p) {

if (*s == *p) return s ; p++ ;

} s++ ; }

return NULL ; }

基本的な考え方は

string_char

などと同じです.

t

の先頭の文字を見つけたら,

t

の残りの文字が一致しているかどうかを調べています.

参照

関連したドキュメント

・電源投入直後の MPIO は出力状態に設定されているため全ての S/PDIF 信号を入力する前に MPSEL レジスタで MPIO を入力状態に設定する必要がある。MPSEL

理由:ボイラー MCR範囲内の 定格出力超過出 力は技術評価に て問題なしと確 認 済 み で あ る が、複数の火力

基準の電力は,原則として次のいずれかを基準として決定するも

Dual I/O リードコマンドは、SI/SIO0、SO/SIO1 のピン機能が入出力に切り替わり、アドレス入力 とデータ出力の両方を x2

(2)燃料GMは,定格熱出力一定運転にあたり,原子炉熱出力について運転管理目標を

(1)  研究課題に関して、 資料を収集し、 実験、 測定、 調査、 実践を行い、 分析する能力を身につけて いる.

基準の電力は,原則として次のいずれかを基準として各時間帯別

また、同制度と RCEP 協定税率を同時に利用すること、すなわち同制 度に基づく減税計算における関税額の算出に際して、 RCEP