C言語のポインタの解説

2024/10/23に公開

はじめに

こんにちは。日本一のプログラミングスクールにこれからなる42Tokyoに合格したばかりのgostachanと申します!
この記事ではC言語のポインタと配列について解説します。
かなり初歩的なことから、自分が大きく躓いた配列とポインタの関係まで解説します!

メモリとアドレス

メモリとはコンピュターが一時的にデータを保存するための場所です。プログラムが実行される際、変数、配列、関数などのデータはメモリに保存されます。
アドレスとはデータが保存されているメモリの位置を示す数値です。
変数名の前に&を記述することでその変数のアドレスを取得することができます。

以下のコードが変数のアドレスを出力するコードです。

#include <stdio.h>

int main(void)
{
	int a = 0;
	int b = 1;
	int c = 2;
	int d = 3;

    // &演算子を変数の前に記述することで変数のアドレスを取得する
	printf("&a = %p\n", &a);
	printf("&b = %p\n", &b);
	printf("&c = %p\n", &c);
	printf("&d = %p\n", &d);
}

出力は以下のようになりました。

&a = 0x16d33b33c
&b = 0x16d33b338
&c = 0x16d33b334
&d = 0x16d33b330

私の環境では以下の図のように変数が保存されているようです。

ポインタ

ポインタの基本

ここまではメモリとアドレスについて解説しました。
この章ではポインタについて解説します。

ポインタとはざっくり言うとアドレスを保存するためののことです。
intやchar型とは異なり、ポインタ型は単独では定義できず、intやchatなどの基本的な型から派生することでしか定義できません。
ポインタ型の変数の定義は以下のように変数名の前に*をつけることで宣言します。

#include <stdio.h>

int main(void)
{
	int a;
	int *a_ptr;

	a = 42;
	a_ptr = &a;

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

	printf("a = %d\n", a);
	printf("*a_ptr = %d\n", *a_ptr);
}

&a = 0x16bb8b33c
a_ptr = 0x16bb8b33c

a = 42
*a_ptr = 42

変数aのアドレスをintへのポインタ型(int型から派生したポインタ型)a_ptrに代入できていることがわかります。
また、*a_ptrと記述することでa_ptrが示すメモリに保存されている値(今回は42)を取得することができます。
ポインタの分かりにくい文法が出てきましたね。
ポインタ型を宣言するときも、ポインタ型が指す値を取得するときも「*」演算子を使う。
変数のアドレスを取得するときは「&」を使う。
はぁ〜。なんて分かりにく文法なんだ、、、

ポインタ型は基本型からの派生型である

前の章ではポインタについてざっくり説明しました。
この章ではポインタを深掘りします。

ポインタとはすなわちポインタ型というのことでした。
しかし、普通のint, charなどとは少し異なります。
ポインタ型は単独で定義することができません。int, charなどの単独で定義できる型の派生として存在します。
この記事では、int, charなど、単独で存在することができる型を基本型、ポインタ型のような基本型から派生することができる型を派生型と呼ぶことにします。

ポインタ型があれば、ポインタ型の値も、ポインタ型の変数も存在します。
しかし厄介なことに世間ではこれらの型、値、変数のこと全てまとめてポインタと呼びます。(ポインタ型の値はアドレスと呼ぶこともある)
これらを混同しないようにすることがポインタを理解するのには必須です。
はぁ〜。なんて分かりにく呼び方なんだ、、、

intへのポインタとcharへのポインタは別物

ここで以下のようなコードを実行するとどうなるでしょうか?

#include <stdio.h>

int main(void)
{
	char c;
	int i;

	char *c_ptr;
	int *i_ptr;

	c_ptr = &c;
	i_ptr = c_ptr;

	printf("c_ptr = %p\n", c_ptr);
	printf("i_ptr = %p\n", i_ptr);
}

一応実行はできますが、以下のような警告文が出ました。

pointer_type_mismatch.c:12:8: warning: incompatible pointer types assigning to 'int *' from 'char *' [-Wincompatible-pointer-types]
        i_ptr = c_ptr;
              ^ ~~~~~
1 warning generated.

要は、intへのポインタ型とcharへのポインタ型は違う型だから代入はできないとのことです。
コンパイラもintへのポインタ型とcharへのポインタ型は別物として認識していると言うことになります。
「でも、指す型が違くても変数の値は同じアドレスだろ?代入したって問題ないだろ?」と思いますよね。
この代入ができない理由は後述のポインタ演算を解説する章で記述します。

配列とポインタ演算

配列の基本

ここまではポインタについて解説しました。
この章では配列について解説します。
ここまでのポインタの解説では読者の皆さんも「なんだポインタって難しいって聞いてたけどチョロいじゃないか」と思っていることだと思います。しかし、問題はここからです。「はぁ〜。」の連続です。

配列とは同じ型の変数が決まった数だけ連続する領域に並んでいるデータ構造のことを指します。(その様子は後に実際のコードを動かして確認します。)
データ構造とは、データの保存方法で型とは異なります。配列の場合メモリの連続領域にデータを保存しますが、他のデータ構造として、各要素が次の要素へのアドレスを持つリスト、任意のデータ型をキーにしてバリューにアクセスするハッシュマップなど、他にもたくさんあります。
さて、話を配列に戻しましょう。よく配列とポインタを混同している人がいますが、配列とポインタは別物です。

ここからは、コードを動かして配列について理解を深めましょう!

#include <stdio.h>

int main(void)
{
	int array[5];

	for (int i = 0; i < 5; ++i) {
		array[i] = i;
	}

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

まず配列は array[5]のように定義します。他にC99から追加されたarray[n]のように[]内に変数を指定できる機能(VLA)が追加されましたが、自動変数でしか宣言できないなど基本的な配列と仕様が違う点があるので今回は説明を省きます。
さて、上のコードを実行してみると以下のように出力されます。

                      array    = 0x16b1cb324
array[0] = 0,        &array[0] = 0x16b1cb324
array[1] = 1,        &array[1] = 0x16b1cb328
array[2] = 2,        &array[2] = 0x16b1cb32c
array[3] = 3,        &array[3] = 0x16b1cb330
array[4] = 4,        &array[4] = 0x16b1cb334

int型は4byteなのでやはりarrayの要素は以下のように連続するメモリ領域に配置されていることが見て取れます。
また、arrayは配列の先頭要素のアドレスを返していることがわかります。

ポインタ演算

これまでは、配列の基本について解説しました。
ここからはポインタ演算について解説します。
以下のコードと出力を見てください

#include <stdio.h>

int main(void)
{
	int array[5];

	for (int i = 0; i < 5; ++i) {
		array[i] = i;
	}

	for (int i = 0; i < 5; ++i) {
		printf("%26s +%d = %p\n", "array", i, array + i);
		printf("array[%d] = %d,        ", i, array[i]);
		printf("&array[%d] = %p\n", i, &array[i]);
	}
}
                     array + 0 = 0x16d31f324
array[0] = 0,        &array[0] = 0x16d31f324
                     array + 1 = 0x16d31f328
array[1] = 1,        &array[1] = 0x16d31f328
                     array + 2 = 0x16d31f32c
array[2] = 2,        &array[2] = 0x16d31f32c
                     array + 3 = 0x16d31f330
array[3] = 3,        &array[3] = 0x16d31f330
                     array + 4 = 0x16d31f334
array[4] = 4,        &array[4] = 0x16d31f334

arrayは配列arrayの先頭要素を指すんでしたね。arrayに1を足すと4byte、つまり、sizeof(int)だけポインタがずれています。
これが文字列(char型の配列)の先頭要素を指すポインタに1を足すと1byteずれます。

これはポインタに1を足すとsizeof(派生元の要素)だけアドレスがズレることを意味しています。
これが先述した下記コードで警告が発生される理由です。int型から派生したポインタと、char型から派生したポインタはポインタ演算の挙動が異なるので代入できません。

#include <stdio.h>

int main(void)
{
	char c;
	int i;

	char *c_ptr;
	int *i_ptr;

	c_ptr = &c;
	i_ptr = c_ptr;

	printf("c_ptr = %p\n", c_ptr);
	printf("i_ptr = %p\n", i_ptr);
}
pointer_type_mismatch.c:12:8: warning: incompatible pointer types assigning to 'int *' from 'char *' [-Wincompatible-pointer-types]
        i_ptr = c_ptr;
              ^ ~~~~~
1 warning generated.

配列とポインタの違い

ここからは配列とポインタの違いについて仮説します。
まずは少し復習しましょう。以下のコードを見てください。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int array[10];
	int *ptr;

	ptr = &array[0];
	printf("ptr = %p\n", ptr);
	printf("&array[0] = %p\n", &array[0]);
	printf("array = %p\n", array);
}

出力結果が以下です。ここで問題です。???の部分にはどのようなアドレスになるでしょうか?
全て同じ値でしょうか?異なる値でしょうか?

ptr = ???
&array[0] = ???
array = ???

正解は以下のような出力になります。

ptr = 0x16f03f310
&array[0] = 0x16f03f310
array = 0x16f03f310

全て同じ値が出力されています。
この結果より、array&array[0]はどちらも配列の先頭ポインタを指すことがわかります。
では以下のコードの出力はどうなるでしょうか?
ptr[2]とarray[2]の値は同じでしょうか?

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int array[10];
	int *ptr;

	for (int i = 0; i < 10; ++i) {
		array[i] = i;
	}

	ptr = array;
	printf("ptr[2] = %d\n", ptr[2]);
	printf("array[2] = %d\n", array[2]);
}

正解は以下のようになります。

ptr[2] = 2
array[2] = 2

ptr[2]とarray[2]が示す値は同じです。
ptr = arrayにおいてarrayは配列の先頭ポインタを指しえているからと言えます。
この2つの例から分かるように配列とポインタの間には妙な互換性があります。
それらの互換性により配列とポインタを同じものだと考えている人が多いように思えますが、配列とポインタは全くの別物です。
例えば以下のコードはどうなるでしょうか。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int array[10];
	int *ptr;

	printf("sizeof(array) = %ld\n", sizeof(array));
	printf("sizeof(ptr) = %ld\n", sizeof(ptr));

}

出力は以下のフォーマットになります。???にはどのような値が入るでしょうか?

sizeof(array) = ???
sizeof(ptr) = ???

正解は以下のようになります。

sizeof(array) = 40
sizeof(ptr) = 8

おっと!楽しくなってきましたね!
sizeof(array)は40を示すのにsizeof(ptr)は8を示します。
これこそが配列とポインタの一番大きな違いです。
『配列は要素数を記憶します。一方ポインタは配列の先頭要素を示しているときもその配列の要素数を記憶しません。』
sizeof(array)はsizeof(int) * (配列の要素数)なので4 * 10 = 40を示します。
一方でsizeof(ptr)はintへのポインタ型のsizeなので8が出力されました。
私の環境では全てのポインタ型のサイズは8byteで扱われえているようです。
ここでもう一度復習します。
『配列は要素数を記憶します。一方ポインタは配列の先頭要素を示しているときもその配列の要素数を記憶しません!!!!!!!!!!!!!』

C言語の配列の妙で奇怪な仕様

ここまで配列とポインタの違いを説明してきました。それを踏まえた上で、C言語での配列の妙な仕様について学びます。
以下のコードを見てください。

#include <stdio.h>
#include <stdlib.h>

void func(int *array, int *ptr)
{
	printf("from func: sizeof(array) = %ld\n", sizeof(array));
	printf("from func: sizeof(ptr) = %ld\n", sizeof(ptr));
}

int main(void)
{
	int array[10];
	int *ptr;

	printf("from main: sizeof(array) = %ld\n", sizeof(array));
	printf("from main: sizeof(ptr) = %ld\n", sizeof(ptr));

	func(array, ptr);
}

ここでも例に漏れず出力フォーマットを以下に示します。

from main: sizeof(array) = ???
from main: sizeof(ptr) = ???
from func: sizeof(array) = ???
from func: sizeof(ptr) = ???

ここまで読んだ方なら予想がつくと思います。
1行目:arrayは配列を指します。配列は要素数を記憶するのでsizeof(int) * (要素数) = 40なので40!
2行目:ptrはポインタを指します。筆者の環境でポインタは8byteなので出力は8!
3行目:1行目と同じ!
4行目:2行目と同じ!
どうだ!!と言ったところでしょうか。
正解は以下のようになります。

from main: sizeof(array) = 40
from main: sizeof(ptr) = 8
from func: sizeof(array) = 8
from func: sizeof(ptr) = 8

はぁ〜。なんだこれ?となりますよね。面白くなってきました。もしかしたら面白さを通り越してここから先は明日読もー...となっている方もいそうですが無視して続行します。
この記事の重要ポイントその2です。
『配列は式の中では先頭要素のポインタとして扱われる』です。
そもそも式とはなんぞや?ってことでまずは式の説明からです。

式(しき、expression)とは、プログラミングにおいて、言語によって定められた優先順位や結びつきの規定に則って評価される値、変数、演算子、関数などの組み合わせである。
引用:[wikipedia](https://ja.wikipedia.org/wiki/式_(プログラミング)

はぁ〜。わかりにくいですね。
厳密ではないですが、ここではわかりやすさを優先した説明をします。
基本的にソースコードに変数名が出てきた時は、式として記述されています。ただし以下の条件を満たすものは式とは言いません。(この記事では1, 2, 3の使い方だけ式でないと覚えれば十分です。)
1, 変数の宣言

int array[10]

2, 関数の仮引数(要はこれも変数の宣言です)

void func(int *array);

3, sizeofの引数

sizeof(array);

4, 構造体や共用体のメンバー
5, 型定義
6, 配列のサイズ指定
など。

それでは、式がなんとなく分かったところで本題の『配列は式の中では先頭要素のポインタとして扱われる』について解説します。

まず以下のコードと出力を見てください。

#include <stdio.h>

int main(void)
{
	// ここのarrayは式ではないので「配列」を指す
	int array[3];

	for (int i = 0; i < 3; i++) {
		// ここのarrayは式
		array[i] = i;
	}

	for (int i = 0; i < 3; ++i) {
		// ここのarrayは式
		printf("array[%d] = %d\n", i, array[i]);
		// ここのarrayは式
		printf("*(array + %d) = %d\n", i, *(array + i));
	}
}
array[0] = 0
*(array + 0) = 0
array[1] = 1
*(array + 1) = 1
array[2] = 2
*(array + 2) = 2

結果から*(array + 1)array[1]は同じ値を指しています。また、array[1]のarrayは式ですので配列ではなく先頭要素のポインタを指します
つまりarray[1]*(array + 1)のシンタックスシュガーでしかないのです。
(シンタックスシュガーとは、コンピュータの処理には関係ない人間が分かりやすくするためだけの文法)
その証拠に以下のコードを見てくさい。

#include <stdio.h>

int main(void)
{
	int array[3];

	for (int i = 0; i < 3; i++) {
		array[i] = i;
	}

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

足し算では交換法則が成立するのでarray + 11 + arrayと等しいです。
そしてarray[1]array + 1のシンタックスシュガーなので1[array]のようにarrayとインデックスを交換してもコードは動作します。

*(array + 0) = 0
*(0 + array) = 0

array[0] = 0
0[array] = 0

*(array + 1) = 1
*(1 + array) = 1

array[1] = 1
1[array] = 1

*(array + 2) = 2
*(2 + array) = 2

array[2] = 2
2[array] = 2

配列が式では先頭要素のポインタになることがわかったので、この章の最初に出てきたコードの種明かしをしましょう。

#include <stdio.h>
#include <stdlib.h>

void func(int *array, int *ptr)
{
	printf("from func: sizeof(array) = %ld\n", sizeof(array));
	printf("from func: sizeof(ptr) = %ld\n", sizeof(ptr));
}

int main(void)
{
	int array[10];
	int *ptr;

    // ここでのarrayは配列
	printf("from main: sizeof(array) = %ld\n", sizeof(array));
	printf("from main: sizeof(ptr) = %ld\n", sizeof(ptr));

    // ここでのarrayは式
	func(array, ptr);
}
from main: sizeof(array) = 40
from main: sizeof(ptr) = 8
from func: sizeof(array) = 8
from func: sizeof(ptr) = 8

main関数内のsizeof(array)のarrayは配列なので要素数を記憶します。よって、sizeof(array) = sizeof(int) * 10 = 40 となり40を出力します。
一方、funcの引数arrayは式なので配列の先頭要素のポインタに降格します。よって、func関数内のsizeof(array)のarrayはポインタですからsizeof(array) = 8 となります。
要するに、関数の引数で渡された配列はポインタに降格します。(多次元配列の場合は一番外側の配列のみポインタに降格)

終わりに

ここまで、かなりたっぷりめの解説でしたが、読んでくださりありがとうございました。
しかし、C言語のポインタは深くこの記事では語り尽くせなかった内容も山ほどあります。
最後にこの記事で語れなかった内容を示して終わります。

  • 多次元配列の式の場合、一番外側の配列のみポインタに降格し、それ以外の配列は配列のままである
  • プロトタイプ宣言での配列の表記 例えばint array[10]を受け取るfuncのプロトタイプ宣言は以下の記法があるのはなぜか?
void func(int array[10]);
void func(int array[]);
void func(int *array);
  • 多次元配列を引数にとる関数のプロトタイプ宣言
  • そもそもC言語には多次元配列は存在しない?
  • Cで多次元配列はどのようにメモリに確保されるか
    などなどなどなど、
    他にも本記事では一番の本質である「なぜポインタを利用しなくてはならないのか」などの解説もしませんでした。
    もし需要があればこの記事の続編も書きたいと思います。
    最後まで読んでくださりありがとうございました!

Discussion