避けて通れない文字列の話

文字列とC言語での文字列についてひととおり解説します。

行と文字列

文字と文字列

文字列とは、その名のとおり、文字です。

A

これは、'A'という文字です。

ABCDEFGEH

これは、"ABCDEFGEH"という文字列です。

例として'A'という文字と"ABCDEFGEH"という文字列を示しました。文字列"ABCDEFGEH"には文字'A'が含まれていますし、この文字列を構成する要素のひとつである'B'や'C'なども、文字のひとつです。このように、文字の集まりによって構成されるひとかたまりのデータのことを文字列といいます。

じゃあ文字って?

では、文字列を構成する要素としての文字とは何なのでしょうか。コンピュータでいう文字とは、何らかのコード体系によって定義された文字集合のうちのいずれかの要素をさします。なにやら小難しい話ですが、ASCIIコードやシフトJISコードなどといった言葉なら、一度くらいは耳にしたことがあるでしょう。そういったものが文字集合です。細かい部分まではここに取り上げきれないので、http://euc.jp/i18n/charcode.ja.htmlhttp://www.kanzaki.com/docs/jcode.htmlの文字コードの話や、http://www.psl.ne.jp/perl/のASCII文字コード表などを参照してください。

ここで注意してほしいのは、文字といっても目に見えるものだけではないということです。ASCII文字コードの0x20(16進数で20という意味)より若いコードに、制御文字というコンピュータに何らかの指示を与えるためのものがあり、それらも文字に含まれます。

論理行と物理行

私たちが行という場合、たとえばA4の紙に活字で印刷された横書きの文章があったとして、文章が左端から始まり、右端に到達して次の左端に移る前の、その右端までを1行というのが普通です。

しかし、コンピュータ上での文字列には別の概念があります。たとえば、ウィンドウの右端で折り返し表示された文章などは、表示上では複数行あるように見えていても、論理行では1行と数えることがあります。ではその論理行における行の基準は何かというと、「改行」になります。

この「改行」とは、ウィンドウ右端で折り返すようなものではなく、改行を表す何らかの文字によって指定されるものです。一般には、ASCIIコードの改行を示すコード(LF(0x0A)またはCR(0x0D)、あるいはその両方)で指定されます。この改行コード以外で改行を表すものにはHTMLがあり、HTMLで改行を任意の位置に指定する場合には、br要素を用います。

基本的に改行の前までを行とし、改行コードの次からが新しい行と考えます。最初の改行コードの前の行の始まりはその文字列自体の先頭で、最後の改行コードの後の行の終わりは、そこに改行がない場合には不定といえます。

このような論理行の概念に対して、私たちが一般に使っている行の概念のことを物理行ということもあります。

正規表現での「行」

正規表現で文字列を扱う場合、文字列の先頭を行頭、文字列の終端を行末とするのが一般的なようです。改行コードの有無による行の概念とは全く関係ありません。したがって、次の一般的な正規表現"^ABC"(行頭がABCで始まる文字列にマッチする)は、

"123\nABCD"

のような文字列にはマッチしません。以前、私はこれを知らずに数日間ハマったことがあります。

このような場合、一般的な正規表現ライブラリでは、REG_NEWLINEオプションを指定することで\nの次から始まる文字列にマッチさせることができるようです。

C言語での文字列

とても重要なことですが、C言語には文字列を文字列として扱う型はありません。そこで、多くの場合、char型変数の配列を使います。つまり、文字が格納されたchar型変数の配列を文字列と見なすわけです。場合によってはchar型ではなくwchar_t型を用いることもありますが、ここではchar型を使います。

文字と文字列の表現方法

C言語では、代入などの操作において、文字を''、文字列を""で囲みます。

char ch = 'A';

これは、char型変数に文字定数を代入する例です。

char str[] = "Coffeehouse MOK_P";

これは、char型変数の配列に文字列を代入する例です。

"と'

文字を'(シングルクォーテーション)で囲んで表現したものを、文字定数といいます。この表現は主として、文字を値として表すときに用います。C言語ではこの文字定数は整数型の定数値になります。値はその文字が持つ文字コードです。任意の文字を表すために文字コードを知らなければならないというようなことであれば不便極まりないので、文字をわかりやすい形で表現できるようにしてあるということですね。

'で囲むのは1文字の場合が多いのですが、実は複数の文字を囲むこともできることはできます。ただし'\0'も付加されず、値は処理系依存となります。特にメリットがあるわけでもないので普通はそのような使い方はしません。

文字列の終了

文字列の終了は、'\0'を用いて表します。文字列に含まれる文字の中に'\0'を見つけたら、そこでその文字列は終わりです。

つまり、配列の初期化代入方法を利用して、ばかばかしいのですが、次のような方法で代入することもできます。

char str[] = {'A','B','C','\0'};

この'\0'は何なのかといえば、ASCII文字コードでいう0x00のことです。どうして0x00や0でないのかといえば、これは単に0という値の指定ではなく、文字列の終了を表すことを明示するためだからです。Cの定義上、文字列の終了は「8ビット以上の連続する0」で表されることになっていますが、文字列の終了は\でエスケープされた0で表すのが一般的です。文字定数を代入するとき、'\0'ではなく'0'と書くと、0という文字そのものになってしまいます。これは当然ですね。

C言語の文字列には終了を表すこの'\0'が必要なため、char型配列の要素数は、文字列自体の長さ+1バイトが必要になります。

ついでなので、文字列の終わりにこの'\0'がないとどうなるか試してみましょう。

#include <stdio.h>

int main(void)
{
	char str[]={'A','B','C'};
	printf("%s\n",str);

	return 0;
}

このようなプログラムをgomi.cという名前で保存して実行してみます。

実行結果

プログラムで代入した'A''B''C'の後に変なものが表示されていますね。これは、printf関数が文字列の終わりを判断できなかったからです。これが実際にどうなっているのか、バイナリエディタを使って見てみましょう。

バイナリで見る文字列

これはgomi > gomi.txtとリダイレクトして、gomi.txtというファイルに表示される結果を格納し、BZエディタというフリーのバイナリエディタで見たものです。

できれば、この結果をASCII文字コード表と見比べてみてください。0x41,0x42,0x43はA,B,Cの文字コードです。その後に表示されたおかしな文字は、もともとメモリに存在したゴミです。ここではたまたま3バイトで終わっていますが、これは運良くメモリ内のこのあたりに1バイト以上の0が続いていたからで、最悪の場合は延々とゴミが表示され続けることもあるわけです。

"で文字列を囲んだ場合には、実は自動的に最後に'\0'が付加され、終端された文字列になります。この種の文字列については、後でもう少し述べます。

C言語での改行

7バイト目と8バイト目は、改行のコードになります。このプログラムを実行した環境はWindows2000で、コンパイラは上の画像にあるように、Borland C++ 5.5.1です。Windowsの改行コードはCR(0x0D)+LF(0x0A)なので、プログラム中で改行を指定すると、実際にはこのふたつのコードになります。

C言語では、上のリストのように'\n'によって改行を表します。実はこの'\n'はLFの拡張表記で、'\n'自体ではLFのみを表します。ではなぜprintf文で表示した段階でCRが付いているかといえば、これはCコンパイラ(というよりプリプロセッサ)が処理系に合わせて自動的に変換しているためです。厳密にいえばWindowsでの改行の指定は"\r\n"でなければならないはずですが、C言語は改行コードがLFのみのUNIX起源であり、文字列の中に改行を指定するには\nで指定することが一般的になってしまっていたので、コンパイラ側が対応するようになったのでしょう。

文字列リテラル

""で囲んで直接指定する文字列のことを、文字列リテラルといいます。

char str[] = "Coffeehouse MOK_P";

この初期化代入文の右辺が文字列リテラルです。char型変数の配列を宣言し、それに文字列"Coffeehouse MOK_P"を代入しています。

文字列リテラルそのものにポインタを設定し、文字配列と似たような使い方をすることもできます。

char* strlit = "Coffeehouse MOK_P";

ただし、この文字列の実体のための領域は、文字配列のようにスタック領域に確保されるわけではなく、したがって基本的に変更するものではないという点に注意が必要です。コンパイラによっては、文字列リテラルを配置する領域をコンパイルオプションで指定でき、変更もできますがあまり良いことではありません。たとえば、最初のchar型変数の配列strに対し、

str[0] = 'c';

のように文字を後から改めて代入することは可能ですが、

strlit[0] = 'c';
*strlit = 'c';

はどちらも、通常は書き換えることが想定されていない領域に対する操作になるため、正しくありません。コンパイラによっては禁止されていたり、実行するとセグメントエラーが発生します。したがって、文字列リテラルにポインタを設定する使い方は、初期化時に一度設定してしまえば二度と変更する必要がないような文字列の場合に用います。

Microsoft Visual C++と文字列リテラル

Microsoft Visual C++の多くのバージョンでは文字列リテラルを書き換える操作が許容されていますが、これはコンパイラがたまたまそういう仕様を持っているだけです。Cの仕様上、文字列リテラルが書き換え可能な領域に置かれることは一切保証されないため、Visual C++を使用していてもそのような操作は控えるべきでしょう。

【補足】上記のように文字列リテラルの内容を変更しようとするコードがあっても、Visual C++に限らず、警告を表示しないコンパイラは多いようです(当然といえば当然なのかもしれませんが)。VC++で警告レベルを/W4にしてもこの点の警告はないようです。ただし、デバッグモードで実行すると、アクセス違反であることがわかります。リリースモードで実行すると……先頭の1文字程度の書き換えであれば、ほとんどの場合でアクセス違反にならずに実行できちゃいます。


Index Page