文章目錄
- 1.列表初始化
- 1.1 C++98傳統的{}
- 1.2 C++11中的{}
- 1.3 C++11中的std::initializer_list
- 2.右值引用和移動語義
- 2.1左值和右值
- 2.2左值引用和右值引用
- 2.3 引用延長生命周期
- 2.4左值和右值的參數匹配問題
- 2.5右值引用和移動語義的使用場景
- 2.5.1左值引用主要使用場景
- 2.5.2移動構造和移動賦值
- 2.5.3右值引用和移動語義解決傳值返回問題
- 2.5.1右值對象構造,只有拷貝構造,沒有移動構造的場景->拷貝構造
- 2.5.2右值對象構造,有拷貝構造,也有移動構造的場景->移動構造
- 2.5.3右值對象賦值,只有拷貝構造和拷貝賦值,沒有移動構造和移動賦值的場景->拷貝構造和拷貝賦值
- 2.5.4右值對象賦值,既有拷貝構造和拷貝賦值,也有移動構造和移動賦值的場景->移動構造和移動賦值
- 3.面試經常問到的
1.列表初始化
1.1 C++98傳統的{}
C++98的{}主要支持數組和結構體的初始化
struct Hello
{int _a;int _b;
};int main()
{int a[] = { 1,2,3,4,5 };int b[5] = { 0 };Hello c = { 1,2 };return 0;
}
1.2 C++11中的{}
- C++11規定了一切對象都可以用{}初始化,{}初始化也叫列表初始化
- 內置類型可以用{}初始化
- C++98支持單參數的類型轉換,也可以不用{}
Date d3 = { 2025 };// C++11
Date d4 = 2025;// C++98
string s = "11111";
// 單參數的支持隱式類型轉換
// 不支持,只有{}初始化才能省略=
// Date d7 2025;vector<Date> v;
v.push_back(d5);// 有名對象
v.push_back(Date( 2025,1,2 ));// 匿名對象
v.push_back({ 2025,1,1 });// {}map<string, string> dict;
dict.insert({ "string","字符串" });
class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}
private:int _year;int _month;int _day;
};int main()
{// 1. 內置類型int x1 = { 2 };// 2.自定義類型,構造,隱式類型轉換,構造 + 拷貝構造// 優化為了直接構造Date d1 = { 2025,1,12 };// 直接調用構造Date d10(2025,1,1);// d2引用的是{2025,1,15}構造的臨時對象const Date& d2 = {2025,1,15};// 可以省略掉=Hello p{ 1,2 };int a{ 2 };Date d5{ 2025,1,1 };const Date& d6{ 2025,1,2 };return 0;
}
1.3 C++11中的std::initializer_list
- ?個vector對象,我想?N個值去構造初始化,那么我們得實現很多個構造函數才能?持,因為參數的個數不同
vector v1 ={1,2,3};
vector v2 = {1,2,3,4,5};- C++11庫中提出了?個std::initializer_list的類底層是一個數組,將數據拷貝過來,std::initializer_list中有兩個指針,分別指向數組的開始和結束,
- std::initializer_list支持迭代器遍歷
- 有了std::initializer_list就可以進行多個值的初始化,比如{x1,x2,x3…}
vector<int> v1 = {1,2,3,4};
vector<int> v2 = {10,20,30,1,2,3,4};
const vector<int>& v3 = {10,20,30,1,2,3,4};// {}初始化可以省略=
vector<int> v1{1,2,3,4};
vector<int> v2{10,20,30,1,2,3,4};
const vector<int>& v3{10,20,30,1,2,3,4};// initializer_list 構造
vector<int> v4({1,2,3,4,5,6});// a數組的指針和il1的兩個指針都在棧上,數組也在棧上
// a數組和這兩個指針的位置非常接近
initializer_list<int> il1 = {1,2,3};
int a[] = {1,2,3};
- map的initializer_list的初始化
外層是initializer_list,內層是pair的鍵值對的初始化
// initializer_list + {}pair初始化的隱式類型轉換
map<string, string> dict = { {"sort","kai"},{"string","men"} };
2.右值引用和移動語義
1. C++98中的引用是左值引用,C++11中有了右值引用
type & x 左值引用
type && y 右值引用
2.1左值和右值
1.左值是一個表示數據的表達式(如變量名或解引用的指針),一般是有持久狀態,存儲在內存中,我們可以獲取它的地址,左值可以出現賦值符號的左邊,也可以出現在賦值符號右邊。定義時const修飾符后的左值,不能給他賦值,但是可以取它的地址。
2.右值也是?個表示數據的表達式,要么是字面值常量、要么是表達式求值過程中創建的臨時對象等,右值可以出現在賦值符號的右邊,但是不能出現出現在賦值符號的左邊,右值不能取地址。
3.區分左值和右值的重要特點就是是否可以取地址
int main()
{// 1.左值 p,b,c,*p,s,s[0]// 變量int* p = new int(0);int b = 1;const int c = b;// 解引用*p = 10;// 有名對象string s("111111");// 函數的返回類型是引用s[0] = 'x';// 2.右值 10 x + y fmin(x,y) string("111")// 右值通常在寄存器中或者#define直接替換了// 所以不能夠直接取地址int x = 0, y = 0;// 字面量常量// 10// 表達式的返回值x + y;// 函數的返回值是存在寄存器中的或是臨時對象的拷貝fmin(x, y);// 匿名對象string("111");return 0;
}
2.2左值引用和右值引用
1. Type& r1 = x; Type&& rr1 = y;
第一個語句就是左值引用,左值引用就是給左值取別名;第二個就是右值引用,右值引用就是給右值取別名
左值引用取別名
int*& r1 = p;
int& r2 = b;
int& r6 = *p;
const int& r3 = c;
string& r4 = s;
char& r5 = s[0];右值引用取別名
int&& p1 = 10;
int&& p2 = x + y;
int&& p3 = fmin(x, y);
string&& p4 = string("111");
2. 左值引用不能直接引用右值,但是const左值引用可以引用右值,因為右值通常具有常性
對上面的解釋
1.右值到左值要權限放大(但是權限不能放大)
2.權限可以平移// 左值引?不能直接引?右值,但是const左值引?可以引?右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
3.右值引用不能直接引用左值,但是右值引用可以引用move(左值)
// 右值引?不能直接引?左值,但是右值引?可以引?move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
// move的底層其實是強制類型轉換
// 把左值強轉為右值(s有名對象為左值)
string&& rrx5 = (string&&)s;
move實際是一個函數模版,其實也涉及到了引用折疊,后面會細講
// (左值)強轉之后不會改變本身的屬性
// 強轉之后再使用左值,還是左值本身的屬性
// 用的只是臨時對象
// b、r1、rr1都是變量表達式,都是左值
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;int i = 1;
int* pi = (int*)i;
i還是int類型不會改變類型
4. 需要注意的是變量表達式都是左值屬性,也就意味著一個右值被右值引用綁定后,右值引用變量變量表達式的屬性是左值
左值引用的屬性是左值
右值引用的屬性也是左值
int&& rr1 = 10;
引用完rr1的屬性變為左值
rr1的屬性是左值,所以不能被右值引用綁定,除非move一下
int& rr6 = rr1;// 不報錯
int&& rrx6 = rr1;// 報錯
int&& rrx6 = move(rr1);
5. 在語法層面,左值引用和右值引用都是取別名,不開空間;在底層都是指針實現的
2.3 引用延長生命周期
1. 可以延長臨時對象和匿名對象的生命周期,臨時對象和匿名對象的生命周期只在當前的一行
std::string s1 = "Test";
const左值引用延長生命周期
const std::string& r1 = s1 + s1;
右值引用延長生命周期
std::string&& r2 = s1 + s1;
延長到r1和r2使用完
2.4左值和右值的參數匹配問題
1.C++98中,我們實現?個const左值引用作為參數的函數,那么實參傳遞左值和右值都可以匹配。
template<class T>
void func(const T& x)
{}
傳左值,權限縮小,左值傳左值
傳右值,權限平移,右值傳const左值
2. C++11以后,分別重載左值引用、const左值引用、右值引用作為形參的f函數,那么實參是左值會
匹配f(左值引用),實參是const左值會匹配f(const 左值引用),實參是右值會匹配f(右值引用),也就是編譯器會調用最匹配的類型,如果沒有右值引用,會調用const左值引用
void f(int& x)
{std::cout << "左值引用重載 f(" << x << ")\n";
}void f(const int& x)
{std::cout << "到 const 的左值引用重載 f(" << x << ")\n";
}void f(int&& x)
{std::cout << "右值引用重載 f(" << x << ")\n";
}int main()
{int i = 1;const int ci = 2;f(i); // 調用 f(int&)f(ci); // 調用 f(const int&)f(3); // 調用 f(int&&),如果沒有 f(int&&) 重載則會調用 f(const int&)f(std::move(i)); // 調用 f(int&&)return 0;
}
3. 右值引用變量在用于表達式時屬性是左值
右值引用本身的屬性是左值int&& x = 1;f(x);調用f(int& x)f(std::move(x));調用f(int&& x)
2.5右值引用和移動語義的使用場景
2.5.1左值引用主要使用場景
1.左值引用的主要場景是在函數中,左值引用傳參和左值引用傳返回值減少拷貝,同時左值引用傳參在函數中修改形參可以改變實參,傳引用返回可以修改返回對象
2.左值引用已經解決大多數場景的拷貝效率問題,但是有些場景不能使用傳左值引用返回,如addStrings和generate函數里面返回的是局部對象,C++98中的解決方案只能是被迫使用輸出型參數解決(傳值返回)。
3.那么C++11以后這里可以使用右值引用做返回值解決嗎?顯然是不可能的,因為這里的本質是返回對象是一個局部對象,函數結束這個對象就析構銷毀了,右值引用返回也無法概念對象已經析構銷毀的事實。
class Solution
{
public:// 傳值返回需要拷?string addStrings(string num1, string num2) {string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;// 進位int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());return str;}
};class Solution
{
public:
// 這?的傳值返回拷?代價就太?了
vector<vector<int>> generate(int numRows)
{vector<vector<int>> vv(numRows);for(int i = 0; i < numRows; ++i){vv[i].resize(i+1, 1);}for(int i = 2; i < numRows; ++i){for(int j = 1; j < i; ++j){vv[i][j] = vv[i-1][j] + vv[i-1][j-1];}}return vv;
}
};
2.5.2移動構造和移動賦值
右值引用如何解決返回的對象是局部變量的問題?
1. 移動構造函數是一種構造函數,類似拷貝構造,要求第一個參數必須是類類型的引用,第一個參數必須是右值引用,拷貝構造是左值引用,如果有其他參數,必須有缺省值
2. 移動賦值是一個賦值運算符的重載,他跟拷貝賦值構成函數重載,類似拷貝賦值函數,移動賦值函數要求第一個參數是該類類型的引用,但是不同的是要求這個參數是右值引用
3. 對于像string/vector這樣的深拷貝的類或者包含深拷貝的成員變量的類,移動構造和移動賦值才有意義,因為移動構造和移動賦值的第一個參數都是右值引用的類型,它的本質是要“竊取”引用的右值對象的資源,而不是像拷貝構造和拷貝賦值那樣去拷貝資源,從提高效率
void swap(string& ss)
{::swap(_str, ss._str);::swap(_size, ss._size);::swap(_capacity, ss._capacity);
}// 移動構造
string(string&& s)
{// 右值引用本身具有左值的屬性// 因為要和this交換,左值可以修改值,右值不可以修改值cout << "string(string&& s) -> 移動構造" << endl;swap(s);// 交換需要的資源,轉移掠奪你的資源
}
int main()
{string s1("111");string s2 = s1;string s3 = string("222");// s1本來的地址是xxxa510,現在s5的地址是a510// 說明移動構造掠奪了s1的資源給了s5string s5 = move(s1);return 0;
}
2.5.3右值引用和移動語義解決傳值返回問題
2.5.1右值對象構造,只有拷貝構造,沒有移動構造的場景->拷貝構造
1. vs2019debug下,左邊為不優化的場景,傳值返回會發生拷貝構造,會產生臨時對象,臨時對象再拷貝構造ret,右邊的場景是直接優化為一次拷貝構造
2.vs2019release和vs2022下,會直接將str對象的構造,str拷貝構造臨時對象,臨時對象拷貝構造ret對象,合三為一,變為直接構造。本質就是str對象本質是ret對象的引用,底層是指針,str對象和ret對象的地址是一樣的,所以就沒有拷貝,沒有構造,只有引用了
3. Linux下也是和2022一樣的,編譯時用 g++ test.cpp -fno-elide-constructors 的方式關閉構造優化,就是兩次拷貝構造
2.5.2右值對象構造,有拷貝構造,也有移動構造的場景->移動構造
1. 傳值返回會被編譯器識別為右值,圖2展示了vs2019 debug環境下編譯器對拷貝的優化,左邊為不優化的情況下,兩次移動構造,右邊為編譯器優化的場景下連續步驟中的拷貝合二為一變為一次移動構造。
2. 需要注意的是在vs2019的release和vs2022的debug和release,會直接將str對象的構造,str移動構造臨時對象,臨時對象移動構造ret對象,合三為一,變為直接構造。
2.5.3右值對象賦值,只有拷貝構造和拷貝賦值,沒有移動構造和移動賦值的場景->拷貝構造和拷貝賦值
1. 左圖不優化,一次拷貝構造,一次拷貝賦值,右圖優化為直接拷貝賦值,str是臨時對象的別名,直接用臨時對象拷貝賦值ret
2.5.4右值對象賦值,既有拷貝構造和拷貝賦值,也有移動構造和移動賦值的場景->移動構造和移動賦值
1. 需要注意的是在vs2019的release和vs2022的debug和release,下面代碼會進一步優化,直接構造要返回的臨時對象,str本質是臨時對象的引用,底層角度用指針實現。運行結果的角度,我們可以看到str的析構是在賦值以后,說明str就是臨時對象的別名。(臨時對象的生命周期只存在于當前行,賦值完之后就會銷毀)
2. 如果是傳值返回并且是右值對象,會移動你的資源,不用拷貝了
3.面試經常問到的
1. 左值引用和右值引用的最終目的是減少拷貝提高效率
2.左值引用還可以修改參數和返回值,比如輸出型參數(在函數體內修改參數可以影響實參)和operator
3. 左值引用的不足:
部分函數返回場景,只能傳值返回,不能左值引用返回,比如當前函數的局部對象,出了當前函數的作用域,生命周期就到了,就銷毀了,不能左值引用返回,只能傳值返回
解決方案1,2,3:
1.不用返回值,用輸出型參數解決問題,不足:犧牲了可讀性
2.編譯器的優化
3.右值引用和移動語義
更早的編譯器只能使用這種輸出型參數,避免大量拷貝數據
class Solution
{
public:void generate(int numRows, vector<vector<int>>& vv) {vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}}
};int main()
{// vector<vector<int>> ret = Solution().generate(100);vector<vector<int>> ret;Solution().generate(100, ret);// 輸出型參數可以解決這個問題,傳引用過去,就不需要拷貝了return 0;
}