見出し画像

C言語教室 解答編 第12回(続)

今回は、C言語教室 解答編 第12回 と オーバーフローのはなし

の続きで、以下の教室で出た課題と演習の解答編です。

C言語教室 第12回(続) - 課題と演習

まだ課題や演習に挑戦していなければ、ここから先は読まずに挑戦していただければ幸いです。頂いた答案に関しては、解答例の後にコメントさせていただきます。というころで恒例の「うっかり解答をみないための閑話」です。


今回の演習では、main関数の引き数についてを出したのですが、ここにはコマンドラインからプログラムを走らせた時に、どのように呼び出したかの情報が入ります。GUI や、他のプログラムから直接、走らせたときには何も入らないこともありますが、何らかの方法(メニューコマンドなど)でコマンドラインから走らせたときと同じような情報を指定することが出来るようになっているはずです。

環境によっては指定する方法がないこともあると思うので、この課題の為にダミーの main を呼び出す方法を以下に書きました。必要に応じて参考にしていただければと思います。

C言語 - main関数の引き数を組み立てる

コマンドラインからプログラムを実行する時に、* や ? といった記号を含んだ文字列をコマンド名に続けて打ち込むことがあると思います。C言語のお里である UNIX(linux)の sh(シェル)やbashなどは、シェルがこれらの記号を解釈してから、解釈した結果をプログラムに渡します。同じ UNIX でも csh (や tcsh)は、記号の解釈の仕方が少し変わるのですが、いずれにせよ、プログラムの側で解釈する必要はありません。* が指定されれば、カレント・ディレクトリのファイルとして、ファイル名に展開してからプログラムに渡されます(一部、例外はあります)。

ただあくまでこれはシェルの機能なので、MS-DOS の COMMAND には、この機能が無かったので、記号で表されるワイルド・カードなどの処理などを行いたければ、プログラムの方で解釈する必要がありました。今の CMD や PowerShell は * を展開してくれるようですが、今度はエスケープの表現に癖があります。

演習のプログラムを用意できたら、いろいろな方法で、そのプログラムを走らせて、どのように引き数が展開されるのか確かめると面白い発見があると思います。シェルやコマンドプロンプトの勉強が必要ですが、* や ? だけではなく、. や () “ ‘ ` \ あたりの記号にどのような意味があるのかよくわかります。


では、そろそろ課題に戻ろうと思います。今回は前回の続きですので、課題8からです。

課題8
引き数で渡された文字列の中に、もっとも多く含まれる文字と、その数を返す関数を書きなさい。含まれる文字の数が同じ場合には、どの文字を返しても良い。

課題7で、数値に関して最小値と、最小値を持つ要素の数を数えたので、今回は、それを応用すればと考えていたのですが、文字コードをそのまま添字として使おうとしたために、char と int の間の型変換がややこしくなってしまいました。

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

void clear_array(int* p, int size) {
  int i;

  for (i = 0; i < size; i++)
    *p++ = 0;
}

int search_max(int *table, int size, int *count) {
  int *p = table;
  int max;
  int i;

  max = *p++;
  *count = 1;
  for (i = 1; i < size; i++) {
    if (max == *p)
      *(count++);
    else if (max < *p) {
      max = *p;
      *count = 1;
    }
    p++;
  } 
  return max;
}

int max_array(int *p, int size, int* pos) {
  int max = *p;
  int i;

  *pos = 0;
  p++;
  for (i = 1; i < size; i++) {
    if (*p > max) {
      max = *p;
      *pos = i;
    }
    p++;
  }
  return max;
}

#define CHAR_SIZE (256)

int most_common_char(char* src, char* most) {
  int table[CHAR_SIZE];
  char *p = src;
  int max;
  int index;
  int i;

  clear_array(table, CHAR_SIZE);
  while(*p != '\0') {
    table[(unsigned char)(*p)]++;
    p++;
  }
  max = max_array(table, CHAR_SIZE, &index);
  *most = (char)index;
  return max;
}

#define SAMPLE_TEXT "ABCDBDEDBFJL[]*\xff\xff\xff\xff\xff*"

void main() {
  char src[] = SAMPLE_TEXT;
  char most_char;
  int count;

  count = most_common_char(src, &most_char);
  if (isprint(most_char))
    printf("Most common char:'%c(%d)'\n", most_char, count);
  else
    printf("Most common char:'&h%x(%d)'\n", (unsigned char)most_char, count);
}

このコード(文字列)だと結果は以下のようになります。

Most common char:'&hff(5)'

char が符号付きであるのに対し、int 配列の添字は負になってはいけないので、値が負になる128以上の文字に対して手当をしなければなりません。添字にする時に符号なし文字型に型変換することで回避しました。最も多く含まれる文字を返す際には、逆に添字は整数なので文字型に戻す必要があります。

最後に見つけた文字を表示する際に、見つかった文字が必ずしも表示できるとは限らないので、ひと工夫したのですが、ここでも符号が悪さをするので、型変換が必要でした。

どうもスッキリしないコードになってしまったので、もう少し上手に書けるような気もするのですが、あくまで「例」なので勘弁してください。

基本的な考え方としては、文字列に含まれる文字を文字コードを添え字として配列を使って文字ごとに数を数え上げ、数え上げた配列の最大値と、その文字を返すようにしています。このように返すものが複数ある場合は、次回以降で触れる構造体を使わないと、引き数で渡されたポインタで返すしかなく、ちょっとわかりにくくなっているかもしれません。


課題9
引き数で渡された3つの文字列を動的に確保した領域に連結して返す関数を書きなさい。2つの文字列を連結する関数を書いて使っても良い。stdlib.h にある realloc() も参考にすること。

動的な領域確保に malloc を使ってきましたが、既に確保した領域の大きさを変更する realloc を使うような課題を考えました。

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

char* string_concat(char *str1, char* str2) {
  char *rstr = NULL;
  char *temp;

  if (str1 != NULL) {
    rstr = (char*)malloc(strlen(str1) + 1);
    if (rstr == NULL)
      return NULL;
    strcpy(rstr, str1);
  }

  if (str2 != NULL) {
    if (rstr == NULL) {
      rstr = (char*)malloc(strlen(str2)+ 1);
      if (rstr == NULL)
        return NULL;
      strcpy(rstr, str2);
    } else {
      temp = (char*)realloc(rstr, strlen(rstr) + strlen(str2) + 1);
      if (temp == NULL) {
        free(rstr);
        return NULL;
      }
      rstr = temp;
      strcat(rstr, str2);
    }
  }
  return rstr;
}


char* string_triple_concat(char *s1, char *s2, char *s3) {
  char *rstr = NULL;

  rstr = string_concat(s1, s2);
  rstr = string_concat(rstr, s3);
  return rstr;
}

void main() {
  char str1[] = "ABCDEFG";
  char str2[] = "HIJKLMN";
  char str3[] = "OPQRSTU";
  char *str0;

  str0 = string_triple_concat(str1, str2, str3);
  printf("%s+%s+%s=%s\n", str1, str2, str3, str0);

  free(str0);
}

もちろん、一つの関数で一度に3つの文字列を連結してしまっても構いません。渡されたポインタが NULL に場合の処理は神経質にならなくても構いません。例ではあたかも空文字列が渡されたかのように振る舞うはずです。但し全てが NULL であったり領域が確保できない時は NULL が返ります。

例にあげたコード(文字列)では、結果は以下になります。

ABCDEFG+HIJKLMN+OPQRSTU=ABCDEFGHIJKLMNOPQRSTU

ところで realloc が失敗した時に元となる領域は、そのままとなるようなので、もう使わないのであればしっかり free する必要があるようです。こういうところで、メモリリークが発生しないように気を使う必要がありますね。


演習
main関数は実は int main(int argc, char** argv) という形でコマンドラインの引き数を受け取ることが出来ます。最初の引き数にコマンドライン引き数の数、次の引き数にコマンドラインで渡された文字列へのポインタが入っている配列が渡されます。これを解釈してコマンドラインで与えられた文字列を表示するコードを書きなさい。

2番めの引き数が char** という型で「なんじゃこりゃ」となったかもしれませんが、意味としては「文字列(へのポインタ)の配列」が渡されてくるという意味です。この領域はプログラムで解放する必要はありませんし(解放してはいけません)、読むだけなので決まりきった手順で使えば大丈夫です。

int main(int argc, char **argv) {
  int i;

  for (i = 0 ; i < argc ; i++ ) {
    printf("%s\n", argv[i]);
  }
  return 0;
}

結果は、このプログラムをどのように実行したかで大きく異なりますので、自分の環境でやってみてください。コマンド名の後ろにも空白で区切っていろいろな文字列を与えてみてください。 "*" をつけると、知らなければビックリするかもしれません。

./a.out *

閑話で触れましたが、コマンドラインから渡された文字列を使って処理を進めることが良くあります。対象となるファイルを受け取ったり、オプションを解釈したりします。オプションの指定方法がプログラムによって異なると使うのが大変なので、標準的な解釈としてライブラリ関数の getopt を使います。

#include <unistd.h>
int getopt(int argc, char * const argv[], const char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;

[getopt]

コマンドラインオプションの処理

この動作を理解すると、コマンドラインからオプションを与える時に役立つかもしれません。

なお、main関数の引き数は実はもう一つあります。そちらはいずれ。


さて、頂いた回答を見てみます。回答を作るのもなかなか大変なので、本当にありがとうございます。最初は Akio van der Meer さんからです。

(答案提出) C言語教室 第12回(続) - 課題と演習 - 2023.01.29更新

提出してから修正して頂いたようです。1.29版を見ています。

課題8については、動作確認はして頂いているので、概ね問題は無さそうですが、SAMPLE_TEXT を char* で宣言しているのが気になります。文字列リテラルは存在するだけで領域が確保されるので動作に影響はありませんが、char[] で宣言したほうがスッキリします。また文字コード自身を配列の添字として使っているので、おそらく文字に 0x80 以上の値が含まれると負の添字となってしまい、問題がでると思われます。ここは符号なし型への型変換を入れてください。せっかく表示可能な文字以外も対処したのであれば、最後に表示する時に、そのまま表示してしまうのはもったいないです。

課題9は、最初から realloc 攻めでわかりやすいです。領域が確保できなかったときに呼び出された関数の中から一気にプログラムを終わらせてしまうのは、ちょっとビックリですが、メモリが無いんであれば、これもアリかもしれません。でも異常終了なので引き数は0以外の方が良い気もします。

(参考) C言語のexit関数の使い方

演習は、コード自身よりも渡された引き数の使い方を覚えてほしいものでした。環境を用意しないとならないのですが、利用するシェルの種類でかなり結果が異なることがわかれば充分です。同じ呼び出し方を windows の powershell なんかでやってみると、こちらはまとめてくれました。

PS C:\Users\kazushi\Documents\langc> .\a.exe "Hello world"
C:\Users\kazushi\Documents\langc\a.exe
Hello world

C言語は書き方にかなり自由度があり、同じ目的であっても、かなり違うコードになることが普通です。チームで作業をしていると、コードを見ただけで誰が書いたのかがわかるようになるくらいです。動作確認ができていれば、必ずしも間違いということにはなりませんが、コードを利用する環境が変わった時にも耐えられるようにするには、いろいろと気を使う必要がありますし、コードを読んだ時に意図が伝わるようにするのが大切だとは思います。

これから、さらにややこしい内容に進んでいく予定ですが、ボチボチで構わないので、お付き合いいただければ幸いです。


次は AyumiKatayama さんの回答です。本当に毎回ありがとうございます。

 C言語教室 第12回(続) - 回答提出

課題8ですが、最初から文字の範囲を絞ったのは、ある意味正解です。出現頻度を調べるのには 0x100 全部調べるけど最大値を探すのは 0x80 未満だけ。フムフム。結果を書いていただいたのですが、結局、どの文字がいくつと表示されるのかがわからなかったので、添付していただいたコードをダウンロードして実行してみました。環境はWindows11にMinGWを入れた gcc です。

まず、微妙な違いで string.h の include が必要でした。それはさておき、あれぇ?途中で無限ループに入ってしまっているなぁ。count_char 関数内の

 for (c = 1; c < 0x80; ++c)

の部分ですが、c が char なので、そもそも常に 0x80 よりも小さいです。0x7f をインクリメントすると 0x80 にはなるのですが、これは -128 なので、やはり 0x80 よりも小さくループを抜けられません。そこでちょっと「コスイ」やりかたですが、ループが 1 から始まるので 

 for (c = 1; c > 0; ++c)

と変更したら無事にループから出られるようになりました。

str = #define M_AREA_RECT(side1, side2) ((side1) * (side2))
mac_char = e (6 times)

これもCコンパイラに依る差異なんでしょうかねぇ。

課題9は楽しんで頂けたようで何よりです。例で出したコードでは最初は malloc を使う形にしましたが、realloc 最強ですね。心配なので確認したところ「 ptr が NULL の場合には malloc(size) と等価である。 size が 0 で ptr が NULL でない場合には、 free(ptr) と等価である。 ptr が NULL 以外の場合、 ptr は以前に呼び出された malloc(), calloc(), realloc() のいずれかが返した値でなければならない。 ptr が指す領域が移動されていた場合は free(ptr) が実行される。」となっていました。注意点としては NULL を返した場合、ptr はそのまま確保されているので、使わないのであればちゃんと free しなければならない点です。

演習は、コードよりもシェルがワイルドカードをどのように展開するか、またいろいろなエスケープ文字がどう働くのかを試していただければと思います。

本当にお疲れさまでした。これからも末永くお付き合いいただければ幸い。

ヘッダ画像は、いらすとや さんより、
https://www.irasutoya.com/2020/04/blog-post_243.html

この記事が気に入ったらサポートをしてみませんか?