C言語のポインタと文字列を完全理解!配列との違いと使い分け

C言語

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";

この書き方では、以下のことが起こります。

  1. メモリ上に6バイト分(5文字 + '\0')の領域が確保される
  2. 文字列 "Hello" の内容がその領域にコピーされる
  3. str は配列の先頭アドレスを表す

方法2:char*ポインタを使う

char *ptr = "Hello";

この書き方では、以下のことが起こります。

  1. 文字列リテラル "Hello" がメモリ上(読み取り専用領域)に配置される
  2. ポインタ変数 ptr が作られ、文字列リテラルの先頭アドレスを保存する
  3. 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;
}

なぜ両方とも渡せるのか?

配列名は、式の中では自動的に先頭要素へのポインタに変換されます。つまり、arrptr も関数には「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言語の最初の難関ですが、メモリの仕組みを理解すれば必ず乗り越えられます。実際にコードを書いて試しながら、少しずつ理解を深めていってくださいね!

コメント

タイトルとURLをコピーしました