M.Hiroi's Home Page

Linux Programming

お気楽C言語プログラミング超入門

[ PrevPage | Clang | NextPage ]

Yet Another Clang Problems (3)

●問題21

ファイルをコピーするプログラムを作ってください。コマンドラインでファイル名が省略された場合は標準入出力を使うものとします。

prog [input_file [output_file]]

解答

●問題22

ファイルの文字数 (ファイルサイズ) と行数をカウントするプログラムを作ってください。コマンドラインでファイル名が省略された場合は標準入力を使うものとします。なお、Unix 系 OS には同等の機能を持つコマンド wc があります。

prog [input_file]

解答

●問題23

ファイルから文字列を探索し、見つけたらその行を出力するプログラムを作ってください。コマンドラインでファイル名が省略された場合は標準入力を使うものとします。

なお、Unix 系 OS には正規表現を使って文字列を検索するコマンド grep があります。

prog key [input_file]

解答

●問題24

ファイルを読み込んで、ある特定の文字を他の文字で置換するプログラムを作ってください。コマンドラインでファイル名が省略された場合は標準入出力を使うものとします。

コマンドラインの第 1 引数が置換対象となる文字、第 2 引数が置き換える文字とします。これらの引数は文字列で指定することができ、たとえば prog abc ABC とすると、a -> A, b -> B, c -> と置換します。引数の長さが異なる場合はエラー終了するものとします。

なお、Unix 系 OS にはもっと多くの機能を持つコマンド tr があります。

prog char_set1 char_set2 [input_file [output_file]]

解答

●問題25

ファイルを読み込んで、第 1 引数で指定した文字列を、第 2 引数で指定した文字列に置換するプログラムを作ってください。コマンドラインでファイル名が省略された場合は標準入出力を使うものとします。

なお、Unix 系 OS ではコマンド sed で文字列の置換を行うことができます。もちろん、正規表現も利用することができます。

prog key_str replace_str [input_file [output_file]]

解答

●問題26

ファイルの単語をカウントするプログラムを作ってください。単語は空白文字で区切られた文字列とします。コマンドラインでファイル名が省略された場合は標準入力を使うものとします。

なお、Unix 系 OS には同等の機能を持つコマンド wc があります。

prog [input_file]

解答

●問題27

タブを空白文字に展開するプログラムを作ってください。タブの値は 8 個とします。コマンドラインでファイル名が省略された場合は標準入出力を使うものとします。

なお、Unix 系 OS には同等の機能を持つコマンド expand があります。

prog [input_file [output_file]]

解答

●問題28

空白文字をタブに置き換えるプログラムを作ってください。タブの値は 8 個とします。コマンドラインでファイル名が省略された場合は標準入出力を使うものとします。

なお、Unix 系 OS には同等の機能を持つコマンド unexpand があります。

prog [input_file [output_file]]

解答

●問題29

ファイルを「ランレングス」で符号化するプログラムを作ってください。

ランレングスとは「連続して現れるものの長さ」という意味で、データ内で同じ値が並んでいる場合はその値と個数で符号化する方法のことを、「ランレングス圧縮」または「ランレングス符号化」といいます。

ランレングスはとても簡単な符号化方式ですが、それでもいくつかの方法が考えられます。いちばん簡単な方法は、データの値とデータの個数で表す方法です。たとえば、次の文字列を考えてみましょう。

文字列  abccddeeeeffffgggggggghhhhhhhh  (30)

同じ文字が連続して並んでいますね。これを文字と個数で表すと、次のようになります。

=>1b1c2d2e4f4g8h8  (16)

元データ 30 文字を 16 文字で表すことができます。また、復号も簡単にできることはすぐにわかると思います。このように、ランレングスはとても単純な方法ですが、画像データには大きな効果を発揮する場合があります。たとえば、白黒の画像ではデータが 2 種類しかないので、最初のデータが白か黒のどちらであるか区別できれば、あとは個数だけの情報でデータを圧縮することができます。

prog input_file output_file

解答

●問題30

問題 29 のランレングスで符号化したファイルを展開するプログラムを作ってください。

prog input_file output_file

解答


●解答21

リスト : ファイルのコピー

#include <stdio.h>

#define N 1024

// ファイルのコピー
void copy(FILE *in, FILE *out)
{
  int c;
  while ((c = fgetc(in)) != EOF) fputc(c, out);
}

void copy1(FILE *in, FILE *out)
{
  char *buff[N];
  int size;
  do {
    size = fread(buff, sizeof(char), N, in);
    fwrite(buff, sizeof(char), size, out);
  } while (size == N);
}

int main(int argc, char *argv[])
{
  FILE *in = stdin;
  FILE *out = stdout;
  if (argc >= 2) {
    in = fopen(argv[1], "r");
    if (in == NULL) {
      perror(argv[1]);
      return 1;
    }
    if (argc >= 3) {
      out = fopen(argv[2], "w");
      if (out == NULL) {
        perror(argv[2]);
        fclose(in);
        return 1;
      }
    }
  }
  copy1(in, out);
  fclose(in);
  fclose(out);
  return 0;
}

変数 in を stdin に、out を stdout に初期化します。そして、argc が 2 以上であれば、入力ファイル名の指定があるので、fopen で argv[1] をリードオープンして変数 in にセットします。返り値が NULL であれば、perror でエラーメッセージを表示して異常終了します。なお、これ以降の問題で、コマンドライン引数の処理はほとんど同じなので、説明は割愛することにします。

argc が 3 以上であれば出力ファイル名の指定があります。fopen で argv[2] をライトオープンします。返り値が NULL であれば、in をクローズしてから異常終了します。あとは、関数 copy (または copy1) を呼び出してファイルをコピーし、最後に in と out を fclose してから終了します。

関数 copy は fgetc と fputc を使ってファイルをコピーします。これは簡単ですね。関数 copy1 は fread と fwrite を使ってファイルをコピーします。バッファの大きさは N (1024) としました。fread は読み込んだデータ数を返します。この値が N よりも小さい場合はファイルを最後まで読み込んだことがわかります。do - while 文で、読み込んだデータ数 size が N と等しければ処理を繰り返します。

●解答22

リスト : ファイルの文字数と行数

#include <stdio.h>

int main(int argc, char *argv[])
{
  FILE *fp = stdin;
  if (argc >= 2) {
    fp = fopen(argv[1], "r");
    if (fp == NULL) {
      perror(argv[1]);
      return 1;
    }
  }

  int c, line = 0, size = 0;
  while ((c = fgetc(fp)) != EOF) {
    if (c == '\n') line++;
    size++;
  }
  printf("size = %d, line = %d\n", size, line);
  fclose(fp);
  return 0;
}

行数を変数 line, 文字数を変数 size でカウントします。どちらの変数も 0 で初期化します。あとは、fgetc で 1 バイトずつ読み込み、それが改行文字ならば line を +1 します。それから size を +1 します。最後に、size と line を printf で表示するだけです。

●解答23

リスト : 文字列の探索

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

#define N 1024

void search_key(const char *key, FILE *fp)
{
  char buff[N];
  while (fgets(buff, N, fp) != NULL) {
    if (strstr(buff, key) != NULL)
      printf("%s", buff);
  }
}

int main(int argc, char *argv[])
{
  if (argc < 2) {
    fprintf(stderr, "引数が足りません\n");
    return 1;
  }
  FILE *in = stdin;
  if (argc >= 3) {
    in = fopen(argv[2], "r");
    if (in == NULL) {
      perror(argv[2]);
      return 1;
    }
  }
  search_key(argv[1], in);
  fclose(in);
  return 0;
}

文字列の検索は関数 search_key で行います。search_key は fgets で fp から 1 行ずつバッファ buff に読み込み、ライブラリ (string.h) 関数 strstr で buff から key を探します。見つかった場合は printf で buff を出力します。

なお、ここではプログラムを簡単にするため、バッファの大きさを N (1024) に設定しています。1023 文字よりも長い行があると正常に動作しません。興味のある方は、この欠点を修正してみてください。

●解答24

リスト : 文字の置換

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

// 文字 key を検索して、その位置を返す
int position(const char key, const char *buff)
{
  for (int i = 0; buff[i] != '\0'; i++) {
    if (buff[i] == key) return i;
  }
  return -1;
}

// 文字の置換
void tr(const char *s1, const char *s2, FILE *in, FILE *out)
{
  int c;
  while ((c = fgetc(in)) != EOF) {
    int i = position(c, s1);
    if (i >= 0) c = s2[i];
    fputc(c, out);
  }
}

int main(int argc, char *argv[])
{
  if (argc < 3) {
    fprintf(stderr, "引数が足りません\n");
    return 1;
  }
  if (strlen(argv[1]) != strlen(argv[2])) {
    fprintf(stderr, "%s と %s の長さが違います\n", argv[1], argv[2]);
    return 1;
  }
  FILE *in = stdin;
  FILE *out = stdout;
  if (argc >= 4) {
    in = fopen(argv[3], "r");
    if (in == NULL) {
      perror(argv[3]);
      return 1;
    }
    if (argc >= 5) {
      out = fopen(argv[4], "w");
      if (out == NULL) {
        perror(argv[4]);
        return 1;
      }
    }
  }
  tr(argv[1], argv[2], in, out);
  fclose(in);
  fclose(out);
  return 0;
}

最初に引数をチェックして、argv[1] と argv[2] の長さが異なればエラー終了します。実際の処理は関数 tr で行います。fgetc で in から 1 バイト読み込み、それが文字列 s1 にあるか関数 position で検索します。position は単純な線形探索です。見つかった場合は変数 c の値を s2[i] に書き換えます。あとは文字 c を fputc で出力します。

●解答25

リスト : 文字列の置換

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

#define N 1024

void replace(const char *key, const char *s, FILE *in, FILE *out)
{
  char buff[N];
  int len = strlen(key);
  while (fgets(buff, N, in) != NULL) {
    char *p;
    char *q = buff;
    while ((p = strstr(q, key)) != NULL) {
      *p = '\0';
      fprintf(out, "%s%s", q, s);
      q = p + len;
    }
    fprintf(out, "%s", q);
  }
}

int main(int argc, char *argv[])
{
  FILE *in = stdin;
  FILE *out = stdout;
  if (argc < 3) {
    fprintf(stderr, "引数が足りません\n");
    return 1;
  }
  if (argc >= 4) {
    in = fopen(argv[3], "r");
    if (in == NULL) {
      perror(argv[3]);
      return 1;
    }
    if (argc >= 5) {
      out = fopen(argv[4], "w");
      if (out == NULL) {
        perror(argv[4]);
        return 1;
      }
    }
  }
  replace(argv[1], argv[2], in, out);
  fclose(in);
  fclose(out);
  return 0;
}

文字列の置換は関数 replace で行います。replace は fgets で fp から 1 行ずつバッファ buff に読み込みます。変数 len は文字列 key の長さをセットします。一致する文字列をすべて置換するため、関数 strstr の返り値が NULL になるまで while ループで処理を繰り返します。変数 q が検索開始位置を表します。最初は buff に初期化します。strstr の返り値は変数 p にセットします。

p が NULL でなければ、key が見つかりました。*p にヌル文字をセットすると、fprintf の %s 変換指示子で q から p - 1 までの内容を出力することができます。そのあと引数 s を続けて出力すれば、文字列 key を s に置換することができます。そして、検索開始位置 q を p + len に更新します。見つからない場合は q からバッファの最後まで fprintf で出力します。

なお、ここではプログラムを簡単にするため、バッファの大きさを N (1024) に設定しています。1023 文字よりも長い行があると正常に動作しません。興味のある方は、この欠点を修正してみてください。

●解答26

リスト : 単語のカウント

#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>

int wc(FILE *fp)
{
  bool inword = false;
  int c, n = 0;
  while ((c = fgetc(fp)) != EOF) {
    if (isspace(c))
      inword = false;
    else {
      if (!inword) {
        inword = true;
        n++;
      }
    }
  }
  return n;
}

int main(int argc, char *argv[])
{
  FILE *fp = stdin;
  if (argc >= 2) {
    fp = fopen(argv[1], "r");
    if (fp == NULL) {
      perror(argv[1]);
      return 1;
    }
  }
  printf("%d\n", wc(fp));
  return 0;
}

単語のカウントは関数 wc で行います。単語を読んでいるときは、変数 inword を true にし、空白文字が現れたら inword を false にします。そして、inword を false から true に変更するとき、単語の個数 n をインクリメントします。空白文字のチェックはライブラリ (ctype.h) の関数 isspace で簡単に行うことができます。

<ctype.h>
int isspace(int c);

isspace は文字 c が空白文字であれば真を返します。ASCII コードの場合、isspace が真を返す空白文字には次の種類があります。

' '  : 空白     (0x20)
'\t' : 水平タブ (0x09)
'\n' : 改行     (0x0a)
'\v' : 垂直タブ (0x0b)
'\f' : 書式送り (0x0c)
'\r' : 復帰     (0x0d)

●解答27

リスト : タブの展開

#include <stdio.h>

void detab(FILE *in, FILE *out)
{
  int col = 0;
  int c;
  while ((c = fgetc(in)) != EOF) {
    if (c == '\t') {
      do {
        fputc(' ', out);
        col++;
      } while (col % 8 != 0);
    } else {
      if (c == '\n')
        col = 0;
      else
        col++;
      fputc(c, out);
    }
  }
}

int main(int argc, char *argv[])
{
  FILE *in = stdin;
  FILE *out = stdout;
  if (argc >= 2) {
    in = fopen(argv[1], "r");
    if (in == NULL) {
      perror(argv[1]);
      return 1;
    }
    if (argc >= 3) {
      out = fopen(argv[2], "w");
      if (out == NULL) {
        perror(argv[2]);
        fclose(in);
        return 1;
      }
    }
  }
  detab(in, out);
  fclose(in);
  fclose(out);
  return 0;
}

実際の処理は関数 detab で行います。変数 col が欄の位置を表します。fgetc で読み込んだ文字 c がタブ (\t) の場合、col % 8 が 0 になるまで fputc で空白文字を出力します。これでタブを空白に展開することができます。c が改行文字ならば col を 0 にリセットします。そうでなければ、col をインクリメントして、文字 c を fputc で出力します。

●解答28

リスト : 空白をタブに置換

#include <stdio.h>

void entab(FILE *in, FILE *out)
{
  int col = 0;
  int c;
  while ((c = fgetc(in)) != EOF) {
    int sc = 0;
    // 空白を集める
    if (c == ' ') {
      do {
        sc++;
        col++;
        if (col % 8 == 0) {
          fputc('\t', out);
          sc = 0;
        }
      } while ((c = fgetc(in)) == ' ');
      if (sc > 0) while (sc-- > 0) fputc(' ', out);
    }
    if (c == '\n') {
      col = 0;
    } else {
      col++;
    }
    fputc(c, out);
  }
}

int main(int argc, char *argv[])
{
  FILE *in = stdin;
  FILE *out = stdout;
  if (argc >= 2) {
    in = fopen(argv[1], "r");
    if (in == NULL) {
      perror(argv[1]);
      return 1;
    }
    if (argc >= 3) {
      out = fopen(argv[2], "w");
      if (out == NULL) {
        perror(argv[2]);
        return 1;
      }
    }
  }
  entab(in, out);
  fclose(in);
  fclose(out);
  return 0;
}

実際の処理は関数 entab で行います。変数 col が欄の位置を表します。fgetc で文字を読み込み、文字 c が空白であれば、連続している空白の個数を変数 sc に求めます。このとき、col % 8 が 0 になったならば、連続している空白をタブに置換することができます。fputc でタブを出力して、sc の値を 0 にリセットします。

do - while ループが終了して、sc が 0 よりも大きい場合、その空白はタブに置換することができません。fputc で空白を sc 個だけ出力します。文字 c が空白文字でなければ、文字 c が改行文字化チェックします。そうであれば、col を 0 にリセットします。あとは、文字 c をそのまま fputc で出力するだけです。

●解答29

リスト : ランレングス符号化

#include <stdio.h>
#include <stdbool.h>

void encode(FILE *in, FILE *out)
{
  int c = fgetc(in);
  if (c == EOF) return;
  do {
    int n = 0;
    int x = c;
    while (n < 255 && (c = fgetc(in)) == x) n++;
    fputc(x, out);
    fputc(n, out);
  } while (c != EOF);
}

int main(int argc, char *argv[])
{
  if (argc < 3) {
    fprintf(stderr, "引数が足りません\n");
    return 1;
  }
  FILE *in = fopen(argv[1], "r");
  if (in == NULL) {
    perror(argv[1]);
    return 1;
  }
  FILE *out = fopen(argv[2], "w");
  if (in == NULL) {
    perror(argv[2]);
    return 1;
  }
  encode(in, out);
  fclose(in);
  fclose(out);
  return 0;
}

実際の処理は関数 encode で行います。最初に fgetc で 1 文字読み込みます。EOF であれば return で処理を終了します。次の do - while ループで、ファイルの終了まで処理を繰り返します。この中で文字 c を変数 x にセットして、文字 x が連続していれば、その個数を変数 n でカウントします。このとき、n の値は 個数 - 1 になることに注意してください。

n が 255 になる、もしくは x と c が異なる場合は、while ループを終了します。たとえば、a が 512 個続いていたとすると、符号化は a, 255, a, 255 になります。あとは、x と n を fputc で出力するだけです。

ところで、通常のテキストファイルは、同じ文字が続くことはあまりないので、この方法ではほとんど効果がありません。次の例を見てください。

abccdd (6) => a1b1c2d2 (8)

ランレングスを使うと、6 文字のデータが 8 文字に増えてしまいます。これではデータを圧縮するどころか、かえって膨らませることになってしまいます。もしも、同じデータが一つも連続していない場合、たとえば "abcdefgh" をランレングスで符号化すると、"a1b1c1d1e1f1g1h1" のようにデータ数は 2 倍にまで増えてしまいます。

これを軽減するアルゴリズムがいくつか考案されています。興味のある方は拙作のページ Algorithms with Python 連長圧縮 をお読みください。

●解答30

リスト : ランレングス復号

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

void decode(FILE *in, FILE *out)
{
  int c;
  while ((c = fgetc(in)) != EOF) {
    int x = fgetc(in);
    if (x == EOF) {
      fprintf(stderr, "圧縮ファイルが壊れています\n");
      exit(1);
    }
    while (x-- >= 0) fputc(c, out);
  }
}

int main(int argc, char *argv[])
{
  if (argc < 3) {
    fprintf(stderr, "引数が足りません\n");
    return 1;
  }
  FILE *in = fopen(argv[1], "r");
  if (in == NULL) {
    perror(argv[1]);
    return 1;
  }
  FILE *out = fopen(argv[2], "w");
  if (out == NULL) {
    perror(argv[2]);
    return 1;
  }
  decode(in, out);
  fclose(in);
  fclose(out);
  return 0;
}

実際の処理は関数 decode で行います。処理は簡単で、ファイルから 2 バイト読み込み、変数 c と x にセットします。あとは、文字 c を x 個 fputc で出力するだけです。これでランレングス符号化されたファイルを復号することができます。


初版 2015 年 2 月 28 日
改訂 2023 年 4 月 2 日

Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Clang | NextPage ]