C言語教室 番外編5 - 第8回の課題について
最初は教室の中で、前回の課題について説明すれば良いと思っていたのですが、素晴らしい答案を頂くようになり、書くことがかなり増えてしまったので独立した投稿になりました。これはとても嬉しいことで、いろいろな結果があることで教室の内容も充実させることができます。答案を頂けてとても感謝しています。
実は我が家の教室でもそうなのですが、次回の教室までに課題をこなしきれないこともあると思います。そこで次回からは番外編ではなくて課題解説は独立させるようにして、解説が投稿された後であっても答案は受け付けるようにします。noteは更新がやりやすいので、頂いた時点でコメントなどを追記していくようにします。ということで教室のタイミングとはわけて、課題を出してから早めに解説していしまうこともあるかもしれませんし、次の教室が済んでから前回の解説を書くこともあるかもしれません。と、予め言い訳をしておきます。答案を出す予定のある方は、解説が投稿されてもしばし読まずにいてくださいね。
今回も、Akio van der Meer さんと AyumiKatayama さんからの答案を頂きました。ご両名とも立派な生徒さんです。本当にありがとうございます。さて、答案へのコメントは後回しにして、課題に取り組もうと思います。
前回の教室は
C言語教室 第8回 - 文字列を扱う標準ライブラリ
でした。課題は以下の内容です。
まず最初の課題です。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *string_concat(char *dest, char *src1, char *src2) {
return strcat(strcpy(dest, src1), src2);
}
void main() {
char s1[] = "abc";
char s2[] = "def";
int len;
char *s3;
len = strlen(s1) + strlen(s2);
s3 = (char *)malloc(len + 1);
string_concat(s3, s1, s2);
printf("%s+%s->%s\n", s1, s2, s3);
free(s3);
}
実行すれば以下のように出力されるはずです。
abc+def->abcdef
string_concat は、かなり圧縮した書き方をしてみました。こういう書き方ができるところがC言語らしいかなとは思っています。戻り値を指定していますが、mainでは使っていないので void で書いても間違いにはなりません(当然 return は不要になります)。
もちろんライブラリ関数を使わずに、文字列の連結を実装しても構いませんが、そろそろライブラリ関数を使うことにも慣れておいてください。
さて次の課題です。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *dm_string_concat(char *src1, char *src2) {
char *p = (char *)malloc(strlen(src1) + strlen(src2) + 1);
return strcat(strcpy(p, src1), src2);
}
void main() {
char s1[] = "abc";
char s2[] = "def";
char *ds;
ds = dm_string_concat(s1, s2);
printf("%s+%s->%s\n", s1, s2, ds);
free(ds);
}
結果は、最初の課題と同じ出力になるはずです。
dm_string_concat も少しばかり圧縮した表現になりましたが、さすがに1行にまで詰めるのは避けました。
さて、課題への回答としては上記で十分ですが、やはり多少のチェックを入れないと落ち着きません。そこであくまで私流の解釈ですが、いくつかのチェックを入れてみました。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *string_concat(char *dest, char *src1, char *src2) {
if (dest != NULL) {
if (src1 != NULL)
strcpy(dest, src1);
if (src2 != NULL)
if (src1 != NULL)
strcat(dest, src2);
else
strcpy(dest, src2);
}
return dest;
}
void main() {
char s1[] = "abc";
char s2[] = "def";
int len;
char *s3;
len = strlen(s1) + strlen(s2);
s3 = (char *)malloc(len + 1);
string_concat(s3, s1, s2);
printf("%s+%s->%s\n", s1, s2, s3);
free(s3);
}
引き数に NULL がある時の動作チェックをすべては試していないので、抜けがあったらゴメンナサイ。ご指摘を待ちます。次の課題もほぼ同じです。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *dm_string_concat(char *src1, char *src2) {
int len;
char *p;
if ((src1 == NULL) && (src2 == NULL))
p = NULL;
else {
len = 0;
if (src1 != NULL)
len += strlen(src1);
if (src2 != NULL)
len += strlen(src2);
p = (char *)malloc(len + 1);
if (p != NULL) {
if (src1 != NULL)
strcpy(p, src1);
if (src2 != NULL)
if (src1 != NULL)
strcat(p, src2);
else
strcpy(p, src2);
}
}
return p;
}
void main() {
char s1[] = "abc";
char s2[] = "def";
char *ds;
ds = dm_string_concat(s1, s2);
printf("%s+%s->%s\n", s1, s2, ds);
free(ds);
}
やはりエラーを確認すると、圧縮した書き方が出来なくなりますし、コードも複雑になってバグを仕込む可能性も高くなってしまいますね。条件判定の部分はもっとエレガントに出来ると思います。
頂いた答案を見てみましょう。最初は Akio van der Meer さんの答案からです。
(答案提出)C言語教室 第8回 - 文字列を扱う標準ライブラリ
文字列処理とポインタの扱いについては、もう充分にご理解されていると思います。
ところで、ライブラリ仕様は処理系のマニュアルを確認するのが基本です。UNIX出身者なので普通は man を見ます。ライブラリであれば3章ですね。今はオンラインでも検索できますね。
Linux で man コマンドのセクション番号が何か調べるには manual を見る
それとヘッダファイルです。文字列処理関数を使う時に読み込む string.h を開いて、プロトタイプ宣言を確認したり、自分の環境にあった config (適切な環境変数が設定されているかなど)が機能するかを確認します。ヘッダファイルの中身はそれなりに面倒な分岐がたくさんあるので、ここはサボってもいいのですが、コメントに有用な情報が書いてあることもあります。
ご紹介したブラウザ環境は、あまりきちんとしたドキュメントが無いので、元になるソースを確認することにしています。結構インプリをサボっているところもあるようです。
https://gitlab.com/zsaleeba/picoc
あ、予告してしまいますが、次回の課題はお休みです。
次は AyumiKatayama さんの答案です。
C言語教室 第8回 回答
基本的な動作については、もうコメントするようなことはありません。とはいえ、徐々にお里を出されているようで、それはそれで興味深いです。
インデントはそれぞれの流儀があるので、慣れた形式をお使いになるのが一番です。これが違うと落ち着かなくなったり、閉じる場所を間違っても見落とすことがありますよね。実務的にはエラーが出る時の行番号を確認するためであったり、デバッガを使う時にブレークポイントは行にしか設定できないので、狙った場所に仕掛けられるように行を整えることがあります。ですので for 文を 3 行に分けることもあります。
引き数のチェックで引っかかった場合に処理に入らずにリターンするというのも、ひとつの流儀です。複数の場所でリターンするのは、好ましくないとされることもあるのですが、これを頑張りすぎると条件式が複雑になって見通しが悪くなることも多いので、一貫していればかまわないと思います。
気になるのが、引き数で渡したポインタが共に NULL であるときには、空文字列が返ることです。この場合の動作を指定していないのですが、無いものを渡して中身が返るのは落ち着かない感じはします。
さて、const 指定についてですが、まだ const の說明をしていないのと、ブラウザ環境で const が正しく機能しないので、例では使っていないのですが、ここをしっかりと書き込んでみました。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *string_concat(char* const dest, const char* const src1, const char* const src2) {
if (dest != NULL) {
if (src1 != NULL)
strcpy(dest, src1);
if (src2 != NULL)
if (src1 != NULL)
strcat(dest, src2);
else
strcpy(dest, src2);
}
return dest;
}
void main() {
const char s1[] = "abc";
const char s2[] = "def";
int len;
char *s3;
len = strlen(s1) + strlen(s2);
s3 = (char *)malloc(len + 1);
string_concat(s3, s1, s2);
printf("%s+%s->%s\n", s1, s2, s3);
free(s3);
}
ポインタ変数の const には2種類があって、ポインタ自身を読み出し専用にする指定と、ポインタが参照する領域を読み出し専用にする指定があります。今回は引き数で指定されたコピー元文字列に関しては共に変更をしないので、両方に const が指定できます。
const 指定の面倒なところは、他の関数を呼び出す時に、const で呼び出せるかで、ここでキャストしなければならないようであれば、そもそもの const 指定をしてはいけません。でも強引にキャストできてしまうことから、C言語での const はアテにはできないんですよね。もちろん不用意なキャストはしてはいけませんし、const を正しく指定することで、うっかり値を変えるような処理を発見できるようになります。もちろんこれを見てコンパイラがより強力な最適化を行うことも期待できます。C++の場合ですが、const の有無でオブジェクトをコピーするか参照で済ますかの判断が行われることがあるので、正しく指定する習慣はつけておくのに越したことはありません。
空文字列を作るときですが、これは終端文字の付与と同じことですから、str[0] = ‘\0’ で良いのではないでしょうか。どちらかというと初期化構文で { 0 } を使っている方が大胆で、構文としては正しいのですが意図を見逃しそうです。なお厳密には、ここで型変換が発生しているので、{'\0’} がベターかも知れません。
三項演算子ですが、時折、これを使わないとうまく実現できないことがあるので、食わず嫌いにならずに、使える時は使った方が良いとは思います。もっとも古い人なので、C言語の場合は初期化で「処理」を行うのには少し抵抗があって、const 問題が無ければ宣言で初期化せずに本体で処理をする癖があります。C++であれば、むしろ積極的に宣言時に初期化するのですけどね。両言語を覚えた時期に差があるので、どこかで頭の中でモードチェンジが働くみたいです。
今回もだいぶ長くなってしまいました。頂いた答案の中にもっと突っ込んで見たいネタがたくさんあるのですが、深い部分があるのでなかなか書き終わりません。我が家の教室でもポインタになかなか慣れてこないので、もう少し復習が必要になりそうです。早く次の課題に取り組みたくてウズウズしているの「かも」しれませんが、ボチボチのペースで進めさせてください。
ヘッダ画像は今回も、以下のところのものを使わせていただきました。
https://photosku.com/photo/2615/