C言語で文字列を扱うときに必ず登場するのが文字列リテラルとchar配列です。
一見よく似ていますが、メモリ上の扱いや書き換えの可否など、実は重要な違いがあります。
ここでは、初心者の方でも混乱しがちなこの2つを、図やサンプルコードを使いながら丁寧に説明し、実際のプログラムでどう使い分けるべきかを解説します。
C言語の文字列の基本
文字列リテラルとは
C言語で文字列リテラルとは、ソースコード中に直接書かれた“abc”のようなダブルクォートで囲まれた文字の並びのことです。
プログラムをコンパイルしたときに、これらは読み取り専用の領域に配置されます。
具体的には、次のようなものが文字列リテラルです。
"Hello""123"""(空文字列)
文字列リテラルには、コンパイラが自動的に終端文字'\0'を付け足します。
したがって、"abc"というリテラルは、実際には{'a', 'b', 'c', '\0'}という4バイトのデータになります。
文字列リテラルの特徴
文字列リテラルには次のような特徴があります。
1つ目は書き換えてはいけないという点です。
C言語の規格では、文字列リテラルを書き換えようとする行為は未定義動作とされています。
コンパイラや環境によっては、プログラムが異常終了することもあります。
2つ目はプログラムのどこからでも同じ文字列リテラルを共有する可能性があるという点です。
同じ"abc"というリテラルがソースコード中に何度も登場する場合、コンパイラはそれらを1つだけ確保して共有することがあります。
3つ目は実行中にサイズを変更できないという点です。
文字列リテラルは固定された長さを持ち、メモリ上の位置も固定されています。
char配列とは
char配列は、char型の要素が連続して並んだ配列で、よく自分で変更できる文字列を保持するために使います。
例えば次のような宣言があります。
char s[4] = "abc";
この配列は、次のような4つの要素を持ちます。
- s[0] = ‘a’
- s[1] = ‘b’
- s[2] = ‘c’
- s[3] = ‘\0’
配列の要素は書き換え可能なので、後からs[0] = 'A';のように変更することができます。
char配列の特徴
char配列の主な特徴は次の通りです。
1つ目は「自分の領域を持つ」ことです。
配列は連続したメモリ領域を確保し、その中に文字列を格納します。
文字列リテラルから初期化した場合でも、その時点でリテラルの内容が配列にコピーされます。
2つ目は書き換え可能であることです。
配列の各要素は、別の文字で上書きできます。
ただし、配列の大きさ自体(要素数)は固定で、途中で増減させることはできません。
3つ目は寿命がスコープに依存することです。
関数内で定義されたローカルなchar配列は、関数を抜けると無効になります。
これに対して、グローバル変数として定義されたchar配列はプログラム全体の実行中有効です。
文字列リテラルとchar配列の共通点と違いの概要
ここで一度、文字列リテラルとchar配列の共通点と違いを整理しておきます。
まず共通点として、どちらも終端文字'\0'で終わる文字列データを扱うために使われます。
そのため、printfやstrlenなど、標準ライブラリの文字列関数で同じように扱うことができます。
一方で、違いは次のように整理できます。
| 項目 | 文字列リテラル | char配列 |
|---|---|---|
| メモリの場所 | 読み取り専用領域(多くは静的領域) | 多くの場合スタック(ローカル変数の場合) |
| 書き換えの可否 | 原則書き換え禁止 | 書き換え可能 |
| サイズの変更 | 実行時に変更不可 | 配列サイズは変更不可だが、中身は書き換え可 |
| 寿命 | プログラム実行中ずっと有効(静的記憶域期間) | 宣言されたスコープに依存 |
| 初期化 | ソース中に書いた文字列 | リテラルや文字の並びで初期化可能 |
この違いを踏まえて、次の章からはもっと具体的な例で、両者を詳しく見ていきます。
「”abc”」と「char s[] = “abc”;」の具体的な違い
メモリ配置の違いを図でイメージする
“abc”という文字列リテラルと、char配列であるchar s[] = "abc";は、見た目は似ていますがメモリ上の配置が異なります。
とても単純化したイメージとしては、次のようになります。
| 種類 | メモリ領域のイメージ |
|---|---|
文字列リテラル"abc" | 静的な読み取り専用領域に「a」「b」「c」「\0」が並ぶ |
char s[] = "abc"; | 関数内であればスタック上に「a」「b」「c」「\0」をコピーして格納 |
図としては、次のように考えるとイメージしやすいです。
- 文字列リテラル
"abc"はどこか1か所に置かれる char s[] = "abc";はそのリテラルを元に、自分専用の領域を確保してコピーされる
このように、「同じ文字列であっても、置かれている場所と性質が違う」という点を意識することが大切です。
文字列リテラルは読み取り専用領域に置かれる
多くのコンパイラや環境では、文字列リテラルは読み取り専用のメモリ領域に配置されます。
このため、そこを書き換えようとすると、アクセス違反でプログラムが落ちる可能性があります。
次のコードは、典型的な危険な例です。
#include <stdio.h>
int main(void) {
char *p = "abc"; // 文字列リテラルへのポインタ
printf("%s\n", p);
// 文字列リテラルを書き換えようとする(危険)
p[0] = 'A'; // ここで未定義動作になる
printf("%s\n", p);
return 0;
}
このプログラムはコンパイルは通る場合がありますが、実行するとクラッシュする可能性があります。
理由は、pが指しているのは読み取り専用の領域であり、そこへの書き込みが禁止されているからです。
char配列はスタック上に確保される
一方で、関数の中で宣言されたchar s[]のような配列は、多くの処理系でスタック領域に確保されます。
ここはローカル変数が置かれる場所であり、基本的に読み書きが可能です。
次のプログラムで、その違いを確認してみます。
#include <stdio.h>
int main(void) {
// char配列を文字列リテラルから初期化
char s[] = "abc";
printf("初期値: %s\n", s);
// 配列の中身を書き換える(安全)
s[0] = 'A';
s[1] = 'B';
s[2] = 'C';
printf("書き換え後: %s\n", s);
return 0;
}
初期値: abc
書き換え後: ABC
このように、char配列であれば文字列の中身を自由に書き換えることができます。
ただし、配列の「大きさ」以上の場所を書き換えると配列外アクセスとなり危険です。
sizeofとstrlenの違いで確認する
文字列リテラルとchar配列の違いは、sizeofとstrlenを使うとさらによく分かります。
#include <stdio.h>
#include <string.h>
int main(void) {
char s[] = "abc"; // 配列
char *p = "abc"; // 文字列リテラルへのポインタ
printf("sizeof(s) = %zu\n", sizeof(s));
printf("strlen(s) = %zu\n", strlen(s));
printf("sizeof(p) = %zu\n", sizeof(p));
printf("strlen(p) = %zu\n", strlen(p));
return 0;
}
実行結果の一例は次の通りです(64ビット環境を想定)。
sizeof(s) = 4
strlen(s) = 3
sizeof(p) = 8
strlen(p) = 3
ここでのポイントは、sizeofは「型やオブジェクトの大きさ」を返し、strlenは「文字列の長さ(終端’\0’までの文字数)」を返すことです。
sはchar[4]なので、sizeof(s)は4になります(「a」「b」「c」「\0」の4文字)。pはchar *型なので、sizeof(p)はポインタのサイズ(多くの64ビット環境では8バイト)になります。- どちらも中身は
"abc"なので、strlenは3を返します。
配列とポインタでsizeofの結果が大きく異なることは、文字列操作に限らずC言語の重要なポイントです。
初期化の書き方の違い
文字列を扱うときの初期化には、いくつかの書き方があります。
それぞれの違いを整理しておきます。
#include <stdio.h>
int main(void) {
// 1. 文字列リテラルから配列を初期化
char s1[] = "abc"; // 要素数は4になる("a","b","c","#include <stdio.h>
int main(void) {
// 1. 文字列リテラルから配列を初期化
char s1[] = "abc"; // 要素数は4になる("a","b","c","\0")
// 2. 要素数を明示して初期化
char s2[4] = "abc"; // これも "a","b","c","\0"
// 3. 要素を個別に指定して初期化
char s3[4] = { 'a', 'b', 'c', '\0' };
// 4. ポインタに文字列リテラルのアドレスを代入
char *p = "abc"; // pは読み取り専用の"abc"を指す
printf("s1: %s\n", s1);
printf("s2: %s\n", s2);
printf("s3: %s\n", s3);
printf("p: %s\n", p);
return 0;
}
")
// 2. 要素数を明示して初期化
char s2[4] = "abc"; // これも "a","b","c","#include <stdio.h>
int main(void) {
// 1. 文字列リテラルから配列を初期化
char s1[] = "abc"; // 要素数は4になる("a","b","c","\0")
// 2. 要素数を明示して初期化
char s2[4] = "abc"; // これも "a","b","c","\0"
// 3. 要素を個別に指定して初期化
char s3[4] = { 'a', 'b', 'c', '\0' };
// 4. ポインタに文字列リテラルのアドレスを代入
char *p = "abc"; // pは読み取り専用の"abc"を指す
printf("s1: %s\n", s1);
printf("s2: %s\n", s2);
printf("s3: %s\n", s3);
printf("p: %s\n", p);
return 0;
}
"
// 3. 要素を個別に指定して初期化
char s3[4] = { 'a', 'b', 'c', '#include <stdio.h>
int main(void) {
// 1. 文字列リテラルから配列を初期化
char s1[] = "abc"; // 要素数は4になる("a","b","c","\0")
// 2. 要素数を明示して初期化
char s2[4] = "abc"; // これも "a","b","c","\0"
// 3. 要素を個別に指定して初期化
char s3[4] = { 'a', 'b', 'c', '\0' };
// 4. ポインタに文字列リテラルのアドレスを代入
char *p = "abc"; // pは読み取り専用の"abc"を指す
printf("s1: %s\n", s1);
printf("s2: %s\n", s2);
printf("s3: %s\n", s3);
printf("p: %s\n", p);
return 0;
}
' };
// 4. ポインタに文字列リテラルのアドレスを代入
char *p = "abc"; // pは読み取り専用の"abc"を指す
printf("s1: %s\n", s1);
printf("s2: %s\n", s2);
printf("s3: %s\n", s3);
printf("p: %s\n", p);
return 0;
}
実行結果は次のようになります。
s1: abc
s2: abc
s3: abc
p: abc
見た目の出力は同じですが、s1,s2,s3は「自前の領域を持つ配列」であり、pは「文字列リテラルを指すポインタ」です。
この違いが、後からの書き換えの可否に大きく影響します。
文字列リテラルの落とし穴と注意点
文字列リテラルを書き換えてはいけない理由
先ほども触れましたが、文字列リテラルを書き換えることは未定義動作です。
これは、C言語の規格でそのように定められています。
多くの処理系では、文字列リテラルは読み取り専用の領域に置かれます。
このため、そこに書き込みを行うと、OSによって保護違反とみなされ、セグメンテーションフォルト(アクセス違反)などを引き起こします。
さらに厄介なのは、環境によっては「たまたま動いてしまう」場合もあることです。
あるコンパイラや設定では動いてしまい、別の環境に移植した途端に落ちる、といった問題が起きます。
これが未定義動作が危険と言われる理由です。
char *p = “abc”; でハマりがちなバグ例
初心者がよく書いてしまうコードとして、次のようなものがあります。
#include <stdio.h>
int main(void) {
// 文字列リテラルへのポインタ
char *p = "hello";
// ここで文字列を大文字に変換したいとする
for (int i = 0; p[i] != '#include <stdio.h>
int main(void) {
// 文字列リテラルへのポインタ
char *p = "hello";
// ここで文字列を大文字に変換したいとする
for (int i = 0; p[i] != '\0'; i++) {
if ('a' <= p[i] && p[i] <= 'z') {
p[i] = p[i] - 'a' + 'A'; // ここが危険
}
}
printf("%s\n", p);
return 0;
}
'; i++) {
if ('a' <= p[i] && p[i] <= 'z') {
p[i] = p[i] - 'a' + 'A'; // ここが危険
}
}
printf("%s\n", p);
return 0;
}
このコードは、「小文字を大文字に変換する」つもりで書かれていますが、pが指しているのは文字列リテラル"hello"です。
そのため、p[i]への書き込みは未定義動作になります。
安全に書き換えたい場合は、書き換え可能な配列を用意する必要があります。
#include <stdio.h>
int main(void) {
// 書き換え可能な配列として確保
char s[] = "hello";
for (int i = 0; s[i] != '#include <stdio.h>
int main(void) {
// 書き換え可能な配列として確保
char s[] = "hello";
for (int i = 0; s[i] != '\0'; i++) {
if ('a' <= s[i] && s[i] <= 'z') {
s[i] = s[i] - 'a' + 'A'; // 安全に書き換え可能
}
}
printf("%s\n", s);
return 0;
}
'; i++) {
if ('a' <= s[i] && s[i] <= 'z') {
s[i] = s[i] - 'a' + 'A'; // 安全に書き換え可能
}
}
printf("%s\n", s);
return 0;
}
このように、文字列を変更したいときにchar *p = "..."と書くのは危険だと覚えておくと良いです。
const修飾子(const char *p)を付けるべき場面
文字列リテラルを扱うときには、const修飾子を使うことで、安全性を高めることができます。
const char *p = "abc";
このように書くと、pは「書き込み禁止の文字列を指すポインタ」という意味になります。
コンパイラはp[0] = 'A';のようなコードをコンパイルエラーにしてくれます。
関数の引数で「文字列を受け取るが、その中身は書き換えない」ことを明示したい場合も、const char *を使うと良いです。
#include <stdio.h>
// 文字列を表示するだけで、中身を書き換えない関数
void show_message(const char *msg) {
printf("メッセージ: %s\n", msg);
// msg[0] = 'X'; // これはコンパイルエラーになる
}
int main(void) {
show_message("Hello");
return 0;
}
このようにconstを付けることで、「この関数は引数の文字列を壊さない」という約束を明示できるため、バグを防ぎやすくなります。
配列外アクセスと終端文字(‘\0’)の重要性
C言語の文字列は終端文字'\0'までが有効な文字列とみなされます。
そのため、この終端文字がないと文字列関数はどこまでが文字列か分からず、メモリの外側まで読み続けてしまう危険があります。
また、配列に文字列を格納するときは、終端文字を含めて必要なサイズを確保することが非常に重要です。
#include <stdio.h>
int main(void) {
// サイズ3の配列に"abc"を入れようとすると…
char s[3] = "abc"; // 実は危険なコード
printf("%s\n", s); // 未定義動作の可能性
return 0;
}
このコードでは、"abc"は実際には{'a','b','c','\0'}という4文字必要ですが、配列は3要素しかありません。
そのため、'\0'が入りきらず終端されない文字列になってしまいます。
正しくは次のように、終端文字用の1バイトを余分に確保する必要があります。
#include <stdio.h>
int main(void) {
char s[4] = "abc"; // 正しい: "a","b","c","#include <stdio.h>
int main(void) {
char s[4] = "abc"; // 正しい: "a","b","c","\0" の4文字が入る
printf("%s\n", s); // 安全に表示できる
return 0;
}
" の4文字が入る
printf("%s\n", s); // 安全に表示できる
return 0;
}
「文字数 + 1」バイトが必要というルールは、C言語の文字列を扱う際の基本中の基本です。
文字列リテラル・配列・ポインタの使い分け
「配列」と「ポインタ」の関係を一歩ずつ整理
文字列の話になると、配列char s[]とポインタchar *pの違いで混乱しがちです。
ここで、少し丁寧に整理します。
配列char s[]には、次のような性質があります。
- sは「配列そのもの」であり、メモリ上に連続した領域を持っている
- 式の中で<s>が使われると、ほとんどの場合先頭要素へのポインタ
(&s[0])に自動変換される - ただし、
sizeof(s)や&sでは配列として扱われる
ポインタchar *pには、次のような性質があります。
- pは「どこかのchar型データを指すアドレス」である
- どこを指すかは代入によって変えられる
sizeof(p)はポインタのサイズ(アドレスの大きさ)になる
例えば次のコードで、配列とポインタの違いを確認できます。
#include <stdio.h>
int main(void) {
char s[] = "abc"; // 配列
char *p = s; // 配列の先頭要素を指すポインタ
printf("s[0] = %c, p[0] = %c\n", s[0], p[0]);
// sとpは似て見えるが…
printf("sizeof(s) = %zu\n", sizeof(s)); // 配列全体のサイズ
printf("sizeof(p) = %zu\n", sizeof(p)); // ポインタのサイズ
// pは別の文字列を指すこともできる
p = "xyz"; // pの指す先を変更(ただし書き換え禁止の領域)
printf("p: %s\n", p);
return 0;
}
配列は自前の領域を持つ固定的な存在であり、ポインタはどの領域を指すかを変えられる柔軟な存在だとイメージすると理解しやすくなります。
関数引数での書き方
関数で「文字列を受け取る」とき、次のような宣言をよく見かけます。
void func(char s[]);
void func(char *s);
この2つは、関数の引数としては実質的に同じ意味になります。
どちらも「char型を指すポインタ」として扱われます。
具体例で見てみます。
#include <stdio.h>
// 引数を配列風に書いた例
void print1(char s[]) {
printf("print1: %s\n", s);
}
// 引数をポインタとして書いた例
void print2(char *s) {
printf("print2: %s\n", s);
}
int main(void) {
char msg[] = "hello";
print1(msg); // 配列の先頭アドレスが渡される
print2(msg); // 同じく先頭アドレスが渡される
return 0;
}
このように、関数引数では「配列」と書いても「ポインタ」として扱われるため、どちらの書き方も実質同じです。
初心者のうちは、「関数の引数のchar s[]はchar *sと同じ」と覚えておくとよいでしょう。
文字列を書き換えない関数の場合は、次のようにconstを付けておくと安全です。
void print_message(const char *s);
文字列を変更したいときの安全なchar配列の書き方
文字列の中身を変更したい場合は、書き換え可能な配列を使うのが基本です。
例えば、ユーザーから入力を受け取る場合や、文字列を編集する場合がこれに当たります。
次のコードは、文字列を安全に反転する(逆順にする)例です。
#include <stdio.h>
#include <string.h>
// 文字列をその場で逆順にする関数
void reverse_string(char s[]) {
int len = (int)strlen(s);
for (int i = 0; i < len / 2; i++) {
char tmp = s[i];
s[i] = s[len - 1 - i];
s[len - 1 - i] = tmp;
}
}
int main(void) {
// 書き換え可能な配列として十分なサイズを用意
char s[100] = "hello";
printf("before: %s\n", s);
reverse_string(s);
printf("after: %s\n", s);
return 0;
}
before: hello
after: olleh
この例では、配列char s[100]としてゆとりを持って確保し、その中に"hello"を初期値として入れています。
こうすることで、関数内での書き換えも安全に行えます。
文字列を変更しないときの効率的な書き方
一方、文字列を変更せずに参照するだけなら、文字列リテラルとconstポインタを使うのが簡単で効率的です。
例えば、エラーメッセージや固定のラベル文字列などは、次のように扱います。
#include <stdio.h>
int main(void) {
// 変更しない文字列は文字列リテラル+constポインタで十分
const char *msg = "処理が完了しました。";
printf("%s\n", msg);
// msg[0] = 'A'; // コンパイルエラーになるので安心
return 0;
}
この場合、文字列の内容をコピーする必要がないため、メモリ使用量も少なく、処理も高速です。
「絶対に書き換えない文字列」は文字列リテラルで持つと覚えておくと良いでしょう。
初心者がもう迷わないための選び方の目安
最後に、初心者の方が「どの場面で何を使えばよいか」を判断しやすいよう、目安をまとめます。
1つ目の基準は「文字列を変更するかどうか」です。
- 変更しない文字列であれば
→文字列リテラル +const char *を使う
例:const char *msg = "error"; - 変更したい文字列であれば
→十分なサイズのchar配列を用意する
例:char buf[256];にscanfやstrcpyで文字列を入れる
2つ目の基準は「寿命とスコープ」です。
- 一時的に関数内で使うだけなら
→ ローカルなchar s[]で十分 - プログラム全体で共有したい定数的な文字列なら
→ ファイルスコープでconst char *やconst char []を使う
3つ目の基準は「APIの設計」です。
- 関数が「文字列を読むだけ」なら
→const char *sを引数にする - 関数が「文字列を書き換える」なら
→char *sまたはchar s[]を引数にし、呼び出し側は配列を渡す
「書き換えるなら配列」「書き換えないならリテラル+constポインタ」というシンプルなルールから始めると、実務で迷いにくくなります。
まとめ
C言語では、文字列リテラルとchar配列がどちらも文字列として使われますが、メモリ上の配置や書き換え可否など、本質的な違いがあります。
文字列リテラルは読み取り専用領域に置かれ、書き換えは未定義動作となる一方、char配列は自前の領域を持ち、終端'\0'を含めて書き換え可能です。
配列とポインタの振る舞いやsizeofとstrlenの違いを押さえつつ、「変更したいなら配列」「変更しないならリテラル+constポインタ」という目安を守ることで、安全で分かりやすい文字列処理ができるようになります。
