見出し画像

C言語教室 解答編 第10回

今回から解答編としてまとめることにしました。今回の課題と演習は以下の記事の中にあります。

C言語教室 第10回 - 文字種判定と文字コード

まだ課題をやってみようと思う方は、この記事を読まないで先に挑戦してみてください。

頂いた回答については、最後に追記していきます。この記事が出たからと言って締め切ることはしませんのでご心配なく。


という事で、うっかり中身を見ないために、少しばかり閑話など。

今回の教室では文字コードそのものに関するコメントを頂きました。コンピュータは2進数しか扱えませんから、文字を扱いたければ何らかの形で2進数で表現しなければなりません。最初はアルファベットのAからZと数字の0から9にコードを振れば間に合っていたのですが、そのうちプログラミング言語でも使う記号たちも入れる必要に迫られ、必要に応じて都度コードを決めていました。対象となる処理がひとつのコンピュータの中に閉じていればよかったのですが、データやプログラムを他のコンピュータでも使いたいのであれば、コードがバラバラなので、コード変換というのをいちいちやらなければならなかったようです。

そこで「交換用コード」という取り決めが出来て、他のコンピュータで使うときには、それぞれのコンピュータのコードから、この交換用コードに変換して渡す形になりました。これで多対多ではなくて1対多の関係に整理されたのでデータを渡すのが楽になったわけです。

今でも話題になる文字コードは、ほとんどの場合は、この交換用コードを指していて、文字コードの実装は機種ごと、OSごと、デバイスごとに独自の実装になっていることが普通です。という訳でプログラムを書くような人、特にC言語なんか使ってデバドラを書こうとすれば、この実装された方のコードを扱わなければならなくなります。

もっとも交換用コードも実に多くの種類と方言があって、WEBアプリケーションを書こうとするとサーバサイドとクライアントで文字コードが違うこともあり、バグなんか出ようものなら、訳がわからなくなることもシバシバです。

文字コードの話は、少しずつ書いていくつもりですので、このあたりにして、そろそろ本題に入りましょう。


まず課題の方からです。

呼び出し側で用意した文字列に含まれる英小文字をすべて英大文字に書き換える関数を書きなさい。

素直に書いてみました。

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

void string_upper(char *src, char *dst) {
  char *p = src;
  char *q = dst;

  while (*p != '\0') {
    *q++ = (char)toupper(*p++);
  }
  *q = '\0';
}

void main() {
  char src[] = "abcdefg";
  char *dst;

  dst = (char*)malloc(strlen(src)+1);

  string_upper(src, dst);

  printf("Source'%s'\n", src);
  printf("Upper '%s'\n", dst);

  free(dst);
}

実は呼び出し元からリテラルを渡して、それを書き換えてしまうこともC言語的には出来てしまうのですが、気持ちが悪いので別の領域を用意する形にしました。もちろん呼び出し元でコピーしてから引き数は書き換えられるんだよという使い方もできるとは思います。書き直してみると

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

void string_upper(char *s) {
  char *p = s;

  while (*p != '\0') {
    *p = (char)toupper(*p++);
  }
}

void main() {
  char src[] = "abcdefg";
  char *buf;

  buf = (char*)malloc(strlen(src)+1);

  strcpy(buf, src);
  string_upper(buf);

  printf("Source'%s'\n", src);
  printf("Upper '%s'\n", buf);

  free(buf);
}

こちらは strcpy で終端文字もコピーされているので、ヌル文字付加は不要ですね。toupperの行にあるpのインクリメントは左から評価されるので後ろのpでないとうまくいきません。これ言語仕様的に正しいのかなぁ?無理せず次の行でインクリメントしたほうが安全ですね。


次は演習です。演習はまだ手を付けていないという方は、ここから先を読んでしまってから挑戦しても良いですし、ここでページを閉じてイチから考えて頂いても構いません。

渡された文字列に表示文字以外が含まれる時、それらの文字を”\x”で始まる16進コードの文字列に変換する関数を書きなさい。なお表示文字列でも”\”に関しては”\\”に変換する。変換後の文字列領域は関数を呼び出す側で用意しても関数で動的に確保しても良い。変換後の文字列を格納する領域は最大でも元の文字列の4倍までであることは仮定しても良い。

説明文の中にある "なお表示文字列でも”\”に関しては”\\”に変換する。" という文章の後ろのバックスラッシュがコピペの時にバックスラッシュが1文字になってしまっていました。既に直してしまいましたが、意味のわからないことを書いてしまい申し訳ありませんでした。

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

void string_printable(char *s, char *d) {
  char tbl[] = "0123456789abcdef";
  char *p = s;
  char *q = d;

  while (*p != '\0') {
    if (isprint(*p)) {
      if (*p == '\\')
        *q++ = '\\';
      *q++ = *p++;
    } else {
      *q++ = '\\';
      *q++ = 'x';
      *q++ = tbl[(unsigned char)*p / 16];
      *q++ = tbl[(unsigned char)*p % 16];
      p++;
    }
  }
  *q = '\0';
}

void main() {
  char src[] = "abc\\\r\n\x03\x7f\x80\xff";
  char *buf;

  buf = (char*)malloc(strlen(src)*4);

  string_printable(src, buf);

  printf("printable'%s'\n", buf);

  free(buf);
}

これで以下のような結果が得られました。

printable'abc\\\x0d\x0a\x03\x7f\x80\xff'

シングルクオートで2文字を囲むのは気持ちが悪いのですが、これが正しい書き方です。元の文字列にあるバックスラッシュも2文字で1バイトですので、お間違えなく。16進数を作るところは sprintf を使う方法もあるのですが、テーブルを使いました。元の文字が 0x80~0xff の場合は、*pが負になるので、ここだけは符号なしにキャストしました。


頂いた回答を見てみます。最初は AyumiKatayama さんです。

C言語教室 第10回 文字種判定 答案提出

さすが慣れた感じですね。16進数を作るのには sprintf をお使いですか。後書きにも書いてありますけど、この”%02x”に対応する変数が負の値だと、どうやら破滅的な結果になるようです(gccの場合)。負の値をスキップするようにするか、符号なしへのキャストだけは付けたほうが良さそうです。

それから ”なお表示文字列でも”\”に関しては”\\”に変換する。” が說明が間違ってしまっていたので抜けていますので、char_to_str() にもうひとつ手を入れてください。

ええと、なんかおかしいな。何かが足りない気がする。見つけた見つけた。ctrl_to_str() を呼ぶ時点で data がヌルターミネートされていません。えっと iから 1 を引いて元の文字を作っているので、

data[sizeof(data)-1] = ‘\0’;

という行が ctrl_to_str() を呼ぶ直前に必要みたいです。

const なりはC言語の場合は、効果が薄いこともあるのですが(C++では厳密に書いたほうが良さそうです)、型名が長いのが嫌であれば、ここは積極的に typedef をお使いになるのはいかがでしょうか。ただ uchar とするとユニコード文字と紛らわしいので他の名前が良さそうです。しかし文字に関する処理は -1 に特別な意味があることが多いので、int に変換したりいろいろ気を使う必要がありますね。

こちらも参考に。
C言語のunsigned char型が想像以上に沼だった話


さて、こちらも常連である Akio van der Meer さんの回答もみてみましょう。

(答案提出)C言語教室 第10回 - 文字種判定と文字コード

ええとええと、ああ大丈夫だ。最初に strlen を取った後で長さ自身(len)に+1してあるので、終端文字まで処理されているんですね。ちょっとわかりにくかったです。三項演算子って便利でしょ?こう書くことで dst のインクリメントも1箇所で済みますしね。年末年始はお忙しかったようで、こういう時は無理をせずにノンビリやってください。

少し時間を頂きましたが、演習の方も見ました。

(答案提出)C言語教室 第10回 - 文字種判定と文字コード - 演習編

見た時点では「修正済みコード(v2.0)」となっていました。ご自身で気がついたところは、どんどん修正してください。でも、私が見たのとミスマッチにならないように、こっそりと教えて下さいね。今回はちゃんと教えていただいたので助かりました。

演習だと思って、設問にテを抜いてしまったようで、意図が充分に説明できておらず申し訳ありませんでした。要は表示できない文字をC言語のリテラルで使える形で出力したいということでした。これで結果の文字列をソースコードに戻せば、同じ文字列になりますよね?(エスケープ表現は少し異なりますが値は同じになるはず)

このような変換はエラーメッセージやログファイルに出力するときや、テストを外部で実行する時に必須の処理だったりします。テキストが期待されているところに制御文字が入ってしまうと困るので。

キホンは文字列変換に過ぎないので、入力から読み取って出力すれば良いだけです。文字に対する処理(char_to_str)があって、文字列に含まれるそれぞれの文字に対して実行する(ctrl_to_str)という作りはわかりやすいと思います。

動作は確認されているのでOKです。気になったのは、害はないのですが、main関数での data のサイズが 0x101 になっています。値を代入している文字の添字の範囲は 0~254で、これに終端文字を付加しているので、255までしか使っていないはずです。ですから配列のサイズは256で良いと思います。

それから、これは参考にした Ayumi さんのコードにあったからだと思うのですが、ctrl_to_str の配列変数 s のサイズが 8 なのは、少しばかり冗長かもしれません。ここは 5 で間に合うかと。実際コードが走る時は 8バイト境界で割り当てられるような気もするので、気にする必要はないかもしれません。それよりも {0} で初期化している方が気になります。この初期化は関数が呼び出される都度、実行されるので、char_to_str を呼び出す際に必ず終端文字が付与されることが担保されるのですが、ここを気にするくらいなら sprintf の戻り値をちゃんと確認したほうが良さそうです(成功すれば生成した文字列の「長さ」、エラーなら -1)。

まあきちんとテストされて、きちんと動作しているので、私のコメントは参考にしていただくだけで充分です。それにしてもC言語で文字列を扱う時には配列のサイズと最大要素番号がひとつずれているのと、長さに含まれない終端文字の領域が1バイトあるのと、ライブラリ関数が-1を返すので符号ありで使うと8ビット目が1になっている文字が負の値になってしまうなど、本質的でない部分の面倒さが目に付きますね。

毎回の答案を本当にありがとうございます。だんだん面倒な課題も増えてくるかもしれませんが、說明がわからないときは、コメントでご質問いただければと思います。もう少し丁寧に書かないといけませんね。


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


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