ポインタとはメモリのアドレスを扱うための枠組みです。C言語ではアドレスを意識してコーディングする必要があります。このことは煩わしく感じますが、メモリ管理や効率的なデータ構造の設計を学べるというメリットもあります。知能情報コースでは2年次以降の授業「OS」「アルゴリズムとデータ構造」あたりで習いますが、そのとっかかりとしてポインタに触れてみましょう。
このチュートリアルでは、C言語の重要な概念である「ポインタ」について学びます。
通常の変数は値を保存します。これに対し、ポインタは値が格納されているメモリのアドレスを保存します。例として以下のコードを考えます。
int x = 10;ここで、
- 変数
xは値10を保存しています。 xのデータはメモリのどこかに格納されています。- 格納している場所を「メモリセル(もしくはメモリ領域、メモリセグメント)」と呼びます。
- そしてそのメモリセルの場所(住所のようなもの)をアドレスと呼びます。
ポインタを使うことで、このメモリのアドレスを取得したり、アドレスを介して値を操作することができます。
アドレス演算子 & を使うと、変数のメモリアドレスを取得できます。またアドレスをprint出力する際には %p を使いましょう。これはポインタ型(メモリアドレス)を表示するためのフォーマット指定で、アドレスを16進数で出力してくれます。
#include <stdio.h>
int main() {
int x = 10;
printf("x の値: %d\n", x); // 変数の値を表示
printf("x のアドレス: %p\n", &x); // 変数のアドレスを表示
return 0;
}x の値: 10
x のアドレス: 0x7ffee6b4c78c
ここで、0x7ffee6b4c78c は変数 x が格納されているメモリのアドレスです。具体的なアドレスは環境によって異なります。
ポインタ変数は、あるデータ型の変数のアドレスを保存するために使用します。ポインタ変数を宣言するには、データ型の前に * を付けます。
int* p;このコードは、p が int 型の値を指すポインタ変数であることを意味します。ポインタ変数は int* p のように書いたり int *p のように書くことができます。どちらも同じ意味ですが、混在させずにどちらかに統一して書きましょう。(當間個人としては「ポインタ変数のための型を指定しているように見えやすく感じる」という点で前者が好みです)
ポインタ変数にアドレスを代入して使用してみましょう。
#include <stdio.h>
int main() {
int x = 10;
int* p = &x; // x のアドレスを p に代入
printf("p の値(x のアドレス): %p\n", p);
return 0;
}p の値(x のアドレス): 0x7ffee6b4c78c
繰り返しになりますが、実際のアドレスは環境により異なります。
ポインタが指しているアドレスの値にアクセスするには、間接参照演算子 * を使用します。
#include <stdio.h>
int main() {
int x = 10;
int* p = &x; // x のアドレスを p に代入
printf("p が指す値: %d\n", *p); // p が指しているアドレスの値を表示
return 0;
}p が指す値: 10
ここで、*p はポインタ p が指すアドレスに保存された値(この場合は x の値)を取得します。
ポインタを使うと、元の変数の値を間接的に変更できます。
#include <stdio.h>
int main() {
int x = 10;
int* p = &x; // x のアドレスを p に代入
*p = 20; // p が指すアドレスの値を変更
printf("x の値: %d\n", x); // x の値が変更されている
return 0;
}なお、今回は予めポインタ変数pに対しアドレスを保存した上で「ポインタ変数pが指すアドレス先を操作」しています。この手順を踏まずに、つまりアドレスを用意せずに「ポインタ変数pが指すアドレス先を操作」しようとすると未定義動作(Wikipedia)となり、コンパイラ実装依存で何かしら不具合となることが多いです。実際に以下のコード例を実行するとどうなるか確認してみよう。
// ダメなコード例
#include <stdio.h>
int main() {
int x = 10;
int* p; // ポインタ変数pを用意した(用意しただけでアドレスは設定していない)
*p = 20; // p が指すアドレスの値を変更
printf("x の値: %d\n", x); // x の値が変更されている
return 0;
}x の値: 20
*p = 20; によってポインタ p が指す先の値が 20 に変更され、結果として x の値が変更されたことを確認できました。
ポインタは配列の操作にも使えます。
#include <stdio.h>
int main() {
int arr[3] = {10, 20, 30};
int* p = arr; // 配列の先頭要素のアドレスを取得
for (int i = 0; i < 3; i++) {
printf("arr[%d] の値: %d\n", i, *(p + i));
}
return 0;
}arr[0] の値: 10
arr[1] の値: 20
arr[2] の値: 30
ここで、p + i は配列の i 番目の要素を指すアドレスを意味し、*(p + i) でその値を取得します。
細かいですが、これは「int型のポインタ変数 + インデックス」の動作が次のように処理されていることになります。
- 仮定:int型リテラルを保存するために4バイト利用する環境があるとする。
- 上記仮定に基づくと、「int型の要素3つを保存する配列」を用意するためには「メモリ内で連続した領域として12バイト確保」する必要がある。
- 便宜上、上記配列の先頭アドレス(0番目の要素が保存されているアドレス)を 0x1000 とする。
- このとき、インデックス0番目のアドレスは
0x1000 + 4*0 = 0x1000として計算することができる。 - インデックス1番目のアドレスは
0x1000 + 4*1 = 0x1004として計算することができる。 - インデックス2番目のアドレスは
0x1000 + 4*2 = 0x1008として計算することができる。 - 以下同様であり、配列のアドレスをアドレスで求めるためには
先頭アドレス + (型サイズ*要素数)として算出することができる。このことは「配列はメモリ空間上に連続して並んだ領域にデータを格納している。連続しているため、N個目の要素を参照する(アドレスを求める)ためには、先頭アドレスとインデックスが分かれば単純な四則演算でアドレスを求めるだけで良い(だから高速に動作する)。
- このとき、インデックス0番目のアドレスは
ポインタを使うと、関数内で変数の値を変更できます。
#include <stdio.h>
void updateValue(int* p) {
*p = 50; // ポインタが指す先の値を変更
}
int main() {
int x = 10;
printf("変更前の x の値: %d\n", x);
updateValue(&x); // x のアドレスを渡す
printf("変更後の x の値: %d\n", x);
return 0;
}変更前の x の値: 10
変更後の x の値: 50
ここで、updateValue 関数に x のアドレスを渡し、ポインタを使って x の値を変更しています。
ポインタを使う際には、以下の点に注意してください:
-
未初期化のポインタを使用しない
- 初期化されていないポインタを使用すると、未定義の動作を引き起こす可能性があります。
int* p; // 初期化されていない *p = 10; // エラーの原因
-
無効なアドレスを参照しない
- 解放されたメモリや不正なアドレスを参照しないようにしましょう。
-
メモリリークに注意する
- 動的メモリ確保(
malloc)を使う場合は、必ずfreeを忘れないようにしてください。(このチュートリアルでは未使用。一般的な用途ではでてくる話)
- 動的メモリ確保(