文字列操作関数の実装

こんにちは、めのんです!

前回まででポインタと配列の関係について基本的なことを解説してきました。
今回はそれらの内容を踏まえた上で文字列の操作について解説することにします。

文字列の操作については以前にもある程度解説したことがあります。
そこで紹介したstrlen関数やstrcpy関数などを使えば文字列の操作はできます。
あとはライブラリにあるいろんな関数を覚えていけばできることも増えていくことでしょう。

でも、せっかくCをやるのであれば、自分でそうした関数を設計&実装できるようになってほしいんですよね。
もちろん既にあるものは利用すればいいんですけど、「関数が無いからできません」というのはCプログラマーのあるべき姿ではないんですよ。

ここではCの文字列操作に慣れるために以前紹介した3つの関数

  • strlen
  • strcpy
  • strcat

を実際に実装してみることにします。
既存のライブラリ関数と名前が衝突しないように前に「my_」を付けますが、内容は一緒だと思ってください。
できればsnprintf関数も実装すればいいんでしょうけど、膨大な量になるのでブログで書くのはやめておきます。

my_strlen関数の実装

strlen関数は文字列の長さを求める関数でした。
先ほども書いたように名前の衝突を避けるために前に「my_」を付けて「my_strlen」関数という名前で実装することにします。

my_strlen関数の関数原型は次のようになります。

size_t my_strlen(const char *s)

Cの文字列はchar型の配列ですが、配列を関数に実引数として渡すときは暗黙の型変換でポインタになってしまうんでしたね。
そういうことがあるので、my_strlen関数ではポインタを受け取る仕様になっています。

ひとつ注意してほしいのは、char *の前に「const」というキーワードが付いていることです。
これは「const修飾子」というもので、オブジェクトが更新できないことを指定するための型修飾子です。
更新できないというのは、代入やインクリメントなどの操作ができないことを意味します。

今回の場合はポインタとして関数に渡された文字列の内容を関数の中で改変できないことを保証しています。
constを含めた型修飾子については別の機会に詳しく解説しますので、今回はこの程度にしておきますね。

それでは、お待たせしました。
関数の中身を作っていきますね。
まずは一番安直な方法からです。

size_t my_strlen(const char *s)
{
  size_t n = 0;

  while (*s++ != '\0')
    ++n;
  return n;
}

この実装ではポインタsが指している文字列を先頭からたどっていきナル文字が見つかるまでsとnをインクリメントしてます。
これでも正しい結果は得られるのですが、sとnの両方をインクリメントしているのでちょっと無駄が多いですね。

そこで次のように書き換えればもっと効率がよくなります。

size_t my_strlen(const char *s)
{
  const char *first = s;

  while (*s != '\0')
    ++s;
  return s - first;
}

この実装では、最初にポインタsの値をfirstにコピーしています(文字列のコピーではなくポインタのコピーです)。
そして、ポインタsを参照先がナル文字になるまで進めています。

ちょっとしたことですが、前回は*s++としていたところを*sにして、while文のブロックの中でsをインクリメントしてます。
こうすることで1回インクリメントが少なくなります。

この時点でsはナル文字を指していますので、文字列の先頭を指すfirstとの差を取ってあげれば文字列の長さが求まります。

my_strcpy関数

次は文字列をコピーするstrcpy関数に相当する「my_strcpy」関数を実装していきます。
まずは関数原型からです。

char *my_strcpy(char * restrict s1, const char restrict *s2)

my_strcpy関数は、s2が指す文字列をs1が指す配列にコピーしてs1を返します。
コピー元の文字列s2はコピーの途中でもあとでも変化しませんからconst修飾子が付いています。
一方、コピー先のs1は値を格納しますからconst修飾子を付けてはいけません。

my_strcpy関数にも新しいキーワードが登場していますね。
restrictがそうです。
これは「restrict修飾子」というものでポインタ型しか就職できません。

restrict修飾子を付けるとポインタが指す配列が重複していないことをコンパイラに保証します。
保証するのはもちろんプログラマーのあなたです。
配列が重複していないことがわかっていれば、それだけ最適化されて効率のよい機械語が生成される可能性があります。

それではmy_strcpy関数の実装を見ていきますね。

char *my_strcpy(char *s1, const char *s2)
{
  char *ss = s1;
  int c;

  do
  {
    c = *s2++;
    *ss++ = c;
  } while (c != '\0');
  return s1;
}

今回も素直に実装してみました。
do文が出てくるのはこの講座でははじめてだと思いますけど、PHPのdo文と同じだと思ってかまいません。

この実装でもインクリメントが2回発生していますね。
インクリメントを1回にすればもう少し効率がよくなるかもしれませんね。
試してみましょう。

char *my_strcpy(char *s1, const char *s2)
{
  int i = 0;
  int c;

  do
  {
    c = s2[i];
    s1[i] = c;
    ++i;
  } while (c != '\0');
  return s1;
}

たしかに見た目上はインクリメントが1回になりました。
ですが、添字演算子を使ったp[i]は*(p + 1)と等価だったことを思い出してください。
つまり、s2[i]やs1[i]でも見えないところで加算が行われていますので、これをやってもあまり効果がないでしょうし、場合によっては逆効果になることもあります。

別のアプローチで効率化を図ってみます。

char *my_strcpy(char *s1, const char *s2)
{
  char *ss = s1;

  while ((*ss = *s2) != '\0')
  {
    ++ss;
    ++s2;
  }
  return s1;
}

この実装であれば、少なくともナル文字をコピーしたあとは各ポインタをインクリメントしなくて済みますね。
微々たるものかもしれませんが効果はありそうです。

ところで、while文の継続条件式では

while ((*ss = *s2) != '\0')

のように書いていますね。
Cでは結構こういう書き方をします。

Cの代入演算子は評価結果が代入後のオブジェクトの値になります。
今回の場合は*ssの値ということになります。
これをナル文字と比較してあげれば、代入した文字がナル文字だったかどうかがわかります。

my_strcat関数

ここまでで文字列操作に必要なことは大体網羅したのですが、せっかくなので文字列を連結する「my_strcat」関数も実装してみましょう。
今回も最初は関数原型から見ていきましょう。

char *my_strcat(char * restrict s1, const char restrict *s2)

my_strcat関数はmy_strcpy関数とそっくりの形をしています。
もう新しいキーワードは出てきませんので関数の仕様だけを説明することにしますね。

my_strcat関数は文字列s1に文字列s2を連結します。
連結した文字列はs1と同じ配列に追記されますので、s1が指す配列には十分な空きがなければなりません。
十分な空きがあることを保証するのはもちろんプログラマーです。

まずは素直に実装してみますね。

char *my_strcat(char *s1, const char *s2)
{
  char *ss = s1 + my_strlen(s1);

  while ((*ss = *s2) != '\0')
  {
    ++ss;
    ++s2;
  }
  return s1;
}

たしかにこれでも動くんですけど、中でmy_strlen関数を呼び出していますね。
あくまでも一般論としてですが、関数の呼び出しというのは結構なコストがかかるんです。
それにオリジナルのstrcat関数はstrlen関数に依存してはならない仕様になっています。

というわけで、今回はmy_strlen関数の処理をmy_strcat関数の中に展開してしまいましょう。

char *my_strcat(char *s1, const char *s2)
{
  char *first = s1;

  while (*s1 != '\0')
    ++s1;

  char *ss = first + (s1 - first);

  while ((*ss = *s2) != '\0')
  {
    ++ss;
    ++s2;
  }
  return first;
}

最初なので、仮引数名を合わせたぐらいで本当に素直にmy_strlen関数の中身を展開してみました。
これでも動くんですけど、できればもう少し無駄を省きたいと思います。

char *my_strcat(char *s1, const char *s2)
{
  char *ss = s1;

  while (*ss != '\0')
    ++ss;

  while ((*ss = *s2) != '\0')
  {
    ++ss;
    ++s2;
  }
  return s1;
}

これでかなり無駄を省けたのでコードがすっきりしましたね。

今回はmy_strlen関数、my_strcpy関数、my_strcat関数の3つ実際に実装してみました。
ついでにちょっとだけ最適化もやってみました。

ソースコードの最適化というと「時期尚早な最適化だ!」と怒り出す方もいらっしゃるのですが、今回やった程度の最適化であれば時期尚早とはいいませんし、ある程度経験を積んだCプログラマーなら反射的にできる程度の内容です。
コードの効率に必要以上に執着する必要はありませんが、日頃からいろんな方のソースコードに触れていれば自然に身に付くと思います。


それでは本日の解説は以上となります。
次回は構造体を解説しようと思います。
どうぞご期待ください!