2.12.6. ポインタ#

値を変数に保存するとき、その値はメモリ上のどこかに保存されます。C/C++ 言語では、値が保存されているメモリのアドレスを取得することができます。メモリのアドレスを取り扱うときには、ポインタ型の変数を利用します。例えば、int x = 5; を実行すると、5 という値はメモリ上のある番地に保存され、その値に x という名前が付けられます。したがって、変数 x からは、実体である値(5)と、その値が保存されている住所(メモリアドレス)の 2 種類の情報を取り出すことができます。

ある変数のメモリ上のアドレスを別の変数に保存したい場合は、ポインタ型の変数を使用します。例えば、変数 x のアドレスを変数 y に保存するには、int* y のようにポインタ型として宣言を行い、変数 x のアドレスを &x のように取り出して y に代入します。

#include<stdio.h>

int main(){

    int x = 5;
    int* y = &x;

    return 0;
}

int 型の変数およびポインタ型の変数を宣言したときに確保されるメモリの大きさは、システムの環境やコンパイラによって異なります。上図の例では int 型は 4 バイトであり、ポインタ型は 8 バイトです。なお、ポインタ型はアドレスを保存するため、整数へのポインタや文字へのポインタなど、指している型に関係なく同じサイズになります。

2.12.6.1. ポインタの宣言と初期化#

変数からは、実体と住所という 2 種類の情報を取り出すことができます。住所を取り出すときには、アドレス演算子 & を利用します。

#include<stdio.h>

int main(){
    int a = 1;

    printf("%d\n", a);   // 1
    printf("%p\n", &a);  // 0x7ffeecde48d8

    return 0;
}

メモリアドレスを変数として保存したい場合は、ポインタ型の変数を利用します。ポインタ型の変数を使うことで、メモリ上の値を直接編集でき、さまざまな操作を柔軟に行えるようになります。ポインタ型の変数を宣言するときは、そのアドレスに保存されている値の型に応じて int*char* のように記述します。ポインタ型の変数にはメモリアドレスが保存され、そのアドレスに保存されている実体(値)を取り出すには間接演算子 * を使用します。

#include<stdio.h>

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

    printf("%p\n", p);   // 0x7ffeef4e38d8
    printf("%d\n", *p);  // 1
    
    char b = 'A';
    char* q  = &b;

    printf("%p\n", q);   // 0x7ffeef4e38cf
    printf("%c\n", *q);  // A
    
    return 0;
}

C 言語では、配列の名前は配列の先頭要素のアドレスを表します。そのため、配列の名前とポインタ変数をほぼ同様に扱うことができます。

#include<stdio.h>

int main(){
    int arr[5] = {11, 22, 33, 44, 55};
    int* p  = arr;

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

    return 0;
}

// 11 11 0x7ffeebea08c0	11 11 0x7ffeebea08c0
// 22 22 0x7ffeebea08c4	22 22 0x7ffeebea08c4
// 33 33 0x7ffeebea08c8	33 33 0x7ffeebea08c8
// 44 44 0x7ffeebea08cc	44 44 0x7ffeebea08cc
// 55 55 0x7ffeebea08d0	55 55 0x7ffeebea08d0

2.12.6.2. ポインタと文字列#

C 言語では、文字列を文字の配列として扱うため、上述の配列と同様に、文字列とポインタ変数をほぼ同じように扱うことができます。

#include<stdio.h>

int main(){
    char arr[6] = "hello";
    char* p = arr;

    for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
        printf("%c %c %p\t", arr[i], *(arr + i), &arr[i]);
        printf("%c %c %p\n", p[i], *(p + i), p + i);
    }

    return 0;
}

// h h 0x7ffee54ac8d6	h h 0x7ffee54ac8d6
// e e 0x7ffee54ac8d7	e e 0x7ffee54ac8d7
// l l 0x7ffee54ac8d8	l l 0x7ffee54ac8d8
// l l 0x7ffee54ac8d9	l l 0x7ffee54ac8d9
// o o 0x7ffee54ac8da	o o 0x7ffee54ac8da
//   0x7ffee54ac8db	  0x7ffee54ac8db

また、次のように文字の配列を作成せず、直接ポインタに代入することもできます。

#include<stdio.h>

int main(){
    char* p = "hello";

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

    return 0;
}
// h h 0x10c827f9a
// e e 0x10c827f9b
// l l 0x10c827f9c
// l l 0x10c827f9d
// o o 0x10c827f9e
//  0x10c827f9f

2.12.6.3. ポインタと関数#

関数の引数として値を渡す方法と、ポインタを渡す方法があります。値渡しの場合は、変数の値のコピーを関数に渡すため、関数内でその値を変更しても、関数を呼び出した側の値は変更されません。一方、ポインタ渡しの場合は、変数のアドレスそのものを関数に渡すため、関数内でそのアドレスに対して直接操作できるようになります。その結果、関数内で値を変更すると、関数を抜けた後も値は変更されたままになります。

#include<stdio.h>

void f(int x) {
    x += 1;
}

void g(int* x) {
    *x += 1;
}

int main(void) {
    int x = 10;

    printf("%d\n", x); // 10

    f(x);

    printf("%d\n", x); // 10

    g(&x);

    printf("%d\n", x); // 11

    return 0;
}

関数の引数として値渡しやポインタ渡しのほかに、C++ で追加された機能として参照渡しがあります。参照渡しは機能的にポインタ渡しとほぼ同じです。

#include <iostream>

void f(int x) {
    x += 1;
}

void h(int& x) {
    x += 1;
}

int main(void) {
    int x = 10;

    std::cout << x << std::endl;  // 10

    f(x);

    std::cout << x << std::endl;  // 10

    h(x);

    std::cout << x << std::endl;  // 11

    return 0;
}