一,列表初始化
1.1C++98中傳統的{}
C++98中一般數組和結構體可以使用{}進行初始化:
struct Date
{int _year;int _month;int _day;
};int main()
{int a[] = { 1,2,3,4,5 };Date _date = { 2025,2,27 };return 0;
}
?1.2C++11中的{}
- C++11以后想統一初始化方式,試圖實現一切對象皆可用{}初始化,{}初始化也叫做列表初始化。?
- 內置類型支持,自定義類型也支持,自定義類型本質是類型轉換,中間會產生臨時對象,最后優化了以后變成直接構造。?
- {}初始化的過程中,可以省略掉=。?
- C++11列表初始化的本意是想實現一個大統一的初始化方式,其次他在有些場景下帶來的不少便利,如容器push/inset多參數構造的對象時,{}初始化會方便許多。
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()
{int a[] = { 1,2,3,4,5 };Date _date = { 2025,2,27 };const Date& d2 = { 2027,2,29 };//右邊{2027,2,29}是一個臨時對象,需要加上const//也可以不加括號Date d3{ 2028,2,27 };//非{}初始化必須要加上等號Date d4 2025;//編譯器報錯Date d5 = 2025;return 0;
}
1.3C++11中的std::initializer_list?
????????上面的{}初始化已然很方便,但是如果說我們要初始化像vector這樣的對像時,他的參數個數是會發生變化的,顯然僅僅只有上面的{}遠遠無法滿足這種需求。所以c++中就提供了std::initializer_list的類, auto il = { 10, 20, 30 }; // thetype of il is an initializer_list ,這個類的本質是底層開一個數組,將數據拷貝過來,std::initializer_list內部有兩個指針分別指向數組的開始和結束。
這是他的文檔:initializer_list - C++ Reference,std::initializer_list支持迭代器遍歷。?
區別是{}還是initializer_list的方法也很簡單,{}傳入的值個數是固定的,由需要初始化的對象類型決定,里面所有數據的類型可能不同。后者可寫入的值不固定,但類型一定相同。
map<int, string> m{ {1,string("hallo")} };
vector<string> v{ "a","b","c" };
二,右值引用與移動語義?
我們之前學習的是C++98中的引用,比如原本有一個int類型的對象x,那么int& b = x,此時b其實就是x的別名。C++11之后,我們之前學過的這種引用方式被叫做左值引用。需要注意的是,無論是左值引用還是右值引用,本質上都是取別名。?
2.1左值與右值?
左值是一個表示數據的表達式(如變量名或解引用的指針),一般是有持久狀態,存儲在內存中,我
們可以獲取它的地址,左值可以出現賦值符號的左邊,也可以出現在賦值符號右邊。定義時const
修飾符后的左值,不能給他賦值,但是可以取它的地址。
右值也是一個表示數據的表達式,要么是字面值常量、要么是表達式求值過程中創建的臨時對象
等,右值可以出現在賦值符號的右邊,但是不能出現出現在賦值符號的左邊,右值不能取地址。?
值得一提的是,左值的英文簡寫為lvalue,右值的英文簡寫為rvalue。傳統認為它們分別是left
value、right value 的縮寫。現代C++中,lvalue 被解釋為loactor value的縮寫,可意為存儲在內
存中、有明確存儲地址可以取地址的對象,而 rvalue 被解釋為 read value,指的是那些可以提供
數據值,但是不可以尋址,例如:臨時變量,字面量常量,存儲于寄存器中的變量等,也就是說左
值和右值的核心區別就是能否取地址。
//常見的左值比如:
int p;
const int p1;
int a[10];
char c;
a[0] = 1;
string s("111111111");
s[0] = 'b';//....
//它們都有一個共同的特性:可以取地址//常見的右值比如:
10;
s[0] + a[0];
string("111111111111111");
min(s[0],a[0]);
//它們均是臨時對象,無法取地址
2.2左值引用與右值引用?
Type& x = y;//左值引用
Type&& x = y;//右值引用
同樣,右值引用其實就是給右值取別名。
1.對于左值引用,不可以直接引用右值,引用右值時需要加上const。
int& x = 10;//error
const int& x = 10;//true
2.對于右值引用,當然也不可以直接引用左值,但是可以引用move移動之后的左值。
int x = 10;
int&& y = x;//error
int&& y = std::move(x);//true
//move可以將左值強制類型轉化為右值,是庫里面的一個函數模板,本質內部是進行強制類型轉換,當然他還涉
//及一些引用折疊的知識,我們后面會詳細介紹。
3.需要注意的是變量表達式都是左值屬性,也就意味著一個右值被右值引用綁定后,右值引用變量變量表達式的屬性是左值。
int&& y = 10;//y此時是一個左值
4.語法層面看,左值引用和右值引用都是取別名,不開空間。從匯編底層的角度看無論左值引用還是右值引用,底層都是用指針實現的,沒有本質區別。
2.3引用改變生命周期?
右值引用和帶有const的左值引用都可以延長臨時變量的聲明周期,但后者無法被修改。
std::string s1 = "1111111111";
std::string&& s2 = s1 + s1;
const std::string& s3 = s1 + s1;s2 += "Text";//true
s3 += "Text";//error
?2.4左右值引用的參數匹配
在C++98中,實現一個const左值引用作為參數的函數,傳入左值還是右值都可以匹配?
void f(int& x)
{std::cout << "f函數的左值引用重載(" << x << ")" << std::endl;
}void f(const int& x)
{std::cout << "f函數的const左值引用重載(" << x << ")" << std::endl;
}int main()
{int x = 10;f(x);f(10 + 20);return 0;
}
C++11中,對右值進行了明確定義,此時便可以分擔const左值引用對于右值的引用任務給右值引用。即實參是左值會匹配f(左值引用),實參是const左值會匹配f(const 左值引用),實參是右值會匹配f(右值引用)。?(當然沒有右值引用的重載下還可以通過const左值引用來引用右值)。
void f(int& x)
{std::cout << "f函數的左值引用重載(" << x << ")" << std::endl;
}void f(const int& x)
{std::cout << "f函數的const左值引用重載(" << x << ")" << std::endl;
}void f(int&& x)
{std::cout << "f函數的右值引用重載(" << x << ")" << std::endl;
}int main()
{int x = 10;const int y = 20;f(x);f(y);f(10 + 20);return 0;
}
2.5右值引用和移動語義的使用場景?
2.5.1移動構造與移動賦值?
- 1.移動構造函數是一種構造函數,類似拷貝構造函數,移動構造函數要求第一個參數是該類類型的引用,但是不同的是要求這個參數是右值引用,如果還有其他參數,額外的參數必須有缺省值。
- 2.移動賦值是一個賦值運算符的重載,他跟拷貝賦值構成函數重載,類似拷貝賦值函數,移動賦值函數要求第一個參數是該類類型的引用,但是不同的是要求這個參數是右值引用。
- 3.對于像string/vector這樣的深拷貝的類或者包含深拷貝的成員變量的類,移動構造和移動賦值有意義,因為移動構造和移動賦值的第一個參數都是右值引用的類型,他的本質是要“竊取”引用的右值對象的資源,而不是像拷貝構造和拷貝賦值那樣去拷貝資源,從提高效率。下面ELY::string例實現了移動構造和移動賦值,我們需要結合場景理解。
之前文章中我們自己實現的string類:
namespace ELY
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str)-構造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}string(const string& s):_str(nullptr){cout << "string(const string& s) -- 拷貝構造" << endl; reserve(s._capacity);for (auto ch : s){push_back(ch);}}//移動構造string(string&& s){cout << "string(string&& s) -- 移動構造" << endl;swap(s);//這樣寫的原因是因為移動構造底層實現原理與直接和臨時對象交換資源類似}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷貝賦值" <<endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}~string(){cout << "~string() -- 析構" << endl;delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];if (_str){strcpy(tmp, _str);delete[] _str;}_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}size_t size() const{return _size;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};
}
2.5.2右值引用和移動語義解決傳值返回問題
我們知道,在函數傳參的時候使用左值引用能夠減少拷貝,比如下面通過字符串實現高精度計算的函數:
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;
}int main()
{std::string s1 = "11111";std::string s2 = "22222";std::string s3 = AddStrings(s1, s2);std::cout << s3 << std::endl;return 0;
}
但是此時返回str時,如果str過大,那么s3在接收的時候會進行兩次拷貝構造(在string沒有實現移動構造以及編譯器沒有進行編譯優化的前提下):
這樣子代價特別巨大,但是有了上面的右值引用之后。因為右值是一個臨時對象,我們完全可以走移動構造來完成上面的拷貝構造過程。直接把臨時對象中的資源給搶過來。但是我們不能說將上面AddString的返回值改為string&&,因為str中存儲的資源生命周期在函數的作用域內。無法達到預期效果,我們只有對string類實現了移動構造才能實現搶過來的那一步:
當我們把移動構造屏蔽后,s3接收str會經歷以下過程,走了兩次拷貝構造,等于說進行兩次新的資源構建:
但是我們實現移動構造之后,全程只有移動構造,沒有任何新資源的構建,極大節省了資源消耗:
2.5.3右值引用與移動語義在傳參中的提效
- 查看STL文檔我們發現C++11以后容器的push和insert系列的接口否增加的右值引用版本。
- 當實參是一個左值時,容器內部繼續調用拷貝構造進行拷貝,將對象拷貝到容器空間中的對象。
- 當實參是一個右值,容器內部則調用移動構造,將實參的資源直接掠奪過來構造當前對象
測試代碼:
int main()
{std::list<bit::string> lt;bit::string s1("111111111111111111111");lt.push_back(s1);cout << "*************************" << endl;lt.push_back(bit::string("22222222222222222222222222222"));cout << "*************************" << endl;lt.push_back("3333333333333333333333333333");cout << "*************************" << endl;lt.push_back(move(s1));cout << "*************************" << endl;return 0;
}
運行結果:
2.6類型分類(了解)
- C++11以后,進一步對類型進行了劃分,右值被劃分純右值(pure value,簡稱prvalue)和將亡值(expiring value,簡稱xvalue)。
- 純右值是指那些字面值常量或求值結果相當于字面值或是一個不具名的臨時對象。如: 42、true、nullptr 或者類似str.substr(1, 2)、str1 + str2 傳值返回函數調用,或者整形a、b、a++,a+b 等。純右值和將亡值C++11中提出的,C++11中的純右值概念劃分等價于C++98中的右值。
- 將亡值是指返回右值引用的函數的調用表達式和轉換為右值引用的轉換函數的調用表達,如move(x)、static_cast<X&&>(x)。
- 泛左值(generalized value,簡稱glvalue),泛左值包含將亡值和左值。
- 有名字,就是glvalue;有名字,且不能被move,就是lvalue;有名字,且可以被move,就是xvalu;沒有名字,且可以被移動,則是prvalue。?
官方文檔:
值類別 - cppreference.com?,????https://en.cppreference.com/w/cpp/language/value_category
2.7引用折疊
1.C++中不能直接定義引用的引用如int& && r = i; 這樣寫會直接報錯,通過模板或 typedef
中的類型操作可以構成引用的引用。
using lref = int&;
using rref = int&&;int main()
{int y = 10;int& &&r = y;//errorlref&& z = y;//truereturn 0;
}
2.對于兩種引用的四種組合,只有&& 與 && 組合時才是右值引用:
lref& x = y;//x類型是int&
lref&& x = y;//x類型是int&
rref& x = y;//x類型是int&
rref&& x = 10;//x類型是int&&
template<class T>
void f1(T& x)
{}template<class T>
void f2(T&& x)
{}
int main()
{int n = 0;//無折疊->類型實例化為int&f1<int>(n);f1<int>(0);//報錯//折疊->類型實例化為int&f1<int&>(n);f1<int&>(0);//報錯//折疊->類型實例化為int&f1<int&&>(n);f1<int&&>(0);//報錯//折疊->示例化為const int&f1<const int&>(n);f1<const int&>(0);//折疊->實例化為const int&f1<const int&&>(n);f1<const int&&>(0);//無折疊->類型實例化為int&&f2<int>(n);//報錯f2<int>(0);//折疊->類型實例化為int&f2<int&>(n);f2<int&>(0);//報錯//折疊->類型實例化為int&&f2<int&&>(n);//報錯f2<int&&>(0);//折疊->示例化為const int&f2<const int&>(n);f2<const int&>(0);//折疊->實例化為const int&&f2<const int&&>(n);//報錯f2<const int&&>(0);return 0;
}
template<class T>
void Function(T&& t)
{int a = 0;T x = a;x++;cout << &a << endl;cout << &x << endl << endl;
}int main()
{//無折疊-模板類型實例化為int&&Function(10);//右值int a = 0;//折疊,因為a為左值,模板類型實例化為int&Function(a);//左值// std::move(a)是右值,推導出T為int,模板實例化為void Function(int&& t)Function(std::move(a)); // 右值const int b = 8;// b是左值,推導出T為const int&,引用折疊,模板實例化為void Function(const int&// 所以Function內部會編譯報錯,x不能++Function(b); // const 左值// std::move(b)右值,推導出T為const int,模板實例化為void Function(const int&&t)// 所以Function內部會編譯報錯,x不能++Function(std::move(b)); // const 右值return 0;
}
我們也稱Function(T&& t)這種為萬能引用,傳左值為左值,而傳右值則為右值。?
2.8完美轉發
我們在實際過程中,可能在函數中再去調用別的函數,比如如下這種情況:
void f1(int& x)
{}void f1(int&& x)
{}template<class T>
void fc(T&& t)
{f1(t);
}
int main()
{int n = 0;fc(n);fc(0);return 0;
}
我們上面也說過,對于int&& x = y;此時的x為左值屬性。我們發現對于上圖中的兩次fc調用,n是左值直到傳到f1時也可以保持其左值屬性不變。但0在傳入fc之后,0是一個右值,一個右值被右值引用綁定后,右值引用變量表達式的屬性是左值,也就是說fc函數中t的屬性是左值。此時便會調用f1的左值引用版本,換言之0在函數傳遞中失去了其本身的右值屬性。如果我們想要保持0自身的右值屬性在傳遞中不丟失,就需要使用完美轉發。
完美轉發:
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvaluereturn static_cast<_Ty&&>(_Arg);
}
完美轉發forward本質是一個函數模板,他主要還是通過引用折疊的方式實現。此時我們只需要將上面的代碼改為如下格式:
void f1(int& x)
{}void f1(int&& x)
{}template<class T>
void fc(T&& t)
{f1(forward<T>(t));
}
int main()
{int n = 0;fc(n);fc(0);return 0;
}
這樣0在經fc傳入f1時便會調用右值引用版本的重載。保留了其本身的右值屬性。?