ポインタの基礎

ポインタは住所のようなものです——住所そのものは家ではありませんが、それを通じてまさにその家を見つけられます。アドレスをマスターすれば、Cの最も強力な機能をマスターできます。

ポインタとは何か

メモリの各バイトには一意の番号が付いており、それをアドレスと呼びます。ポインタ変数は、アドレスを格納するために特別に設計された変数です。

C
int x = 42;
int *p = &x;

ポインタ自体もメモリを占有します。32ビットシステムでは4バイト、64ビットシステムでは8バイトで、指している型に関係ありません。

アドレス演算子&

&は変数のアドレスを取得するために使います。

C
int a = 10;
printf("Value of a: %d\n", a);
printf("Address of a: %p\n", (void *)&a);

%p書式指定子はアドレスを出力します。実引数はvoid *にキャストしてください。

アドレスを取得できるもの:

アドレスを取得できないもの:

間接演算子*

*はポインタが指すデータにアクセスするために使います。

C
int x = 42;
int *p = &x;

printf("%d\n", *p);
*p = 100;
printf("%d\n", x);

1つ目の出力は42、2つ目は100です。*p = 100はポインタを通じてxを変更しています。

💡 ヒント: 宣言内の*は「これはポインタである」を意味し、式の中の*は「間接参照(指している値を取得)」を意味します。文脈によって意味が異なります。

未初期化のポインタの間接参照は重大なエラーです。

C
int *p;
*p = 10;

pの値はランダムであり、ランダムなアドレスへの書き込みは他のデータを破損させたり、ただちにクラッシュさせたりする可能性があります。

NULLポインタ

NULLは「どこも指していない」ことを意味する特別なポインタ値です。<stddef.h>などの複数のヘッダで定義されており、値は通常0です。

C
int *p = NULL;

if (p == NULL) {
    printf("Pointer does not point to a valid address\n");
}

NULLポインタの間接参照はセグメンテーション違反を引き起こし、プログラムがクラッシュします。したがって、ポインタを使う前にNULLかどうかを常に確認してください。

C
void safe_print(int *p) {
    if (p != NULL) {
        printf("%d\n", *p);
    }
}
⚠️ 注意: ポインタを定義してすぐに代入しない場合は、必ずNULLで初期化してください。未初期化の「ワイルドポインタ」は値が不定であり、NULLチェックで検出できないため、NULLポインタより危険です。

ポインタの型と歩幅

ポインタの型は、間接参照時に何バイト読み取るかと、ポインタ演算時に何バイト移動するか(歩幅)を決定します。

C
int a = 10;
double b = 3.14;

int *pi = &a;
double *pd = &b;

ポインタの歩幅は指している型のサイズに等しいです。

C
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;

printf("%d\n", *p);
printf("%d\n", *(p + 1));
printf("%d\n", *(p + 3));

出力:10、20、40。p+1はアドレス値に1を加算するのではなく、sizeof(int)(4バイト)を加算し、次のint要素を指します。

ポインタの減算結果も要素単位で測られます。

C
int *q = &arr[4];
printf("%ld\n", (long)(q - p));

出力:4。2つのポインタの間に4つのint要素があることを意味します。

💡 ヒント: 異なる型のポインタは直接代入できません。int *p = &d;(dはdouble)はコンパイラの警告が出ます。型の不一致は間接参照時に読み取るバイト数が間違う原因になります。

ポインタを使って2つの変数の値を交換します。

C
#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 10, y = 20;
    printf("Before swap: x=%d, y=%d\n", x, y);
    swap(&x, &y);
    printf("After swap: x=%d, y=%d\n", x, y);
    return 0;
}
▶ 試してみよう
TEXT
Before swap: x=10, y=20
After swap: x=20, y=10

ここではアドレスを関数に渡し、関数は間接参照で元の変数を変更しています。これが「参照渡しのシミュレーション」の古典的な技法です。関数の仮引数はint *ポインタで、呼び出しでは&xを渡します。

ポインタとconst

constとポインタの組み合わせには2つの意味があります。

定数へのポインタ

C
const int *p = &x;
*p = 20;

不正。pを通じてデータを変更できません。しかしp自身は別の変数を指すことができます。

C
p = &y;

有効。

定数ポインタ

C
int * const p = &x;
p = &y;

不正。pは指す先を変更できません。しかしpを通じてデータを変更できます。

C
*p = 20;

有効。

記憶のコツ:const*の左にあればデータを修飾し、右にあればポインタ自身を修飾します。

ポインタ走査で配列の合計を求めます。

C
#include <stdio.h>

int array_sum(const int *arr, int len) {
    int sum = 0;
    const int *end = arr + len;
    while (arr < end) {
        sum += *arr;
        arr++;
    }
    return sum;
}

int main(void) {
    int data[] = {3, 7, 1, 9, 5};
    int total = array_sum(data, 5);
    printf("Sum: %d\n", total);
    return 0;
}
▶ 試してみよう
TEXT
Sum: 25

仮引数const int *arrは、関数が配列の内容を変更しないことを示しています。ポインタendは停止位置を示し、添字の必要性をなくしています。

❓ よくある質問

Q ポインタ変数は何バイト占有しますか?
A システムのアドレス幅に依存します。32ビットシステムでは4バイト、64ビットシステムでは8バイトで、指している型に関係ありません。
Q int *p*は型に属するのですか、変数に属するのですか?
A 変数に属します。int* p, q;はpはintポインタ、qは普通のintとして宣言されます。1行につき1つのポインタ宣言を推奨します。
Q NULLと未初期化のポインタの違いは何ですか?
A NULLはチェック可能な確定値(0)です。未初期化のポインタはランダムな値で確実に検出できず、より危険です。
Q NULLポインタの間接参照は常にクラッシュしますか?
A 保証はありません。一部の組込みシステムではアドレス0が有効な場合があります。しかし主流のOSではアドレス0へのアクセスはセグメンテーション違反を引き起こします。

📖 まとめ

📝 練習問題

  1. 商と剰余をポインタの仮引数で返す関数void divide(int a, int b, int *quotient, int *remainder)を書いてください。
  2. int変数とdouble変数を定義し、それぞれint *double *で指し、アドレスと値を出力して歩幅の違いを観察するプログラムを書いてください。
  3. 配列内の最大要素へのポインタを返す関数int *find_max(int *arr, int len)を書いてください。
100%