こんにちは! C言語学習中の皆さん。前回は、変数のアドレスを取得する&(アドレス演算子)と、そのアドレスが指す先の値にアクセスする*(間接参照演算子)について学びましたね。
今回は、その「アドレス」を格納するための特別な変数、ポインタ変数の定義方法と基本的な使い方をマスターしていきましょう! ポインタはC言語の強力な機能の一つですが、同時に初心者がつまずきやすいポイントでもあります。焦らず、一つずつ理解していきましょう💪
1. ポインタ変数とは? 🤔
通常の変数がデータ(数値や文字など)そのものを格納するのに対し、ポインタ変数は他の変数のメモリアドレスを格納するための変数です。
例えるなら、通常の変数が「家(データ)」そのものだとすると、ポインタ変数はその「家の住所(アドレス)」を記録しておくメモ帳のようなものです。このメモ帳(ポインタ変数)を見れば、目的の家(データ)がどこにあるかを知ることができます。
なぜアドレスをわざわざ変数に格納する必要があるのでしょうか? それは、アドレスを操作することで、プログラムの柔軟性や効率を大幅に向上させることができるからです。特に関数間でデータをやり取りしたり、大量のデータを効率的に扱ったりする際に、ポインタは非常に役立ちます。(詳細は今後のステップで学んでいきます!)
2. ポインタ変数の定義方法 ✍️
ポインタ変数を定義するには、格納したいアドレスが指すデータの型に * (アスタリスク) を付けて変数名を宣言します。
基本構文:
データ型 *ポインタ変数名;
具体例:
int
型の変数のアドレスを格納するポインタ変数:int *ptr_int;
float
型の変数のアドレスを格納するポインタ変数:float *ptr_float;
char
型の変数のアドレスを格納するポインタ変数:char *ptr_char;
double
型の変数のアドレスを格納するポインタ変数:double *ptr_double;
int* ptr;
のように、*
をデータ型の直後に書くスタイルもありますが、int *ptr;
のように、*
を変数名の直前に書く方が一般的です。これは、int *ptr1, ptr2;
と書いた場合に、ptr1
は int
型へのポインタになりますが、ptr2
は通常の int
型変数として宣言されるため、*
が変数名に付随することを示すためです。どちらのスタイルでもコンパイラは認識しますが、一貫性を保つことが大切です。この時点では、ポインタ変数は宣言されただけで、まだ具体的なアドレスを指していません。中身は不定値(ゴミデータ)が入っている可能性があります。
3. ポインタ変数へのアドレス代入 🏠➡️📝
ポインタ変数を定義したら、次にそれに有効なメモリアドレスを代入する必要があります。これには、前回学んだアドレス演算子 &
を使います。
構文:
ポインタ変数名 = &変数名;
具体例:
#include <stdio.h>
int main() {
int age = 30; // int型の変数 age を宣言し、30で初期化
int *ptr_age; // int型のポインタ変数 ptr_age を宣言
ptr_age = &age; // 変数 age のアドレスを ptr_age に代入
// ptr_age に格納されているアドレス(ageのアドレス)を表示
// %p はアドレスを表示するための書式指定子
printf("変数 age の値: %d\n", age);
printf("変数 age のアドレス: %p\n", &age);
printf("ポインタ変数 ptr_age に格納されているアドレス: %p\n", ptr_age);
return 0;
}
上記のコードを実行すると、&age
と ptr_age
の値(表示されるアドレス)が同じになるはずです。これで、ポインタ変数 ptr_age
が変数 age
を指している状態になりました。
ポインタ変数を宣言しただけでは、どこのアドレスを指しているか分かりません(不定)。このような初期化されていないポインタを使うのは非常に危険です(プログラムがクラッシュする原因になります!)。
すぐに有効なアドレスを代入しない場合は、NULL という特別な値で初期化しておくのが安全な習慣です。NULL は「どこも指していない」ことを示す特別なポインタ値です。
int *ptr = NULL; // NULLで初期化
// または <stddef.h> をインクルードして
// int *ptr = NULL;
// (NULLは多くの場合、(void *)0 として定義されています)
後でこのポインタを使う前に、NULLでないか(有効なアドレスが代入されたか)を確認することができます。4. ポインタ変数を使った値へのアクセス(間接参照) 📝➡️🏠
ポインタ変数にアドレスが格納されたら、今度はそのアドレスが指し示す先の値を取得したり、変更したりすることができます。これには、前回も登場した間接参照演算子 *
を使います。
構文:
- 値の取得:
*ポインタ変数名
- 値の変更:
*ポインタ変数名 = 新しい値;
具体例:
#include <stdio.h>
int main() {
int score = 85; // int型の変数 score
int *ptr_score; // int型のポインタ変数 ptr_score
ptr_score = &score; // scoreのアドレスをptr_scoreに代入
// ポインタ経由で score の値を取得して表示
printf("変数 score の値 (直接アクセス): %d\n", score);
printf("ポインタ ptr_score が指す先の値 (間接参照): %d\n", *ptr_score);
// ポインタ経由で score の値を変更
printf("\nポインタ経由で値を 95 に変更します...\n");
*ptr_score = 95;
// 変更後の値を確認
printf("変数 score の値 (変更後): %d\n", score);
printf("ポインタ ptr_score が指す先の値 (変更後): %d\n", *ptr_score);
// ptr_score 自体の値(アドレス)は変わらない
printf("ポインタ変数 ptr_score に格納されているアドレス: %p\n", ptr_score);
return 0;
}
実行結果の例 (アドレスは環境によって異なります):
変数 score の値 (直接アクセス): 85
ポインタ ptr_score が指す先の値 (間接参照): 85
ポインタ経由で値を 95 に変更します...
変数 score の値 (変更後): 95
ポインタ ptr_score が指す先の値 (変更後): 95
ポインタ変数 ptr_score に格納されているアドレス: 0x7ffeeabc1234
ptr_score
と *ptr_score
の違いptr_score
: ポインタ変数そのもの。中にはアドレスが格納されている。 (例:0x7ffeeabc1234
)*ptr_score
: ポインタ変数ptr_score
が指しているアドレスにある値。 (例:85
や95
)
5. ポインタ変数の使い方:もう少し例を見てみよう 👀
ポインタの基本的な流れ(定義 → アドレス代入 → 間接参照)を掴むために、もう少し例を見てみましょう。
例1: 複数のポインタが同じ変数を指す
#include <stdio.h>
int main() {
int value = 100;
int *p1 = NULL; // ポインタp1をNULLで初期化
int *p2 = NULL; // ポインタp2をNULLで初期化
p1 = &value; // p1 に value のアドレスを代入
p2 = p1; // p2 に p1 の値(つまり value のアドレス)を代入
printf("value のアドレス: %p\n", &value);
printf("p1 が指すアドレス: %p\n", p1);
printf("p2 が指すアドレス: %p\n", p2);
printf("\nvalue の値: %d\n", value);
printf("p1 経由の値: %d\n", *p1);
printf("p2 経由の値: %d\n", *p2);
// p1 経由で値を変更
*p1 = 200;
printf("\n*p1 = 200; を実行後...\n");
printf("value の値: %d\n", value);
printf("p1 経由の値: %d\n", *p1);
printf("p2 経由の値: %d\n", *p2); // p2経由でも値が変わっている!
return 0;
}
この例では、p1
と p2
の両方が変数 value
のアドレスを保持しています。そのため、*p1
を変更すると、value
の値が変わり、結果として *p2
でアクセスする値も変わります。これは、複数の「住所メモ」が同じ「家」を指している状態と同じです。
例2: ポインタ変数のサイズ
ポインタ変数が格納するのはメモリアドレスです。コンピューターのアーキテクチャ(32ビットか64ビットかなど)によってアドレスのサイズは決まります。そのため、どの型のポインタ変数であっても、そのサイズは通常同じになります。
#include <stdio.h>
int main() {
int *ptr_int = NULL;
char *ptr_char = NULL;
double *ptr_double = NULL;
printf("sizeof(int*): %zu バイト\n", sizeof(ptr_int));
printf("sizeof(char*): %zu バイト\n", sizeof(ptr_char));
printf("sizeof(double*): %zu バイト\n", sizeof(ptr_double));
// 参考:各データ型のサイズ
printf("sizeof(int): %zu バイト\n", sizeof(int));
printf("sizeof(char): %zu バイト\n", sizeof(char));
printf("sizeof(double): %zu バイト\n", sizeof(double));
return 0;
}
64ビット環境で実行すると、多くの場合、ポインタ変数のサイズは 8 バイトと表示されるでしょう(環境によって異なります)。int*
も char*
も double*
も、格納するのはアドレスなのでサイズが同じになるわけです。 `%zu` は `sizeof` 演算子の結果 (size_t
型) を表示するための書式指定子です。
6. ポインタを使う上での注意点 ⚠️
ポインタは強力ですが、使い方を誤ると厄介なバグの原因となります。以下の点に注意しましょう。
- 未初期化ポインタの使用禁止!
宣言しただけでアドレスが代入されていない(またはNULLで初期化されていない)ポインタを使うと、プログラムは予期せぬ動作をしたり、クラッシュしたりします。これは「未定義動作」と呼ばれ、絶対に避けなければなりません。必ず有効なアドレスを代入するか、NULLで初期化しましょう。
int *p; // 未初期化。どこを指しているか不明! *p = 10; // 危険! 未定義動作を引き起こす。絶対ダメ!
- NULLポインタの間接参照禁止!
ポインタがNULL(どこも指していない状態)のときに、間接参照演算子
*
を使って値にアクセスしようとすると、ほとんどの環境でプログラムがクラッシュします(セグメンテーションフォルトなど)。ポインタを使用する前には、NULLでないことを確認する習慣をつけましょう。int *p = NULL; if (p != NULL) { *p = 10; // pがNULLでない場合のみ実行される } else { printf("ポインタはNULLです。\n"); }
- 解放済みメモリや無効なアドレスへのアクセス禁止!
(これは後のステップで学ぶ「動的メモリ確保」と関連しますが)存在しない変数や、既に使用権限がなくなったメモリ領域を指すポインタ(ダングリングポインタ)を使うことも非常に危険です。ポインタが常に有効な場所を指しているか、注意深く管理する必要があります。
まとめ ✨
今回は、C言語のポインタの核心である「ポインタ変数」について学びました。
- ✅ ポインタ変数は、他の変数のメモリアドレスを格納するための変数です。
- ✅ 定義は
データ型 *ポインタ変数名;
のように行います (例:int *ptr;
)。 - ✅ 変数のアドレスを代入するにはアドレス演算子
&
を使います (例:ptr = #
)。 - ✅ ポインタが指す先の値にアクセスするには間接参照演算子
*
を使います (例:value = *ptr;
,*ptr = 20;
)。 - ✅ ポインタ変数は、使用前に必ず有効なアドレスを代入するか、NULLで初期化することが重要です。
- ✅ 未初期化ポインタやNULLポインタの間接参照は絶対に避ける必要があります。
ポインタ変数の概念は、最初は少し難しく感じるかもしれません。しかし、コードを書き、実際に動かしてみることで、徐々に理解が深まっていきます。ptr
(アドレス)と *ptr
(値)の違いを常に意識することがポイントです。
次回は、「配列とポインタの関係」について学びます。ポインタが配列とどのように密接に関わっているかを知ることで、さらにC言語の理解が深まるはずです。お楽しみに! 😊
コメント