1.はじめに
・なぜこのテーマを選んだか
現在じゃんけんを戦闘システムに組み込んだRPGゲームをC++で作成しています。
C言語については、ある程度大学の授業で触ったことがありましたが、C++は授業で少しだけ使った程度で、本格的に扱うのは初めてです。そのため、ゲーム全体の構成や細かいコードの書き方に悩むことが多く、Webで調べたり、ChatGPTに相談しながら進めていきました。
作業を進める中で、自分が書いたコードとChatGPTに提案されたコードで処理の仕組みに違いがあることに気づき、特に「ポインタと参照渡し」の違いに関して強く印象に残ったため、今回 「ポインタと参照渡し」 について記事をまとめることにしました。
・対象読者
C++初学者や「ポインタと参照」でつまずいた人
2.C++におけるオブジェクトとは(lvalueとrvalueの違い)
コメントを受けて補足した方が良いと考えたので、オブジェクトについて説明します。
C++におけるオブジェクトとは、以下の要件を満たすものとなります。
- sizeofで測れるサイズがある
- aligonofで求まるアラインメント要求がある
- 記憶域期間がある(自動、静的、動的、スレッドローカル)
- 有効期間(=ライフタイムがある)
- 型がある
- (オプションとして)名前がついている
上記に加えて、C/C++におけるオブジェクトとは 「実行環境におけるメモリ(データストレージ)の領域」 であり、その内容は「値」とも解釈されます。
ここで値とは、特定の型を持つと解釈された場合のオブジェクトの内容のことを指します。
つまり、メモリ上のどこにあるかを示す領域である 「アドレス」を持たないものは「オブジェクト」とみなせない とも言い換えられます。
例えば、以下のように宣言された変数は典型的な「オブジェクト」としてみなされます。
int a = 10;
char b[6] = "aiueo";
色々と書きましたが、私のような初学者は、とりあえず 「変数」=「オブジェクト」 と思って問題ないと思います。
・lvalueとrvalueとは
とはいえ、じゃあ具体的に「オブジェクト」と「そうじゃないもの」ってどう見分けるの?と思われる方もいらっしゃると思うのでここで、見分ける際に有用な「lvalue」と「rvalue」という概念について説明していきます。
lvalue(左辺値)は、代入式の左側にくることができる値です。
rvalue(右辺値)は、代入式の右側にくることができる値です。
int a = 10;
int b = a*2;
上の例の場合だと、「aとb」がlvalue、「10とa*2」がrvalueであると言えます。
ここで大事なのは、
lvalueは多くの場合、 名前がついており、アドレスを持つことが多いためポインタや参照として扱えます。 一方、rvalueは通常 名前を持たず、アドレスを明示的に取得できないことが多いのでポインタや参照として直接扱えない 、ということです。(例外あり)
例
int a = 10;
int &c = a;//OK
int &e = 10;//NG
int *p = &a;//OK
int *q = 10;//NG
初学者は、通常lvalue(式の左側)をオブジェクト として思ってもらえれば大丈夫です。
(例外については「8.2の例外について」で説明します。)
3.ポインタとは?
・定義と使い方
C/C++におけるポインタとは、「変数やnewで作った オブジェクト のアドレスを格納する変数」といえます。つまり、メモリ上にそのオブジェクトがどこに格納されてるか、ということを保持する変数です。
通常の変数(オブジェクト)は 「数値や文字などの値そのもの」 を保持しますが、ポイントは、「その値が格納されている場所」 を指します。
・使い方
int a = 10; //普通の変数(値そのものを持つ)
int *p = &a; //ポインタ変数(変数aのアドレスを持つ)
std::cout<<*p<<std::endl;//10(aの値)
std::cout<<p<<std::endl;//0x61fe44(aのアドレス)
&a → aのアドレス(参照演算子)
int *p → 整数型へのポインタ変数p
*p → アドレスpが指す先の値を取得(間接参照演算子)
・使い方の例:自分の戦闘システム内での使用方法(ポインタ)
C++では、関数の引数は基本的に「値渡し」、すなわち呼び出し元のコピーを渡す方式になります。値渡しであれば関数の中でその値を変更させても、元の変数は影響ありません。しかし、ポインタを使うと呼び出し元の変数に直接アクセスすることとなるので、値の変更が反映されます。
以下のコードは現在作成中のゲーム内でのダメージ処理部分です。
int main(){
int player_hp = 10;
int enemy_hp = 10;
doing_jyanken(&player_hp,&enemy_hp,ROCK);
}
void doing_jyanken(int *player_hp, int *enemy_hp, int hand) {
int damage = rand() % 6 + 1; //ダメージを1~6のランダムな値とする
if (jyanken(hand) == WIN) {
std::cout << "敵に" << damage << "ダメージ与えた!" << std::endl;
*enemy_hp -= damage;//敵のHPをダメージ分減らす
} else {
std::cout << "敵から" << damage << "ダメージくらった!" << std::endl;
*player_hp -= damage;//プレイヤーのHPをダメージ分減らす
}
}
このように、関数の引数にプレイヤーのHPのポインタと、敵のHPのポインタを持たせることで、ダメージをHPに反映することが出来ます。
4.参照とは?
参照とは、ある変数への別名のようなものです。C++では参照を用いることで、ポインタのようにアドレス操作を使わなくとも元の変数を直接操作することが出来ます。
ポインタと違って、参照を用いることで、コードがシンプルになり、意図も読み取りやすくなります。
・基本的な使い方
int a = 10;
int &ref = a;//aの参照をrefとして宣言
ref = 20; //aの値も20になる
std::cout<<a<<std::endl; //→20
std::cout<<&a<<std::endl; //0x61fe44(aのアドレス)
std::cout<<&ref<<std::endl; //0x61fe44(refのアドレス。aと同じ)
上のようなコードだと、refはaの別名となるので、refに代入するとaの値も変わります。
・使い方の例:自分の戦闘システム内での使用方法(参照)
以下のコードは先ほどのダメージ処理のコードを参照を使って書き直したバージョンです。
変更点としては、関数の引数であるplayer_hpとenemy_hpの頭に*ではなく&を付け、関数の中でアドレス演算子である「*」を使わなくなった2点が挙げられます。
int main(){
int player_hp = 10;
int enemy_hp = 10;
doing_jyanken(player_hp,enemy_hp,ROCK);
}
void doing_jyanken(int &player_hp, int &enemy_hp, int hand) {
int damage = rand() % 6 + 1;
if (jyanken(hand) == WIN) {
std::cout << "敵に" << damage << "ダメージ与えた!" << std::endl;
enemy_hp -= damage;
} else {
std::cout << "敵から" << damage << "ダメージくらった!" << std::endl;
player_hp -= damage;
}
}
このように参照渡しにすることでアドレス演算子(*や&)を使わなくてよいため、関数の中身が簡潔になります。それにより、直感的で安全なコードになります。また、参照はnullにならないため、バグが起きにくくなります。
それに加えて、参照は初期化時に必ず何かの変数を結びつける必要があり、結びつける先を変更することはできないという特徴を持ちます。
一方ポインタの場合、宣言時にアドレスを設定していなくても問題なく、なおかつ後から任意のアドレスを代入することが出来ます。
例(ポインタ)
int *p;
int a = 10;
int b = 20;
p = &a;//OK
p = &b;//OK
例(参照)
int &ref;
int a = 10;
ref = a;//コンパイルエラー
表を使って比較をすると以下のようになります。
項目 | ポインタ | 参照 |
---|---|---|
初期化 | 後からでもOK | 初期化時に結びつけが必要 |
null | 可(nullptr あり) |
不可(常に参照先が必要) |
再代入 | 可 | 不可(結びつけた変数固定) |
記述の簡潔さ | やや複雑(* や& 多い) |
シンプル(直感的) |
5.使い分け方
参照渡しが適している場面
・関数に変数を渡してその中で直接書き換えたい時
・引数が絶対にnullになってほしくない時
nullにならないことでバグが起きにくくなります。
・可読性・安全性を重視したい時
ポインタ渡しが適している場面
・変数を「渡すか渡さないか」を動的で決めたい時
例えば「ポインタがnullなら処理しない」といった設計が出来ます。
・配列やメモリを直接扱いたい時
newやmallocを使った動的メモリ確保に有用です。
(参照でも動的領域を扱うことはできますが、動的メモリ確保をすることはできないので
ポインタと比べて自由度が下がります)
・参照先を後から変更したい時
表にすると以下のようになります。
条件 | 参照(&) | ポインタ(*) |
---|---|---|
わかりやすさ・安全性優先 | ◎(コードが直感的) | △(記号が多く初心者には難解) |
NULLを許容する必要があるか | ×(必ず結びつく) | ○(nullptr使用可能) |
結びつける変数を後から変更したい | ×(変更不可) | ○(再代入可能) |
引数として配列や大きなデータを渡す場合 | ○(簡潔に渡せる) | ○(柔軟に扱える) |
6.自分が理解できたきっかけ
自分はポインタに関しては大学の授業内で学んだことがあったので使い方はなんとなく理解していました。しかし、参照渡しに関しては存在すらよく知らず、「なぜここでポインタではなく参照渡しを使うのか」がわかりませんでした。そこで、ChatGPTと対話を繰り返したり、Web記事を読み漁ったりするうちに違いが分かり、「この場面では参照渡しを使った方がいいコードになるんだ」と納得することが出来ました。
追記(2025/05/19)
コメントで様々なご指摘ありがとうございました。なんとなく使っていた言葉の定義が定まっていなかったり(主にオブジェクト)、lvalueやrvalueなどの新たな知識を得られたのでためになったな、と思っています。
大学であまり触れなかった領域であるため自分の理解がまだ足りて居ないところも多く、まだ不正確な表現もあるとは思いますが、気づいたときやご指摘をいただいたときにまた更新していけたらいいなと思っています。
7.まとめ
- ポインタは「アドレスを操作したいとき」に使う
- 参照は「シンプルに元の値を変更したいとき」に使う
- 参照の方が安全で読みやすいが、柔軟性はポインタが上
-「参照渡し」を使ったコードを書いて比較してみると実感しやすい - ゲームのような処理で「どっちがいいか」を考える体験が成長につながる!
ChatGPTに頼りつつ、色々考えて、自分なりに理解できるようになって嬉しいです。この記事が誰かの助けになれば幸いです。
8. 2の例外について
2.の中で
「ここで大事なのは、
lvalueは多くの場合、名前がついており、アドレスを持つことが多いためポインタや参照として扱えます。 一方、rvalueは通常名前を持たず、アドレスを明示的に取得できないことが多いので、ポインタや参照として直接扱えない、ということです。(例外あり)」
というような説明をしましたが、例外もあるのでそれについて補足説明をしたいと思います。
というのも、rvalueでもアドレスを持つことがあり、
必ずしもlvalue=オブジェクトとは限らないのです。
rvalueでもアドレスを持つパターンの例
const char b[6] = "aiueo";
const char *p = b; // OK;
const char *q = "aiueo";//OK
const char (&s)[6] = "aiueo";//OK
この"aiueo"という部分はrvalueではありますが、アドレスが取得できます。よってポインタや参照として扱うことが可能です。ただし、スコープを抜けると消えるので、未定義動作が起こりやすく、ポインタで使い続けるのは危険です。
lvalue=オブジェクトとは限らないパターンの例
int a=10;
int &i=a;
このとき、int&は参照そのものなのでiはオブジェクトとは言えないです。
おそらく、iのアドレスはaのアドレスと一緒になることから、i独自のアドレスがないためオブジェクトとは言えないからであると考えられます。
また、C++の規格書内で参照はオブジェクトではないと明言されています。
(https://isocpp.org/files/papers/N4860.pdf の23ページの4.5 The C++ object modelのあたり)
9.参考
- 【ももの知恵の樹】(https://momo-chienoki.com/Cpp/Cpp_Reference/)
(C++ の参照型) - 【侍エンジニア】(https://www.sejuku.net/blog/25094)
(ポインタの説明) - 【Zenn】(https://zenn.dev/somahc/articles/a03c1f3f012979)
(ポインタと参照渡しの違いについて) - 【cppreference.com】(https://en.cppreference.com/w/cpp/language/object )(C++におけるオブジェクトの説明)
- 【cppreference.com】(https://en.cppreference.com/w/c/language/object )
(Cにおけるオブジェクトの説明) - 【C++17規格書】(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf)
(C++におけるオブジェクトの説明) - 【C++20規格書】(https://isocpp.org/files/papers/N4860.pdf)
【2025/05/18 訂正】ポインタの定義に関する表現を修正しました。ご指摘ありがとうございます。
【2025/05/19 訂正】オブジェクトやlvalue,rvalueに関する部分を追加しました。ご指摘ありがとうございます。