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

先頭へ戻る

この章の概要

この章の概要です。

ポインタとは

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

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

変数だけでなく、関数の実体、つまり関数の中身のプログラム(をコンパイルして生成されたコード)についても、 メモリ上に存在していることに注意して下さい。
C言語のプログラムをビルドすると実行ファイルが生成されます。 実行ファイル自体は、SSD とかハードディスクといったディスク上に保存されていますが、 これを実行すると、コンパイル後のプログラムコードがメモリ上に展開されます。

このようにメモリ上に置かれている変数や関数は、 メモリアドレス(あるいは単にアドレス)という整数値を使って、その置き場所を表現できます。 例えば、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 */

    return 0;
}

実行結果:

15

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

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

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

int* p;

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

int *p;

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

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

int* p = &num;

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

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

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

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

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

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

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

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

この「*」は、ポインタ変数を宣言するときの「*」とは別物です。 今度の「*」は、間接演算子と呼ばれる演算子です。 間接演算子は、ポインタ変数とセットで使用し、「そのポインタ変数が指し示す先にあるものを参照する」という効力を持ちます。
つまり、ポインタ変数 p が指し示す先にある、変数 num を参照し、その値を printf関数に渡している訳です。 このようにポインタが指し示す先にあるものを参照することを、間接参照(逆参照)といいます。


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

ポインタの使用例

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

#include <stdio.h>

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

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

    return 0;
}

実行結果:

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 */

    return 0;
}

実行結果:

30

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

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

#include <stdio.h>

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

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

    return 0;
}

実行結果:

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

    return 0;
}

ヌルポインタ

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

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

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

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

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

C++ にも NULL がありますが、C++ では NULL の使用を避けて、nullptr(Modern C++編【言語解説】第3章)を使うべきです。

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

ヌルポインタは、何も指し示していないという状態なので、間接演算子を使って間接参照してはなりません。これを行った場合の動作は未定義です。そのため、ヌルポインタになっている可能性があるときには、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 );
    }

    return 0;
}

実行結果:




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

#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 );

    return 0;
}

実行結果:

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( "同じ" );
    }

    return 0;
}

実行結果:

p1 の方が小さい

構造体へのポインタ

構造体変数を指し示すポインタ変数も作ることができます。

#include <stdio.h>

typedef struct {
    int    x;
    int    y;
} Point;

int main(void)
{
    Point point;
    Point* p = &point;

    point.x = 10;
    point.y = 20;

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

    return 0;
}

実行結果:

10 20

ポインタ変数を経由して、構造体のメンバにアクセスする際には、少し面倒な記述が登場しています。
つまり、まず間接演算子を使って間接参照を行います。 この時点で参照しているのは、構造体変数そのものですから、そこから更にドット演算子を使って任意のメンバを参照します。

これはこれで正しいのですが、ポインタ経由で構造体のメンバにアクセスする機会は結構多いので、いちいちこういう記述をするのは面倒です。そこで、-> で表現されるアロー演算子(矢印演算子)を使うのが一般的です。 「->」は「-」と「>」の2文字を合体させたものです。

次の2つの文は同じ意味になります。

x = (*p).x;
x = p->x;

アロー演算子は、(*p).x のような記述に対する構文糖であると言えます。


なお、構造体のメンバへのポインタ変数を作ることもできます。

int* p = &point.x;

同じ構造体に含まれているメンバを指すポインタ同士は、関係演算子で比較することができます。その場合、手前側(構造体定義内で先に宣言されているメンバ)にある方が小さいことになります。

#include <stdio.h>

typedef struct {
    int    x;
    int    y;
} Point;

int main(void)
{
    Point point = { 10, 20 };
    int* p1 = &point.x;
    int* p2 = &point.y;
    
    if( p1 < p2 ){
        puts( "p1 の方が小さい" );
    }
    else if( p1 > p2 ){
        puts( "p2 の方が小さい" );
    }
    else{
        puts( "同じ" );
    }

    return 0;
}

実行結果:

p1 の方が小さい


練習問題

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

#include <stdio.h>

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

    num = 50;
    p = &num;

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

    return 0;
}

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

#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 );

    return 0;
}

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

#include <stdio.h>

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

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

    return 0;
}

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

#include <stdio.h>

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

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

int main(void)
{
    RectF   rect;
    RectF*  pRect = &rect;
    PointF* pPoint = &rect.lt;

    pPoint->x = 15.0f;
    pPoint->y = 20.0f;
    pRect->rb.x = 10.0f;
    pRect->rb.y = 30.0f;

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

    return 0;
}


解答ページはこちら

参考リンク

更新履歴

'2018/5/15 「ヌルポインタ」の項を更新。ヌルポインタ定数という用語を絡めるように書き換えた。また、NULL の置換結果に関して、C++ での事情も含めて説明していた部分を削除。
配列要素へのポインタ」「構造体へのポインタ」の項に、ポインタの大小比較について追記。

'2018/3/7 全面的に文章を見直し、修正を行った。
実質的に何も説明していないので、「汎用ポインタ」の項を削除。第34章で初登場させる。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2018/2/21 文章中の表記を統一(bit、Byte -> ビット、バイト)

'2018/2/1 C言語編全体で表記を統一するため、「"%d"フォーマット」のような表現に「変換指定子」を使うように改めた。

'2015/8/25 リンク先を修正。

'2013/8/11 ポインタが必ず変数であるかのように受け取れるので、冒頭の解説を修正。

'2009/11/4 新規作成。





前の章へ(第30章 理解の定着・小休止③)

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

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS