文章目錄
- 列表初始化
- initializer_list
- 左值引用和右值引用
列表初始化
在 C++98 中可以使用{}對數組或者結構體元素進行統一的列表初始值設定
struct Point
{int _x;int _y;
};
int main()
{int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };return 0;
}
C++11擴大了用大括號括起的列表(初始化列表)的使用范圍,使其可用于所有的內置類型和用戶自定義的類型,使用列表初始化時,可添加等號(=),也可不添加
列表初始化可以防止窄化轉換。窄化轉換是指可能導致數據丟失或精度降低的轉換。例如,將一個浮點數轉換為整數時,如果浮點數的小數部分非零,就會丟失小數部分的數據。如果嘗試進行窄化轉換,編譯器會報錯
一切皆可用列表初始化,可以省略等號
class A
{
public:A(int a):_a(a){}A(int a,int b):_a(a),_b(b){}
private:int _a;int _b;
}
A a1{1};
A a2{11,45};
//本質上就是多參數的隱式類型轉換
//在C++98單參數隱式類型轉換的基礎上引入了多參數的隱式類型轉換
int arr[]{12,34,56,78,90};
創建對象時也可以使用列表初始化方式調用構造函數初始化
本質是生成一個 A 的臨時對象,然后將這個臨時對象賦給變量,編譯器優化后就變成了隱式類型轉換,可以通過在類前面加上關鍵字 explicit 來阻止隱式類型轉換
A& a1{12};//臨時對象具有常性,A&對象不能引用
const A& a2{12};
構造生成一個臨時對象,臨時對象具有常性,可以賦值給const變量
A a1=11;//單參數的隱式類型轉換
A a2={11,12};//多參數的隱式類型轉換,實質上是構造了一個A對象,然后調用了拷貝構造傳給a2對象
initializer_list
本質是個常量數組,里面只存有指向first和last的指針,因此32位下只有8字節,支持迭代器,因此可以遍歷
統一容器初始化方式:標準容器(如std::vector
、std::list
等)都支持使用std::initializer_list
進行初始化。這使得容器的初始化方式更加統一和直觀。也可以作為operator=
的參數,這樣就可以用大括號賦值
如果想要自定義類型支持列表初始化,就可以在自定義類型的構造函數中使用std::initializer_list
std::initializer_list
是一個不可變的類型,即一旦創建,它的元素不能被修改。它提供了begin
和end
函數來訪問其中的元素,就像訪問數組一樣,但不提供修改元素的接口
vector(const T& x1);
vector(const T& x1,const T&x2);
...
vector(initializer_list<T> il);
//這個構造一勞永逸的解決了問題,不用一個個寫構造
vector(initializer_list<T>& il)
{vector(std::initializer_list<T> il)//可以不用加引用,和const原因:這里的initializer_list本來要的就是淺拷貝//實質上是il的指針分別指向常量數組的第一個元素和常量數組的最后一個元素,不用拷貝,直接就構造了,因此加上const和引用對性能提升沒有影響//{1,2,3,4,5,6,78,75}
{reserve(il.size());for (auto& e : il){push_back(e);}
}
}
vector<int> v1={1,5,6,8,6,4}//隱式類型轉換
vector<int> v2({1,5,5,6,9,1})//構造函數
map<int,int> m1={{1,2},{2,3},{3,4}};
//實際上是生成了隱式類型轉換成了pair對象,然后再使用initializer_list<pair>進行構造
中間實際生成了臨時對象,然后拷貝構造,實質也是隱式類型轉換
實質是pair多參數的隱式類型轉換和initializer_list
decltype
與typeid類似,但decltype推斷出的類型可以定義變量,也可以用來模板傳參,而typeid不行,typeid 只是一個字符串,不能用于定義對象
list<int>::iterator it1;
cout << typeid(it1).name();
//結果:class std::_List_iterator<class std::_List_val<struct std::_List_simple_types<int> > >
typeid(it1).name() it2;//錯誤,typeid推出的類型不能用于定義變量,只是一個字符串
decltype(it1) it2;
cout << typeid(it2).name();
//結果:class std::_List_iterator<class std::_List_val<struct std::_List_simple_types<int> > >
//用于賦值的類型和原類型一致
decltype 的通常用于推導 auto 的類型
class A
{
public:T* func(){return new T;}private:int _a=10;
};
auto func()
{list<int> l;return l.begin();
}
auto i = func();
A<decltype(i)> a;
decltype 和 auto 的使用會增加代碼的閱讀難度
左值引用和右值引用
左值和右值的區分:
左值可以取地址,右值不能取地址
匿名對象不能取地址,是右值
表示數據的表達式,如字面常量,表達式返回值,函數返回值等都是右值
右值引用:給右值取別名,常見右值: 字面常量、表達式返回值,函數返回值
左值是一個表示數據的表達式(如變量名或解引用的指針),我們可以獲取它的地址+可以對它賦值,左值可以出現賦值符號的左邊,右值不能出現在賦值符號左邊。定義時const修飾符后的左值,不能給他賦值,但是可以取它的地址。
左值引用和右值引用都是給對象起別名
純右值(內置類型的)
將亡值(自定義類型的)
string&& ref1 = string("123");
string&& ref2 = to_string(123);
int&& ref3 = 10;
int&& ref4=(x+y);
左值引用不可以給右值起別名,但const修飾的左值可以
右值引用不可以給左值起別名,但是可以給move以后的左值起別名
const int& leftref = 10;
int a = 10;
int&& rightref = move(a);
move 只是會將左值強轉為右值,但并不會涉及到資源的分配,只是告訴編譯器可以進行右值操作,允許將這個變量的資源進行分配
被 move 之后的左值,一旦被分配,則自己就不再擁有原來的資源,而是分配出去了,就像真正的右值一樣
引用的意義是減小拷貝,提高效率
左值引用沒有徹底解決這個問題,因為局部變量無法引用
移動構造:傳入右值引用的構造,將傳入的右值的資源剝奪后分配給要構造的對象,避免了拷貝造成的資源占用
如果是右值,那么直接把資源轉移
可以直接返回局部本來要銷毀的變量,不用拷貝構造
移動構造本質是將參數右值的資源竊取過來,占位已有,那么就不用做深拷貝了,所以它叫做移動構造,就是竊取別人的資源來構造自己
string func()
{string s="12456";return ret;//return move(ret);編譯器自動優化成右值
}
string ret=func();
//這里由于編譯器優化,會將原本ret需要拷貝構造一個臨時對象
//然后ret通過移動構造將這個臨時對象的資源拿到,優化為直接進行移動構造
//相當于將ret強行識別為右值
純右值: 內置類型,返回類型為非引用類型的函數調用或運算符表達式屬于純右值,lambda 表達式為純右值, 因為表達式本身沒有名字,本質是臨時值,例如 42、a + b 或 func()(函數返回非引用類型)
將亡值: 自定義類型,返回類型為對象右值引用的表達式為將亡值,右值類對象的成員為將亡值,右值數組的成員為將亡值,標識一個具名對象,但該對象即將被銷毀(如通過 std::move 轉換)。它允許安全地移動資源,而非拷貝
移動語義允許高效地轉移資源,避免不必要的復制,特別是對于大型或資源密集型的對象,當一個變量或者右值被移動后,資源將會被分配到其他位置,原來的變量或右值不能夠訪問
**移動賦值:**移動賦值運算符(operator=
的移動賦值版本)用于將一個對象的資源轉移到另一個已經存在的對象中。與移動構造函數類似,它的主要目的是高效地處理資源的轉移,避免不必要的資源復制。
移動賦值運算符通常也期望右值引用作為參數。當有右值(如臨時對象)出現在賦值表達式的右側時,編譯器會優先選擇移動賦值運算符(如果定義了的話)來進行資源的轉移。
如果想對左值進行移動賦值操作,可以像移動構造函數一樣,使用std::move
函數將左值轉換為右值引用
不能被移動的左值
- 常量對象(const 左值)
移動操作需要修改源對象,常量禁止對象修改,嘗試移動常量對象會調用拷貝而非移動
const std::string s = "Hello";
std::string s2 = std::move(s);
// 調用拷貝構造函數,s中的值還是Hello,說明沒有移動,只是進行了拷貝
- 基本數據類型的對象(int,float,double)
沒有動態資源,移動等同于拷貝
int a = 10;
int&& rightref = move(a);//a中的值還是10,說明只是拷貝,并沒有移動
- 無移動操作的類對象
若類未定義移動構造函數/賦值運算符,或編譯器未隱式生成,則移動會退化為拷貝
class NoMove {
public:NoMove(){cout << "constructor function" << endl;}NoMove(const NoMove&){cout << "copy constructor" << endl;} // 只有拷貝構造函數
};
int main()
{NoMove obj1;NoMove obj2 = std::move(obj); // 調用拷貝構造函數
}
- 移動操作被顯式刪除的類對象
若移動構造函數/賦值運算符被標記為= delete,嘗試移動會引發編譯錯誤
class DeletedMove {
public:DeletedMove(){}DeletedMove(DeletedMove&&) = delete;
};
DeletedMove dm1;
DeletedMove dm2 = std::move(dm); // 編譯錯誤
- 包含不可移動成員的類對象
若類的成員或基類不可移動,隱式移動操作會被刪除,導致只能拷貝
struct NonMovableMember {NonMovableMember(NonMovableMember&&) = delete;
};
class Wrapper {NonMovableMember m;
};
Wrapper w;
Wrapper w2 = std::move(w); // 隱式移動被刪除,嘗試調用拷貝構造函數
struct S { int i : 4; };
S s;
int x = std::move(s.i); // 實際拷貝位域的值
右值引用可以引用字面常量:int&& a=5
,這里的 a 實際上是 const int&&
類型,因此不能通過 a 修改這個值
右值引用還能引用表達式產生的臨時對象,如,在函數調用返回一個臨時對象時,這個臨時對象是右值,可以用右值引用綁定它
- 假設存在一個函數
std::vector<int> createVector()
,它返回一個std::vector<int>
對象。可以這樣使用右值引用:std::vector<int>&& v = createVector();
。這里createVector
返回的臨時vector
對象被右值引用v
綁定 - 另一個例子是算術表達式的結果,如
int&& result = (3 + 5);
。表達式(3 + 5)
產生一個臨時的int
值8
,右值引用result
綁定了這個臨時值
在移動語義的場景下,右值引用用于引用那些即將被移動資源的對象,當一個對象要將自己的資源(如動態分配的內存)轉移給另一個對象時,會使用右值引用
右值引用本身是左值,只有右值本身處理成左值,才能實現移動構造,因此右值引用的函數要將接收到的右值傳給下一個函數時,要進行 move,才能調用下一個函數的 move 形式
如果右值引用的屬性是右值,那么移動構造和移動賦值,要轉移的語法邏輯是矛盾的,因為右值無法被改變(可以理解為右值默認自帶 const 屬性)因此右值引用本身要被處理成左值
std::vector<int> createVector() {std::vector<int> v = {1, 2, 3};return v;
}
std::vector<int> v2 = std::move(createVector());
這里使用 move 的原因,由于不確定編譯器是否會優化,此時,即使返回值在理論上是右值,仍然可能會調用拷貝構造函數進行復制,因此需要使用 move 告訴編譯器需要使用,移動語義來傳遞對象,而不是依賴可能會發生也可能不會發生的返回值優化,函數返回值雖然在語義上是右值,但在語法形式上可以被當作左值處理
移動拷貝對于效率的提升是針對于自定義類型的深拷貝的類,因為只有深拷貝的類才有移動函數
對于內置類型和淺拷貝自定義類型,沒有移動函數