ポインタ

Last-modified: 2022-04-19 (火) 12:09:03
 

目的

ポインタを理解し、使いこなせるようになること。

変数のアドレス

宣言した変数は、メモリ上に配置されています。
メモリ上のどこに置かれているかを表す際に使う番地のことを、アドレスといいます。

変数のアドレスを取得するには、変数名の前に & を付けます。

以下のようにすると、変数のアドレスを画面に表示することができます。
printf 関数でポインタを表示する際には、%p を用います。

int a;

printf("%p\n", &a); // 初期化しなくてもアドレスは取得できる

実行例

0x7fff6d542c8c

scanf 関数を呼び出す際に、変数名の前に & をつけていましたが、それは変数のアドレスを渡していたということです。

ポインタとは

変数が配置されているメモリ上のアドレスのことをポインタといい、
ポインタを入れる変数のことをポインタ変数といいます。

ポインタ変数を宣言する際には、変数宣言時に変数名の前にアスタリスク * を付けます
このアスタリスク * は、変数がポインタ変数であることを表しているだけで、変数名には含まれません

int a;
int *b; // int 型のポインタを格納するポインタ変数を宣言

b = &a; // ポインタ変数 b に、変数 a のアドレスを代入

printf("%p\n", &a); // 変数 a のアドレスを表示
printf("%p\n", b); // ポインタ変数 b の値 (= 変数 a のアドレス) を表示

実行例

0x7fffb5c90b74
0x7fffb5c90b74

ポインタ変数には型があり、型が違うポインタは代入できません。
たとえば、int 型のポインタ変数には、char 型のポインタは格納できません。

char a; // char 型に変更
int *b;

b = &a;

printf("%p\n", &a);
printf("%p\n", b);

コンパイル結果例

[pine@natsuki Seminar] $ gcc -W -Wall pointer.c
pointer.c: 関数 ‘main’ 内:
pointer.c:7:4: 警告: 互換性のないポインタ型からの代入です [デフォルトで有効]

ポインタを介して変数へアクセスする

ある変数へのポインタがあれば、その変数を使わなくても、その変数の値を書き換えることができます。

int a;
int *b;

b = &a; // ポインタ変数 b に、変数 a のアドレスを代入
*b = 1024; // ポインタ変数 b のポインタが示す先に代入

printf("%d\n", a); // 変数 a の内容を表示

実行例

1024

ポインタ変数のポインタが示す先を表すには、変数使用時に変数名の前にアスタリスクを付けます

pointer.png

変数宣言時のアスタリスクと変数使用時のアスタリスクは、意味が全く異なりますので注意してください。

関数の引数へポインタを渡す

ポインタを関数へ引数として渡すと、その関数から変数を書き換えることができます

 #include <stdio.h>

void func1(int a){

    // a はローカル変数なため、書き換えても呼び出し元には影響しない
    a = 10;
}

void func2(int *a){ // 変数宣言のアスタリスク

    // ポインタ変数 a が指す先を書き換える
    *a = 10; // 変数使用時のアスタリスク
}

int main(){
    int a = 0;

    printf("%d\n", a);

    func1(a);

    printf("%d\n", a);

    func2(&a); // 変数 a のアドレスを渡す

    printf("%d\n", a);

    return 0;
}

実行結果

0
0
10

この性質を利用して、関数の戻り値の代わりにポインタを利用することもできます。

 #include <stdio.h>

// 整数の2乗を求める関数 power
void power(int a, int *out){
    *out = a * a; // a * a を計算して、out が指す先へ代入
}

int main(){
    int a = 3, result;

    power(a, &result);

    printf("%d\n", result);

    return 0;
}

関数 power は、結果を戻り値として返す代わりに、ポインタを利用しています。
戻り値は値を1つしか返せませんが、このようにポインタを利用することで複数の結果を返す関数を作成できます。

値渡しとアドレス渡し

次のような関数があったとき、この関数では呼び出し元の変数が書き換えられないことは既に説明しました。

void hoge(int a){ // 変数 a は引数のコピー
    a = 10;
}

これは、C言語が値渡しという変数の受け渡し方法を用いているからです。
値渡しとは、変数の値のみコピーして渡す方法です。
値渡しでは、変数を渡しているわけではないので、変数を書き換えられないのです。

void hoge(int *a){ // 変数 a は引数 (ポインタ) のコピー
    *a = 10;
}

変数を直接渡したいときは、前述通り、ポインタを引数として渡します。
これを、アドレス渡しといいます。
アドレス渡しも、アドレスという値のコピーを渡しているだけで、値渡しの一種です。

ヌルポインタ

無効なポインタを表す特別な値として NULL が定義されています。
NULL は整数の 0 とマクロで定義されています。

次のように利用できます。

// ポインタが指す先の値を表示する関数
void hoge(int *a){
    if(a == NULL){ // 無効なポインタ
        printf("ぬるぽ\n");
    }

    else {
        printf("%d\n", *a);
    }
}

ポインタと配列

ポインタと配列には深い関係があります。

次のプログラムは、配列配列の要素の先頭のアドレスを表示する例です。

int a[3];

printf("%p\n", a);
printf("%p\n", &a[0]);

実行結果

0x7fffbe695140
0x7fffbe695140

信じられないかもしれませんが、同じアドレスが出力されるはずです。
配列配列の先頭の要素のアドレスで表されています

これを図で表すと、次のようになります。

pointer2.png

配列を宣言すると、配列の領域が確保され、配列名が先頭の要素のポインタとして動作します。
つまり、次のプログラムが正常に実行できます。

int a[3], *b;

a[0] = 1;
a[1] = 2;
a[2] = 3;

b = a; // a は &a[0] と同じ (= a は int 型のポインタ)

printf("%d\n", b[0]);
printf("%d\n", b[1]);
printf("%d\n", b[2]);

出力結果

1
2
3

ポインタの示す先が配列であれば、ポインタは配列と同じように利用できます。
どうしてこのように利用できるかを次で説明します。

ポインタと演算

次のプログラムを実行すると、どのような実行結果になるでしょうか。

int *a;

a = 0;

printf("%ld\n", (long)a); // 10進数で出力するために、long 型にキャスト

++a;

printf("%ld\n", (long)a);

実行結果

0
4

予想した結果通りでしたでしょうか?
ポインタの演算は整数と違い、インクリメントしたときに変数のバイト数増えます
また、足し算をしたときも、「足す数 * 整数のバイト数」が足されます。

int は 4 バイトなので、上の例ではインクリメントしたときに 4 増えています。
char 型は 1 バイトですが、char 型の場合はどうなるでしょうか。

char *a;

a = 0;

printf("%ld\n", (long)a); // 10進数で出力するために、long 型にキャスト

++a;

printf("%ld\n", (long)a);

実行結果

0
1

このように、変数のサイズに応じて、演算が行われています。
これは、次のような処理を可能にする為です。

int a[3];
int *b = a;

a[0] = 1;
a[1] = 2;
a[2] = 3;

printf("%d\n", *(b + 0));
printf("%d\n", *(b + 1));
printf("%d\n", *(b + 2));

実行例

1
2
3

メモリには、1バイトごとにアドレスが割り当てられています。
int 型の配列の場合、最初の要素のアドレス + 4 が次の要素のアドレスになります。

そのため、ポインタの演算は、変数の型に応じて変化するのです。
上の例のアドレス「b + 1」は、先頭の要素の 4 バイト後のアドレスを示しています。
「b + 0」は b と書いても構いませんが、統一性をもたせつために記述してあります。

この性質を利用すると、配列のアクセスは次のように書けます。

int a[3];

*(a + 0) = 1;
*(a + 1) = 2;
*(a + 2) = 3;

printf("%d\n", a[0]);
printf("%d\n", a[1]);
printf("%d\n", a[2]);

実行例

1
2
3

「*(a + 1)」と「a[1]」は全く同じです。

ダブルポインタ

ポインタ変数へのポインタをダブルポインタといいます。

int a;
int *b = &a; // int 型のポインタ
int **c = &b; // int 型のポインタへのポインタ

ポインタの配列はダブルポインタになります。

 #include <stdio.h>

void hoge(char **p, int n){
    int i;

    for(i = 0; i < n; ++i){
        printf("%s\n", p[i]);
    }
}

int main(){
    // ポインタの配列
    // s = &s[0]
    // s[0] はポインタなので、s はポインタのポインタ
    char *s[3];

    s[0] = "foo";
    s[1] = "bar";
    s[2] = "baz";

    hoge(s, 3);

    return 0;
}

構造体とポインタ

構造体へのポインタも作成できます。

 #include <stdio.h>

typedef struct {
    int x, y;
} T;

int main(){
    T a;
    T *p = &a;

    a.x = 16;
    (*p).y = 32; // a.y

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

    return 0;
}

実行例

16
32

また、構造体のポインタの場合、アロー演算子という特別な演算子を使うことができます。

// 構造体 T は上記の例と同等とする

T a;
T *p = &a;

p->x = 64; // アロー演算子、(*p).x と同じ
p->y = 128; // (*p).y

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

実行例

64
128

void 型のポインタ

戻り値が無い関数の戻り値の型として void と指定するのは既に説明しました。
ここでいう void はそれとは別です。

特に型を定めないポインタを表す際に、void 型のポインタを使います。
void 型の変数は存在しませんが、void 型のポインタは作れます。

int a;
void *p;

p = &a; // 型は違うがエラーはでない

scanf("%d", (int*)p); // キャストして利用する

printf("%d\n", *((int*)p)); // キャストして利用する

課題

課題1

int 型の変数を入れ替える関数 swap を作成しなさい。
また、その関数を利用して、入力された整数の順番を逆にして表示するプログラムを作成しなさい。

関数呼び出し例

int a = 2, b = 5;

swap(&a, &b); // a と b を入れ替える

printf("%d %d\n", a, b); // 5 2

実行例

./a.out
4 8
8 4
./a.out
5 5
5 5

課題2

2乗根、3乗根、4乗根を求める関数 sqrt234 を作成しなさい。
また、その関数を利用して、2乗根、3乗根、4乗根を求めるプログラムを作りなさい。

sqrt234 は、次のプロトタイプ宣言で表される関数とする。

void sqrt234(double in, double *out1, double *out2, double *out3);

答えを求めたい数を第1引数へ渡し、第2,3,4引数で結果を返す仕様とすること。

実行例

./a.out
16
4.00 2.52 2.00
./a.out
1
1.00 1.00 1.00
 

課題3

コンマ区切りの文字列を受け取り、スペース区切りにして出力する関数 split を作成しなさい。
また、その関数を利用して、入力されたコンマ区切りの文字列をスペース区切りになおすプログラムを作成しなさい。

関数 split は次のプロトタイプ宣言で表される関数とすること。

void split(char *s);

入力される文字列は、最大 255 文字とする。

実行例

1行目が入力、2行目が出力です。

./a.out
windows,mac,linux,solaris
windows mac linux solaris

課題4

構造体 課題1 の distance 関数を以下のように変更し、同じ動作をするプログラムを作成せよ。

double distance(Point *a, Point *b);