ポインタ①(概要) | Programming Place Plus C言語編 第31章

トップページC言語編

このページの概要

以下は目次です。


ポインタとは

この章からしばらく、ポインタ (pointer) という機能について説明します。

変数にせよ関数にせよ、実体のあるものは必ずメモリ上にあります。メモリ上に実体を作る行為が「定義」です。

今までの章でも、これから先でも、単に「メモリ」と書きますが、もう少し正確にいえばメインメモリのことを言っています。メインメモリをメモリと略すのは一般的によくあることですが、メモリという用語はほかの意味でも使えますから注意してください(USBメモリとか)。

変数だけでなく、関数の実体、つまり関数の中身のプログラム(をコンパイルして生成されたコード)についても、メモリ上にあることに注意してください。

C言語のプログラムをビルドすると実行ファイルが生成されます。実行ファイル自体は、SSD とかハードディスクといったディスク上に保存されていますが、これを実行すると、コンパイル後のプログラムコードがメモリ上に展開されます。

このようにメモリ上に置かれている変数や関数は、メモリアドレス (memory address、address)、あるいは単にアドレス (address) と呼ぶ整数値を使って、その置き場所を表現できます。

たとえば、1000 というメモリアドレスに変数 num の値が置かれていて、4000 というメモリアドレスに main関数のコードが置かれている、といった具合です。実際には、近年のコンピュータのメモリは非常に大きく、数ギガバイト級でしょうから、メモリアドレスの桁数も非常に大きいはずです。また、使われるメモリアドレスは一定であるとは限りません。

同じ例で、変数 num が int型だとして、その大きさが 4バイトあるとしましょう。すると、実際に変数 num が使っているメモリは、1000~1003 という範囲です。この範囲は、変数 num が使っているので、他の用途には使えません。

さて、本題であるポインタですが、これはメモリアドレスを保存し、その位置にあるものを指し示す(ポイントする)機能です。マウスポインタとかレーザーポインタといったものと同じで、何かを指し示すのだということを意識してください。それを実現するために、メモリアドレスという位置情報を使う訳です。

ポインタを使った最小のプログラム

それでは、ポインタを使ったプログラムを実際に見てみましょう。

#include <stdio.h>

int main(void)
{
    int num = 15;
    int* p = &num;

    printf("%d\n", *p);  // 15
}

実行結果:

15

単にポインタといったとき、多くの場合、ポインタ型 (pointer type) の変数のことをいっています。ポインタ変数は変数であることに代わりはないので、まず宣言しなければいけません。

ポインタ型は、どんなものを指し示したいのかによって、その表現が異なります。たとえば、メモリ上にある int型の値を指し示したいのであれば、int型を指し示すポインタ型を使わなければなりません。これを「int型のポインタ(型)」と表現することが多いです。関数を指し示す例は、第38章であらためて取り上げます。

int型のポインタ変数を宣言するには、次のように記述します。

int* p;

型の直後に付いている「*」が、これがポインタであることを示しています。以下のように書いても同じ意味です。

int *p;

先ほどとの違いは、「*」を型の方ではなく、変数名の方に付けていることですが、両者の意味に違いはありません。これはプログラマーによって好みが大きく分かれるところです。

サンプルプログラムの場合、宣言と同時に初期値を与えています。

int* p = &num;

ポインタ変数が持つ値は、「どこを指し示したいのか」を表す値、つまりはメモリアドレスです。前述したように、メモリアドレスは単なる整数値ですが、ある変数がメモリ上のどこにあるかは分からないのが普通です。そのため、次のように直接的にメモリアドレスを指定できません。

int* p = 1000;  // 1000 というメモリアドレスに何があるかは分からない

そこで、「&」という記号で表現される、アドレス演算子 (address operator) を使います。アドレス演算子は、変数や関数のメモリアドレスを取得する演算子です。たとえば「&num」とすると、変数num のメモリアドレスを得られます。

慣れるまで、ポインタ関係の記号の意味が混乱することがあります。アンドマーク(&) でアドレスを取得できるので、「アンド」レスと覚えておくと良いでしょう。

register指定子付きで宣言された変数(第57章)については、アドレス演算子でメモリアドレスを取得できません。このような変数は、その値をメモリ上に置かないように最適化されることがあるためです。

これまでにも scanf関数sscanf関数を使うときに、実引数に & を付けることがありましたが、この正体はアドレス演算子です。つまり、「このメモリアドレスにある変数に、入力された値を格納せよ」という指示を与えていた訳です。

これらの関数のように、引数でポインタを使う例は、第33章であらためて取り上げます。

先ほどのサンプルプログラムでは、printf関数を呼び出すときに、「*p」という表記が登場します。

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

この * は、ポインタ変数を宣言するときの * とは別物です。ここでの * は、間接演算子 (indirection operator) と呼ばれる演算子です。間接演算子は、ポインタ変数とセットで使用し、「そのポインタ変数が指し示す先にあるものを参照する」という効力を持ちます。

つまり、ポインタ変数 p が指し示す先にある、変数 num を参照し、その値を printf関数に渡している訳です。このようにポインタが指し示す先にあるものを参照することを、間接参照(逆参照) (indirection) といいます。

ポインタは少し難しい概念ですが、最初の難関は構文の方だという話もあります。ここまでに登場した構文についてまとめるとこうなります。

ポインタの使用例

次のプログラムを見てください。

#include <stdio.h>

int main(void)
{
    int num = 15;
    int* p = &num;

    *p = 30;
    printf("%d\n", num);  // 30
}

実行結果:

30

変数num の初期値は 15 であり、その後、num に何かほかの値を代入しているようには見えません。しかし、printf関数で num の値を出力してみると、30 と出力されました。

これはポインタの仕業です。変数num のメモリアドレスを、ポインタ変数p に格納していますから、「*p = 30」という式を実行することにより、間接的に変数num の値を書き換えているのです。

反対に、変数num の方を直接書き換えてから、ポインタ変数p を間接参照しても結果は同じです。

#include <stdio.h>

int main(void)
{
    int num = 15;
    int* p = &num;

    num = 30;
    printf("%d\n", *p);  // 30
}

実行結果:

30

メモリアドレスを確認する

メモリアドレスが実際にはどんな値なのかは、printf関数で確認できます。その際には、“%p” 変換指定子を使います。

#include <stdio.h>

int main(void)
{
    int num = 100;
    int* ptr = &num;

    printf("%p\n", ptr);
    printf("%p\n", &num);
}

実行結果:

0021FE40
0021FE40

実行結果は、環境によって大きく異なる可能性がありますが、2つの値は同じになるはずです。ポインタ変数ptr は、変数num のメモリアドレスを保持しているので、同じにならなければおかしいです。

前述したとおり、メモリアドレスはいつも同じになるわけではありません。そのため、先ほどのサンプルプログラムの実行結果を踏まえて、次のようなプログラムに直してはいけません。

#include <stdio.h>

int main(void)
{
    int num = 100;
    int* ptr = 0x0021FE40;  // このメモリアドレスが正しい保証はない

    printf("%p\n", ptr);
    printf("%p\n", &num);
}

ヌルポインタ

ポインタが、何も指し示していない状態を作ることが可能です。このようなポインタを、ヌルポインタ(ナルポインタ、空ポインタ) (null pointer) と呼びます。

ヌルポインタを作るためには、ヌルポインタ定数を使います。ヌルポインタ定数とは、整数型の 0 という定数(あるいは、それを voidポインタ(第34章)にキャストしたもの)です。

実際のところ、話は簡単で、NULL という、ヌルポインタ定数に置換されるマクロがあるので、これを使えばよいです。NULL は、<stdio.h><stddef.h> をはじめとして、いくつかの標準ヘッダに重複して定義されています

NULL(ヌルポインタ定数)は、どんな型のポインタ変数にも代入できます。こうして、何らかのポインタ型に格納されたものを、ヌルポインタと呼びます。

int* p1 = NULL;
double* p2 = NULL;
struct Student* p3 = NULL;

ヌルポインタは、ヌルポインタでないポインタと比較したとき、一致しないことが保証されています。また、ヌルポインタ同士を比較した場合は、その実際の型を問わず、必ず一致します。

ヌルポインタは、何も指し示していないという状態なので、間接演算子を使って間接参照してはなりません。これを行った場合の動作は未定義です。そのため、ヌルポインタになっている可能性があるときには、if文でチェックする必要があります。

#include <stdio.h>

int main(void)
{
    int num = 100;
    int* ptr = NULL;

#if 0
    ptr = &num;
#endif

    if (ptr != NULL) {
        printf("%d\n", *ptr);
    }
}

実行結果:

コメントアウトされている箇所を有効にすれば、100 が出力されます。安全性を求めるのなら(通常は求めるべきです)、ポインタ変数がヌルポインタになっていないかどうかは逐一チェックするようにコーディングした方が良いと言えます。もし、「ここでヌルポインタになっていることは、絶対にありえない」というつもりなのであれば、アサートマクロ(第30章)を使う良い場面です。

#include <stdio.h>
#include <assert.h>

int main(void)
{
    int num = 100;
    int* ptr = NULL;

#if 0
    ptr = &num;
#endif

    assert(ptr != NULL);
    printf("%d\n", *ptr);
}

実行結果:

Assertion failed: ptr != NULL, file c:\main.c, line 13

if文を使うと、その条件判定のために、わずかながら実行速度を犠牲にしてしまいますが、assertマクロであれば、リリース版のプログラムでは何も行わないので、実行速度への影響をなくせます。

最後に、文字列の末尾に付く ‘\0’ がヌル文字と呼ばれることを思い出してください(第6章)。「ヌル」という共通の文字が付いていますが、ヌル文字とヌルポインタはまったく別物です。よく両者を混同したプログラムを書く人がいますが、‘\0’ には文字列の終端としての意味合いしかないですし、ヌルポインタ(NULL) には、何も指し示していないポインタとしての意味合いしかありません。

配列要素へのポインタ

配列の要素を指すようなポインタ変数の宣言できます。

int array[10];
int* p = &array[5];

int型配列の要素は、単なる int型の変数に過ぎないので、ポインタ変数の型は int* で構いません。

【上級】配列の要素ではなく、配列そのものを指すポインタという考え方もあります。これは、配列が複数集まった多次元配列で使うことがあります。第36章で取り上げます。

同じ配列の要素を指すポインタ同士は、関係演算子で比較できます。その場合、手前側(添字が小さい側)にある方が小さいです。

#include <stdio.h>

int main(void)
{
    int array[10];
    int* p1 = &array[3];
    int* p2 = &array[5];

    if (p1 < p2) {
        puts("p1 の方が小さい");
    }
    else if (p1 > p2) {
        puts("p2 の方が小さい");
    }
    else {
        puts("同じ");
    }
}

実行結果:

p1 の方が小さい


練習問題

問題① 次のプログラムの実行結果を答えてください。

#include <stdio.h>

int main(void)
{
    int num, *p;

    num = 50;
    p = &num;

    printf("%d\n", *p);
}

問題② 次のプログラムの実行結果を答えてください。

#include <stdio.h>

int main(void)
{
    int num1 = 0, num2 = 15;
    int* p;

    p = &num1;
    *p = *p + num2;

    p = &num2;
    *p = *p + num1;

    printf("num1:%d  num2:%d\n", num1, num2);
}

問題③ 次のプログラムの実行結果を答えてください。

#include <stdio.h>

int main(void)
{
    int array[5] = {1, 2, 3, 4, 5};
    int* p;

    for (int i = 0; i < 5; ++i) {
        p = &array[i];
        printf("%d ", *p);
    }
    printf("\n");
}

問題④ 次のプログラムの実行結果を答えてください。

#include <stdio.h>

typedef struct {
    float    x;
    float    y;
} PointF;

typedef struct {
    PointF  lt;
    PointF  rb;
} RectF;

int main(void)
{
    RectF   rect;
    RectF*  p_rect = &rect;
    PointF* p_point = &rect.lt;

    p_point->x = 15.0f;
    p_point->y = 20.0f;
    p_rect->rb.x = 10.0f;
    p_rect->rb.y = 30.0f;

    printf("%f %f %f %f\n", rect.lt.x, rect.lt.y, rect.rb.x, rect.rb.y);
}


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第30章 アサート)

次の章へ (第32章 ポインタ②(配列や文字列との関係性))

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る