C
C言語

C言語のポインタきらい

More than 3 years have passed since last update.

C言語のポインタがきらいなので愚痴りつつ解説してみる。

よくある話題なので詳しい人は特に得るものないと思います。ANSIとかISOとかの規格書読んだわけじゃないので間違ってたらツッコミお願いします。暇な時に直します。

ポインタとは

ポインタ (pointer)とは、あるオブジェクトがなんらかの論理的位置情報でアクセスできるとき、それを参照するものである。有名な例としてはC/C++でのメモリアドレスを表すポインタが挙げられる。(ja.wikipedia.org)

ふむふむ。ポインタ=メモリアドレスだな。

ポインタ型変数

int *pointer;

はい、これがポインタです。って書いても、何を指しているのかよく分からん。*がポインタなのか!と思うひともいれば、*pointerがポインタなのか!と思う人もいるだろう。いや、そもそもポインタってメモリアドレスってさっき言うたやん?これのどこにメモリアドレスがあるんだ?ポインタって何だ!?

とならないためにここでは、「ポインタ型」、「ポインタ型変数」の2つを分けて記す。ちなみに、英文のwikipediaだと、ポインタはオブジェクトと書かれているので、単にポインタと表すときは「ポインタ型変数」のことと考えて良いだろう。

In computer science, a pointer is a programming language object, whose value refers to (or "points to") another value stored elsewhere in the computer memory using its address.(en.wikipedia.org)

C言語の宣言は、変数の型 変数名 なので、変数の型はint *、変数名はpointerになる。

pointerポインタ型変数*pointerじゃなくて。そしてint *ポインタ型 になる。ところでこの時の*ってなんて呼べばいいんだろう。ポインタ型修飾子?

型と変数名を明確に分けるために、ポインタ型の宣言を下のように書くとより明確になる。

int* pointer;

でも、

int* pointer1, other;

って書くとotherはint型になる。くやしい。

間接参照

int val;
int* pointer_of_val = &val;

と書くと、pointer_of_valがvalのアドレスで初期化される。
pointer_of_valを使ってvalのメモリにアクセスするには、

*pointer_of_val = 10;

と書く。この時の*は間接演算子と呼ぶ。そう、ポインタ型宣言時の*と間接参照時の*は違う。同じ*であっても役割は逆なのだ。逆なので「int *p;の時に*pはint型」とうまいこと覚えられるようになっている。うまく出来てるんだけどそのせいでこの2つが別という意識が働かなくなるのだ。ぐぬぬ。

引数の参照渡し

関数の引数に値渡しと参照渡しがあると言われるが、関数の引数は値渡ししかできない

void func(int* pointer){
    *pointer = 0;
}

この時、pointerというポインタ型変数が値渡しされたint型へのアドレスを受け取っている。*pointer=0は値渡しされたアドレスを間接参照して値を書き換えている。このことを便宜的に参照渡しと呼んでいるだけ。参照渡しは文法ではなくて使い方の一種なのだ。

さらに、C++には参照型があるがそれは別のお話。ややこしや。

配列のポインタ型

int *array_of_pointer[10];

これはintポインタ型の配列なのか。int配列のポインタ型なのか。どっちやねん。
これは配列記号が変数の右側につくせいで生まれる戸惑いだ。

私はこんな風にして読んでいる。我流なので注意。

脳内翻訳
int *array_of_pointer[10];

int* array_of_pointer[10];

int*[10] array_of_pointer; /* あくまで読み方ね */

実際にこう置き換えられるわけではないが、配列を型の右端に持って行くと、intポインタ型の要素が10個の配列と理解しやすいのではないだろうか。

さて、じゃあこれはどうなるだろう。

int (*pointer_of_array)[10];

これは脳内でこんな風に置き換える

脳内翻訳
int (*pointer_of_array)[10];

int[10] (*pointer_of_array);

int[10]* pointer_of_array;

()のほうが型定義で優先されるので、先に[10]が左側にやってきてint[10] (*pointer_of_array)となる。その次に()が解かれて上記のようになる。
これなら、int型の10個の配列へのポインタ型と解釈できるだろう。

このことの改善のためか、C#では

csharp.cs
int[] array = new int[5];

というように配列記号を左側に書くようになっている。これでC#好きになった。

const型へのポインタ型

(この章は分かりにくいので修正しました)
constは右側でも左側でも修飾できる。

/* constが左側を修飾する例 */
int const constant_int1;

/* constが右側を修飾する例(上記と同じ意味) */
const int constant_int2;

ポインタ型と組み合わさると下記のようになる。

/* (constant int)型へのポインタ型 */
int const *pointer_to_constant_int1;
const int *pointer_to_constant_int2;

/* int型へのconstantポインタ型 */
int * const constant_pointer_to_int = NULL;

見慣れてないとわかりにくいことだろう。修飾子は*の左側か右側かで修飾する対象が異なる。
*の左側にconstがあれば、ポインタの指す先の型に対して修飾するが、*の右側にconstがついた時はポインタ型そのものを修飾する。

*の右側にconstがついた時は、〜(左側のポインタ型)〜 is constで読みかえよう。

脳内翻訳
/* (constant int)型へのポインタ型 */
(const int)* pointer_to_constant_int1;
(int const)* pointer_to_constant_int2;

/* int型へのconstantポインタ型 */
((int *) is const) constant_pointer_to_int = NULL;

少しはわかりやすくなっただろうか。こうすると、pointer_to_constant_int1(2)は、const int型へのポインタ型なので、*pointer_to_constant_int1=0としてもコンパイルエラーになる。また、pointer_to_constant_int1=NULLはエラーにならない。対して、constant_pointer_to_intはint型へのconstポインタ型なので、constant_pointer_to_int=&some_int;がコンパイルエラーとなる。

constポインタ型は関数の引数で有用なので超基本テクニックである。読み取り専用の参照ができるので、関数を使用する側が渡した値を変更される心配をしなくてよくなるし、constポインタ型でないとconst型のアドレスは渡せなくなってしまう。
ただ、入力専用の参照をconstポインタ型にせずとも、プログラムは動いてしまうのでconst教一族は今日も他人のconstついてないポインタ型でイラつきを覚えるのだ。

文字列リテラル

"hello"等の文字列リテラルは、その先頭文字へのアドレスを返す。なので、下記のように受け取れる。

const char* str = "hello";

"hello"のh,e,l,l,oが入ったメモリそのものは、コンパイラがどっかに用意しておいてくれるので、ポインタ変数で先頭アドレスを受け取る。ただし、その"hello"が入ったどっかのメモリは、書き換え禁止の領域なのでconst char*型((constant char)へのポインタ型)で受け取るようにする。

他のリテラルはその値を返すのに、文字列だけアドレスを返すわけだ。特に、文字列型をimmutableなオブジェクトとして扱える言語に先に触ってしまった人は戸惑うんじゃないだろうか。

おわりに

以上、愚痴終わり。最後まで読んでくれてありがとうございます。内容はご理解いただけたでしょうか。もし理解できなかったなら、貴殿もポインタが嫌いになったことでしょう。私の拙い説明で新たな知見が得られたなら、もしかしたら貴殿は今よりポインタを好きになれるかもしれませんね。私はきらいですけどね。

ポインタを完璧に扱えているプログラムってなかなかないです(ポインタを完璧に扱えるプログラマって少ないです)。T *引数を読み取り専用にするためにconst T *にしたりと、C言語は習熟度が上がっていくほど書き方がシンプルじゃなくなっていくせいで、同じ仕様でも習熟度によって出来上がるプログラムに差ができるんですよね。その場は動くけばいいかもしれませんが、この違いは後々のリファクタリングやデバッグで効いてくるわけです。そして引き継ぎの人が苦労するのです。だからきらいなのです。

Rustはmutable指定しない限り読み取り専用の参照になるようになってます。この方針はいいですね。Rust流行ってほしいなぁ。

追記

ポインタの型の解釈方法を我流で書いちゃいましたが、これは良くなかったかもしれません。
JISのC99規格書を見つけたので、近々真正面から仕様通りの解釈を試みたいと思います。その際はここにリンクを貼ります。
→投稿しました。
const int *p と int * const p の違いを構文規則から理解する