<演習編>


C言語の研究・演習編1

前回まで一生懸命C言語の文法や標準ライブラリを学んできた。長い道のりであった....

これからは、プログラムを作成する上で知っておかねばならないアルゴリズムについて、例題を多用しながら研究して行こう。

第1章 文字列の操作

C言語では文字列の取り扱いは重要なテーマである。

これは標準ライブラリ<string.h>がかなり充実していることからもうかがい知ることができるだろう。

それでも、やはり荒削りなC言語である。BASICなどのように、便利で多種多彩な関数はない。

必要な関数は標準ライブラリをもとにして自分で作って行くしかない。それがまた楽しいところでもある

1 文字列の探索

たとえば、長い文字列から特定の文字列を検索するにはどうしたらいいだろう。標準関数strcmpやstrncmpはそのままでは使えない。

長い文字列a[n]から文字列b[m]を探す場合、a[0]から順次、m字分をb[m]とstrncmpすればいいだろう。

この場合、a[n]の残り字数がmより少なくなったら検索は終わりだ。

それでは、この方法で例題をやってみよう。

文字列a[]の中に、文字列b[]がいくつあるかを数えさせる。各文字列はscanf関数によってユーザに入力させるようにしておこう。

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

#define FOUND 0

void main()
{
    char a[31];
    char *pa = a;
    char b[11];
    char *pb = b;
    int n,m,i;
    int cnt = 0;

    printf("検索される文字列a[]を入力してください(30文字以内) : ");
    scanf("%s",pa);

    printf("検索する文字列を入力してください(10文字以内) : ");
    scanf("%s",pb);

    n = strlen(pa);
    printf("a[]の長さは%d\n",n);
    m = strlen(pb);
    printf("b[]の長さは%d\n",m);

    for(i=0; i <= n-m; i++){
        if (strncmp(pa+i, pb, m) == FOUND) {
            cnt++;
            printf("iが%dの時、%d個目\n",i, cnt);
            i += m - 1; /*配列番号であることに注意*/
        }
    }
    printf("-----Result-----\n");
    printf("%sを%d個発見しました\n",pb,cnt);
}

いかがだろうか。このようにこまめにprintf関数を挿入して、検索の経過がよく分かるようにするのも手だ。

せっかくだから、この検索アルゴリズムを今度はファイルから文字列を数えるようにしてみよう。これは例の、argcとargvをコマンドラインからキー入力する方法だ。

どこかに適当な名前のテキストファイルを作成して、これに何行か好きな文字を書いておこう、これが検索されるテキストファイルになる。そこで、次のプログラムをコンパイルして実行ファイルを作成しよう。

それで、MS-DOSプロンプトを立ち上げて、chdirでこのプログラムの実行ファイル(.EXE)のあるディレクトリに移動させたのち、「プログラム名と検索されるテキストファイル名」とコマンドラインからそれぞれの間をスペースで区切ってキーボードで入力しよう。

なお、検索されるテキストファイルがプログラム実行ファイルと別のディレクトリにある場合は、フルパス名も打ちこむことを忘れないでね。

それでは、レッツノート、いやレッツビギン。

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

#define FOUND 0
#define GYOMAX 256
void main(int argc ,char **argv)
{
    FILE *fp;
    char buff[GYOMAX];
    char *pa=buff;
    char b[11];
    char *pb=b;
    int m,n,i;
    int cnt=0;

    argv++;
    if ( argc != 2 || ( fp = fopen(*argv, "r") ) == NULL )
        printf("File Opening Error!\n");
    else{

        printf("検索する文字列を入れてください : ");
        scanf("%s",pb);
        m=strlen(pb);

        while(fgets(pa,GYOMAX,fp) !=NULL ){
            n = strlen(pa);
            for(i=0; i <= n-m; i++){
                if (strncmp(pa+i,pb,m) == FOUND){
                    cnt++;
                    i += m-1;
                }
            }
        }
        printf("-----Result-----\n");
        printf("%sを%d個発見しました\n",pb,cnt);
        fclose(fp);
    }
}

いかがだろうか。うまく検索結果が返って来ただろうか。このファイル、次も利用するから置いておいてね。

2 文字列の置換

今度は、さっきの検索アルゴリズムを利用して発見した文字列を別の文字列に置き換えて新たなる文字列を作り出すことをやってみよう。

先ほどと違う点は、新たなる文字列d[] を用意することと、探索する文字列を見つけた場合、別の文字列に置き換えるわけだが、文字列の最後にはNULLが入っているから、次にd[] にコピーする文字列の先頭文字はこのNULLの位置からはじめることになる。

しかし、strlen関数はNULLは文字列の長さに加えないから、次の文字位置は前回と同様のカウント方法でいいわけだ。ややこしいことを言うより見た方が早いかな。

さっそく見てみよう。

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

#define FOUND 0

void main(int argc, char **argv)
{
    char a[31];
    char *pa;

    char b[11];
    char *pb;

    char c[11];
    char *pc;

    char d[100]; /*新規文字列*/
    char *pd;
    int na, nb, nc;
    int i, j ;

    pa = a;
    pb = b;
    pc = c;
    pd = d;

    printf("元となる文字列a[]をいれてください(30文字以内) : ");
    scanf("%s",pa);
    printf("置換される文字列b[]をいれてください(10文字以内) : ");
    scanf("%s",pb);
    printf("置換する文字列c[]を入れてください(10文字以内) : ");
    scanf("%s",pc);

    na = strlen(pa);
    nb = strlen(pb);
    nc = strlen(pc);

    for( i = 0, j = 0; i <= (na-nb); i++, j++){
        if(strncmp(pa + i, pb, nb) == FOUND){
            printf("a[%d]で%sに一致\n",i, pb );
            strcpy(pd + j , pc);
            i += nb - 1;
            j += nc - 1;
        }
        else
            *( pd +j ) = *( pa + i );
    }
    strcpy( pd + j , pa + i );

    printf("-----Result-----\n");
    printf("新規文字列は%s\n", pd);
}

どうだろうか、先ほどと同様一致した場合はそれぞれ文字列分だけ配列位置を進め、一致しない場合は一個ずつコピーしていけばいいだけだ。ただし、配列位置は0から始まるから、一致した場合、配列位置は配列数よりいつも1少ないことになるわけだ。

さあ、今度は約束どおり前回のファイルを使って、コマンドラインからプログラム名とファイル名をキーボード入力するやり方でやってみよう。

今回は前回使ったファイルのほかに、新たに書き込みファイルとして適当な名前を指定するため仮引数が1つ増える。

つまり、プログラム名 読み取りファイル名 書き込みファイル名の3つを実行ファイルのあるディレクトリからコマンド入力しよう。

しかも、今回は読み取りファイルの内容を一度全部表示させた上で、fseek関数によって読み取りファイル位置を先頭に戻した後で、置換される文字列と置換する文字列を入力させるという、ちょっとばかり凝った方法を採用した。

さあ、やってみようか。

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

#define FOUND 0
#define GYOMAX 256

void str_input(char *, char *);
void str_change(char*, char *, char *, char *);

void main(int argc, char **argv)
{
    FILE *fp_in;
    FILE *fp_out;
    char buff[GYOMAX];
    char *pa=buff;
    char b[11];
    char *pb=b;
    char c[11];
    char *pc=c;
    char output[GYOMAX];
    char *pd=output;

    if(argc != 3)
        printf("File Names Error!\n");
    else{
        argv++;
        if( ( fp_in = fopen(*argv, "r") ) == NULL )
            printf("Can't open Input File\n");
        else{
            argv++;
            if( ( fp_out = fopen(*argv,"w") ) == NULL )
                printf("Cant'open Output File\n");
            else{
                printf("------Start------\n");
                while( fgets(pa, GYOMAX, fp_in) != NULL)
                    printf("ファイル内容は→%s",pa);
               
                fseek(fp_in, 0L, SEEK_SET); /*fp_inのファイル位置を先頭に戻す*/

                printf("\n");

                str_input(pb,pc);

                while( fgets(pa, GYOMAX, fp_in) != NULL){
                    str_change(pa, pb, pc, pd);
                    fputs(pd, fp_out);
                }
                fclose(fp_out);
            }
            fclose(fp_in);
        }
    }
}

void str_input(char *pb, char *pc)
{
    printf("置換される文字列を入れてください : ");
    scanf("%s",pb);

    printf("置換する文字列を入れてください : ");
    scanf("%s",pc);
}

void str_change(char *pa, char *pb, char *pc, char *pd)
{
    int na, nb, nc,i,a;
    na = strlen(pa);
    nb = strlen(pb);
    nc = strlen(pc);

    for(i=0, a=0; i <na-nb; i++, a++){
        if(strncmp(pa+i, pb, nb) == FOUND){
            strcpy(pd+a, pc);
            i += nb-1;
            a += nc-1;
        }
        else
            *(pd + a)=*(pa + i);
    }
    strcpy(pd + a, pa + i);
}

どうだろうか、いろいろ工夫を凝らすことが上達への近道だ。

3 タブ処理

今度は、文字置換のアルゴリズムを利用してタブをスペースに変換したり、スペースをタブに変換してみよう。

水平タブの文字定数は'\t'であり、16進数では0x09である。

タブは次のタブ位置までスペースで埋めて、次のタブ位置から次の文字を表示させる機能を持つ。

たとえばタブ幅を8文字に設定するならば、{'a', 'k', 'i', 'r', 'a', '\t', 'p', 'e', 'l', 'l', '\n'}という文字列は、

"akira   pell"と表示され、pellは9文字目から始まる。

これを利用してタブをスペースに置き換えるプログラムを考えよう。

タブを含む文字列a[i]をi=0からa[i]=NULLまで見て行き、

a[i]='\t'の場合、次のタブ位置の前までスペースを埋める。次のタブ位置は、文字列配列b[]のこれから格納しようとする要素の添字を j とすると、

次のタブ位置 = ( j / タブ幅 + 1) ×タブ幅  

で求められる。

つまり、タブ幅を8とすると、タブ位置は8,16,24,32となっているわけだ。

a[i] != '\t' のときはa[i]をそのままb[j]に格納する。

配列b[]の最後にNULLを格納する。

さあ、フログラムを見てみよう。これは上記の理論どおり書けば良いだけだ。

#include <stdio.h>

void henkan(char *, char *, int);
void main()
{
    char a[] = "Akira    Pell    Bruce     Lee    Dragon";
    char *pa;
    char b[100];
    char *pb;
    int t_w;

    pa=a;
    pb=b;

    printf("TABの字数を入れてください : ");
    scanf("%d",&t_w);

    henkan(pa, pb, t_w);

    printf("-----Result----\n");
    printf("%s\n",pb);
}

void henkan(char *pa, char *pb, int n)
{
    int n_t;
    int i, j ;

    for(i = 0, j = 0 ; *(pa + i) != NULL ; i++){
        if(*(pa+i) == '\t'){
            n_t =( j / n + 1) * n;
            for( ; j < n_t ; j++) *( pb +j ) = ' ';
        }
        else
            *(pb + (j++)) = *(pa + i);
    }
    *( pb + j )=NULL;
}

本当は元の文字列もscanf関数で入力させたかったが、タブキーを押すとタブ幅分飛んでしまい。プログラム結果と比較できないので残念であった。

次は逆に、タブを含まない文字列a[]をタブを含む文字列b[]に格納してみよう。

タブを含まない文字列a[i]をi=0からNULLまで見て、

a[i]がスペースでないとき、b[j] = a[i]と、そのままコピーする。

a[i]がスペースの場合、b[j]へコピーしないで、i をstにコピーする。そして、スペースが始まったことを示すフラグをONにし、次のタブ位置を計算する。つまり、次のタブ位置 = ( i / タブ幅 + 1) ×タブ幅。

フラグONのまま、つまりずっとスペースが連続して次のタブ位置まで来たとき、b[j]に'\t'をコピーし、フラグをOFFにしてやる。

これが、次のタブ位置に達するまでに、スペース以外の文字が来た場合は、進んだ分のスペースとa[i]を配列のb[]にコピーしてやる。この場合コピーするスペースはstからiまでの分があるので注意すること。このときもフラグはOFFしておこう。

文字列がNULLだけでもNULLをコピーする、つまり1回は必ず処理するという意味でdo〜while文を使うのに適しているね。 

今回は変換したい文字列とタブ幅はscanf関数でキーボードから入力させよう。

#include <stdio.h>

#define NASHI 0
#define ARI 1

void henkan(char *, char *, int);

void main()
{
    char a[81];
    char *pa;
    char b[81];
    char *pb;
    int i, n;
    int t_w;

    pa=a;
    pb=b;

    printf("変換する文字列を入れてください(80文字以内) : ");
    gets(pa);

    printf("タブの幅を入れてください : ");
    scanf("%d",&t_w);

    henkan(pa, pb, t_w);

    printf("-----Result-----\n");
    printf("最初の文字列の16進表示\n");
    for(i = 0, n = 1; *(pa+i) != NULL; i++, n++){
        printf("%2.2X ",*(pa+i));
        if(n % 8 == 0) printf(",");
    }
    printf("%2.2X\n",*(pa+i));

    printf("変換後文字列の16進表示\n");
    for(i=0; *(pb+i) != NULL; i++){
        printf("%2.2X ",*(pb+i));
        if(*(pb+i) == '\t') printf(",");
    }
    printf("%2.2X\n",*(pb+i));
}

void henkan(char *pa, char *pb, int n)
{
    int flg;
    int i = -1;
    int j = 0;
    int n_t = 79;
    int st;

    flg = NASHI;

    do{
            i++;
            if(flg == ARI && i == n_t){ /*80文字目で切る*/
                *(pb + (j++)) = '\t';
                flg = NASHI;
            }

                if(flg == NASHI){
                    if(*(pa+i) != ' ') *(pb+(j++)) = *(pa+i);
                    else{
                        flg = ARI;
                        st = i;
                        n_t = (i/n + 1) * n;
                    }
                }
                else{
                    if(*(pa+i) != ' '){
                        for( ; st<=i ; st++) *(pb+(j++)) = *(pa+st);
                        flg=NASHI;
                    }
                }
    } while( *(pa+i) != NULL );
}

どうだろうか、結果は16進数表示にしないとよく分からない。16進数表示では、20がスペース、09がタブを意味する。8文字ごとにカンマで区切ってなんとか結果が分かるような感じだね。

結果の見づらいプログラムは困るね。今度はファイルを読み込んで、スペース⇔タブ変換を実施しよう。

いまの両方の変換関数を記述するわけだが、実行はコマンドラインから、プログラム名に続いて、次の仮引数をt8またはs8のようにする。これは、tならタブをスペースに変換し、sならスペースをタブに変換すると言う意味で続く数字はタブ幅とする。よってタブ幅8としてタブをスペースに変換するならt8のように、タブ幅8でスペースをタブに変換する場合はs8と仮引数を入れる。

そして、つづいて、変換するテキストファイル、変換後のテキストファイルと入れる。たとえば、タブ幅8で、スペースをタブに変換する場合で、変換するテキストフォイルをspace.txt、書き込みファイルをtab.txtとすると、

プログラム名 t8 space.txt tab.txt のようにコマンドラインから入力しよう。それでは見て行こう。

#include <stdio.h>
#include <stdlib.h>

#define NASHI 0
#define ARI 1
#define GYOMAX 256

void file_tab(FILE *, FILE *, char *);
void tab_space(char *, char *, int);
void space_tab(char *, char *, int);

void main(int argc, char **argv)
{
    FILE *fp_in;
    FILE *fp_out;

    if(argc!=4)
        printf("パラメータ エラー\n");
    else{
        if((fp_in=fopen(*(argv+2),"r"))==NULL)
            printf("入力ファイルが開けません\n");
        else{
            if((fp_out=fopen(*(argv+3),"w"))==NULL)
                printf("出力ファイルがオープンできません\n");
            else{
                file_tab(fp_in, fp_out, *(argv+1));
                fclose(fp_out);
            }
            fclose(fp_in);
        }
    }
}

void file_tab(FILE *fp_in, FILE *fp_out, char *p)
{
    char a[GYOMAX];
    char b[GYOMAX];
    char *pa;
    char *pb;
    int t_w;

    pa=a;
    pb=b;

    if(*p == 't' || *p == 's'){
        t_w = atoi(p+1);
        while(fgets(pa, GYOMAX, fp_in) != NULL){
            switch(*p){
                case 't':
                    tab_space(pa,pb,t_w);
                    break;
                case 's':
                    space_tab(pa,pb,t_w);
                    break;
            }
            fputs(pb, fp_out);
        }
    }
    else printf("パラメータエラー");
}

void tab_space(char *pa, char *pb, int w)
{
    int n_t;
    int i, a;

    for(i=0,a=0 ; *(pa+i) != NULL ; i++){
        if(*(pa+i) == '\t'){
            n_t = (i/w + 1) * w;
            for( ; a<n_t ; a++) *(pb+a) = ' ';
        }
        else
            *(pb+(a++))=*(pa+i);
    }
    *(pb+a)=NULL;
}

void space_tab(char *pa, char *pb, int w)
{
    int flg = NASHI;
    int i = -1;
    int j = 0;
    int st;
    int n_t = GYOMAX-1;

  do{
        i++;
       
        if(flg == ARI && i == n_t){
            *(pb+(j++)) = '\t';
            flg = NASHI;
        }

        if(flg == NASHI){
            if(*(pa+i) != ' ') *(pb+(j++)) = *(pa+i);
            else{
                flg = ARI;
                st = i;
                n_t = (i/w + 1) * w;
            }
        }
        else{
            if(*(pa+i) != ' '){
                for( ; st <= i ; st++) *(pb+(j++)) = *(pa+st);
                flg = NASHI;
            }
        }

  } while(*(pa+i) != NULL);

}

いかがだろうか。これもテキストファイルの文字列に8桁ごとにカンマでも入れておいた方が分かりやすいね。

これらは基本書に掲載されている例題であり、特に情報処理技術者試験を受ける人は、ここでしっかり理解しておいた方がいいだろう。

4 数値の編集

標準ライブラリに文字列を数値に変えるatoi関数があったが、数値を文字列に変える標準ライブラリはない。

したがってC言語の基本とおり自分で作らなければならない。

与えられた整数を文字列に変換するが、負の整数は最初に'-'を付けて、指定された桁に右詰めの文字列に書き換えよう。また、桁が不足する場合は*で全体を埋めることとしよう。また、3桁ごとにカンマを加えよう。

方法としては、最初に配列全体をスペースで埋める。その後、配列の最後にNULLを加えて、与えられた整数の最下位の桁から文字定数に変えて配列に入れて行けばいい。

x = 整数 % 10 で最下位の桁の数値が算出できる。

次に整数を文字に変えるのは、'0' + x で簡単に文字定数を求められる。

3桁ごとのコンマが来る位置は、桁数-今の位置が4の倍数のときだ。

この条件でプログラム作成といこう。

#include <stdio.h>

char *itoan(int, int, char *);

void main()
{
    int x;
    char s[11];
    char *ps;
    int n;

    ps = s;

    printf("数値を入力してください : ");
    scanf("%d",&x);

    printf("桁数を入力してください(10桁以内) : ");
    scanf("%d",&n);

    ps=itoan(x, n, ps);

    printf("-----Result-----\n");
    printf("数値は%5d 桁数は%2d 文字列は%s\n",x,n,ps);
}

char *itoan(int x, int n, char *ps)
{
    int xw;
    int nw;
    int i;

    for(i=0; i < n; i++) *( ps + i) = ' '; /*スペースで配列の初期化*/
    xw = x;
    nw = n;

    if (x < 0) x *= (-1); /*負を正に直しておく*/
    *(ps+(n--)) = NULL; /*最後にNULL、1つ位置を戻す*/

    do{
        if( (n >= 0) && ( (nw-n) % 4 == 0)) *(ps+(n--)) = ',';
        if(n < 0){
            for(i = 0; i < nw; i++) *(ps+i) = '*'; /*桁不足と判明*/
            x = 0;
        }
        else{
            *( ps + (n--))= x % 10 + '0'; /*10で割った余りは最下位桁の値*/
            x /= 10;
        }
    }while(x != 0);

    if(xw < 0){
        if(n >= 0) *(ps+n) = '-';
        else{
            for (i = 0; i < nw; i++) *(ps+i) = '*'; /*桁不足と判明*/
        }
    }
    return ps;
}

ちょっとしたアイデアでいろんなことができるんだね、C言語は。

 

それでは、演習編1の最後に第二種情報処理試験に出題された例題を見てみよう。

端末から文字列を1行入力gets関数で読み込んで、スペースまたはカンマを区切り文字として、その読み込んだ文字列を単語に分けるプログラムである。

分けた単語は、文字列(各単語)へのポインタとその単語の文字数(整数値)をメンバとする構造体配列に格納するという極めて短い簡単なプログラムである。

さっそく見てみよう。

#include <stdio.h>

typedef struct tango{
    char *pa;
    int na;
}DATA;

int get_tango(char *, DATA *);

void main()
{
    char a[80];
    char *pa;
    DATA tango_tbl[20];
    DATA *pt;
    int n, i;

    pa = a;
    pt = tango_tbl;

    printf("文字列を入力してください : ");
    gets(a);

    n = get_tango(pa, pt);

    for(i=0; i<n;i++) printf("%d %.*s    %d文字\n",i+1, (pt+i)->na,(pt+i)->pa,(pt+i)->na);
}

int get_tango(char *pa, DATA *pt)
{
    int flag =0;
    int n=0;

    while(*pa != NULL){
        if(flag == 0){
            if(*pa != ' ' && *pa != ','){
                flag = 1;
                n++;
                pt->pa = pa;
                pt->na = 1;
            }
        }
        else{
            if (*pa == ' ' || *pa == ','){
                flag = 0;
                pt++;
            }
            else
                (pt->na)++;
        }
        pa++;
    }
    return n;
}

いかがだろうか。なんかナメてんのかと言いたくなるところだろう。

関数には構造体へのポインタを渡している私の好きな方法である。こうすれば、ポインタをインクリメントするだけで次の配列に移動できるからである。

しかし、問題は赤字の部分が穴埋め式のところだろう。情報処理技術者試験とは全員落としたところで誰も損はしない、つまり落とすための試験であると言っても過言ではない。

これに打ち勝つには、絶え間ない勉強、プラス、試験問題に対する親しみと慣れしかないだろう。

back  next


HOME