C言語での文字列は、他の多くの言語と異なり特別な型を持ちません。
文字列はchar配列と終端ヌル文字(‘\0’)の組み合わせで表現します。
この仕組みを正しく理解すると、表示やコピー、長さの計算などの操作でつまずきにくくなります。
ここでは初心者の方でも迷わないよう、基礎から丁寧に解説します。
C言語の文字列の基本
文字列はchar配列+終端ヌル
C言語ではchar
型は1バイトの文字を表す型で、複数の文字を並べたchar
配列に'\0'
(ヌル文字)を末尾に置くことで文字列を表現します。
末尾の'\0'
があることで、関数は「どこで文字列が終わるか」を判断できます。
もし'\0'
を忘れると、後述のように未定義動作になります。
以下は配列末尾に'\0'
があることを目で確認する例です。
#include <stdio.h>
int main(void) {
// 文字列リテラルで初期化すると、自動的に終端ヌル('#include <stdio.h>
int main(void) {
// 文字列リテラルで初期化すると、自動的に終端ヌル('\0')が入ります
char s[] = "ABC"; // 配列の大きさは 4 バイトになる: 'A','B','C','\0'
// %s は '\0' までを文字列として表示します
printf("as string: %s\n", s);
// バイト列を数値で確認します
printf("bytes: ");
for (size_t i = 0; i < sizeof s; ++i) {
// char は符号付きかもしれないので unsigned char にキャストして数値表示
printf("%u ", (unsigned char)s[i]);
}
printf("\nsizeof(s) = %zu\n", sizeof s);
return 0;
}
')が入ります
char s[] = "ABC"; // 配列の大きさは 4 バイトになる: 'A','B','C','#include <stdio.h>
int main(void) {
// 文字列リテラルで初期化すると、自動的に終端ヌル('\0')が入ります
char s[] = "ABC"; // 配列の大きさは 4 バイトになる: 'A','B','C','\0'
// %s は '\0' までを文字列として表示します
printf("as string: %s\n", s);
// バイト列を数値で確認します
printf("bytes: ");
for (size_t i = 0; i < sizeof s; ++i) {
// char は符号付きかもしれないので unsigned char にキャストして数値表示
printf("%u ", (unsigned char)s[i]);
}
printf("\nsizeof(s) = %zu\n", sizeof s);
return 0;
}
'
// %s は '#include <stdio.h>
int main(void) {
// 文字列リテラルで初期化すると、自動的に終端ヌル('\0')が入ります
char s[] = "ABC"; // 配列の大きさは 4 バイトになる: 'A','B','C','\0'
// %s は '\0' までを文字列として表示します
printf("as string: %s\n", s);
// バイト列を数値で確認します
printf("bytes: ");
for (size_t i = 0; i < sizeof s; ++i) {
// char は符号付きかもしれないので unsigned char にキャストして数値表示
printf("%u ", (unsigned char)s[i]);
}
printf("\nsizeof(s) = %zu\n", sizeof s);
return 0;
}
' までを文字列として表示します
printf("as string: %s\n", s);
// バイト列を数値で確認します
printf("bytes: ");
for (size_t i = 0; i < sizeof s; ++i) {
// char は符号付きかもしれないので unsigned char にキャストして数値表示
printf("%u ", (unsigned char)s[i]);
}
printf("\nsizeof(s) = %zu\n", sizeof s);
return 0;
}
as string: ABC
bytes: 65 66 67 0
sizeof(s) = 4
ポイントとして、配列の大きさは「文字数+1(終端ヌル分)」になることを意識してください。
ヌル文字 ‘\0’ の役割
'\0'
は「ここで文字列が終わり」という印です。
printf("%s")
や各種文字列関数はこの印に出会うまで処理を続けます。
仕組みを確かめるため、標準関数strlen
に似た処理を自作してみます(標準関数の詳細は別記事で扱います)。
#include <stdio.h>
// '#include <stdio.h>
// '\0' に出会うまで数える簡易版 strlen
size_t my_strlen(const char *s) {
size_t n = 0;
while (s[n] != '\0') { // ヌル文字で終端
++n;
}
return n;
}
int main(void) {
char msg[] = "Hello";
printf("my_strlen(\"%s\") = %zu\n", msg, my_strlen(msg));
return 0;
}
' に出会うまで数える簡易版 strlen
size_t my_strlen(const char *s) {
size_t n = 0;
while (s[n] != '#include <stdio.h>
// '\0' に出会うまで数える簡易版 strlen
size_t my_strlen(const char *s) {
size_t n = 0;
while (s[n] != '\0') { // ヌル文字で終端
++n;
}
return n;
}
int main(void) {
char msg[] = "Hello";
printf("my_strlen(\"%s\") = %zu\n", msg, my_strlen(msg));
return 0;
}
') { // ヌル文字で終端
++n;
}
return n;
}
int main(void) {
char msg[] = "Hello";
printf("my_strlen(\"%s\") = %zu\n", msg, my_strlen(msg));
return 0;
}
my_strlen("Hello") = 5
この例からも終端ヌルが正しく入っていないと長さ計算すら破綻することがわかります。
メモリ上のイメージ
配列の中身は連続したバイト列です。
例えばchar s[] = "CAT";
は次のように並びます。
最後に0
(ヌル)が必ず入る点が重要です。
インデックス | 0 | 1 | 2 | 3 |
---|---|---|---|---|
文字 | ‘C’ | ‘A’ | ‘T’ | ‘\0’ |
16進 | 43 | 41 | 54 | 00 |
文字列処理はこの連続メモリ+終端ヌルというルールで動いています。
char配列の宣言とサイズの決め方
固定長のchar配列を宣言する
固定長バッファを扱う基本は、十分なサイズをあらかじめ確保し、必要に応じて空文字に初期化しておくことです。
まず確保、そして必ず終端ヌルを意識が基本です。
#include <stdio.h>
int main(void) {
// 16バイトの固定長バッファを宣言し、空文字で初期化
char name[16] = ""; // すべて 0 で埋まる(未指定要素は 0 初期化)
printf("capacity(bytes) = %zu\n", sizeof name);
printf("as string: \"%s\"\n", name); // 空文字として表示される
printf("last byte = %u\n", (unsigned char)name[15]); // 0 のはず
return 0;
}
capacity(bytes) = 16
as string: ""
last byte = 0
初期化子""
を使えば空文字になり、未使用領域も0で埋まります。
文字数+1(終端ヌル分)を確保
例えば"Tokyo"
は5文字なので、配列は最低でも6バイト必要です。
1バイトは'\0'
専用です。
#include <stdio.h>
int main(void) {
char city[6] = "Tokyo"; // OK: 'T','o','k','y','o','#include <stdio.h>
int main(void) {
char city[6] = "Tokyo"; // OK: 'T','o','k','y','o','\0'
printf("%s\n", city);
return 0;
}
'
printf("%s\n", city);
return 0;
}
Tokyo
コンパイル時にサイズ不足だとエラーや警告になります。
// コンパイルエラー例(サイズ不足)
// char city2[5] = "Tokyo"; // 5 + 1(終端) = 6 が必要
1文字でも足りないとオーバーランや未定義動作に直結します。
空文字列を作る
空文字を作る代表的な方法は2つあります。
初期化子""
を使うか、先頭要素を'\0'
にする方法です。
#include <stdio.h>
int main(void) {
char buf1[32] = ""; // 宣言と同時に空文字
char buf2[32]; // 未初期化
buf2[0] = '#include <stdio.h>
int main(void) {
char buf1[32] = ""; // 宣言と同時に空文字
char buf2[32]; // 未初期化
buf2[0] = '\0'; // 実行時に空文字にする
printf("buf1: \"%s\"\n", buf1);
printf("buf2: \"%s\"\n", buf2);
return 0;
}
'; // 実行時に空文字にする
printf("buf1: \"%s\"\n", buf1);
printf("buf2: \"%s\"\n", buf2);
return 0;
}
buf1: ""
buf2: ""
「まず空文字にしてから使う」習慣は安全性を高めます。
文字列の作り方
文字列リテラルで初期化する
一番簡単なのはchar 配列
を文字列リテラルで初期化する方法です。
配列の大きさは自動で「文字数+1」になります。
#include <stdio.h>
int main(void) {
char s1[] = "Hello"; // 配列サイズは 6 ('H','e','l','l','o','#include <stdio.h>
int main(void) {
char s1[] = "Hello"; // 配列サイズは 6 ('H','e','l','l','o','\0')
printf("%s (size=%zu)\n", s1, sizeof s1);
return 0;
}
')
printf("%s (size=%zu)\n", s1, sizeof s1);
return 0;
}
Hello (size=6)
リテラルの内容を変更したい場合は配列に初期化してから変更します(ポインタとリテラルの違いは別記事で扱います)。
文字の配列で初期化する
個別の文字で初期化する場合は自分で'\0'
を入れる必要があります。
#include <stdio.h>
int main(void) {
char ok1[] = { 'O', 'K', '#include <stdio.h>
int main(void) {
char ok1[] = { 'O', 'K', '\0' }; // 明示的に終端ヌル
char ok2[5] = { 'H', 'i', '!', '\0' }; // 余裕のある配列
printf("ok1: %s\n", ok1);
printf("ok2: %s\n", ok2);
return 0;
}
' }; // 明示的に終端ヌル
char ok2[5] = { 'H', 'i', '!', '#include <stdio.h>
int main(void) {
char ok1[] = { 'O', 'K', '\0' }; // 明示的に終端ヌル
char ok2[5] = { 'H', 'i', '!', '\0' }; // 余裕のある配列
printf("ok1: %s\n", ok1);
printf("ok2: %s\n", ok2);
return 0;
}
' }; // 余裕のある配列
printf("ok1: %s\n", ok1);
printf("ok2: %s\n", ok2);
return 0;
}
ok1: OK
ok2: Hi!
終端ヌルを忘れると後述のように危険です。
実行時に1文字ずつ設定する
ループや代入で1文字ずつ詰める場合も、最後に'\0'
を置くのが必須です。
#include <stdio.h>
int main(void) {
char s[4]; // "SUN" + '#include <stdio.h>
int main(void) {
char s[4]; // "SUN" + '\0' で 4 バイト必要
s[0] = 'S';
s[1] = 'U';
s[2] = 'N';
s[3] = '\0'; // 終端ヌルを忘れない!
printf("%s\n", s);
return 0;
}
' で 4 バイト必要
s[0] = 'S';
s[1] = 'U';
s[2] = 'N';
s[3] = '#include <stdio.h>
int main(void) {
char s[4]; // "SUN" + '\0' で 4 バイト必要
s[0] = 'S';
s[1] = 'U';
s[2] = 'N';
s[3] = '\0'; // 終端ヌルを忘れない!
printf("%s\n", s);
return 0;
}
'; // 終端ヌルを忘れない!
printf("%s\n", s);
return 0;
}
SUN
実行時構築では「確保→詰める→終端」を常にセットで考えます。
printf(“%s”)は’\0’まで表示される
printf("%s")
は最初の'\0'
までしか表示しません。
途中に'\0'
があると、そこで止まります。
#include <stdio.h>
int main(void) {
char a[] = { 'A', 'B', '#include <stdio.h>
int main(void) {
char a[] = { 'A', 'B', '\0', 'C', 'D', '\0' };
char b[] = "Hi\0Hidden"; // リテラル中の \0 以降は文字列として扱われない
printf("a as string: %s\n", a); // 'A','B' まで
printf("b as string: %s\n", b); // "Hi" まで
return 0;
}
', 'C', 'D', '#include <stdio.h>
int main(void) {
char a[] = { 'A', 'B', '\0', 'C', 'D', '\0' };
char b[] = "Hi\0Hidden"; // リテラル中の \0 以降は文字列として扱われない
printf("a as string: %s\n", a); // 'A','B' まで
printf("b as string: %s\n", b); // "Hi" まで
return 0;
}
' };
char b[] = "Hi#include <stdio.h>
int main(void) {
char a[] = { 'A', 'B', '\0', 'C', 'D', '\0' };
char b[] = "Hi\0Hidden"; // リテラル中の \0 以降は文字列として扱われない
printf("a as string: %s\n", a); // 'A','B' まで
printf("b as string: %s\n", b); // "Hi" まで
return 0;
}
Hidden"; // リテラル中の #include <stdio.h>
int main(void) {
char a[] = { 'A', 'B', '\0', 'C', 'D', '\0' };
char b[] = "Hi\0Hidden"; // リテラル中の \0 以降は文字列として扱われない
printf("a as string: %s\n", a); // 'A','B' まで
printf("b as string: %s\n", b); // "Hi" まで
return 0;
}
以降は文字列として扱われない
printf("a as string: %s\n", a); // 'A','B' まで
printf("b as string: %s\n", b); // "Hi" まで
return 0;
}
a as string: AB
b as string: Hi
「%sは最初のヌルまで」という規則を覚えておくと表示の不具合の原因を早く特定できます。
よくあるミスと注意点
終端ヌル忘れで不正動作
終端ヌルを入れ忘れた配列を文字列として扱うと未定義動作です。
たまたま止まる場所まで読み進めて意味不明な文字列になったり、クラッシュすることもあります。
#include <stdio.h>
int main(void) {
// 終端ヌルが無い(危険)
char bad[3] = { 'N', 'G', '!' }; // ← '#include <stdio.h>
int main(void) {
// 終端ヌルが無い(危険)
char bad[3] = { 'N', 'G', '!' }; // ← '\0' が無い
// %s は '\0' を探してメモリを読み続けるため未定義動作
printf("bad: %s\n", bad);
return 0;
}
' が無い
// %s は '#include <stdio.h>
int main(void) {
// 終端ヌルが無い(危険)
char bad[3] = { 'N', 'G', '!' }; // ← '\0' が無い
// %s は '\0' を探してメモリを読み続けるため未定義動作
printf("bad: %s\n", bad);
return 0;
}
' を探してメモリを読み続けるため未定義動作
printf("bad: %s\n", bad);
return 0;
}
実行例(環境により異なる): bad: NG!▒▒▒▒▒▒
このコードは動作が保証されません。
必ず末尾に'\0'
を入れてください。
配列サイズ不足でバッファオーバーラン
必要なサイズ(文字数+1)を満たしていないと、配列境界外への書き込みが起きます。
// コンパイル時に検出される例
// char s[5] = "Hello"; // エラー: "Hello" は 6 バイト必要
// 実行時に境界外アクセスする危険な例(やってはいけない)
// char t[4] = { 'D', 'a', 'n', '// コンパイル時に検出される例
// char s[5] = "Hello"; // エラー: "Hello" は 6 バイト必要
// 実行時に境界外アクセスする危険な例(やってはいけない)
// char t[4] = { 'D', 'a', 'n', '\0' };
// t[4] = '!'; // 配列の外に書き込む: 未定義動作
' };
// t[4] = '!'; // 配列の外に書き込む: 未定義動作
安全のためにはサイズ計算を丁寧に行い、余裕をもって確保するのが実務でも基本です。
配列への文字列代入は不可
配列は代入できません。
char a[10]; a = "Hi";
のような代入はコンパイルエラーです。
// コンパイルエラー例
// char a[10];
// a = "Hi"; // error: array type 'char[10]' is not assignable
// 正しいやり方の一例
// 1) 初期化で与える
// char a[10] = "Hi";
// 2) 実行時に1文字ずつ詰めて最後に '// コンパイルエラー例
// char a[10];
// a = "Hi"; // error: array type 'char[10]' is not assignable
// 正しいやり方の一例
// 1) 初期化で与える
// char a[10] = "Hi";
// 2) 実行時に1文字ずつ詰めて最後に '\0'
// a[0] = 'H'; a[1] = 'i'; a[2] = '\0';
'
// a[0] = 'H'; a[1] = 'i'; a[2] = '// コンパイルエラー例
// char a[10];
// a = "Hi"; // error: array type 'char[10]' is not assignable
// 正しいやり方の一例
// 1) 初期化で与える
// char a[10] = "Hi";
// 2) 実行時に1文字ずつ詰めて最後に '\0'
// a[0] = 'H'; a[1] = 'i'; a[2] = '\0';
';
「配列には初期化子、実行時は要素代入」という原則を覚えましょう。
未初期化の配列にはゴミが入る
自動変数のchar 配列
は、初期化しないと中身は不定(ゴミ)です。
そのまま%s
で表示すると未定義動作になります。
まず空文字にしてから使うのが安全です。
#include <stdio.h>
int main(void) {
char buf[8]; // 未初期化(中身は不定)
// printf("%s\n", buf); // 危険: 未定義動作
// 安全な初期化
buf[0] = '#include <stdio.h>
int main(void) {
char buf[8]; // 未初期化(中身は不定)
// printf("%s\n", buf); // 危険: 未定義動作
// 安全な初期化
buf[0] = '\0'; // まず空文字にする
// 使うときに埋める
buf[0] = 'O'; buf[1] = 'K'; buf[2] = '\0';
printf("buf: %s\n", buf);
return 0;
}
'; // まず空文字にする
// 使うときに埋める
buf[0] = 'O'; buf[1] = 'K'; buf[2] = '#include <stdio.h>
int main(void) {
char buf[8]; // 未初期化(中身は不定)
// printf("%s\n", buf); // 危険: 未定義動作
// 安全な初期化
buf[0] = '\0'; // まず空文字にする
// 使うときに埋める
buf[0] = 'O'; buf[1] = 'K'; buf[2] = '\0';
printf("buf: %s\n", buf);
return 0;
}
';
printf("buf: %s\n", buf);
return 0;
}
buf: OK
使う前に必ず終端ヌルをセット。
これだけで多くのトラブルを未然に防げます。
まとめ
C言語の文字列は「char配列」と「終端ヌル文字(‘\0’)」の二本柱で成り立っています。
メモリ上の実体は連続したバイト列で、配列サイズは文字数+1、表示や処理は'\0'
までというルールが全ての基本です。
終端ヌルの入れ忘れ、サイズ不足、未初期化の利用、配列への直接代入などは典型的な落とし穴です。
この記事で紹介した正しい初期化と終端の意識を習慣化すれば、文字列周りの不具合は大きく減らせます。
次の学習では、文字列リテラルと配列の違い、標準関数strlen
やstrcpy
などの具体的な操作を別記事で段階的に扱っていきます。