最終更新:2026-10-12
C++ ポインタと参照¶
著者について:¶
個人公式アカウント(WeChat):

1.参照とポインタ¶
まとめ:
| 参照 | ポインタ |
|---|---|
| 初期化が必須 | 初期化しなくてもよい |
| nullになれない | nullになり得る |
| 指す先を変更できない | 指す先を変更できる |
参照は初期化が必須ですが、ポインタは初期化しなくてもかまいません。
参照を定義するときは初期値を指定しなければなりませんが、ポインタには必要ありません。
参照はnullになれませんが、ポインタはnullになり得ます。
参照はnullになれないため、参照を使うときはその有効性をテストする必要がありません。一方、ポインタを使うときは、まずポインタがnullポインタでないか判断する必要があります。さもないと、プログラムがクラッシュする恐れがあります。
void test_p(int* p)
{
if(p != null_ptr) //对p所指对象赋值时需先判断p是否为空指针
*p = 3;
return;
}
void test_r(int& r)
{
r = 3; //由于引用不能为空,所以此处无需判断r的有效性就可以对r直接赋值
return;
}
参照は指す先を変更できない
ポインタはいつでも指す先を変更できますが、参照は初期化時に指したオブジェクトしか指せず、変更することはできません。
int a = 1;
int b = 2;
int &r = a; //初始化引用r指向变量a
int *p = &a; //初始化指针p指向变量a
p = &b; //指针p指向了变量b
r = b; //引用r依然指向a,但a的值变成了b
2.参照¶
左辺値参照¶
通常の参照で、一般的にオブジェクトの同一性を表します。
右辺値参照¶
右辺値参照とは、右辺値(一時オブジェクト、まもなく破棄されるオブジェクト)に必ず束縛される参照のことで、一般的にオブジェクトの値を表します。
右辺値参照によってムーブセマンティクス(Move Sementics)と完全転送(Perfect Forwarding)を実現できます。その主な目的には2つの側面があります:
- 2つのオブジェクトが相互作用するときの不要なオブジェクトのコピーをなくし、演算と記憶資源を節約し、効率を高める。
- ジェネリック関数をより簡潔かつ明確に定義できる。
参照の畳み込み¶
X& &、X& &&、X&& &はX&に畳み込みできるX&& &&はX&&に畳み込みできる
C++の参照は、プログラマの自由度を減らすと同時に、メモリ操作の安全性とセマンティクスの美しさを高めています。例えば、参照には必ず初期化が要求されるため、参照を使うときに参照がnullかどうかを判断する必要がなく、コードはより簡潔で美しくなり、ポインタが飛び交う状況を避けられます。このような場面以外に、参照は次の2つの場面でも使われます:
参照型の引数
一般的に、読み取り専用の仮引数としてconst reference引数を使います。この場合、引数のコピーを避けられるだけでなく、値渡しの引数と同じ呼び出し方を得られます。
void test(const vector<int> &data)
{
//...
}
int main()
{
vector<int> data{1,2,3,4,5,6,7,8};
test(data);
}
参照型の戻り値
C++は演算子のオーバーロード機能を提供しています。一部の演算子をオーバーロードするとき、参照型の戻り値を使うと、その演算子本来の構文と同じ呼び出し方が得られ、演算子セマンティクスの一貫性が保たれます。1つの例がoperator []演算子です。この演算子は、正しく変更されるためには、一般に参照オブジェクトを返す必要があります。
vector<int> v(10);
v[5] = 10; //[]操作符返回引用,然后vector对应元素才能被修改
//如果[]操作符不返回引用而是指针的话,赋值语句则需要这样写
*v[5] = 10; //这种书写方式,完全不符合我们对[]调用的认知,容易产生误解
3.ポインタと参照の性能差¶
ポインタと参照の間に性能差はあるのでしょうか?このような問題は、アセンブリのレベルに入って見る必要があります。まず、引数の受け渡しにポインタを使うtest1関数を書きます:
このコード断片に対応するアセンブリコードは次の通りです:
(gdb) disassemble
Dump of assembler code for function test1(int*):
0x0000000000400886 <+0>: push %rbp
0x0000000000400887 <+1>: mov %rsp,%rbp
0x000000000040088a <+4>: mov %rdi,-0x8(%rbp)
=> 0x000000000040088e <+8>: mov -0x8(%rbp),%rax
0x0000000000400892 <+12>: movl $0x3,(%rax)
0x0000000000400898 <+18>: nop
0x0000000000400899 <+19>: pop %rbp
0x000000000040089a <+20>: retq
End of assembler dump.
上述のコードの1、2行目は引数呼び出し時の現場保存操作です。3行目は引数の受け渡しで、関数呼び出しの第1引数は一般にrdiレジスタに置かれ、この行のコードはrdiレジスタの値(ポインタpの値)をスタックに書き込みます。4行目はスタック内のpの値をraxレジスタに書き込む操作です。5行目は即値3を**raxレジスタの値が指すメモリ**に書き込む操作です。ここで(%rax)の両側の括弧に注意してください。この括弧は決してあってもなくてもよいものではなく、(%rax)と%raxは全く異なる2つの意味を持ちます。(%rax)はraxレジスタ内の値が表すアドレス部分のメモリを表し、C++コードにおける*pに相当します。一方%raxはraxレジスタを表し、C++コードにおけるpの値に相当します。だからアセンブリでは%raxではなく(%rax)が使われています。
次に、引数の受け渡しに参照を使うC++のコード断片test2を書きます:
このコードに対応するアセンブリコードは次の通りです:
(gdb) disassemble
Dump of assembler code for function test2(int&):
0x000000000040089b <+0>: push %rbp
0x000000000040089c <+1>: mov %rsp,%rbp
0x000000000040089f <+4>: mov %rdi,-0x8(%rbp)
=> 0x00000000004008a3 <+8>: mov -0x8(%rbp),%rax
0x00000000004008a7 <+12>: movl $0x3,(%rax)
0x00000000004008ad <+18>: nop
0x00000000004008ae <+19>: pop %rbp
0x00000000004008af <+20>: retq
End of assembler dump.
test2に対応するアセンブリコードとtest1に対応するアセンブリコードは完全に同じであることがわかりました。これは、C++コンパイラがプログラムをコンパイルするとき、ポインタと参照を完全に同じ機械語にコンパイルすることを示しています。したがって、C++の参照は、ポインタ操作に対するC++の「構文糖」にすぎず、低レベルの実装では、C++コンパイラはこの2つの操作をまったく同じ方法で実装しています。
3.まとめ¶
C++には参照操作が導入されました。参照の使用により多くの制限条件が課されますが、参照使用の安全性と利便性が保証され、コードの優雅さも保たれます。適切な状況で適切な操作を使うことで、参照の使用はある程度「ポインタが飛び交う」状況を避けられ、プログラムの安定性を高めることにも一定の積極的な意味があります。最後に、ポインタと参照の低レベル実装は同じであるため、両者の性能差を心配する必要はありません。
上述の一部は以下を参考にしました:http://irootlee.com/juicer_pointer_reference/#
関連トピック¶
- C++ 純粋仮想関数と抽象クラス | C++那些事
- C++ assert アサーション | C++那些事
- C++ ビットフィールド | C++那些事
- C 言語による C++ 多態の実現 | C++那些事
- C++ const 定数と修飾 | C++那些事
- C++ const ファミリー | C++那些事
- C++ 間接参照演算子 * | C++那些事
- C++ 初期化と代入 | C++那些事
- C++ 複数戻り値の書き方 | C++那些事
- C++ mutable キーワード | C++那些事