コラム
2025/08/29
プログラミングについて 第118回目
『作っておくと便利なちょっとしたプログラム その2』
今回も前回に続いてプログラムを作っていきましょう。
まずは前回までのプログラムです。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#define NEW(type,element) (type *)malloc(sizeof(type)*(element))
#define DELETE(addr) free(addr)
#define PROC_MODE_ULINE 1
#define PROC_MODE_RLINE 2
int proc_mode=PROC_MODE_ULINE;
int disp_total=0;
int enclose =0;
char *fname=NULL;
FILE *fp=stdin;
char *new_strcat(char *string1,char *string2);
void main(argc,argv)
int argc;
char *argv[];
{
if(analize_argment(argc,argv)== -1)goto end;
end:
exit(0);
}
analize_argment(argc,argv)
int argc;
char *argv[];
{
int i;
for(i=1;i<argc;i++){
if(stricmp(argv[i],"-u")==0)proc_mode=PROC_MODE_ULINE;
else if(stricmp(argv[i],"-r")==0)proc_mode=PROC_MODE_RLINE;
else if(stricmp(argv[i],"-n")==0)disp_total=1;
else if(stricmp(argv[i],"-c")==0)enclose =1;
else{
for(;i<argc;i++){
if(fname && fname[0])fname=new_strcat(fname," ");
fname=new_strcat(fname,argv[i]);
}
}
}
return 1;
}
char *new_strcat(char *string1,char *string2)
{
if(string1==NULL){
string1=NEW(char,strlen(string2)+1);
strcpy(string1,string2);
}
else{
int l;
l=strlen(string1)+strlen(string2)+1;
if(l>_msize(string1))string1=realloc(string1,l);
strcat(string1,string2);
}
return string1;
}
今回は簡単な処理の方の連続する同一文字列の行を削除して表示する機能を付加しましょう。メイン関数を次のようにします。
void main(argc,argv)
int argc;
char *argv[];
{
if(analize_argment(argc,argv)== -1)goto end;
if(open_source()== -1)goto end;
if(proc_mode==PROC_MODE_ULINE){
}
else{
process_rline();
}
close_source();
end:
exit(0);
}
関数 open_source はソースファイルをオープンし、関数 close_source はそれを閉じます。ただし標準入力からの読み込みのときには何もしません。これらの関数は、
open_source()
{
if(fname){
fp=fopen(fname,"r");
if(fp==NULL){
printf("入力ファイル\n");
printf("File : %s\n",fname);
printf("のオープン時にエラーが発生しました。\n");
printf("%s\n",strerror(errno));
return -1;
}
}
return 1;
}
close_source()
{
if(fname)fclose(fp);
return 1;
}
で、特に難しいところはありませんが、オープン時にエラーの内容を表示する部分の、
printf("%s\n",strerror(errno));
で、errno という変数は stdlib.h に定義されている変数で、直前のエラーの値が代入されるものです。ここで1つ注意しておかなければならないことは、この変数は暗黙に定義されていますので、プログラムの中で不用意に定義してしまうことが多々あり、コンパイルが通らなかったりバグの原因になることがあることです。私も時々これで無駄な時間を費やしてしまうことがあるので忘れないようにして下さい。
次に関数 process_rline です。1行の文字列の最大長を固定にしてもいいのですが、どれだけの長さか分からないという前提でプログラムを作っていきましょう。ということは1文字ずつ読み込んで処理することが基本になります。
そこで、1文字ずつ読み込む(それだけ)ように記述してみます。
process_rline()
{
int c;
while(1){
if((c=fgetc(fp))==EOF)break;
}
return 1;
}
これでも基本的には問題はありませんが、この方法で実際にファイルを読み込む時間が10Mバイト程度のファイルのとき、私のパソコンでは2秒程度かかってしまいました。この時間を問題にするか否かはプログラムを作成する方の感じ方にもよりますが、ここではこの時間にこだわってみることにします。
どうして2秒かかるのかというと、10Mバイトのファイルのときには fgetc 関数は1千万回程度呼び出されることになりこれに時間がかかってしまっているのです。
関数 fgetc は高水準入出力関数ですので自動的にバッファリングされるため、呼び出される度にファイルから1文字を読むのではないので、やはり関数の呼び出しのためのオーバーヘッドが2秒の殆どを占めていると考えられます。そこで、
process_rline()
{
#define SIZE 100
char data[SIZE];
while(1){
if(fread(data,1,SIZE,fp)==0)break;
}
return 1;
#undef SIZE
}
として任意のバイト数をまとめて読み込むようにしてみました。上の場合100バイト単位で読み込むようにしています。この場合0.6秒程度でした。試しに1万バイトにしてみると0.4秒程度でしたので、実際上はそのあたりのバイト数が妥当だと思われます。そこで、8192バイトにすることにしました。どうして8192バイトなのかという理由を聞かれてもこれといった根拠はないのですが、2のべき乗であることと、UNIXなどでも結構この値を使っているという変な慣習があるだけですので、あまり気にしないで下さい。DOSとはいっても16ビットのもので動かすときにはスタックサイズ等の問題がありますので、小さくするかスタティックにするかの手段が必要になる可能性があります。
関数 process_rline の内容の前にテキストファイルの行とファイルの終わりの部分についてちょっと説明をします。DOSやWindowsのテキストファイルでは、
text1<cr><lf>
text2<cr><lf>
text3<cr><lf>
か、
text1<cr><lf>
text2<cr><lf>
text3<cr><lf>
^Z
になっています。<cr> はキャリッジリターン <lf> はラインフィードで、^Z はEOFです。ところが時々マナーの悪いテキストデータでは
text1<cr><lf>
text2<cr><lf>
text3
や、
text1<cr><lf>
text2<cr><lf>
text3
^Z
というように最終行の終わりに <cr><lf> がないものがあります。Windowsに付録で付いているプログラムではこのようなテキストファイルを正常に読み込めないことがありますので注意が必要です。今回作成するプログラムはマナーの悪いテキストファイルは対象外とします。
ちなみにUNIXでは必ず次のようになっていなければなりません。
text1<lf>
text2<lf>
text3<lf>
EOFを示す ^Z があるとviエディタは、変なコードがあるとか、最終行に <lf> がないと最終行が不完全だとか文句を言います。
関数 process_rline が行の表示を行なうときに使用する関数 display_text です。
display_text(total,text)
int total;
char *text;
{
if(enclose){
if(disp_total)printf("%4d (%s)\n",total,text);
else printf("(%s)\n",text);
}
else{
if(disp_total)printf("%4d %s\n",total,text);
else printf("%s\n",text);
}
return 1;
}
それでは 関数 process_rline の内容です。
process_rline()
{
/* 読み込み用のバッファ。READ_BUF_SIZE は最初に
#define READ_BUF_SIZE 8192 と定義しておきます。
+1 と1バイト大きく定義しているのは、以下で説明
しますがプログラムを簡単にするために門番を立てる
目的で使用します。*/
char data[READ_BUF_SIZE+1];
int ldata;
char *prev_text; /* 前 の行の文字列 */
char * cur_text; /* 現在の行の文字列 */
int is,ie,total; /* is は読み込んだ data の中の行の先頭のインデッ
クスを示す目的に使用します。
ie は文字列の最後を示す目的に使用します。
total は連続する同一文字列行の数のカウンタに使用
します。*/
/* prev_text を初期化します。これもプログラムを簡潔
に記述する目的で、予め行なっておきます。cur_text
は prev_text と同様に初期化しても構いませんが、
ヌルにしておきます。*/
prev_text=new_strcat(NULL,"");
cur_text=NULL;
total=0;
while(ldata=fread(data,1,READ_BUF_SIZE,fp)){
/* 読み込んだデータの最後の文字+1の場所に門番とな
る <lf>を追加します。最大で READ_BUF_SIZE バイト
を読み込みますので、data の定義は1バイト大きく
してあります。*/
data[ldata]='\n';
/* 読み込んだデータの先頭から門番までの文字列を順番
に処理していきます。*/
for(is=ie=0;ie<=ldata;ie++){
/* <lf> でないときにはループを繰返します。
データには必ず門番の <lf> がありますので、最低1
度は次の処理を行ないます。*/
if(data[ie]!='\n')continue;
/* <lf> をヌルに置き換え cur_text に文字列を追加し
ます。コピーではなく、追加するのは、data が前回
読み込んだときに行の途中で終わっているときがある
ためです。 */
data[ie]='\0';
cur_text=new_strcat(cur_text,data+is);
/* 行の先頭を示す is を ie+1 即ち次の行の先頭文字列
の位置に更新する。*/
is=ie+1;
/* 現在のテキストの <lf> の位置が読み込んだデータの
サイズと同じときは門番なので、1行の読み込みが完
了していないことになるので次の読み込みを行ないま
す。*/
if(ie==ldata)break;
/* 前の行の文字列と現在の行の文字列を比較します。*/
if(strcmp(prev_text,cur_text)!=0){
/* 前の行を表示しますが、total が 0 のときはプログ
ラムを簡単にするために初期化したものなので表示
しません。*/
if(total)display_text(total,prev_text);
/* 現在の行の文字列を前の行の文字列にし、合計数を
リセットします。*/
prev_text[0]='\0';
prev_text=new_strcat(prev_text,cur_text);
total=0;
}
/* 合計数を1つ増やし、現在の行の文字列を初期化しま
す。*/
total++;
cur_text[0]='\0';
}
}
/* 上のループが終了しても、最終の文字列の表示が行わ
れていませんので表示します。*/
if(total)display_text(total,prev_text);
/* 不要になったメモリーを解放します。*/
DELETE(prev_text);
DELETE( cur_text);
return 1;
}
短い内容なのですが、じっくり読まないと分かりづらいかも知れません。2バイト文字についての処理はしてはいませんが、2バイト文字といっても2バイト目が 0x0a (LF)になる文字があるときに動作が正常に行われなくなる可能性があるだけです。WindowsやDOSはシフトJISを使っていますので、シフトJISの2バイト文字は1バイト目が 0x81~0x9F または 0xE0~0xFC、2バイト目は 0x40~0x7E または 0x80~0xFCの範囲しかとらないので特に問題はありません。ただし他のOSに移植する場合はシフトJIS以外のこともあるので、2バイト文字コードの処理が必要になる可能性があります。
ここまでで、連続する同一行の削除ができました。
uline -r -n -c ファイル名
として実行してみて下さい。
次回からはユニーク行の表示機能を加えていきます。
また次回。
第118回 プログラミングについて『作っておくと便利なちょっとしたプログラム その2』

プログラミングについて 第118回目
『作っておくと便利なちょっとしたプログラム その2』
今回も前回に続いてプログラムを作っていきましょう。
まずは前回までのプログラムです。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#define NEW(type,element) (type *)malloc(sizeof(type)*(element))
#define DELETE(addr) free(addr)
#define PROC_MODE_ULINE 1
#define PROC_MODE_RLINE 2
int proc_mode=PROC_MODE_ULINE;
int disp_total=0;
int enclose =0;
char *fname=NULL;
FILE *fp=stdin;
char *new_strcat(char *string1,char *string2);
void main(argc,argv)
int argc;
char *argv[];
{
if(analize_argment(argc,argv)== -1)goto end;
end:
exit(0);
}
analize_argment(argc,argv)
int argc;
char *argv[];
{
int i;
for(i=1;i<argc;i++){
if(stricmp(argv[i],"-u")==0)proc_mode=PROC_MODE_ULINE;
else if(stricmp(argv[i],"-r")==0)proc_mode=PROC_MODE_RLINE;
else if(stricmp(argv[i],"-n")==0)disp_total=1;
else if(stricmp(argv[i],"-c")==0)enclose =1;
else{
for(;i<argc;i++){
if(fname && fname[0])fname=new_strcat(fname," ");
fname=new_strcat(fname,argv[i]);
}
}
}
return 1;
}
char *new_strcat(char *string1,char *string2)
{
if(string1==NULL){
string1=NEW(char,strlen(string2)+1);
strcpy(string1,string2);
}
else{
int l;
l=strlen(string1)+strlen(string2)+1;
if(l>_msize(string1))string1=realloc(string1,l);
strcat(string1,string2);
}
return string1;
}
今回は簡単な処理の方の連続する同一文字列の行を削除して表示する機能を付加しましょう。メイン関数を次のようにします。
void main(argc,argv)
int argc;
char *argv[];
{
if(analize_argment(argc,argv)== -1)goto end;
if(open_source()== -1)goto end;
if(proc_mode==PROC_MODE_ULINE){
}
else{
process_rline();
}
close_source();
end:
exit(0);
}
関数 open_source はソースファイルをオープンし、関数 close_source はそれを閉じます。ただし標準入力からの読み込みのときには何もしません。これらの関数は、
open_source()
{
if(fname){
fp=fopen(fname,"r");
if(fp==NULL){
printf("入力ファイル\n");
printf("File : %s\n",fname);
printf("のオープン時にエラーが発生しました。\n");
printf("%s\n",strerror(errno));
return -1;
}
}
return 1;
}
close_source()
{
if(fname)fclose(fp);
return 1;
}
で、特に難しいところはありませんが、オープン時にエラーの内容を表示する部分の、
printf("%s\n",strerror(errno));
で、errno という変数は stdlib.h に定義されている変数で、直前のエラーの値が代入されるものです。ここで1つ注意しておかなければならないことは、この変数は暗黙に定義されていますので、プログラムの中で不用意に定義してしまうことが多々あり、コンパイルが通らなかったりバグの原因になることがあることです。私も時々これで無駄な時間を費やしてしまうことがあるので忘れないようにして下さい。
次に関数 process_rline です。1行の文字列の最大長を固定にしてもいいのですが、どれだけの長さか分からないという前提でプログラムを作っていきましょう。ということは1文字ずつ読み込んで処理することが基本になります。
そこで、1文字ずつ読み込む(それだけ)ように記述してみます。
process_rline()
{
int c;
while(1){
if((c=fgetc(fp))==EOF)break;
}
return 1;
}
これでも基本的には問題はありませんが、この方法で実際にファイルを読み込む時間が10Mバイト程度のファイルのとき、私のパソコンでは2秒程度かかってしまいました。この時間を問題にするか否かはプログラムを作成する方の感じ方にもよりますが、ここではこの時間にこだわってみることにします。
どうして2秒かかるのかというと、10Mバイトのファイルのときには fgetc 関数は1千万回程度呼び出されることになりこれに時間がかかってしまっているのです。
関数 fgetc は高水準入出力関数ですので自動的にバッファリングされるため、呼び出される度にファイルから1文字を読むのではないので、やはり関数の呼び出しのためのオーバーヘッドが2秒の殆どを占めていると考えられます。そこで、
process_rline()
{
#define SIZE 100
char data[SIZE];
while(1){
if(fread(data,1,SIZE,fp)==0)break;
}
return 1;
#undef SIZE
}
として任意のバイト数をまとめて読み込むようにしてみました。上の場合100バイト単位で読み込むようにしています。この場合0.6秒程度でした。試しに1万バイトにしてみると0.4秒程度でしたので、実際上はそのあたりのバイト数が妥当だと思われます。そこで、8192バイトにすることにしました。どうして8192バイトなのかという理由を聞かれてもこれといった根拠はないのですが、2のべき乗であることと、UNIXなどでも結構この値を使っているという変な慣習があるだけですので、あまり気にしないで下さい。DOSとはいっても16ビットのもので動かすときにはスタックサイズ等の問題がありますので、小さくするかスタティックにするかの手段が必要になる可能性があります。
関数 process_rline の内容の前にテキストファイルの行とファイルの終わりの部分についてちょっと説明をします。DOSやWindowsのテキストファイルでは、
text1<cr><lf>
text2<cr><lf>
text3<cr><lf>
か、
text1<cr><lf>
text2<cr><lf>
text3<cr><lf>
^Z
になっています。<cr> はキャリッジリターン <lf> はラインフィードで、^Z はEOFです。ところが時々マナーの悪いテキストデータでは
text1<cr><lf>
text2<cr><lf>
text3
や、
text1<cr><lf>
text2<cr><lf>
text3
^Z
というように最終行の終わりに <cr><lf> がないものがあります。Windowsに付録で付いているプログラムではこのようなテキストファイルを正常に読み込めないことがありますので注意が必要です。今回作成するプログラムはマナーの悪いテキストファイルは対象外とします。
ちなみにUNIXでは必ず次のようになっていなければなりません。
text1<lf>
text2<lf>
text3<lf>
EOFを示す ^Z があるとviエディタは、変なコードがあるとか、最終行に <lf> がないと最終行が不完全だとか文句を言います。
関数 process_rline が行の表示を行なうときに使用する関数 display_text です。
display_text(total,text)
int total;
char *text;
{
if(enclose){
if(disp_total)printf("%4d (%s)\n",total,text);
else printf("(%s)\n",text);
}
else{
if(disp_total)printf("%4d %s\n",total,text);
else printf("%s\n",text);
}
return 1;
}
それでは 関数 process_rline の内容です。
process_rline()
{
/* 読み込み用のバッファ。READ_BUF_SIZE は最初に
#define READ_BUF_SIZE 8192 と定義しておきます。
+1 と1バイト大きく定義しているのは、以下で説明
しますがプログラムを簡単にするために門番を立てる
目的で使用します。*/
char data[READ_BUF_SIZE+1];
int ldata;
char *prev_text; /* 前 の行の文字列 */
char * cur_text; /* 現在の行の文字列 */
int is,ie,total; /* is は読み込んだ data の中の行の先頭のインデッ
クスを示す目的に使用します。
ie は文字列の最後を示す目的に使用します。
total は連続する同一文字列行の数のカウンタに使用
します。*/
/* prev_text を初期化します。これもプログラムを簡潔
に記述する目的で、予め行なっておきます。cur_text
は prev_text と同様に初期化しても構いませんが、
ヌルにしておきます。*/
prev_text=new_strcat(NULL,"");
cur_text=NULL;
total=0;
while(ldata=fread(data,1,READ_BUF_SIZE,fp)){
/* 読み込んだデータの最後の文字+1の場所に門番とな
る <lf>を追加します。最大で READ_BUF_SIZE バイト
を読み込みますので、data の定義は1バイト大きく
してあります。*/
data[ldata]='\n';
/* 読み込んだデータの先頭から門番までの文字列を順番
に処理していきます。*/
for(is=ie=0;ie<=ldata;ie++){
/* <lf> でないときにはループを繰返します。
データには必ず門番の <lf> がありますので、最低1
度は次の処理を行ないます。*/
if(data[ie]!='\n')continue;
/* <lf> をヌルに置き換え cur_text に文字列を追加し
ます。コピーではなく、追加するのは、data が前回
読み込んだときに行の途中で終わっているときがある
ためです。 */
data[ie]='\0';
cur_text=new_strcat(cur_text,data+is);
/* 行の先頭を示す is を ie+1 即ち次の行の先頭文字列
の位置に更新する。*/
is=ie+1;
/* 現在のテキストの <lf> の位置が読み込んだデータの
サイズと同じときは門番なので、1行の読み込みが完
了していないことになるので次の読み込みを行ないま
す。*/
if(ie==ldata)break;
/* 前の行の文字列と現在の行の文字列を比較します。*/
if(strcmp(prev_text,cur_text)!=0){
/* 前の行を表示しますが、total が 0 のときはプログ
ラムを簡単にするために初期化したものなので表示
しません。*/
if(total)display_text(total,prev_text);
/* 現在の行の文字列を前の行の文字列にし、合計数を
リセットします。*/
prev_text[0]='\0';
prev_text=new_strcat(prev_text,cur_text);
total=0;
}
/* 合計数を1つ増やし、現在の行の文字列を初期化しま
す。*/
total++;
cur_text[0]='\0';
}
}
/* 上のループが終了しても、最終の文字列の表示が行わ
れていませんので表示します。*/
if(total)display_text(total,prev_text);
/* 不要になったメモリーを解放します。*/
DELETE(prev_text);
DELETE( cur_text);
return 1;
}
短い内容なのですが、じっくり読まないと分かりづらいかも知れません。2バイト文字についての処理はしてはいませんが、2バイト文字といっても2バイト目が 0x0a (LF)になる文字があるときに動作が正常に行われなくなる可能性があるだけです。WindowsやDOSはシフトJISを使っていますので、シフトJISの2バイト文字は1バイト目が 0x81~0x9F または 0xE0~0xFC、2バイト目は 0x40~0x7E または 0x80~0xFCの範囲しかとらないので特に問題はありません。ただし他のOSに移植する場合はシフトJIS以外のこともあるので、2バイト文字コードの処理が必要になる可能性があります。
ここまでで、連続する同一行の削除ができました。
uline -r -n -c ファイル名
として実行してみて下さい。
次回からはユニーク行の表示機能を加えていきます。
また次回。