ポインタとはなにか
ポインタでほとんどのC言語学習者が一度はつまづくと言っても過言ではないと思います。まず、ポインタのイメージは変数を指示する矢印です。これは変数によく似ていますが、変数そのものではなく、変数のメモリ上のアドレスです。アドレス自体も変数なのでややこしいのですが、メモリの場所と、その場所に格納された値とは異なるものだという点によく注意してください。
一般に変数のアドレスを参照して値を操作することを参照渡し、通常の値の代入を値渡しといいます。C言語ではポインタを使ってこの参照渡しによく似たポインタ渡しということをします。まずは実際のコードで値渡しとポインタ渡しの違いを見ていきましょう。まずは、値渡しの例です。
#include <stdio.h>
void change(int x) {
x = 100;
}
int main() {
int a = 10;
change(a);
printf("aの値: %d\n", a);
return 0;
}
これを実行すると
aの値: 10
と表示されます。つまりchange関数を介してもaは更新されません。これはchange関数の引数にaの値のみが渡され、それがxという別の変数に代入され、その後に更新されたからです。ここで、もし、change関数に渡されるのが変数aのアドレスだったらどうでしょうか。次のソースコードはその疑問に答えてくれます。
#include <stdio.h>
void change(int *x) {
*x = 100;
}
int main() {
int a = 10;
change(&a);
printf("aの値: %d\n", a);
return 0;
}
出力結果は
aの値: 100
となり、change関数の更新が反映されています。前回のコードの値渡しの部分をポインタ渡しに書き換えてみました。&
と*
の記号が少し加わっただけでなぜこのようなことになったのでしょうか。それはポインタのおかげなのです。
まず&
の記号はアドレス演算子といい、そのあとに続く変数のメモリアドレスを出力する演算子です。ここには環境に応じて例えば
0xffffe1dec8ec
というようなアドレスのデータ(デフォルトでは16進数表記)が格納されています。アドレスのデータの型はポインタ型です。
では*
はなんでしょうか。これは間接演算子といって、アドレス演算子とは逆にアドレスからそのアドレスに格納された変数を返す演算子です。先程のコードではchange(&a);
でchange関数にまず変数a
のアドレスを渡しました。change関数の方では, それをint型へのポインタで与えられた住所を元に値を操作します。int型へのポインタとはint *x
のことです。change関数のx
には&a
の内容、すなわちa
のアドレスが代入されているので、*x
と指定して変数をいじれば変数のスコープなど関係なく関数の外から値を制御できるのです。
アドレス演算子と間接演算子の働きが一発で理解できる別のコードの例をお見せしましょう。まずはどのようなものが出力されるかぜひ想像してみてください。
#include <stdio.h>
int main() {
int a = 10;
int *p = &a;
printf("aの値: %d\n", a);
printf("aのアドレス: %p\n", &a);
printf("pの値(aのアドレス): %p\n", p);
printf("*pの値(aの中身): %d\n", *p);
return 0;
}
私の実行環境での出力は,
aの値: 10
aのアドレス: 0xffffd2b3d39c
pの値(aのアドレス): 0xffffd2b3d39c
*pの値(aの中身): 10
でした。
ポインタを使えばもう少し複雑な操作もできます。スワップ関数と呼ばれる関数を一緒に見ていきましょう。これは変数x
と変数y
が与えられたときにそれぞれの値をスワップ、つまり交換する関数です。
#include <stdio.h>
void swap(int *x, int *y) {
int temp = *x; // ポインタxが指す先の値をtempに一時保存
*x = *y; // ポインタyが指す先の値をポインタxが指す先に代入
*y = temp; // tempの値をポインタyが指す先に代入
}
int main() {
int a = 10;
int b = 20;
printf("交換前: a = %d, b = %d\n", a, b);
swap(&a, &b);
printf("交換後: a = %d, b = %d\n", a, b);
return 0;
}
コードのロジック自体はシンプルです。ただしプログラミングに慣れていない人は2変数の入れ替えを行う時、変数の一時的な退避場所一つ用意する必要があることに注意してください。int temp
の宣言は一見無意味に思えるかもしれませんが、これをしないと一方を他方に代入した時点で他方の変数が消滅してしまいスワップができません。このコードではx
の中身を一度temp
に退避しているので実行結果は
交換前: a = 10, b = 20
交換後: a = 20, b = 10
となりスワップ成功です。
余談ですが、tempというのはプログラマーがよく使う変数名の一つです。意味はもちろんtemporary(一時的な)からきています。こうやって一時的にデータを保持させたり退避させたりするときに使います。でもこの変数名そのものは何の意味もないから中に何が入っているかすぐには分かりません。こういう抽象的な命名は名前の競合や誤解によるバグを生みやすいとも言われます。したがって、使うときはメソッドの切り分けをよく行い、変数のスコープに注意しましょう。スコープが小さければ命名が抽象的でも許されることが多いようです。
命名の美学に関して興味がある方にはO'reillyから出ているDustin Boswell他著『リーダブルコード』という青い表紙の美しい本があるのでオススメします。
構造体とはなにか
ポインタの話だけでもお腹いっぱいという感じですが、ポインタは構造体といっしょになって初めて威力を発揮します。しかし、構造体はとても複雑です。まずは一つコードを見てみましょう。
#include <stdio.h>
// 構造体の定義
struct Person {
char name[20];
int age;
};
int main() {
// 2人の人物データを定義
struct Person akari = {"akari", 25};
struct Person hiromu = {"taro", 24};
// ポインタを使ってアクセス
struct Person *p1 = &akari;
struct Person *p2 = &taro;
// 表示
printf("%s の年齢は %d 歳です。\n", p1->name, p1->age);
printf("%s の年齢は %d 歳です。\n", p2->name, p2->age);
// 年齢を変更
p1->age++; // akari の誕生日
p2->age--; // taro はサバを読んだ!
// 再表示
printf("%s の新しい年齢は %d 歳です。\n", p1->name, p1->age);
printf("%s の新しい年齢は %d 歳です。\n", p2->name, p2->age);
return 0;
}
出力結果は、
akari の年齢は 25 歳です。
taro の年齢は 24 歳です。
akari の新しい年齢は 26 歳です。
taro の新しい年齢は 23 歳です。
です。
先に構造体の話をしましょう。構造体は自作の変数のようなものだと言えます。Javaに触れたことがる人ならば、クラスだと思うかもしれません。C言語にはint型やchar型が用意されていますが、プログラミングをしているとそれらをまとめた変数を使いたくなる時があります。
ある人の“年齢”と”名前”や, ゲームキャラクターの“攻撃力”と”防御力”と”HP”などはまとめて扱ったほうが分かりやすいのです。この考え方はオブジェクト指向という形でC言語をもとに作られたC++やJavaなどの言語の設計思想に脈々と引き継がれています。
構造体を使うにはメンバ変数をあらかじめ定義する必要があります。この例では、名前と年齢を持った構造体struct Person
がint型のage
と大きさ20のchar型の配列nameを持っています。このコードではポインタを使って構造体にアクセスしています。
なお、PythonやJavaなど他の言語に親しい人のために補足すると、ここでnameを参照型のStringでいきなり宣言することはできません。C言語の変数はいずれもプリミティブであり、文字列はあくまで”文字の配列”として扱う必要があります。
【参考文献】
[1]MMGames, “苦しんで覚えるC言語”, 秀和システム, (2011).