見出し画像

C言語教室 番外編4 - 第7回の課題について

さあて、今回は課題としては簡単なものにしたつもりだったのですが、常連となりつつある Akio van der Meer さんに加え、AyumiKatayama さんからも答案の提出を頂きました。ありがとうございます。答案に対するコメントは最後にまとめるとして、まずは前回の課題に取り組んでみましょう。

C言語教室 第7回 - 動的なメモリ割り当て

課題は

引き数で渡された文字列を、関数の中で動的に確保したメモリに文字列をコピーし、(引き数で渡した方ではなくて)コピーした文字列の先頭を指すポインタを返す関数を書いてください。

でしたね。今までに「文字列の長さを数える」コードも「文字列をコピーする」コードも、説明が済んでいるので、素直にそれらを組み合わせれば書けるはずです。

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

int string_length(char *str) {
  int len;
  char *p = str;

  for (len = 0; *p++ != '\0'; len++)
    ;

  return len;
}

void string_copy(char *s, char *d) {
  while ((*d++ = *s++) != '\0')
    ;
}

char *dm_strcpy(char *src) {
  char *r = NULL;int len;

  len = string_length(src);
  r = (char*)malloc(len + 1);
  string_copy(src, r);

  return r;
}

void main() {
  char s[] = "abc";
  char *dp;

  dp = dm_strcpy(s);
  printf("%s\n", dp);
  free(dp);
}

実行結果は、ちゃんと

abc

と出るはずです。動的に確保したメモリは不要になったら free で解放する必要があります。これを他のポインタと区別する仕組みはC言語には無いので、コードを書く人が覚えておく必要があります。今回は名前の先頭を d にするという方法にしました。”d で始まる変数を見たら解放しないとね”という自分ルールです。今までに説明したコードを流用したので、長さを数える関数と文字列をコピーする関数を別立てにしましたが、ひとつの関数に押し込んでしまってもOKです。

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

void *dm_strcpy(char *src) {
  int len = 0;
  char *r;
  char *p;
  char *q;

  p = src;
  while (*p++ != '\0')
    len++;
  
  r = (char*)malloc(len + 1);
  
  p = src;
  q = r;
  while (*p != '\0')
    *q++ = *p++;
  *q = '\0';

  return r;
}

void main() {
  char s[] = "abc";
  char *dp;

  dp = dm_strcpy(s);
  printf("%s\n", dp);
  free(dp);
}

動作としては、これで問題は無いのですが、念のため malloc がメモリの割り当てに失敗したケースを補っておきたいです。メモリが確保できなかった場合はコピーする関数が NULL を返すことにします。文字列関連の処理でエラーの場合は NULL を返すという仕様は良くある話なのですが、

【C言語】malloc関数(メモリの動的確保)について分かりやすく解説

この結果、そもそも引き数で渡されたコピー元文字列のポインタが NULL で渡されてしまうことが十分に考えられることになります(呼び出す前に必ずチェックしてくれるとは限りません)。こちらのチェックも加えておきましょう。なお free に NULL を渡しても問題はありません。

【C言語】free関数の使い方と注意点について解説

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

int string_length(char *str) {
  int len;
  char *p = str;

  for (len = 0; *p++ != '\0'; len++);

  return len;
}

void string_copy(char *s, char *d) {
  while ((*d++ = *s++) != '\0')
    ;
}

char *dm_strcpy(char *src) {
  char *r = NULL;
  int len;

  if (src != NULL) {
    len = string_length(src);
    r = (char*)malloc(len + 1);
    if (r != NULL)
      string_copy(src, r);}

  return r;
}

void main() {
  char s[] = "abc";
  char *dp;

  dp = dm_strcpy(s);
  printf("%s\n", dp != NULL ? dp : "error");
  free(dp);
}

printf で文字列を表示するとき(%s)に NULL ポインタを渡してしまうと、よろしくないのですが、ここでわざわざ if で分岐するのが煩雑に思えたので、三項間演算子を使ってみました。

文字列の長さを数えると文字列をコピーする関数は、実はC言語の標準ライブラリにも該当する関数があるので、そちらを使っても構いません。

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

void *dm_strcpy(char *src) {
  char *r = NULL;
  int len;
 
  if (src != NULL) {
    len = strlen(src);
    r = (char*)malloc(len + 1);
    if (r != NULL)
      strcpy(r, src);
  }
  return r;
}

void main() {
  char s[] = "abc";
  char *dp;

  dp = dm_strcpy(s);
  if (dp != NULL) {
    printf("%s\n", dp);
    free(dp);
  } else {
    printf("error\n");
  }
}

この例では表示する側の NULL ポインタチェックは素直に書きました。細かく言えば、長さを格納する型は size_t じゃないの?であるとか、strcpy じゃなくて strncpy の方がいいのでは?などのご指摘もあるかとは思いますが、そこは突っ込まないでくださいね。

dm_strcpy の戻り値が、解放する必要のあるポインタであることが気になるのであれば、まだ説明はしていないのですが、typedef を使って目立たせる方法もあります。

typedef新しい型を作る

これもまだ説明していないのですが構造体を使うと typedef をよく使うようになります。ただ typedef を使っても既存の標準関数を呼び出すときには役に立たないので、あくまで目立たせるまでの効果しか期待はできません。

あらためて処理を見渡すと、文字列をコピーするのに先立って、文字列の長さを数えるのに文字列を舐めてから、必要なメモリを確保して、再び文字列を舐めながらコピーしていくのが何となくもどかしいです。ただ長さを数える前にメモリを確保しない限り、これはどうにもなりません。C言語の文字列は「長さ」を持っているわけではないので、動的なメモリ管理との相性は良くありません。

このように文字列をコピーするだけでも、いろいろな事を考えることになります。文字列に関する標準的な関数は次回の教室で取り上げるつもりですが、必要な領域をキチンと確保して関数を呼び出さないと、エラーが出ることもなく動作が不安定になってしまうのが、C言語の大きな問題点です。仕様をよく読んでも「何が起こるかわからない」と恐ろしい説明が書いてあります。

【C言語入門】strcpyとstrcpy_sの使い方(文字列のコピー)

【C言語】strcpy/strncpy/strcpy_s関数の使い方と自作関数

C言語 strcpyとmemcpyの使い方【コピー方法の違いとは】

こういう問題点があるため、動的な文字列に対してはC++ではSTLによるstring、VisualC++のMFCではCStringが使われるようになっています。これらは文字列の長さとデータ列(Cからも使いやすいように終端文字も付けられます)で管理されており、C言語でも構造体を使って、こういったデータ構造で管理しているケースも見受けられます。そこまでしなくても確保したメモリの大きさを反映できるような標準関数を呼び出すように配慮して、原理的に確保したメモリを超えた処理が行われないようにしましょう。


さて、頂いた回答を見てみましょう。最初は Akio van der Meer さんです。

(答案提出)C言語教室 第7回 - 動的なメモリ割り当て

いつもありがとうございます。今回は空港でデバッグされたとのことで、お疲れさまでした。ブラウザでコードが書けるとiPadでもいけるんですね。そろそろクラウドだけで開発できる時代が近いかもしれません。

提出されたコード自身に気になるところは特にありません。日本語についても検証して頂いていますが、日本語の1文字が何バイトになるかは使っている文字コードに依存するので、必ずしも3バイトになるとは限りません。一般的な文字コードは英数字のASCIIコードに対して矛盾が出ないように設計されているので、特別なことをしなくても大抵は英数字の処理と同じコードで大丈夫です(UNICODEを直接扱う場合はダメ)。

関数で得られたポインタに対して free で解放するのが、対称的ではないことを気にされているようですが、C言語の型はメモリの種類までは面倒を見てくれないので、やれることに限度はあります。typdef を使うこともできますが、変数名の命名規則で逃げてしまうことも多いです。ポインタがローカル変数であれば確保した関数で解放するのが原則ですが、複雑なプログラムになると、グローバル変数ならどこで解放するのかであるとか、エラーが起こったときに解放するには誰の責任なのかなど、ケースバイケースで検討するとC言語の範囲では厳しいものがあります。


今回は AyumiKatayama さんからも答案を頂いてしまいました。

回答してみました ~ C言語教室 第7回 - 動的なメモリ割り当て ~

こちらはC言語の標準的なライブラリを使われていますので、無難にまとめられていると思います。strcpy ではなく strncpy を使われたあたりツワモノです。今では strcpy は推奨されない関数ですものね。まあ malloc がエラーを返すことも事実上ないですし(ここでエラーが出るようだと、どうせプログラムを継続できない)、strlen が出来た以上、目の前で確保したメモリが壊れることもないので strcpy を使っても大丈夫な気はします。

私のコードで長さの型を int にしているのは、ブラウザ環境のC言語が size_t をうまく扱うことができないためで、strlen の戻り値の型は size_t が正しいです。まだ unsigned の說明もしていないので、見逃してください。

NULLと’\0’に関しては、ポインタ型に対してはNULL、文字型に関して’\0’となっていればOKです。ここはもし間違っても、値としてはすべて0で型が違うだけなので、警告は出るかもしれませんが、結果が変わることもなくスルーされやすいところです(‘\0’と”\0”は大違いなことはもちろん大丈夫ですよね)。

C言語の仕様書に従うならNULLは0もしくは(void *)0らしい

たとえ中身が1行でも対称性のある形にするために free を行う関数を用意するという考え方も悪くないと思います。以前にこれをやりすぎて、同じ関数で引き数によって確保と解放を行うような関数を書いてみたこともあるのですが、引き数に「アクション」を持ち込むのはやめてと指摘されたことがあり、確かに「モード」ならまだしも動作の指定はわかりにくいなと思ってやめた覚えがあります(同じ関数だとデバッグに便利ではあったのですが)。


単にメモリを確保してコピーするだけで、こんなにいろいろな話題が出てきて、課題を出した本人もビックリです。C言語はさすがに昔から使われている言語で、高級なアセンブラとも言われるくらい自由で、言語自身が決めていることが少なく、モダンな言語と比べると力不足だったり不十分なところがあります。そこを経験の積んだ人たちがそれぞれのルールを重ねて補っているわけです。しかしながら、CPUが扱う生の情報に近い部分を扱えるだけに、普遍的な問題に取り組む羽目にはなります。そういった意味でも普段はJavaしか使っていないという人であってもC言語を学ぶ価値はあるでしょう。

ヘッダ画像は、以下のところのものを使わせていただきました。
https://photosku.com/photo/2615/

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