目錄
前言
1.初始化列表
2.std::initializer_list
3.auto
4.decltype
5.nullptr
6.左值引用和右值引用?
6.1右值引用的真面目
6.2左值引用和右值引用比較
6.3右值引用的意義
6.3.1移動構造
6.4萬能引用
6.5完美轉發——forward
結語
前言
? C++,這門在系統開發、游戲開發、嵌入式等眾多領域都占據主導地位的編程語言,一直在不斷進化。C++11就像是C++家族中的一顆璀璨明星,閃耀著無數令人驚嘆的新特性。如果你還在使用傳統的C++編碼方式,那你可能正在錯過許多現代編程的便捷與高效。想象一下,用更少的代碼編寫出性能更高的程序,輕松應對復雜的并發場景,還能享受更加簡潔和安全的內存管理。C++11就能讓這一切成為現實。在這篇博文中,我會一點點揭開C++11新特性的神秘面紗,讓你看到C++在這次重大更新之后所蘊含的無限潛力。
1.初始化列表
在C++98標準中,允許使用 花括號{ } 對數組或者結構體元素進行統一的列表初始化
例如:
struct Point{int a;int b;
};int main(){int arry[] = {1,2,3,4};//數組初始化Point p = {1,2};//結構體初始化return 0;
}
而C++11擴大了使用 花括號{ } 進行初始化的使用范圍,使其可以用于所有的內置類型和用戶自定義的類型
//對數組初始化
int arry1[]{1,2,3,4,5};
//對內置類型初始化
int x{9};
//對用戶自定義類型初始化
Point p{9,9};
//對vector容器對象進行初始化
vector<int> vec = {1,2,3,4,5,6,7,8,9};
注意:
使用初始化列表時,可以添加等號 " = ",也可以不添加
2.std::initializer_list
initializer_list是什么?
看名字像一個容器,但我們學過的容器并沒有這個,但并非沒有見過
但C++11支持對各個容器進行初始化列表初始化,離不開它
std::initializer_list
?是一個輕量級的模板類,用于在函數參數中傳遞初始化列表,從而使編譯器能夠正確解析和處理使用花括號?{}
?進行初始化的對象。
我們在諸多容器的構造中,都可以看見它是身影
在vector構造中
在list構造中?
std::initializer_list
?是定義在?<initializer_list>
?頭文件中的一個模板類,用于表示一個初始化列表。
它的基本形式如下:
std::initializer_list<T> init_list = { /* 初始化元素 */ };
std::initializer_list
?提供了一種輕量級的、只讀的視圖來訪問初始化列表中的元素。它常用于構造函數、函數參數以及返回類型,以支持統一初始化語法。
并不是所有的類型都會有std::initializer_list的構造函數,也并不是有std::initalizer_list才能使用花括號 { } 進行初始化
但只要你顯式寫了使用std::initializer_list的構造函數,則進行{ }初始化時,會優先調用該函數進行初始化
關于initializer_list的細節不過多贅述,知道有這個東西,和它的大致用途即可
3.auto
在C++98中auto是一個存儲類型的說明符,表明變量是局部自動存儲類型,但是局部域中定義局部的變量默認就是自動存儲類型,所以auto沒有什么價值
C++11廢棄auto的原本用法,將其用于實現自動類型推斷,這一功能極大地簡化了代碼編寫,特別是在處理復雜類型時。
舉個例子:
當一個類型很復雜時,可以使用auto進行簡寫
std::pair<int, std::string> get_user_info() {return {42, "Alice"};
}int main() {// 使用 auto 簡化返回類型的聲明auto user = get_user_info();reutrn 0;
}
4.decltype
在C++11標準中,decltype
?是一個新增的關鍵字,用于在編譯時推導表達式的類型。與?auto
?關鍵字類似,decltype
?也用于類型推導,但兩者的工作機制和應用場景有所不同。
decltype
?的基本語法
decltype(expression) variable_name;
說明:?
expression
?是任何有效的C++表達式,decltype
?會根據這個表達式的類型來推導出?variable_name
?的類型。
示例:
int a = 5;
double b = 3.14;
decltype(a) c = a; // c 的類型為 int
decltype(b+b) d = b; // b+b表達式的結果類型為double,則b的類型為 double
注意:
decltype
?不會實際計算表達式的值,它只在編譯時進行類型推導。
5.nullptr
由于C++中的NULL被定義為字面量0,但這樣可能會帶來一些問題
因為 NULL 既能表示指針常量,又能表示整形常量
出于清晰和安全的考慮,C++11新增了nullptr,用于表示空指針
6.左值引用和右值引用?
傳統的C++語法中就有引用的語法,而C++11中新增了的右值引用的語法特性
現在,引用就分為兩種
- 左值引用
- 右值引用
什么是左值,什么是左值引用?
左值是一個表示數據的表達式,如變量名、指針等
左值一般出現在賦值符號的左邊
//下面的指針p ,變量b、c 均為左值
int* p = new int(0);
int b =1;
const int c =2;
左值引用就是給左值的引用,給左值取別名
int*& rp = p;
int& rb = b;
const int& rc = c;
注意:
左值引用的類型一定是 左值類型+& ,必須嚴格匹配
什么是右值?什么是右值引用
右值也是一個表示數據的表達式,如字面常量、表達式返回值、函數返回值等等
右值只能出現在賦值符號的右邊
//常見右值
10;//常量
x+y;//表達式
add(x,y)//函數返回值
右值引用就是對右值的引用,給右值取別名
int&& rr1=10;
double&& rr2 = x + y;
double&& rr3 = fmin(x,y);
右值是不能取地址的,但給右值取別名后,會導致右值被存儲到特定的位置(一般是棧區),且可以取到該位置的地址
也就是說:不能取字面量10的地址,但rr1引用后,可以對rr1取地址,也可以修改rr1
誤區:
習慣使用左值引用后
我們對右值引用也會有一個誤解:可以通過修改右值引用來修改右值
這是不切實際的,我們修改的從來只是右值引用,而并非右值!
如上面的rr1,令rr1 = 11,并不會讓 10 = 11!
巧記
- 區分左值和右值:左值在 賦值操作符的左邊,右值在 賦值操作符的右邊
- 區分左值引用和右值引用:左值引用是 類型+&,右值引用是 類型+&&
6.1右值引用的真面目
右值不能被修改,這是公認的,例如?10 永遠不可能變成 11
但為什么被引用后,就可以被修改?10不是在常量區嗎?難道被移到棧區了?
右值引用一個右值,右值本身并沒有被“移動”到某個新的區域,而是右值引用的變量本身存儲了右值的內容
具體來說:
- 右值引用的變量:右值引用本身是一個左值,它有自己的存儲位置(通常是棧上的某個位置)。這個變量存儲了右值的內容
- 右值的原始位置:右值的本身的原始位置(如字符常量存儲在常量區)并不會因為右值引用而改變變。右值引用只是提供了一個訪問右值內容的途徑
右值引用的真面目——左值,左值可以被取地址,非const左值可以被修改
6.2左值引用和右值引用比較
左值引用總結:
- 左值引用不能引用左值,但不能引用右值
- const 左值引用可以引用左值,也可以引用右值
右值引用總結:
- 右值引用可以引用右值,不能引用左值,但可以引用move后的左值
6.3右值引用的意義
const 左值引用既可以引用左值又可以引用右值,那為什么C++11還要提出右值引用呢?
是不是畫蛇添足?
并不是,左值引用在一起場景下存在短板,而右值引用恰恰能解決這個短板
舉個例子:
先今,我們自定義了一個string類
該類中有一個拷貝構造函數,并在類外定義了一個to_string函數
兩者造型如下:
to_string函數
??????bit::string to_string(int value) {bit::string str;//return str;
}
string的拷貝構造函數
string(const string& s) : _str(nullptr)
{ std::cout << "MyString(const MyString& s) -- 深拷貝" << std::endl;string tmp(s.str);swap(tmp);
}
用to_string的返回值去構造一個string對象
?
這就體現左值引用的短板了
當函數返回對象是一個局部對象,出了函數作用域就不存在了,就會調用拷貝構造創建一個新的臨時對象,再用這個臨時對象去拷貝構造
一共會發生兩次拷貝構造,其中,創建臨時對象的時候,涉及開辟新的空間
開辟新空間,很浪費資源,太多次受不了
6.3.1移動構造
有沒有什么簡單,且不吃操作的方法規避深拷貝
有的,兄弟,有的
to_string的返回值是一個右值,用這個右值構造ret2,如果沒有移動構造,調用就會匹配調用拷貝構造,因為const左值引用是可以引用右值的,這里就是一個深拷貝
此時我們在bit::string中增加一個移動構造,就是用右值引用充當形參
具體造型如下
//移動構造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{swap(s);
}
此時再次構造ret2,如果既有拷貝構造,又有移動構造,則會調用更為匹配的移動構造
而移動構造中沒有新開空間,拷貝數據,效率提高
移動構造的本質:將參數右值的資源竊取過來,占為己有,就不用創建臨時對象,開辟空間,而是直接竊取別人的資源來構造自己。
為什么可以這樣呢?
右值引用可以延長右值的生命周期,使其生命周期向右值引用看齊
這樣函數中的返回值本來出了函數作用域就會被銷毀,回收資源,右值引用后,就延長了生命周期,不會被回收,
可以直接拿來構造新的對象,不用再創建臨時對象,造成額外開銷。
6.4萬能引用
“萬能引用”(Universal Reference)是 C++ 編程中的一個術語,主要用于描述一種能夠同時綁定到左值(lvalue)和右值(rvalue)的引用類型。
萬能引用通常出現在模板編程中,允許函數模板接受任意類型的參數,從而實現更靈活和通用的代碼設計。
來看以下例子
// 接受左值引用
void Fun(int& x) {std::cout << "Fun(int&): "<< x << std::endl;
}// 接受右值引用
void Fun(int&& x) {std::cout << "Fun(int&&): "<< x << std::endl;
}// 萬能引用的模板函數,調用相應的 Fun 函數
template<typename T>
void CallFun(T&& arg) {Fun(arg);
}
在main中進行調用模版函數
int a = 10;
const int b = 20;CallFun(a); // 傳遞左值,調用 Fun(int&)CallFun(30); // 傳遞右值,調用 Fun(int&&)
運行結果:
?
按理說,應該會調用一個 Fun(int&&) 和一個Fun(int&),但事實并不是這樣
為什么?
模版的萬能引用只是提供了能夠同時接收左值引用和右值引用的能力
但在后續的使用中,統一當成左值來使用
即進入模版函數體中,只有左值,沒有右值
所以,會調用兩次 Fun(int&)
我們希望事情按照我們的期望發展
在模版函數體中,右值和左值也會保持原來的屬性,不會變化
這時候,就需要完美轉發
6.5完美轉發——forward
完美轉發是 C++11 引入的一項重要特性,旨在函數模板中將參數以其原始的值類別?(左值或右值)傳遞給其他函數,確保在轉發過程中不改變參數的性質。
基本造型:?
std::forward<T>(arg)
說明:
- arg即為要保持原始值類比的對象
示例:
在萬能轉發的模版函數中,添加完美轉發
template<typename T>
void CallFun(T&& arg) {Fun(std::forward<T>arg);
}
main照常
int main()
{int a = 10;const int b = 20;CallFun(a); // 傳遞左值,調用 Fun(int&)CallFun(30); // 傳遞右值,調用 Fun(int&&)return 0;
}
?運行一下:
和我們的預期一致
右值引用和左值引用各有各的用法,并不是隨意創造的,具體怎么用,需要結合實際情況進行分析選擇。
結語
? 綜上所述,C++11引入的初始化列表、右值引用以及nullptr等特性,為C++語言的發展注入了新的活力。初始化列表通過提供統一的初始化語法,增強了代碼的可讀性和可維護性;右值引用憑借其獨特的語義,極大地優化了對象的移動語義和資源管理效率;nullptr則為指針類型提供了一個明確而安全的表示。這些特性的引入,不僅豐富了C++語言的編程范式,更為現代軟件開發中的高效性、可靠性要求提供了有力支持,推動了面向對象編程向更高層次發展。