文字の配列、だから文字列

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

前回予告したように今回は文字列について解説します。
文字列自体はPHPにもありますので概念的な説明は必要ないと思います。
ここではCの文字列に限った話を中心にお話していけたらと考えています。

文字列の表し方

Cに限らずプログラミング一般の話として、文字列を表現するにはいくつかの方法があります。
主なものは次の3つでしょうか?

  • 文字の並びそのものと長さを管理する。
  • 文字の並びの末尾に番兵を入れる。
  • 文字の並びの先頭と末尾(または末尾の次)のアドレスを管理する。

言語によってもライブラリによっても違いますが、ほとんどがこの3つのうちのどれかだと思います。
というか、この3つ以外の表現方法を私は知りません。

こうした方法で、メモリー上の単なる整数値の並びのうちどこからどこまでが文字列なのかを管理します。

ナル文字

Cではこの3つのうちの真ん中の方法「文字の並びの末尾に番兵を入れる」を文字列の表現方法として採用しています。
番兵には「ナル文字」といって、値0の文字を使います。
ナル文字で終わるので、Cの文字列は「ナル終端文字列」とも呼ばれます。

ナル文字は「’\0’」を使って表します。
Cの文字は整数値なので単に「0」と書いてもいいのですが、ナル文字であることを明確にするために普通は「’\0’」と書きます。

文字列は文字の配列

Cの文字列はとてもシンプルで、文字の配列でしかありません。
たとえば「hello」という文字列であれば次のように表現します。

char str[] = { 'h', 'e', 'l', 'l', 'o', '\0' };

Cの配列については以前の記事で詳しく解説していますので、必要な方はそちらを参照してください。

先ほどの例のように書けば文字列は表現できるのですが、普段からこんな書き方をするのは面倒ですし間違いも起きやすいですね。
そこでダブルクォーテーションを使って文字列を表現することができます。

char str[] = "hello";

最後のナル文字は省略してかまいません(というか省略してください)。
ナル文字を明示的に書かなくても、自動的に末尾にナル文字が挿入されますので、先ほどの素直に配列の初期化子を書いた場合と同じ結果になります。

文字列リテラル

このようにダブルクォーテーションで囲った文字列のことを、Cでは「文字列リテラル」といいます。

文字列リテラルは配列の初期化子としても使えますし、文字列が必要となるいろいろな文脈で直接使うことができます。
文字列リテラルを直接使った場合は名前の無いオブジェクトということになりますね。
最初の記事でご紹介した「hello, world!」でも文字列リテラルを直接使っていましたね。

/* hello.c */
#include <stdio.h>

int main(void)
{
  puts("hello, world!");
  return 0;
}

この例では “hello, world!” という文字列リテラルを直接使ってputs関数に渡しています。
PHPでもこういう使い方はよくするので違和感はないと思います。

文字列の操作

Cの文字列は末尾にナル文字を持つ単なる配列だということをおわかりいただけたでしょうか?
これを踏まえた上で、次は文字列の操作について解説していきます。
あまりいろいろやっても大変ですので、文字列の長さを求めるのと、あとは文字列のコピーと連結を解説することにします。

文字列の長さ

文字列の長さを求めるにはナル文字が現れるまでの文字数を求めればOKです。

#include <stdio.h>

int main(void)
{
  char str[] = "hello";
  int i;

  for (i = 0; str[i] != '\0'; i++)
    ;
  printf("%d\n", i);
  return 0;
}

実際に試していただければ「5」が出力されるのが確認できるはずです。

でも、毎回こんなコードを書くのは面倒ですし間違いも起きますよね。
ですので、文字列の長さを求めるための関数がちゃんと用意されています。
「strlen」関数がそれです。
なんか見覚えのある関数ですよね。

strlen関数を使うには「string.h」ヘッダーをインクルードする必要があります。
そして次のように書きます。

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

int main(void)
{
  int n = strlen("hello");
  printf("%d\n", n);
  return 0;
}

簡単ですよね。
strlen関数が返す文字列の長さは末尾のナル文字は含まないことに注意してください。

ところで上の例ではint型のオブジェクト「n」にstrlen関数の結果を格納しました。
実際にはstrlen関数が返す値(「返却値」といいます)の型は「size_t」型です。
size_t型はint型とは違いますので、厳密には次のように書く必要があります。

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

int main(void)
{
  size_t n = strlen("hello");
  printf("%zu\n", n);
  return 0;
}

size_t型の値をprintf関数で書式化する場合は、上の例でも使っていますが「%zu」を使います。
「z」がsize_t型を意味していますので、16進数で出力するのであれば「%zx」とします。

「%zu」の「u」は符号無し整数型を意味しています。
マイナスのサイズというのは無いので符号つまりマイナスを表現できる必要はありませんね。

文字列のコピー

PHPでは「=」を使って数値の変数と同じように文字列や配列のコピーができました。
ところがCでは配列を直接コピーすることができません。
原則として、次のように1要素ずつ順にコピーしないといけないのです。

#include <stdio.h>

int main(void)
{
  char str1[] = "hello";
  char str2[6];

  for (int i = 0; i < 6; i++)
  {
    str2[i] = str1[i];
    if (str2[i] == '\0')
      break;
  }
  puts(str2);
  return 0;
}

先ほどの文字列の長さを求める場合と同じで、こんなのを毎回書くのは面倒ですし間違いも起きます。
ですので、文字列をコピーするための関数ももちろん用意されています。
文字列のコピーには「strcpy」関数を使います。
具体例を挙げますね。

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

int main(void)
{
  char str1[] = "hello";
  char str2[6];

  strcpy(str2, str1);
  puts(str2);
  return 0;
}

少しは簡単になりました。
strcpy関数を使うときもstring.hをインクルードする必要があるので忘れないようにしてください。

上のコードをご覧になれば気付かれたと思いますが、コピーした文字列を格納するための配列(ここでは「str2」)は自分で用意しなければなりません。
配列の大きさが十分でなければ、配列の範囲を超えて変なところに書き込んでしまってプログラムが破綻します。
そういったことを回避するのは、Cではプログラマーの責任です。

文字列の長さを調べてから配列のサイズを決めることもできます。
たとえばこんな感じです。

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

int main(void)
{
  char str1[] = "hello";
  size_t n = strlen(str1);
  char str2[n + 1];

  strcpy(str2, str1);
  puts(str2);
  return 0;
}

まずstrlen関数でstr1に格納された文字列の長さを調べてnに格納しています。
次にそのnを使って配列の要素数を決めていますが、ナル文字の分も確保しないといけませんので1を足しています。
あとは同じですね。

文字列の連結

最後は文字列の連結を解説します。
PHPでは「.」演算子を使って簡単に文字列を連結できますが、Cではそうはいきません。
連結したあとの文字列を格納する配列はユーザーの責任で確保しなければなりません。

文字列の連結も本来も1文字ずつ処理しないといけないのですが、やはり便利な関数が用意されています。
「strcat」関数がそれで、やはりstring.hヘッダーをインクルードして使います。

まずは、連結先の文字列が格納されている配列に十分な空きがある場合を考えてみましょう。

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

int main(void)
{
  char str[100] = "hello";  // 配列には十分な空きがある。

  strcat(str, ", world!");
  puts(str);
  return 0;
}

上の例の配列「str」のように、宣言で指定した要素数より少ない数しか初期化子で指定していない場合は、あまっ多要素には0(この場合はchar型なのでナル文字ですね)が埋められます。

配列strは初期化子で指定した6文字(「hello」+ナル文字)しか使っていませんので、残りの94文字分が空いています。
この空いた領域に “, world!” を連結しています。

次に、連結前の2つの文字列とはまったく別の配列に連結結果を格納する方法を考えてみましょう。
最初の文字列をstrcpy関数で結果を格納する配列にコピーしてから後続の配列をstrcat関数で連結するのが一番素直なやりかたです。

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

int main(void)
{
  char str[100];

  strcpy(str, "hello");
  strcat(str, ", world!");
  puts(str);
  return 0;
}

ほかに「snprintf」関数を使う方法もあります。
PHPにはsprintf関数がありますが、それとよく似た関数です(実はCにはsprintf関数もあるのですが、セキュリティ上の問題もあって使用は推奨されていません)。
とにかく例をご覧ください。

#include <stdio.h>

int main(void)
{
  char str[100];

  snprintf(str, 100, "%s%s", "hello", ", world!");
  puts(str);
  return 0;
}

snprintf関数には書式文字列の前に2つの引数を渡す必要があります。
1つめは格納先の配列です。
2つめは格納先の配列の要素数です。

この方法を使えば、連結する文字列が3つい上になっても同じように扱えますね。

文字列にはほかにもいろいろな操作があるのですが、今回は本当に基本的な操作だけを解説しました。
ぜひご自身でもいろいろ試してみてください。

次回は何にするかまだ決まっていないのですが、そろそろ関数の解説をしようかと考えています(未定なので確約はできません)。
何を解説することになるのか、お楽しみに!