ほぷしぃ

納得C言語!

[第13回]ポインタ

ポインタ


1.ポインタって

ポインタの概要

ポインタとは、変数のアドレスを記憶する変数のことです。
以前、変数を宣言するとコンピュータ上のメモリ領域のどこかに作られるという話をしました。
ポインタを使うことで、宣言した変数がどの場所にあるかを教えることができます。

A「ポインタって何ができるの?」
B「変数がメモリ上のどこにあるか知ることができるんだ」
A「それって必要あるの?」
B「これから勉強する構造体は、ポインタを使うと早く動かすことができるようになるからここで基本を覚えてしまおう」
A「他には?」
B「しつこいね〜 他には、自作関数の戻り値は一つだけど、複数の戻り値が欲しいときが出てくると思うけど、
そういうときにポインタを使えば、戻り値が複数あるように作ることができるんだ」

A「便利だね!」

アドレスの説明

アドレス(番地)とは、メモリ上に与えられた住所のことです。
変数を宣言すると、変数に住所が与えられます。
その住所のところに行くことによって変数の中身を見ることができます。
基本的にアドレスは16進数で表現されます。

メモリ確保

ポインタの使い方

ポインタは変数のアドレスを記憶する変数なので、使うには宣言する必要があります。
ポインタ変数の宣言方法は以下の通りです。

ポインタ変数の宣言

変数名の前に*が付きましたね。
それ以外は普通の変数を宣言する方法と変わりません。
変数名の前に*をつけて宣言すると、宣言した変数はポインタ変数となります。
次にポインタ変数に値を代入する例です。

ポインタ変数に値を代入

ポインタ変数名に*が付いていないときは「ポインタ変数の値」という意味になり、アドレスを代入することができます。
基本的に変数のアドレスを入れて使う時が多いです。
変数のアドレスをポインタ変数に代入する方法は以下の通りです。

変数のアドレスをポインタ変数に代入

変数名の前に&をつけることによって、その変数のアドレスを意味します。
ポインタ変数に代入することによって、ポインタ変数は変数のアドレスを示すことができます。
変数のアドレスを代入する際は、ポインタ変数と同じ型であるかを確認してください。
変数とポインタ変数の型が違うと、コンパイルはエラーを出しますので気をつけましょう。

そういえば、変数名の前に&を付けた形に見覚えがありませんか?

見覚えがありませんか?

scanf()の指定文字列の後で&を使ってますね。
この時のiも変数のアドレスを意味し、入力された整数をiが示すアドレスの中に代入するということになります。


例題1 アドレスの表示

#include <stdio.h>

int main()
{
    int i, *p1;       //int型の変数iとint型のポインタ変数p1の宣言
    i = 25;  
    p1 = &i;          //p1にiのアドレスを代入
    
    char *p2;         //char型のポインタ変数p2の宣言
    char f ='A';      //char型の変数fを宣言
    p2 = &f;          //p2にfのアドレスを代入

    double s, *p3;    //double型の変数sとdouble型のポインタ変数p3の宣言
    s = 2.5;
    p3 = &s;          //p3にsのアドレスを代入

    //iの値とアドレスの16進数表記、10進数表記をそれぞれ出力
    printf("int=%d\n intアドレス(16)=%x\n intアドレス(10)=%d\n ", i, p1, p1);
    printf("\n");
    
    //fの値とアドレスの16進数表記、10進数表記をそれぞれ出力
    printf("char=%c\n charアドレス(16)=%x\n charアドレス(10)=%d\n ", f, p2, p2);
    printf("\n");
    
    //sの値とアドレスの16進数表記、10進数表記をそれぞれ出力
    printf("double=%lf\n doubleアドレス(16)=%x\n doubleアドレス(10)=%d\n ", s, p3, p3);
    printf("\n");

    return 0;
}

結果

アドレスが出力されます

ポインタ変数名に*が付いているときは「ポインタ変数が指すアドレスの中身」という意味になります。

ポインタ変数が指すアドレスの中身

とすると、printf()で出力される値は「ポインタ変数が指すアドレスにある値」すなわち、「*ポインタ変数名に代入した値」となります。
それでは例題2で確かめたいと思います。


例題2 ポインタ変数を使って値を代入

#include <stdio.h>

int main()
{
    int *p, i; //int型の変数iとint型のポインタ変数pの宣言
    p = &i;    //pにiのアドレスを代入
    *p = 100;  //pのポインタ変数が指すアドレス(変数iのアドレス)の

    //中身に100を代入
    //pのポインタ変数が指すアドレスの中身と変数iの値を出力
    printf("*p = %d\ni = %d\n", *p, i);
    return 0;
}

結果

一緒になりましたね

*pとiの値が一緒になりましたね。


2.配列とポインタ

配列も変数と同じように、アドレスを持ち配列のアドレスをポインタ変数に格納できます。

配列のメモリ確保

但し、配列の時は変数の時と違う部分があります。

(1)配列のアドレスをポインタ変数に代入する時は&を記述しない

配列を宣言すると、配列を宣言した変数名は配列要素の先頭アドレスを示します(詳しくはあとで説明)。
よって、変数名が既にアドレスなので、&をつける必要がありません。
逆に&を記述してコンパイルするとエラーになります。
ただし、配列を[ ]を使って直接指定する時は&は必要になります。

(2)「*ポインタ変数名」は配列要素の先頭アドレスの中身を指す

(1)でも言ったとおり、配列を宣言した変数名は配列要素の先頭アドレスを示します。
「*ポインタ変数名」は「ポインタ変数が指すアドレスの中身」を指すので、配列の場合は配列要素の先頭アドレスの中身を指すことになります。

(3)ポインタのアドレス計算が可能

ポインタ変数も普通の変数のように計算をすることができます。
ポインタ変数に+1すると、ポインタ変数の値は「ポインタに格納されているアドレス + 変数の型サイズ」になります。
ポインタ変数に-1すると、ポインタ変数の値は「ポインタに格納されているアドレス - 変数の型サイズ」になります。
変数の型サイズについては第2回で確認してください。
ポインタ変数の計算方法は2種類あります。

1.ポインタ変数の値を更新しないでデータを参照
ポインタ変数の値を変更しないでデータを参照します。

2.ポインタ変数の値そのものを更新してデータを参照
ポインタ変数の値そのものを直接変更してデータを参照します。

この2つの例を今から見ていきましょう。

例題3 ポインタ変数の値を更新しないでデータを参照

#include <stdio.h>

int main()
{
    int *p, a[10], i = 0;  //int型のポインタ変数pを宣言
    p = a;                 //pに配列aの先頭アドレスを代入

    for(i = 0; i < 10; i++){
        *(p + i) = i;                         //ポインタpが指すアドレス+iの中身にiを代入
        printf("a[%d] = %d\n", i, *(p + i));  //結果の出力
    }
    return 0;
}

例題3のようにポインタpの値を直接変更しないで、pの値に+1, +2, ..., +9という感じで計算します。
ここで注意することは、*(p+1), *(p+2), ...みたいに()でくくって計算していますが、必ず()をつけて計算してください。
*p+1, *p+2, ...みたいにすると、「ポインタpが指すアドレスの中身+1」、「ポインタpが指すアドレスの中身+2」となってしまいます。

結果

順番に出てますね

このように、要素番号0の中身、要素番号1の中身、…、要素番号9の中身という具合に結果が出力されました。

例題4 ポインタ変数の値そのものを更新してデータを参照

#include <stdio.h>

int main()
{
    int *p, a[10], i = 0;  //int型のポインタ変数pを宣言
    p = a;                 //pに配列aの先頭アドレスを代入

    for(i = 0; i < 10; i++){
        *p = i;                         //ポインタpが指すアドレスの中身にiを代入
        printf("a[%d] = %d\n", i, *p);  //結果の出力
        p++;                            //ポインタの値をインクリメント(+1)する
    }
    return 0;
}

例題3とは違って、今度はpの値をインクリメントすることによってアドレスの値を変えていますね。
こうすることによってポインタ変数が指すアドレス自身が変化するので、printf()の部分が全て同じでもポインタ変数が指すアドレスが違うので、違う値を出力します。

結果

同じく順番に出てますね

このように、例題3の結果と同じように要素番号0の中身、要素番号1の中身、…、要素番号9の中身という具合に結果が出力されました。
この場合、アドレス自体の数値が変更されているため要素番号の0番から再度読み出したい場合はアドレスの値を戻す必要があります。

先ほど、配列を宣言すると、配列を宣言した変数名は配列要素の先頭アドレスを示すといいました。
果たして本当に配列を宣言した変数名は配列要素の先頭アドレスを示しているのでしょうか?
それを証明するために次のプログラムを入力してください。

例題5 比較してみる

#include <stdio.h>

int main()
{
    int a[5] = {10, 20, 30, 40, 50};
    int *p;
    p = a;
    
    printf("aの値     = %d\n", a);    //変数aの値
    printf("a(16)の値 = %x\n", a);    //変数aの値を進数にした値
    printf("a(p)の値  = %p\n", a);    //変数aのアドレス
    printf("pの値     = %d\n", p);    //ポインタ変数pの値
    printf("a[0]の値  = %d\n", a[0]); //配列a[0]の値
    printf("*pの値    = %d\n", *p);   //ポインタ変数pが指すアドレスの中の値
    return 0;
}

3番目のprintf()で「%p」という指定文字列が出てきました。
「%p」は指定した変数のアドレスを出力するという意味を持ちます。
まずは結果を先に見てもらいます。
但し、結果は使っているコンピュータごとで違います。

結果

比較してください

整数型配列の変数aの値は「こんな値がでましたよ〜」程度で置いておきます。
a(16)の値と、次で表示しているa(p)の値と同じ値になりました。
値が同じだということは、整数型配列aはaのアドレスを示しているということになります。
pが同じなのは「p = a;」をしているから当然ですね。
そして、*pは要素番号0の10を指しています。
実際に要素番号0の値も出力していますが、同じ値の10ですね。
これらから、配列で宣言した変数は配列の先頭アドレスを示し、配列のときの「*ポインタ変数」は配列要素の先頭アドレスの中身を指していることが証明されました。


3.文字列とポインタ

文字列をポインタで扱う方法は以下の通りです。

文字列をポインタで扱う方法

まずはchar型のポインタ変数を宣言します。
そのポインタ変数に文字列を代入しています。

これを見て「ポインタ変数に何で文字列が代入できるんだろう?」と思う人もいるでしょう。
今まで何気なく使っていた"文字列"という表記ですが、これは文字列定数と呼ばれているもので、実は文字列定数自身がアドレスの値なのです。
アドレスの値は"文字列"が格納された先頭番地のアドレスの値が設定されています。
"ISL"自身がアドレスの値になるので、ポインタ変数に値を代入できるという訳です。
第11回でchar型配列を宣言した後にchar型配列の「変数名 = "文字列";」という表記がエラーを起こすといいましたが、実は"文字列"がアドレスの値なので変数の型が合わず、代入できないということでエラーを出していたんですね。

例題6 文字列とポインタの関係

#include <stdio.h>

int main()
{
    int i;
    char *p, a[]="window";  //char型のポインタ変数pと文字列を宣言
    p = a;                  //pにaの先頭アドレスを代入

    for(i = 0; i < sizeof(a); i++){  //iにを代入、iがaの大きさになるまで繰り返す
        printf("%c", *(p+i));          //結果を1文字づつ出力
    }
    return 0;
}

ここで、for文の継続条件の中にsizeof(a)というものが出てきました。
sizeof(変数)は()の中に記述した変数のサイズを調べることができます。
例題6の場合はsizeof(a) = 7となります(6ではなく7なのは文字列の最後にはヌル文字があるため)。

結果

1文字づつ出力しています

このようにwindowと出力されます。
最後の空白はヌル文字ですね。


4.関数とポインタ

ポインタは関数の引数としても利用することができます。
関数では第6回でも習った通り、main関数から自作関数を呼び出すことができmain関数へ値を1つだけ返すことができます(自作関数を呼び出した関数に返す値の事を戻り値といいましたね)。
実は、関数には戻り値の値を1つだけではなく複数個返すことができます。
複数個の値を返す時はreturn文を使うのではなく、ポインタを用いることによって複数個の値を返すことができます。
まずは例題7を見て、どのようにポインタを使って値を複数個返すか見てみましょう。

例題7 関数に渡す引数にポインタを使った例

#include <stdio.h>

//引数にポインタ変数を持ったkeisan関数を宣言
void keisan(int *p_point, int *p_sum, int *p_avg);

int main()
{
    int point[5], sum = 0, avg = 0, i;

    for(i = 0; i < 5; i++){
        printf("%d人目-> ", i+1);
        scanf("%d",&point[i]);
    }
    keisan(point, &sum, &avg);  //それぞれのアドレスをkeisan関数に渡す
    printf("合計:%d\t平均点:%d\n", sum, avg);  //合計点と平均点を出力
    return 0;
}

//pointの先頭アドレスをポインタp_pointに
//sumのアドレスをポインタp_sumに
//avgのアドレスをポインタp_avgに
//それぞれ代入する
void keisan(int *p_point, int *p_sum, int *p_avg)
{
    for(int i = 0; i < 5; i++) {
        //ポインタp_sumが指すアドレスの中身に
        //ポインタp_sumが指すアドレスの中身と
        //ポインタp_pointが指すアドレス+iの中身を代入
        *p_sum = *p_sum + *(p_point + i);
    }
    
    //ポインタp_avgが指すアドレスの中身に
    //ポインタp_sumが指すアドレスの中身を5で割ったものを代入
    *p_avg = *p_sum / 5;
}

今まで、ポインタは関数の本文中に出てきました。
今回は、関数の引数としてポインタが使われています。
ポインタは引数としても利用できるのですね。

keisan(point, &sum, &avg);でpoint, sum, avgの各アドレスの値をkeisan関数に渡しています。
keisan関数内で合計と平均値を計算して、main関数で計算結果を出力しています。
そう言えば第9回で、関数内で宣言された値の有効範囲は宣言された関数内のみと学習しましたね。
main関数で、関数sumとavgを宣言していますが、main関数ではそれらの計算を行っていません。
ですが、結果を見ても分かる通りちゃんと計算結果が出ています。

結果

ちゃんと出ましたね

それは一体何故なんでしょうか? main関数からkeisan関数にpoint, sum, avgの各アドレスの値を渡したというところがポイントです。
ポインタp_pointにはpointのアドレス、ポインタp_sumにはsumのアドレス、ポインタp_avgにはavgのアドレスが入っていますが、keisan関数のポインタ変数には*が付いています。
ポインタ変数の前に*が付くと、「ポインタ変数が指すアドレスの中身」ということになりますね。
よって…

・ *p_pointはポインタpointが指すアドレスの中身、すなわち整数型配列pointの配列要素0番の値
・ *p_sumはポインタsumが指すアドレスの中身、すなわちsum自身の値
・ *p_avgはポインタavgが指すアドレスの中身、すなわちavg自身の値

を指すことになり、それぞれのポインタ変数に値を代入すると、ポインタが指すアドレスの中身の値が変更されます。
こうすることによって他の関数で宣言した値を他の関数内で変更することができます。
このように、自作関数にポインタの値を渡すことを参照渡しといいます。
ちなみに、第6回のように値を関数に渡すことを値渡しといいます。
参照渡しをすることによって、あたかも自作関数から値が返ってきていると見せかけているのです。


5.練習問題

(1)int型配列、double型配列、char型配列をそれぞれ5個用意し、for文でループさせ、
 それぞれの配列のアドレスがどのように変化するか見てみよう

(2)第8回の演習問題2の(2)で作った計算機に割り算の余りを求める関数を作ってみよう。
 この時余りはポインタを作って書き換えること


[第12回]演習問題V ページのトップ 解答