C++中的&符號的運用:引用(從匯編層面分析)、取地址和右值引用
記一下筆記
C++中的引用
引用就是變量的別名
一個變量可以有多個別名
引用在聲明時一定要初始化
引用的用法:
#include<bits/stdc++.h>
using namespace std;
int main() {
int a = 10;
int &b = a;
int &c = a;
b++;
cout << b << " " << a << " " << c << endl;
}
引用的匯編層面
/*int a = 10;
int &b = a;*/
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movq %fs:40, %rax
movq %rax, -8(%rbp)
也就是將a的地址賦予b,引用就是一個從變量獲取到其地址后賦值給引用變量的過程
指針和引用的區別
1.指針有自己的一塊空間,而引用只是一個別名;
2.使用sizeof看一個指針的大小是4,而引用則是被引用對象的大小;
3.指針可以被初始化為NULL,而引用必須被初始化且必須是一個已有對象 的引用;
4.作為參數傳遞時,指針需要被解引用才可以對對象進行操作,而直接對引 用的修改都會改變引用所指向的對象;
5.可以有const指針,但是沒有const引用;
6.指針在使用中可以指向其它對象,但是引用只能是一個對象的引用,不能 被改變;
7.指針可以有多級指針(**p),而引用只有一級;
8.指針和引用使用++運算符的意義不一樣;
9.如果返回動態內存分配的對象或者內存,必須使用指針,引用可能引起內存泄露。
C++中的取地址
&(取地址運算符)==>用來獲取首地址,在給變量賦初值時出現在等號右邊或在執行語句中作為一元運算符出現時表示取對象的地址.
#include<bits/stdc++.h>
using namespace std;
int main() {
int a = 10;
int *b = &a;
cout << b << " " << *b << endl;
}
C++ 中的右值引用
右值引用是C++11中新增加的一個很重要的特性,他主是要用來解決C++98/03中遇到的兩個問題。
第一個問題就是臨時對象非必要的昂貴的拷貝操作。
第二個問題是在模板函數中如何按照參數的實際類型進行轉發。
通過引入右值引用,很好的解決了這兩個問題,改進了程序性能,后面將會詳細介紹右值引用是如何解決這兩個問題的。
首先,對於以下這行代碼
int i = getVar();
會產生兩種類型的值
一種是左值i
一種是getVar()函數返回的一個臨時變量,這個臨時變量在表達式結束后就銷毀了,這個臨時變量是一個右值
我們可以對這個變量i進行右值引用
int &&i = getVar();
對於這一行代碼,只會產生一種類型的值
即getVar()產生的臨時變量,而這個臨時變量會通過右值引用而延續他的聲明周期,一直到i的生命周期結束
測試代碼:
#include <iostream>
using namespace std;
int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;
struct A {
A() {
cout << "construct: " << ++g_constructCount << endl;
}
A(const A &a) {
cout << "copy construct: " << ++g_copyConstructCount << endl;
}
~A() {
cout << "destruct: " << ++g_destructCount << endl;
}
};
A GetA() {
return A();
}
int main() {
A i = GetA();
return 0;
}
由於編譯器會自動優化臨時變量,我們可以通過命令行編譯使用-fno-elide-constructors編譯選項的方式關閉編譯器的優化,這樣就可以看見臨時變量的生命周期了
D:\buerdepepeqi>cd sublime
D:\buerdepepeqi\sublime>g++ -o test A.cpp -fno-elide-constructors
D:\buerdepepeqi\sublime>test
construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3
測試2
int main() {
A &&i = GetA();
return 0;
}
這里處理了一個小bug就是
我本地的環境變量是之前用g++ 編譯沒有加上編譯參數 -std=c++11,使得無法編譯右值引用
報錯如下:
error: expected unqualified-id before '&&' token
A && a = GetA();
加上std=c++11后
D:\buerdepepeqi\sublime>g++ -o test A.cpp -fno-elide-constructors -std=c++11
D:\buerdepepeqi\sublime>test
construct: 1
copy construct: 1
destruct: 1
destruct: 2
測試結果如上,我們可以清楚的看到右值引用后的臨時變量的生命周期延長了,他是和右值一起消失的
這里還有有一個神奇的用法 ,通過常量左值引用也經常用來做性能優化 ,達到和右值引用相同的效果
int main() {
const A & a = GetA();
return 0;
}
因為常量左值引用是一個“萬能”的引用類型
可以接受左值、右值、常量左值和常量右值。需要注意的是普通的左值引用不能接受右值,比如這樣的寫法是不對的:
int main() {
A & a = GetA();
return 0;
}
因為非常量左值引用只能接受左值。
另外
右值引用獨立於左值和右值。意思是右值引用類型的變量可能是左值也可能是右值。比如下面的例子:
template<typename T>
void f(T&& t){}
f(10); //t是右值
int x = 10;
f(x); //t是左值
T&& t在發生自動類型推斷的時候,它是未定的引用類型(universal references),如果被一個左值初始化,它就是一個左值;如果它被一個右值初始化,它就是一個右值,它是左值還是右值取決於它的初始化。(這里與移動語義和完美轉發相關)
需要注意的是,僅僅是當發生自動類型推導(如函數模板的類型自動推導,或auto關鍵字)的時候,T&&才是universal references
轉發
某些函數需要將其中一個或多個實參連同類型不變地轉發給其他函數,
在這種情況下,我們需要保持被轉發實參的所有性質,包括實參是否是const的、以及是左值還是右值。
只是普通的轉發的情況下,eg:
#include<bits/stdc++.h>
using namespace std;
void f(int v1, int &v2) {
cout << v1 << " " << ++v2 << endl;
}
template<typename F, typename T1, typename T2>
void func(F f, T1 t1, T2 t2) {
f(t1, t2);
cout<<t1<<" "<<t2<<endl;
}
int main() {
int i = 1;
int j = 1;
f(42, i);
cout << i << endl;
func(f, 42, j);
cout << j << endl;
}
測試結果:
42 2
2
42 2
42 2
1
可以看到,擋調用函數func的時候,j並不會變化,這是因為j的值被拷貝到了t2中,t2改變了
當我們使用右值引用時
#include<bits/stdc++.h>
using namespace std;
void f(int v1, int &v2) {
cout << v1 << " " << ++v2 << endl;
}
template<typename F, typename T1, typename T2>
void func(F f, T1&& t1, T2&& t2) {
f(t1, t2);
cout<<t1<<" "<<t2<<endl;
}
int main() {
int i = 1;
int j = 1;
f(42, i);
cout << i << endl;
func(f, 42, j);
cout << j << endl;
}
我們將參數列表使用右值引用,
得到的結果是
42 2
2
42 2
42 2
2
j的值改變了,因為T1的值被推斷為int &,而t1就被折疊為int &了,所以實現了參數的轉發,保持了參數類型
當然也可以用forward來轉發參數
forward需要顯示提供實參類型,返回該實參類型的右值引用
#include<bits/stdc++.h>
using namespace std;
void f(int v1, int &v2) {
cout << v1 << " " << ++v2 << endl;
}
template<typename F, typename T1, typename T2>
void func(F f, T1 &&t1, T2 &&t2) {
f(forward<T1>(t1), forward<T2>(t2));
cout << t1 << " " << t2 << endl;
}
int main() {
int i = 1;
int j = 1;
f(i, j);
cout << i << " " << j << endl;
func(f,i, j);
cout << i << " " << j << endl;
}
類函數中的參數用右值引用避免指針懸掛等問題
一個帶有堆內存的類,必須提供一個深拷貝拷貝構造函數
因為默認的拷貝構造函數是淺拷貝,會發生“指針懸掛”的問題。如果不提供深拷貝的拷貝構造函數,上面的測試代碼將會發生錯誤(編譯選項-fno-elide-constructors),內部的m_ptr將會被刪除兩次,一次是臨時右值析構的時候刪除一次,第二次外面構造的a對象釋放時刪除一次,而這兩個對象的m_ptr是同一個指針,這就是所謂的指針懸掛問題。
而深拷貝會在有些時候會造成額外的性能損耗
例如:
#include <iostream>
using namespace std;
class A {
public:
A(): m_ptr(new int(0)) {
cout << "construct" << endl;
}
A(const A &a): m_ptr(new int(*a.m_ptr)) { //深拷貝的拷貝構造函數
cout << "copy construct" << endl;
}
~A() {
cout << "delete" << endl;
delete m_ptr;
}
private:
int *m_ptr;
};
A GetA() {
return A();
}
int main() {
A a = GetA();
return 0;
}
測試結果
D:\buerdepepeqi\sublime>g++ -o test A.cpp -fno-elide-constructors -std=c++11
D:\buerdepepeqi\sublime>test
construct
copy construct
delete
copy construct
delete
delete
Get函數會返回一個臨時變量,然后通過這個臨時對象拷貝構造了一個新的對象a
臨時變量在拷貝構造完成之后就銷毀了,如果堆內存很大的話,那么,這個拷貝構造的代價會很大,帶來了額外的性能損失。
每次都會產生臨時變量並造成額外的性能損失,有沒有辦法避免臨時變量造成的性能損失呢?答案是肯定的,就是將深拷貝拷貝構造函數中的參數變為右值引用
#include <iostream>
using namespace std;
class A {
public:
A(): m_ptr(new int(0)) {
cout << "construct" << endl;
}
A(const A &a): m_ptr(new int(*a.m_ptr)) { //深拷貝的拷貝構造函數
cout << "copy construct" << endl;
}
A(A &&a) : m_ptr(a.m_ptr) {
a.m_ptr = nullptr;
cout << "move construct" << endl;
}
~A() {
cout << "delete" << endl;
delete m_ptr;
}
private:
int *m_ptr;
};
A GetA() {
return A();
}
int main() {
A a = GetA();
return 0;
}
測試結果:
D:\buerdepepeqi\sublime>g++ -o test A.cpp -fno-elide-constructors -std=c++11
D:\buerdepepeqi\sublime>test
construct
move construct
delete
move construct
delete
delete
沒有調用拷貝構造函數
參考博客