文字と文字列 | Programming Place Plus C言語編 第8章

トップページC言語編

このページの概要 🔗

このページの解説は C99 をベースとしています

以下は目次です。


文字 🔗

ここまでのページで、文字列が何度も登場しましたが、ここでは文字列に含まれている1つの文字に目を向けてみます。

文字列に対して次の構文を使うと、文字列に含まれている1つの文字にアクセスできます。

文字列[先頭から数えて何文字目かをあらわす整数]

「何文字目か」という部分に関しては、先頭にある文字を 0 文字目であると考えます。したがって、文字列の文字数が 5文字なら、最後の文字は 4文字目ということになります。また、文字列の最後の文字よりも後ろをアクセスしようとする行為は未定義の動作であることに注意してください。

次のプログラムは、標準入力から姓と名を入力してもらい、イニシャルを出力します。

#include <stdio.h>

int main(void)
{
    puts("Please enter the your family name.");
    char family_name[40];
    fgets(family_name, sizeof(family_name), stdin);

    puts("Please enter the your first name.");
    char first_name[40];
    fgets(first_name, sizeof(first_name), stdin);

    printf("Hello, %c.%c\n", family_name[0], first_name[0]);
}

実行結果:

Please enter the your family name.
Saitou  <-- 入力した文字列
Please enter the your first name.
Ken  <-- 入力した文字列
Hello, S.K

family_name[0] とか first_name[0] といった記述によって、それぞれの 0文字にアクセスしています。printf関数を使って、1つの文字を出力するには %c変換指定子を使います。sscanf関数で文字を取り出す場合も同じく、%c変換指定子が使えます。

このサンプルプログラムでは、姓と名を別個に入力させましたが、1つにまとめることもできます。

#include <stdio.h>

int main(void)
{
    puts("Please enter the your full name.");
    char full_name[80];
    fgets(full_name, sizeof(full_name), stdin);

    char family_name[40];
    char first_name[40];
    sscanf(full_name, "%s%s", family_name, first_name);

    printf("Hello, %c.%c\n", family_name[0], first_name[0]);
}

実行結果:

Please enter the your full name.
Saitou Ken  <-- 入力した文字列
Hello, S.K

printf関数で文字列を出力するときに %s変換指定子を使ったのと同じく、sscanf関数を使って文字列を取り出すときには、%s変換指定子を使います。sscanf関数の %s変換指定子は、空白類文字 (white-space character) が登場するまでを1つの文字列とみなします。空白類文字は、空白文字、水平タブ、垂直タブ、改行文字、書式送りの総称です。そのため、入力時に姓と名を空白で区切って入力してもらう必要があります。

char型 🔗

変数に、1つの文字だけを記憶させたいときには、char型の変数を宣言します。

char型を使ったサンプルプログラムは次のようになります。

#include <stdio.h>

int main(void)
{
    char c = 'a';
    printf("%c\n", c);

    c = 'x';
    printf("%c\n", c);
}

実行結果:

a
x

'a''x' のように、シングルクォーテーションで囲まれた文字は、文字定数 (character constant) です。文字列リテラルと同じく、ソースコード上に書いたとおりに取り扱われ、決してほかのものにならない不変な文字のデータです。

文字定数の型 🔗

普通に考えれば、文字定数の型は char型のように思えますが、実は違います。文字定数は int型です。

【C++プログラマー】C++ の文字リテラルの型は char型です。この違いは、C言語と C++ の代表的な差異の1つになっています。C++ にはテンプレートやオーバーロードのように、型を明確に区別できたほうが都合が良い場面が多くあるため、文字リテラルは char型の方が都合が良いです。

次のコードは型が一致していないことになりますが、文字定数を char型の変数に入れる行為は問題ありません。

char c = `a`;  // int型の値で char型の変数を初期化する
c = 'x';       // int型の値を char型の変数に代入する

【上級】これが問題ないのは、基本文字集合に含まれる文字は 1バイトに収まらなければならないというルールがあり[1]、char型は必ず 1バイトの大きさをもつからです。基本文字集合に含まれない文字を取り扱う可能性がある場合は、char型で扱うことは不適切になります。たとえば、fgetc関数は文字を返す標準ライブラリ関数ですが、失敗したときに EOF という特別な値を返すため、戻り値の型が int になっています。

エスケープシーケンス 🔗

\n が改行を表すのと同じように、\ に1文字加えて、何か特殊な文字を表現できることがあります。このような表現方法は、エスケープシーケンス (escape sequence) と呼ばれています。また、\ そのものは、エスケープ文字 (escape character) と呼ばれます。

エスケープシーケンスは、そもそも文字として書きようがない機能を表現したり、C言語の文法の都合上、文字列リテラルや文字定数の中に記述できない文字を記述したりするためにあります。

C言語には、以下のエスケープシーケンスがあります[2] [3] [4]

エスケープシーケンス 意味
\a アラート(ベル)
\b 位置を1文字分戻す
\f 書式送り(改ページ)
\n 改行(位置を次の行に進める)
\r 復帰(位置を現在の行の頭にする)
\t タブ(水平タブ)
\v 垂直タブ
\’ シングルクォーテーション(’)
\” ダブルクォーテーション(“)
\? 疑問符(?)
\\ バックスラッシュ(日本語環境では、円マークで表示されることが多い)
\o (o は 8進数の数字) 8進コードによる文字の表現
\xh (h は 16進数の数字) 16進コードによる文字の表現
\unnnn (n は 16進数の数字) ISO/IEC 10646 (Unicode) による文字の表現
\Unnnnnnnn (n は 16進数の数 字) ISO/IEC 10646 (Unicode) による文字の表現

現時点で特に知っておくべきなのは、\n\t\'\"\\ といったところでしょう。\r はそのうち使う機会が出てきますが、ほかの物はあまり使う機会がないかもしれません。

【上級】\u\U による文字の表現は、国際文字名 (universal character name) と呼ばれます。国際文字名を使うと、基本文字集合に含まれていない文字を表記できます。ただし、00A0未満(0024($)、0040(@)、0060(`)を除く)とD800~DF00 の範囲の文字は使えません。

\'\"\\ はそれぞれ、「‘」「“」「\」といった文字を表すものです。なぜこんなものが必要かというと、「’」「”」「\」には、それぞれ別の用途があるからです。「\」 はエスケープシーケンスを表現するために使う必要がありますし、「’」や「“」は文字定数や文字列リテラルの開始と終わりをあらわすために使われています。そのため普通の文字として ’、“、\ を使いたいときには、エスケープシーケンスを使う必要があります。ただし、’ は文字列リテラルの中ではそのまま使え("'")、” は文字リテラルの中ではそのまま使えます('"')。

? についてはそのまま使っても問題ない場合が多いですが、\? とした方が安全です。

【上級】? は、使える文字の種類が少ない環境でソースコードを書けるようにするために、3つの記号を並べて代替するトライグラフ (trigraph) という表記方法で使われています。[5]

次のプログラムは、" の使い方が不適切であり、コンパイルエラーになってしまいます。

#include <stdio.h>

int main(void)
{
    puts("ダブルクォーテーション「"」を出力する。");
}

このプログラムがコンパイルエラーになるのは、文字列リテラルの途中にも ” があるからです。

"ダブルクォーテーション「"

ここまでで文字列リテラルが終わっているとみなされます。

エスケープ文字を補って、次のように書く必要があります。

#include <stdio.h>

int main(void)
{
    puts("ダブルクォーテーション「\"」を出力する。");
}

実行結果:

ダブルクォーテーション「"」を出力する。

実行結果のように、エスケープ文字 \ が出力されることはなく、" が正しく出力されています。


なお、エスケープシーケンスは、ソースコード上での見た目では2文字あるようにみえますが、実際には1文字のあつかいであり、char型で取り扱えます。

#include <stdio.h>

int main(void)
{
    char c = '\n';
    printf("***%c***\n", c);
}

実行結果:

***
***

文字コード 🔗

コンピュータは、文字のデータを整数にしてあつかっています。これは、文字コード (character code) と呼ばれる仕組みです。

文字コードは、文字の種類ごとに異なる整数を割り当てたものです。たとえば、「a」が 10、「b」が 11、「c」が 12、「d」が 13 ・・・といったように割り当てておけば、文字のデータを整数で表現できます。このルールでは、「11、10、13」という整数の並びは「bad」を意味しています。

文字コードにはさまざまな種類があって、国際規格などで明確にルールが決められています(さきほど挙げた対応関係は架空のものです)。文字と整数の対応関係は文字コードの種類によって異なっています。また、定義されている文字の種類(つまり、どんな文字が使えるのか)にも違いがあります。プログラミングでよく登場する ASCII(American Standard Code for Information Interchange。読み方はアスキー。ASCIIコードとも)では、アルファベットや数字は含まれていますが、ひらがなやカタカナ、漢字といった日本語で使う文字がまったく含まれていません。

C言語のソースファイルで使う文字コードの種類は定められておらず、環境(コンパイラ)によって異なります。そのため、ソースファイル上で使える文字の種類も環境によって異なります。それでは困りそうですが、以下に挙げる 96種類の文字は基本文字集合 (basic character set) と呼ばれ、必ず使用できます。[6]

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
0 1 2 3 4 5 6 7 8 9
! " # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^ _ { | } ~
スペース
水平タブ
垂直タブ
フォームフィード

基本文字集合 は ASCII に含まれている文字だけで構成されていますが、ASCII そのものではありません。たとえば、ASCII にはある @ が、基本文字集合にはありません。

【上級】基本文字集合の中には、改行に関する文字がありませんが、ソースファイル内で行の終わりを表現する何らかの方法は必要であるとされています[7]。要するに、Enterキーを押すなどして、行の終わりを指示できれば良いということです。

このように、基本ソース文字集合には日本語の文字が含まれていません。だからといってC言語では日本語が使えないということではなく、コンパイラが使用を許していれば使えます。日本向けに作られているコンパイラならばまず間違いなく使えるでしょうし、そうでないなら使えない可能性も高いということになります。

ただし、日本語の文字をソースファイル上で使えるからといって、日本語を使ったプログラムが簡単に作れるということでもありません。たとえば、ソースコードには書ける文字が、出力結果には正しく表示されない現象に悩まされることがあります。日本語を正しくあつかうプログラムを作るにはそれなりの難しさがあるので、当面は日本語を扱わないことにします。

コメントとして日本語を使うことは、結局、ソースファイルに日本語の文字を書いていることになりますから、やはりコンパイラが許すかどうかによります。しかし、コメントとして使うだけであれば、あまりトラブルになることはないので、今後もコメントには日本語の文字も使うことにします(滅多に見かけないような漢字や記号を使うと、コンパイルエラーが出るなどの問題が起こるかもしれません)。

文字を整数あつかいで出力してみると、その文字がどんな整数で表現されているかが分かります。

#include <stdio.h>

int main(void)
{
    printf("%d\n", 'a');  // %d変換指定子で出力すると、文字がどんな整数で表現されているかが分かる
}

実行結果:

97

ここでは 97 という整数が出力されましたが、文字と整数の対応関係は、使われている文字コード次第です(とはいえ、ASCII で表現できる範囲の文字ならば、まず間違いなく同じ結果が得られるはずです)。

文字列 🔗

第6章で少し触れたように、文字列の末尾にヌル文字(ナル文字) (null character) と呼ばれる、ソースコード上には明確に現れてこない文字が隠れています。文字列がメモリに記憶されているとき、その文字列がどこまで続いているのかを知る方法が必要であり、その役割を果たしているのが、ヌル文字です。ヌル文字は、ソースコード上で見えなくても、たしかに存在する文字なので、1文字分の場所を取ります。

たとえば、"Hello" という文字列リテラルは、見た目としては 5文字のようですが、実際にはヌル文字が隠れており、6文字あります。そのため、次のコードは危険です。

char s[5];
strcpy(s, "Hello");  // s は 5文字までしか保存できない

変数s には 5文字分の場所しかないので、6文字目のヌル文字が溢れてしまいます。これは第7章で説明したバッファオーバーフローです。

ヌル文字を明示的に記述できないわけでもありません。文字の正体は整数なので、ヌル文字の正体も整数です。ヌル文字を整数で表現すると必ず 0 であり[8]'\0' と表記できます。とはいえ、"Hello\0" のように書くと、明示的に書いたヌル文字のさらにうしろにもう1つヌル文字が付いた文字列になってしまいます。特別な場面を除いては、ヌル文字は明示的には書きません。

文字列リテラルの型 🔗

文字列リテラルの型は char型の配列です。要素数については、文字数によって自動的に定まりますが、これは当然ヌル文字の分も加えたものになります。[9]

たとえば、"Hello" という文字列リテラルは、要素数が 6 の char型の配列です。

【C++プログラマー】C++ では、文字列リテラルの型は const付きの char型の配列です。C言語では const が付きませんが、だからといって書き換えられるわけではなく、書き換えようとする行為は未定義の動作です。

文字列の変数を初期化する 🔗

これまで、char型の配列は、いったん初期化されていない状態で宣言し、あとから fgets関数によって内容を入れる使い方をしてきましたが、宣言と同時に初期化することもできます。

次のように、文字列リテラルを初期化子にすることで、char型の配列を初期化できます。

char str1[5] = "abcd";   // OK. "abcd" + ヌル文字
char str2[5] = "abcde";  // OK だが注意. "abcde" で初期化され、ヌル文字が欠けている

要素数を明示的に指定している場合、与える文字列リテラルの文字数(繰り返しますが、ヌル文字の分を含みます)が収まっていなければなりません。str1 は何も問題ありませんが、str2 のほうは、ヌル文字を含めると 6文字あるので場所が足りません。このように、ヌル文字が入りきらない場合には、ヌル文字が欠けた状態で初期化されてしまうので注意が必要です。[10]

次のコードは重大なバグです。

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

int main(void)
{
    char s1[5] = "abcde";  // ヌル文字がない

    char s2[10];
    strcpy(s2, s1);  // 危険!
    puts(s2);  // 上のコピーが危険である以上、これも危険
}

【C++プログラマー】C++ では、ヌル文字が欠ける初期化はコンパイルエラーとして検出されます。

strcpy関数 は、コピー元の文字列がどこまで続いているのかを、ヌル文字の存在によって判断するため、ヌル文字が付いていない文字列をコピーしようとすると、たまたまメモリ上でその文字列のうしろにあったデータを巻き込んでコピーしてしまいます。

メモリ上には、きっとどこかに(たまたま)0 が書き込まれている箇所があるでしょうから、いずれそれをヌル文字であると認識して、そこまでを1つの文字列とみなしてコピーします。想定していた文字列の長さよりもずっと長い文字列をコピーすることになるため、バッファオーバーフローにつながる可能性も高く、危険度はかなり高いといえます。

そこで、次のように初期化することを勧めます。

char str3[] = "abcde";   // OK. "abcde" + ヌル文字

このように、配列の宣言のときに要素数の指定を省略すると、初期化子の文字数によって要素数を決めてくれます。この方法ならば、確実に必要十分な場所が確保され、ヌル文字が付加された文字列が作られます。


なお、次のように {} を使い、1文字ずつを , で区切りながら与える方法もありますが、特別な事情がなければ普通は使わない方法です。

char str4[5] = {'a', 'b', 'c', 'd'};             // OK. 足りない部分は補われるので、"abcd" + ヌル文字 になる
char str5[5] = {'a', 'b', 'c', 'd', 'e'};        // OK だが注意. "abcde" で初期化され、ヌル文字が欠けている
char str6[] = {'a', 'b', 'c', 'd', 'e', '\0'};   // OK. "abcde" + ヌル文字。要素数は自動判断されるが、ヌル文字は自動的には付加されない

指定した要素数に対して、初期化子に書いた文字が足りなければ、その部分にヌル文字が補われます。そのため str4 は何も問題ありません。str5 の方は前の例と同じく、ヌル文字が欠けてしまうため危険です。

str6 は要素数を省略しており、初期化子の文字の個数から判断されますが、文字を別個で指定しているだけであって、文字列を指定したわけではないため、ヌル文字は自動的に付加されません。そのため、明示的に '\0' を指定する必要があります。


練習問題 🔗

問題① 次の文字は基本文字集合に含まれますか?

問題② 'a'"a" の違いを説明してください。

問題③ 次のプログラムの実行結果はどうなりますか?

#include <stdio.h>

int main(void)
{
    char s1[] = "\\n";
    char s2[] = "\"\"";
    char s3[] = "\'";

    puts(s1);
    puts(s2);
    puts(s3);
}

問題④ 次のプログラムで、変数 c に入る文字は何ですか?

#include <stdio.h>

int main(void)
{
    char s[] = "\nHello\n";
    char c = s[0];
}

問題⑤ 文字の入力を受け取り、その文字を整数で表したときの値を出力するプログラムを作成してください。


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 新規作成。
「エスケープシーケンス」の項は、第2章から持ってきた。



前の章へ (第7章 初期化と代入)

次の章へ (第9章 関数)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る