C言語で文字列を扱うときには、他の言語と少し違うルールがあり、「char配列」と「ヌル文字(終端文字)」を理解することがとても重要です。
本記事では、C言語初心者の方を対象に、文字列の正しい考え方や、よくある勘違い、バグの原因となるポイントを、サンプルコードを交えながら丁寧に解説していきます。
C言語の文字列とは何か
C言語における文字列の定義
C言語において、文字列は他の多くの言語のように「特別な型」として用意されているわけではありません。
C言語の文字列は「char型の配列に、最後にヌル文字\0が付いたもの」として定義されます。
もう少し正確に表現すると、C言語の標準的な文字列はヌル終端文字列(null-terminated string)と呼ばれ、次のような条件を満たす必要があります。
- 要素の型は
charであること - 配列のどこかに
'\0'(ヌル文字)が存在すること - 文字列として解釈されるのは、配列の先頭から最初の
'\0'まで
このため、C言語では「文字列」と「char配列」は密接に関連していますが、完全に同じものではない点を意識しておくことが大切です。
文字と文字列の違い
まずは、「文字」と「文字列」の違いをはっきりさせておきます。
文字とは、1つの記号や文字を表す値で、Cではchar型の変数に1文字分を格納します。
文字リテラルは'A'や'9'のようにシングルクォートで囲みます。
一方、文字列は、複数の文字が並んだものです。
C言語では、1文字であっても「文字列」として扱う場合はchar配列とヌル文字を伴うため、次のように区別されます。
- 文字: 1つの
char値 (例:cst-code>’A’) - 文字列: 複数の
char値 + 終端の'\0'(例:"A"は実際には{'A', '\0'})
サンプルコードで確認してみます。
#include <stdio.h>
int main(void) {
// 1文字を表すchar型の変数(文字リテラルは ' ' で囲む)
char ch = 'A';
// 文字列を表すchar配列(文字列リテラルは " " で囲む)
// 実際には {'A', '#include <stdio.h>
int main(void) {
// 1文字を表すchar型の変数(文字リテラルは ' ' で囲む)
char ch = 'A';
// 文字列を表すchar配列(文字列リテラルは " " で囲む)
// 実際には {'A', '\0'} という2要素の配列になります
char str[] = "A";
printf("ch = %c\n", ch); // 1文字として表示
printf("str = %s\n", str); // 文字列として表示
return 0;
}
'} という2要素の配列になります
char str[] = "A";
printf("ch = %c\n", ch); // 1文字として表示
printf("str = %s\n", str); // 文字列として表示
return 0;
}
ch = A
str = A
出力は同じように見えますが、chは1バイトの文字、strは「文字+ヌル文字」の2バイトという違いがあります。
文字列リテラルとは何か
文字列リテラルとは、ソースコード中に直接書いた"Hello"のようなダブルクォートで囲まれた文字の並びのことです。
コンパイル時に読み取られ、実行時には読み取り専用のメモリ領域に格納されます。
例えば、次のような書き方があります。
#include <stdio.h>
int main(void) {
// 文字列リテラルを指すポインタ
const char *msg = "Hello";
printf("%s\n", msg);
return 0;
}
Hello
ここでポイントとなるのは次の2点です。
1つめに、文字列リテラル自体は読み取り専用であり、書き換えを行ってはいけません。
2つめに、文字列リテラルはプログラム起動時にどこかのメモリに配置され、その先頭アドレスがポインタなどで利用されるという構造になっていることです。
文字列リテラルと、char配列の違いは後ほど詳しく解説します。
char配列とヌル文字の基本
char配列で文字列を表現する方法
C言語では、文字列はchar配列を使って表現します。
具体的には、次のような書き方が代表的です。
#include <stdio.h>
int main(void) {
// 1. 文字列リテラルで初期化する方法
char str1[] = "ABC"; // {'A', 'B', 'C', '#include <stdio.h>
int main(void) {
// 1. 文字列リテラルで初期化する方法
char str1[] = "ABC"; // {'A', 'B', 'C', '\0'} として初期化される
// 2. 文字を1つずつ並べて初期化する方法
char str2[4] = { 'A', 'B', 'C', '\0' }; // 要素数を明示
// 3. 配列サイズを大きめに確保しておく方法
char str3[10] = "ABC"; // {'A','B','C','\0', その後は自動的に '\0' で埋められる}
printf("str1 = %s\n", str1);
printf("str2 = %s\n", str2);
printf("str3 = %s\n", str3);
return 0;
}
'} として初期化される
// 2. 文字を1つずつ並べて初期化する方法
char str2[4] = { 'A', 'B', 'C', '#include <stdio.h>
int main(void) {
// 1. 文字列リテラルで初期化する方法
char str1[] = "ABC"; // {'A', 'B', 'C', '\0'} として初期化される
// 2. 文字を1つずつ並べて初期化する方法
char str2[4] = { 'A', 'B', 'C', '\0' }; // 要素数を明示
// 3. 配列サイズを大きめに確保しておく方法
char str3[10] = "ABC"; // {'A','B','C','\0', その後は自動的に '\0' で埋められる}
printf("str1 = %s\n", str1);
printf("str2 = %s\n", str2);
printf("str3 = %s\n", str3);
return 0;
}
' }; // 要素数を明示
// 3. 配列サイズを大きめに確保しておく方法
char str3[10] = "ABC"; // {'A','B','C','#include <stdio.h>
int main(void) {
// 1. 文字列リテラルで初期化する方法
char str1[] = "ABC"; // {'A', 'B', 'C', '\0'} として初期化される
// 2. 文字を1つずつ並べて初期化する方法
char str2[4] = { 'A', 'B', 'C', '\0' }; // 要素数を明示
// 3. 配列サイズを大きめに確保しておく方法
char str3[10] = "ABC"; // {'A','B','C','\0', その後は自動的に '\0' で埋められる}
printf("str1 = %s\n", str1);
printf("str2 = %s\n", str2);
printf("str3 = %s\n", str3);
return 0;
}
', その後は自動的に '#include <stdio.h>
int main(void) {
// 1. 文字列リテラルで初期化する方法
char str1[] = "ABC"; // {'A', 'B', 'C', '\0'} として初期化される
// 2. 文字を1つずつ並べて初期化する方法
char str2[4] = { 'A', 'B', 'C', '\0' }; // 要素数を明示
// 3. 配列サイズを大きめに確保しておく方法
char str3[10] = "ABC"; // {'A','B','C','\0', その後は自動的に '\0' で埋められる}
printf("str1 = %s\n", str1);
printf("str2 = %s\n", str2);
printf("str3 = %s\n", str3);
return 0;
}
' で埋められる}
printf("str1 = %s\n", str1);
printf("str2 = %s\n", str2);
printf("str3 = %s\n", str3);
return 0;
}
str1 = ABC
str2 = ABC
str3 = ABC
どの場合でも、文字列として扱うには末尾に'\0'が入っていることが必須です。
文字列リテラルで初期化する場合、コンパイラが自動的に末尾に'\0'を付けてくれます。
ヌル文字(\0)とは何か
ヌル文字(null character)とは、数値としては0の値を持つ特別な文字で、Cでは'\0'と表記します。
ASCIIコードでいうと0番にあたり、印字されない制御文字です。
このヌル文字は、「ここで文字列は終わりです」という印として使われます。
C言語では、文字列は配列の長さではなく、このヌル文字の位置によって長さが決まるという点がとても重要です。
例えば、"ABC"という文字列リテラルは、実際には次の4つのcharで構成されています。
'A''B''C''\0'(終端)
なぜ末尾に\0が必要なのか
C言語の標準ライブラリには、printfやstrlenなど、文字列を扱う関数が多数用意されています。
これらの関数は、配列の大きさではなく、ヌル文字'\0'が現れる位置を見て文字列の終わりを判断します。
例えば、strlenは次のようなイメージで動作します。
#include <stdio.h>
// strlenのイメージ(実装例の一つ)
// 実際の標準ライブラリとは異なる可能性がありますが、考え方を示すためのサンプルです。
size_t my_strlen(const char *s) {
size_t len = 0;
// s[len] が '#include <stdio.h>
// strlenのイメージ(実装例の一つ)
// 実際の標準ライブラリとは異なる可能性がありますが、考え方を示すためのサンプルです。
size_t my_strlen(const char *s) {
size_t len = 0;
// s[len] が '\0' に到達するまでカウントを進める
while (s[len] != '\0') {
len++;
}
return len;
}
int main(void) {
char str[] = "ABC"; // {'A', 'B', 'C', '\0'}
printf("長さ = %zu\n", my_strlen(str));
return 0;
}
' に到達するまでカウントを進める
while (s[len] != '#include <stdio.h>
// strlenのイメージ(実装例の一つ)
// 実際の標準ライブラリとは異なる可能性がありますが、考え方を示すためのサンプルです。
size_t my_strlen(const char *s) {
size_t len = 0;
// s[len] が '\0' に到達するまでカウントを進める
while (s[len] != '\0') {
len++;
}
return len;
}
int main(void) {
char str[] = "ABC"; // {'A', 'B', 'C', '\0'}
printf("長さ = %zu\n", my_strlen(str));
return 0;
}
') {
len++;
}
return len;
}
int main(void) {
char str[] = "ABC"; // {'A', 'B', 'C', '#include <stdio.h>
// strlenのイメージ(実装例の一つ)
// 実際の標準ライブラリとは異なる可能性がありますが、考え方を示すためのサンプルです。
size_t my_strlen(const char *s) {
size_t len = 0;
// s[len] が '\0' に到達するまでカウントを進める
while (s[len] != '\0') {
len++;
}
return len;
}
int main(void) {
char str[] = "ABC"; // {'A', 'B', 'C', '\0'}
printf("長さ = %zu\n", my_strlen(str));
return 0;
}
'}
printf("長さ = %zu\n", my_strlen(str));
return 0;
}
長さ = 3
このループでは、'\0'が見つかるまで配列を読み続けています。
もし'\0'が入っていなかった場合、配列の外側のメモリまで読み続けてしまい、未定義動作(予測不能な動作)になります。
そのため、末尾の'\0'は必須です。
ヌル終端文字列(null-terminated string)という考え方
このように、'\0'によって終わりが示された文字列のことをヌル終端文字列(null-terminated string)と呼びます。
C言語で使われる文字列の基本的な形式は、すべてこのヌル終端文字列です。
ポイントは、文字列の長さは「終端の'\0'の位置−0から数えた要素数」で決まるということです。
配列のサイズや、メモリに確保された領域の大きさとは独立して考えられます。
この考え方をしっかり理解すると、文字列操作関数の挙動や、バッファサイズを決めるときの理由が分かりやすくなります。
配列と文字列の違いを整理
単なる配列と文字列用配列の違い
同じchar型の配列でも、「ただのバイト列」と「文字列として扱う配列」では意味が異なります。
- 単なるchar配列: バイナリデータや固定長のデータを格納するための配列。終端の
'\0'は必須ではない。 - 文字列用のchar配列: ヌル終端文字列として扱うための配列。必ずどこかに
'\0'を含んでいなければならない。
例を見てみます。
#include <stdio.h>
int main(void) {
// 単なるバイト配列(文字列とは限らない)
unsigned char bytes[3] = { 0x41, 0x42, 0x43 }; // 'A','B','C' に相当
// 文字列として使う配列(末尾に '#include <stdio.h>
int main(void) {
// 単なるバイト配列(文字列とは限らない)
unsigned char bytes[3] = { 0x41, 0x42, 0x43 }; // 'A','B','C' に相当
// 文字列として使う配列(末尾に '\0' を含める)
char str[4] = { 'A', 'B', 'C', '\0' };
// bytes を文字列として出力しようとすると危険
printf("bytesを文字列として出力(危険な例) -> %s\n", (char *)bytes);
// strは正しくヌル終端されているので安全に出力可能
printf("strを文字列として出力 -> %s\n", str);
return 0;
}
' を含める)
char str[4] = { 'A', 'B', 'C', '#include <stdio.h>
int main(void) {
// 単なるバイト配列(文字列とは限らない)
unsigned char bytes[3] = { 0x41, 0x42, 0x43 }; // 'A','B','C' に相当
// 文字列として使う配列(末尾に '\0' を含める)
char str[4] = { 'A', 'B', 'C', '\0' };
// bytes を文字列として出力しようとすると危険
printf("bytesを文字列として出力(危険な例) -> %s\n", (char *)bytes);
// strは正しくヌル終端されているので安全に出力可能
printf("strを文字列として出力 -> %s\n", str);
return 0;
}
' };
// bytes を文字列として出力しようとすると危険
printf("bytesを文字列として出力(危険な例) -> %s\n", (char *)bytes);
// strは正しくヌル終端されているので安全に出力可能
printf("strを文字列として出力 -> %s\n", str);
return 0;
}
上のプログラムは動作が未定義になる可能性があるため、実際には実行しない方が安全です。
ヌル終端されていない配列を%sで表示するのは非常に危険ということを示すための例です。
char配列と文字列リテラルの違い
char配列と文字列リテラルは見た目が似ていますが、中身や扱い方に重要な違いがあります。
代表的な違いを表にまとめます。
| 種類 | 例 | 実体 | 書き換え可否 |
|---|---|---|---|
| char配列 | char s[] = "ABC"; | スタック上などの配列領域 | 書き換え可能 |
| 文字列リテラル | const char *p = "ABC"; | 読み取り専用のメモリ上の文字列 | 書き換え不可 |
例えば、次のコードを見てください。
#include <stdio.h>
int main(void) {
// 配列として確保した文字列(書き換え可能)
char str[] = "ABC";
// 文字列リテラルを指すポインタ(書き換え禁止が望ましいので const を付ける)
const char *p = "ABC";
str[0] = 'X'; // OK: 配列の中身を書き換えられる
// p[0] = 'X'; // NG: 文字列リテラルを書き換えようとすると未定義動作
printf("str = %s\n", str);
printf("p = %s\n", p);
return 0;
}
str = XBC
p = ABC
文字列リテラルは変更しない前提で使うため、ポインタにはconst char *を付けるのが良い習慣です。
文字列の長さと配列要素数の関係
文字列を格納する配列を用意する際には、「実際の文字数+1」以上の要素数が必要になります。
この「+1」が、終端の'\0'のためのスペースです。
例えば、"ABC"という文字列を格納したい場合、文字数は3ですが、必要な配列サイズは4です。
#include <stdio.h>
int main(void) {
// 配列の要素数は 4 (A,B,C,#include <stdio.h>
int main(void) {
// 配列の要素数は 4 (A,B,C,\0)
char str[4] = "ABC";
printf("文字列: %s\n", str);
printf("配列の要素数: %zu\n", sizeof(str) / sizeof(str[0]));
return 0;
}
)
char str[4] = "ABC";
printf("文字列: %s\n", str);
printf("配列の要素数: %zu\n", sizeof(str) / sizeof(str[0]));
return 0;
}
文字列: ABC
配列の要素数: 4
文字列の長さ(文字数)と、配列の要素数は必ずしも一致しないことを理解しておくと、バッファ不足のバグを防ぎやすくなります。
「こんにちは」がメモリにどう保存されるか
ここからは、日本語のようなマルチバイト文字も含めて考えてみます。
環境によりますが、多くの日本語Windows環境ではShift JIS、最近の多くのLinuxやmacOS環境ではUTF-8が使われます。
例えば、UTF-8環境で"こんにちは"という文字列を考えます。
UTF-8では、日本語1文字が通常3バイト以上で表現されます。
イメージとしては次のようになります(バイト値は例です)。
| 文字 | バイト列(例:UTF-8) |
|---|---|
| こ | 0xE3 0x81 0x93 |
| ん | 0xE3 0x82 0x93 |
| に | 0xE3 0x81 0xAB |
| ち | 0xE3 0x81 0xA1 |
| は | 0xE3 0x81 0xAF |
| 終端 | 0x00(= ‘\0’) |
この場合、見た目の文字数は5文字ですが、必要なバイト数は 5×3 + 1 = 16バイトとなります。
#include <stdio.h>
int main(void) {
// 環境がUTF-8であることを想定した例
char str[] = "こんにちは";
printf("文字列: %s\n", str);
printf("バイト数(sizeof): %zu\n", sizeof(str)); // '#include <stdio.h>
int main(void) {
// 環境がUTF-8であることを想定した例
char str[] = "こんにちは";
printf("文字列: %s\n", str);
printf("バイト数(sizeof): %zu\n", sizeof(str)); // '\0' 分も含まれる
return 0;
}
' 分も含まれる
return 0;
}
出力例(UTF-8環境の場合):
文字列: こんにちは
バイト数(sizeof): 16
「文字数」と「バイト数」は異なるという点が重要です。
特に日本語を扱う際には、この違いがバッファサイズや文字列の長さ計算に深く関わってきます。
マルチバイト文字とバイト数の違いに注意
ASCII文字(英数字や一部の記号)は1文字が1バイトで表現されますが、日本語などのマルチバイト文字は1文字が2バイト以上になります。
UTF-8では多くの日本語が3バイトです。
「文字数を数えたいのか」「バイト数を知りたいのか」を常に意識することが必要です。
strlenはバイト数を返す関数であり、文字数(見た目の文字の個数)ではない点に注意してください。- 日本語の文字数を正確に数えたい場合には、マルチバイト文字用の関数やライブラリを利用する必要があります。
C言語の入門段階では、「とりあえず、ASCII文字だけを使って文字列の仕組みを理解し、その後で日本語などに応用する」という学び方がおすすめです。
文字列操作の基本ポイント
文字列の初期化方法
C言語では、文字列(ヌル終端文字列)の初期化方法として、主に次のようなパターンがあります。
1つめは、文字列リテラルで初期化する方法です。
#include <stdio.h>
int main(void) {
// 必要な要素数はコンパイラが自動的に計算してくれる
char str1[] = "Hello";
printf("str1: %s\n", str1);
return 0;
}
2つめは、要素を1つずつ指定して初期化する方法です。
#include <stdio.h>
int main(void) {
// 'H','e','l','l','o','#include <stdio.h>
int main(void) {
// 'H','e','l','l','o','\0' の6要素
char str2[6] = { 'H', 'e', 'l', 'l', 'o', '\0' };
printf("str2: %s\n", str2);
return 0;
}
' の6要素
char str2[6] = { 'H', 'e', 'l', 'l', 'o', '#include <stdio.h>
int main(void) {
// 'H','e','l','l','o','\0' の6要素
char str2[6] = { 'H', 'e', 'l', 'l', 'o', '\0' };
printf("str2: %s\n", str2);
return 0;
}
' };
printf("str2: %s\n", str2);
return 0;
}
3つめは、大きめの配列を確保してから、後で文字列を代入する方法です。
#include <stdio.h>
#include <string.h>
int main(void) {
// 十分大きなバッファ(ここでは100バイト)
char buf[100];
// 文字列コピー関数で代入する
strcpy(buf, "Hello"); // buf に "Hello#include <stdio.h>
#include <string.h>
int main(void) {
// 十分大きなバッファ(ここでは100バイト)
char buf[100];
// 文字列コピー関数で代入する
strcpy(buf, "Hello"); // buf に "Hello\0" が書き込まれる
printf("buf: %s\n", buf);
return 0;
}
" が書き込まれる
printf("buf: %s\n", buf);
return 0;
}
str1: Hello
str2: Hello
buf: Hello
このとき、必ず「終端の'\0'のための1バイト」を含めてバッファサイズを決めることが重要です。
文字列の終端ミスで起こるバグの例
終端の'\0'を正しく扱えないと、一見些細なミスでも重大なバグに繋がります。
典型的な例を見てみましょう。
#include <stdio.h>
int main(void) {
// 要素数が3しかない(終端の '#include <stdio.h>
int main(void) {
// 要素数が3しかない(終端の '\0' を入れる余裕がない)
char str[3] = { 'A', 'B', 'C' }; // ヌル終端されていない
// 危険: %s は '\0' が出るまで読み続ける
printf("str = %s\n", str);
return 0;
}
' を入れる余裕がない)
char str[3] = { 'A', 'B', 'C' }; // ヌル終端されていない
// 危険: %s は '#include <stdio.h>
int main(void) {
// 要素数が3しかない(終端の '\0' を入れる余裕がない)
char str[3] = { 'A', 'B', 'C' }; // ヌル終端されていない
// 危険: %s は '\0' が出るまで読み続ける
printf("str = %s\n", str);
return 0;
}
' が出るまで読み続ける
printf("str = %s\n", str);
return 0;
}
このプログラムは、環境によってはたまたま「ABC」と表示されるかもしれませんが、動作は未定義であり、クラッシュしたり、変な文字列が表示されたりする可能性があります。
また、途中で'\0'を上書きしてしまうミスも危険です。
#include <stdio.h>
int main(void) {
char str[4] = "ABC"; // {'A','B','C','#include <stdio.h>
int main(void) {
char str[4] = "ABC"; // {'A','B','C','\0'}
// 誤って終端の '\0' を書き換えてしまう
str[3] = '!'; // {'A','B','C','!'}
// もはや正しいヌル終端文字列ではない
printf("str = %s\n", str); // 未定義動作
return 0;
}
'}
// 誤って終端の '#include <stdio.h>
int main(void) {
char str[4] = "ABC"; // {'A','B','C','\0'}
// 誤って終端の '\0' を書き換えてしまう
str[3] = '!'; // {'A','B','C','!'}
// もはや正しいヌル終端文字列ではない
printf("str = %s\n", str); // 未定義動作
return 0;
}
' を書き換えてしまう
str[3] = '!'; // {'A','B','C','!'}
// もはや正しいヌル終端文字列ではない
printf("str = %s\n", str); // 未定義動作
return 0;
}
このようなバグを防ぐためには、配列サイズと文字列長、終端位置を常に意識しながらコーディングする習慣が大切です。
文字列長とバッファサイズの考え方
安全に文字列を扱うためには、「その文字列が最大で何文字になるか」「それには何バイト必要か」を事前に考え、十分なバッファを確保することが重要です。
例えば、英数字だけを使うユーザー名が最大20文字までという仕様なら、必要なバッファサイズは次のように考えます。
- 最大文字数: 20文字
- 必要なバイト数: 20 + 1(cst-code>’\0’の分) = 21バイト
#include <stdio.h>
int main(void) {
// ユーザー名は最大20文字とする
char username[21]; // 20文字 + 終端の '#include <stdio.h>
int main(void) {
// ユーザー名は最大20文字とする
char username[21]; // 20文字 + 終端の '\0'
// ここでは例として固定文字列を代入
// 実際には入力関数を使って値を受け取ることが多いです
snprintf(username, sizeof(username), "Yamada_Taro");
printf("ユーザー名: %s\n", username);
return 0;
}
'
// ここでは例として固定文字列を代入
// 実際には入力関数を使って値を受け取ることが多いです
snprintf(username, sizeof(username), "Yamada_Taro");
printf("ユーザー名: %s\n", username);
return 0;
}
ユーザー名: Yamada_Taro
ここで使っているsnprintfは、「最大で何バイトまで書き込むか」を指定できる安全な関数です。
バッファサイズを超えて書き込まないためにも、このような関数を積極的に使うことが推奨されます。
日本語を含める場合は、1文字が複数バイトになるので、最大文字数×最大バイト数+1という形で、より慎重にサイズを設計する必要があります。
初心者が混乱しやすいポイントのまとめ
C言語の文字列では、初心者の方が特につまずきやすいポイントがいくつかあります。
ここで整理しておきます。
まず、「文字」と「文字列」の区別です。
'A'と"A"は似ていますが、前者は1バイトの文字、後者は{'A','\0'}という2バイトの文字列です。
次に、文字列リテラルとchar配列の違いも重要です。
文字列リテラルは書き換えてはいけない読み取り専用のデータですが、char str[] = "ABC";のように宣言した配列は自由に書き換えられます。
最後に、ヌル文字'\0'の意味と重要性を理解することが欠かせません。
文字列は必ずヌル終端されていなければならず、そのために「文字数+1」バイトが必要です。
ここを誤ると、バッファオーバーフローやクラッシュといった深刻なバグにつながります。
これらのポイントを意識しながら、まずは英数字だけのシンプルな文字列で練習し、徐々に日本語などのマルチバイト文字へとステップアップしていくと理解しやすくなります。
まとめ
C言語の文字列は、「char配列にヌル文字'\0'が付いたもの」として表現され、配列のサイズではなく終端文字の位置で長さが決まります。
文字と文字列、文字列リテラルとchar配列、文字数とバイト数など、紛らわしい概念が多いですが、それぞれを丁寧に区別することで、バグの少ないコードを書けるようになります。
まずはASCII文字の簡単な例で、配列サイズ、ヌル終端、文字列長の関係をしっかり身につけることから始めてみてください。
