C言語を勉強し始めて、多くの人が最初につまずくのが「ポインタ」と「文字列」の扱いです。
特に、char str[] = "ABC"; と char *str = "ABC"; の違いがよくわからない、という悩みを持つ初心者の方は多いんですよね。見た目はほとんど同じなのに、できることが違ったり、エラーが出たり出なかったり…。
でも安心してください。この記事では、C言語におけるポインタと文字列の関係を、基礎から丁寧に解説していきます。メモリの仕組みも含めて理解すれば、きっと「そういうことだったのか!」とスッキリするはずです。
C言語における文字列の基本

まず、C言語の文字列について基本を確認しましょう。
C言語には「文字列型」がない
JavaやPythonなどの言語には文字列を扱う専用の型がありますが、C言語には文字列型という型は存在しません。C言語では、文字の配列として文字列を表現します。
文字列の終端記号
C言語の文字列には、必ず終端を示す特殊な文字 '\0'(ヌル文字)が付きます。
char str[] = "ABC";
// 実際のメモリ: {'A', 'B', 'C', '\0'}
"ABC" という3文字の文字列でも、メモリ上では4バイト(3文字 + 終端記号1バイト)を使うんです。
このヌル文字があるおかげで、関数は「どこまでが文字列か」を判断できます。逆に言えば、ヌル文字がないと文字列の終わりがわからず、予期しないエラーが発生してしまいます。
ポインタとは何か
文字列とポインタの関係を理解する前に、ポインタの基本を押さえておきましょう。
メモリとアドレス
プログラムで使う変数は、すべてコンピュータのメモリ(RAM)上に保存されます。メモリは1バイトごとに区切られた「箱」のようなもので、それぞれの箱には「アドレス」という番号(住所)が付いています。
int x = 10;
// xという変数がメモリ上のどこかに保存される
// 例えば、アドレス0x1000番地に保存されたとします
ポインタの役割
ポインタとは、「変数のアドレス(住所)を記憶する変数」のことです。つまり、「データがどこにあるか」を指し示す矢印のようなものですね。
int x = 10;
int *ptr = &x; // ptrはxのアドレスを保存するポインタ変数
&は「アドレス演算子」で、変数のアドレスを取得します。*は「ポインタ型の宣言」と「間接参照演算子」の2つの意味があります。
char配列とchar*ポインタの違い
ここからが本題です。文字列を扱う方法は、大きく分けて2つあります。
方法1:char配列を使う
char str[] = "Hello";
この書き方では、以下のことが起こります。
- メモリ上に6バイト分(5文字 +
'\0')の領域が確保される - 文字列
"Hello"の内容がその領域にコピーされる strは配列の先頭アドレスを表す
方法2:char*ポインタを使う
char *ptr = "Hello";
この書き方では、以下のことが起こります。
- 文字列リテラル
"Hello"がメモリ上(読み取り専用領域)に配置される - ポインタ変数
ptrが作られ、文字列リテラルの先頭アドレスを保存する ptr自体は4バイトまたは8バイトのポインタ変数
視覚的な違い
char str[] = "Hello";
メモリ配置:
str[0] str[1] str[2] str[3] str[4] str[5]
'H' 'e' 'l' 'l' 'o' '\0'
↑
strが指すアドレス(変更不可)
char *ptr = "Hello";
メモリ配置:
[読み取り専用領域]
'H' 'e' 'l' 'l' 'o' '\0'
↑
|
|
アドレスを保存(変更可能)
できること・できないこと比較表
具体的に、何ができて何ができないのか見ていきましょう。
char配列の場合
char str[] = "Hello";
// ○ できること
str[0] = 'J'; // 文字の変更が可能
strcpy(str, "World"); // 別の文字列をコピー可能
scanf("%s", str); // 入力を受け取れる
// × できないこと
str = "Goodbye"; // 配列全体への代入はできない
str++; // 配列名のインクリメントはできない
配列は定義時に領域を確保するため、中身の書き換えはできますが、配列自体が指すアドレスは変更できません。
char*ポインタの場合
char *ptr = "Hello";
// ○ できること
ptr = "World"; // 別の文字列を指すように変更可能
ptr++; // ポインタのインクリメント可能
// × できないこと(未定義動作)
ptr[0] = 'J'; // 文字列リテラルの変更は不可
strcpy(ptr, "New"); // 文字列リテラルへのコピーは不可
ポインタは別の場所を指すように変更できますが、文字列リテラル自体は書き換えられません。書き換えようとすると、プログラムがクラッシュする可能性があります。
文字列リテラルと読み取り専用メモリ
なぜ char *ptr = "Hello"; の文字列は書き換えられないのでしょうか?
文字列リテラルの特性
プログラム中に "Hello" のように書かれた文字列リテラルは、コンパイル時にプログラムのテキストセグメント(コードセグメント)という読み取り専用の領域に配置されます。
この領域は、プログラムの実行開始から終了まで常に存在し、書き換えが禁止されています。書き換えようとすると、メモリ保護機能が働いてプログラムが異常終了するんです。
配列とリテラルのメモリ配置の違い
char arr[] = "Text"; // データセグメント(書き換え可能)
char *ptr = "Text"; // テキストセグメント(読み取り専用)
arr は実行中のプログラムが使うデータ領域に配置され、自由に書き換えられます。一方、ptr が指す文字列リテラルはプログラムコードと同じ領域にあり、保護されているわけですね。
関数に文字列を渡す場合
関数の引数として文字列を渡す場合、配列もポインタも同じように扱えます。
void print_string(char *str) {
printf("%s\n", str);
}
int main() {
char arr[] = "Array";
char *ptr = "Pointer";
print_string(arr); // OK
print_string(ptr); // OK
return 0;
}
なぜ両方とも渡せるのか?
配列名は、式の中では自動的に先頭要素へのポインタに変換されます。つまり、arr も ptr も関数には「char型へのポインタ」として渡されるんです。
関数の引数を char str[] と書いても char *str と書いても、実質的に同じ意味になります。
初期化のルール

文字列の初期化にはいくつかのルールがあります。
配列の初期化
// ○ 正しい初期化
char str1[] = "Hello";
char str2[] = {'H', 'e', 'l', 'l', 'o', '\0'};
char str3[10] = "Hello"; // 残りは自動的に'\0'で埋められる
// × 間違った初期化
char str4[10];
str4 = "Hello"; // 宣言後の配列全体への代入はできない
ポインタの初期化
// ○ 正しい初期化
char *ptr1 = "Hello";
char *ptr2;
char arr[] = "World";
ptr2 = arr; // 配列のアドレスを代入
// × 危険な使い方
char *ptr3; // 初期化していない
ptr3[0] = 'A'; // 未定義動作!クラッシュの可能性
初期化していないポインタは、どこを指しているかわからない状態です。その状態でデータを書き込もうとすると、重大なエラーが発生します。
sizeofの違い
sizeof演算子を使った時の結果も異なります。
char str[] = "Hello";
char *ptr = "Hello";
printf("sizeof(str): %lu\n", sizeof(str)); // 6 (配列全体のサイズ)
printf("sizeof(ptr): %lu\n", sizeof(ptr)); // 8 (ポインタ変数のサイズ、64bit環境の場合)
配列の場合は配列全体のバイト数、ポインタの場合はポインタ変数自体のサイズ(アドレスを保存するのに必要なバイト数)が返ってきます。
動的メモリ確保とポインタ
ポインタを使えば、実行時に必要なサイズの文字列領域を確保できます。
#include <stdlib.h>
#include <string.h>
char *ptr = (char*)malloc(100); // 100バイト確保
if (ptr != NULL) {
strcpy(ptr, "Dynamic Memory");
printf("%s\n", ptr);
free(ptr); // 使い終わったら必ず解放
}
mallocで確保した領域は書き換え可能です。ただし、使い終わったら必ずfreeで解放する必要があります。解放しないとメモリリークが発生してしまいます。
よくあるミスと対処法
ミス1:文字列リテラルを書き換えようとする
char *ptr = "Hello";
ptr[0] = 'J'; // クラッシュ!
対処法:書き換える必要があるなら配列を使う
char str[] = "Hello";
str[0] = 'J'; // OK
ミス2:配列全体に代入しようとする
char str[10] = "Hello";
str = "World"; // エラー!
対処法:strcpyを使う
strcpy(str, "World"); // OK
ミス3:配列サイズより長い文字列をコピーする
char str[5];
strcpy(str, "Hello World"); // バッファオーバーフロー!
対処法:strncpyを使うか、十分な配列サイズを確保する
char str[20];
strncpy(str, "Hello World", sizeof(str) - 1);
str[sizeof(str) - 1] = '\0'; // 念のため終端を保証
実践的な使い分け
配列を使うべき場合
- 文字列の内容を変更する必要がある
- 固定サイズの文字列を扱う
- バッファとして使う(入力を受け取るなど)
char buffer[256];
fgets(buffer, sizeof(buffer), stdin);
ポインタを使うべき場合
- 文字列を読み取るだけ
- 複数の文字列を切り替えて使う
- 関数の引数として文字列を受け取る
- 動的にメモリを確保する
const char *message;
if (error) {
message = "Error occurred";
} else {
message = "Success";
}
printf("%s\n", message);
const char*の推奨
文字列リテラルを扱う場合は、const char*と宣言するのが安全です。
const char *ptr = "Hello"; // constで書き換え禁止を明示
これにより、コンパイラが誤った書き換えを警告してくれます。
まとめ:ポインタと文字列の違いを理解しよう
C言語のポインタと文字列について、重要なポイントをまとめます。
char配列の特徴
- メモリ上に実際の領域を確保する
- 内容を自由に書き換えられる
- 配列名自体のアドレスは変更できない
sizeofで配列全体のサイズが取得できる
char*ポインタの特徴
- ポインタ変数自体の領域しか確保しない
- 文字列リテラルは書き換えられない
- ポインタ自体は別の場所を指すように変更できる
sizeofでポインタ変数のサイズが取得できる
正しく使い分けるために
文字列を書き換える必要があるなら配列を、読み取るだけならポインタを使いましょう。そして、文字列リテラルを扱う時はconst char*を使うと安全です。
ポインタと文字列は、C言語の最初の難関ですが、メモリの仕組みを理解すれば必ず乗り越えられます。実際にコードを書いて試しながら、少しずつ理解を深めていってくださいね!

コメント