本節目標
- c++11簡介
- 列表初始化
- 變量類型推導
- 范圍for循環
- 新增加容器
- 右值
- 新的類功能
- 可變參數模板
1. c++11簡介
在2003年標準委員會提交了一份計數勘誤表(簡稱TC1),使得c++03這個名字已經取代了c++98稱為c++11之前的最新的c++標準名稱。不過由于c++03(TC1)主要是對c++98標準中的漏洞進行修復,語言的核心部分沒有改動,因此人們習慣性的把兩個標準合并稱為c++98/03標準。從c++0x到c++11,c++標準10年磨一劍,第二個真正意義上的標準姍姍來遲。相對于c++98/03,c++11則帶來了數量可觀的變化,其中包含了約140個新特性,以及對c++03標準中約600個缺陷的修正,這使得c++11更像是從c++98中孕育出來的新語言。相比較而言,c++11能更好的用于系統開發和庫開發、語法更加泛化和簡單化,更穩定和安全,不僅功能更強大,而且能提升程序員的開發效率,公司實際項目開發中也用的比較多,所以要作為一個重點去學習。c++11增加的語法特性篇幅很多,沒辦法一一講解
https://en.cppreference.com/w/cpp/11
小故事:
1998年是C++標準委員會成立的第一年,本來計劃以后每5年視實際需要更新一次標準,C++國際標準委員會在研究C++03的下一個版本的時候,一開始計劃2007年發布,所以最初這個標準叫C++07。但是到06年的時候,官方覺得2007年肯定完不成C++07,而且官方覺得2008年可能也完不成。最后干脆叫C++0x。x的意思是不知道到底能在07還是08還是09年完成。結果2010年的時候也沒完成,最后在2011年終于完成了C++標準。所以最終定名為C++11
2. 統一的列表初始化
2.1 {} 初始化
在c++98中,標準允許使用花括號{}對數組或者結構體元素進行統一的列表初始值設定。比如
struct Point
{int _x;int _y;
};int main()
{int arr1[] = { 1, 2, 3, 4, 5 };int arr2[5] = { 0 };Point p = { 1, 2 };return 0;
}
c++11擴大了用大大括號的列表(初始化列表)的使用范圍,使其可用于所有的內置類型和用戶自定義類型,使用初始化列表時,可添加等號(=),也可不添加
int x1 = 1;
int x2{ 2 };//new也可以列表初始化
int* pa = new int[4] {0};
創建對象u額可以用列表初始化方式調用構造函數初始化,但有本質區別,是先用括號的內容構造一個臨時對象,再拷貝構造給初始化對象,會優化為直接構造
class Date
{
public:Date(int year, int month, int day):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}
private:int _year;int _month;int _day;
};//創建對象列表初始化
Date d1( 2022, 1 ,1 ); //舊初始化方式
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };//單參數構造函數支持隱士類型轉換
string str = "xxxxx";//證明,這里不能優化,所以轉不了,必須加const
const Date& d4 = { 2023, 11 , 5 };
2.2 std::initializer_list
介紹文檔
http://www.cplusplus.com/reference/initializer_list/initializer_list/
是什么類型:
// the type of il is an initializer_list auto il = { 10, 20, 30 };cout << typeid(il).name() << endl;
Date d1( 2022, 1 ,1 );
vector<int> v1 = { 0, 1, 2, 3, 4 };
可以使用迭代器遍歷
initializer_list<int> l2 = { 0, 1, 2, 3, 4 };
initializer_list<int>::iterator it = l2.begin();
while (it != l2.end())
{cout << *it << " ";it++;
}
這里的v1和d1不一樣,上面的是構造的對象,下面是先構造的initializer_list類型,v1的參數個數可以是隨意的,d1只能是三個
使用場景
std::initializer_list一般是作為構造函數的參數,c++11對stl中的不少容器增加了它作為參數的構造函數,這樣初始化容器就方便多了。也可以作為operator=的參數,就可以用大括號賦值
讓vector也支持{}初始化和賦值
vector(std::initializer_list<T> lt)
{Reserve(lt.size());for (auto& e : lt){PushBack(e);}
}
vector<T>& operator=(initializer_list<T> l) {vector<T> tmp(l);std::swap(_start, tmp._start);std::swap(_finish, tmp._finish);std::swap(_endofstorage, tmp._endofstorage);return *this;}
當參數的個數和構造函數匹配時會識別為對象,不匹配時會認為是initializer_list類型
3. 聲明
c++11通了多種簡化聲明的方式,尤其是在使用模板時
3.1 auto
c++98中auto是一個存儲類型的說明符,表明變了是局部自動存儲類型,但是局部域中定義局部的變量默認就是自動存儲類型,所以auto就沒什么價值了。c++11廢除auto原來用法,將其用于實現自動類型判斷,這樣要求必須顯示初始化,讓比那一期將定義對象的類型設置為初始化值的類型
typeid可以獲得變量的類型字符串
int i = 10;
auto p = i;
auto pf = strcpy;cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
3.2 decltype
上面可以推導類型,但推導的類型不能用來創建變量,如果想根據某個變量類型推導并創建變量,可以用decltype,將變量的類型聲明為表達式指定的類型
double y = 2.2;
decltype(y) ret = 3.3;cout << typeid(ret).name() << endl;
3.3 nullptr
由于c++中NULL被定義為字面量0,這樣就可能帶來一些問題,0既指針常量,又表示整形常量。所以出于清晰和安全的角度考慮,c++11新增了nullptr,表示空指針
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif#endif
4.范圍for循環
略
5. stl一些變化
圈起來的是幾個新容器,但是實際最有用的是unordered_map和unordered_set。
array和內置數組相比,越界訪問會報錯
容器的新方法
增加的新方法都用的比較少。比如提供了cbegin和cedn方法返回const迭代器等待,但意義不大,begin和end也可以返回const迭代器,屬于錦上添花的操作
插入接口函數增加了右值版本
http://www.cplusplus.com/reference/vector/vector/emplace_back/
意義在哪,說能提高效率,如何提高的
6. 右值引用和移動語義
6.1 左值引用和右值引用
傳統c++語法就有引用,c++11新增了右值引用特性,無論是左值還是右值引用,都是給對象取別名
左值是一個表示數據的表達式(變量名或解引用的指針),可以獲取它的地址,可以賦值,左值可以出現在賦值符號左邊,右值不能出現在賦值符號左邊。定義時const修飾后的左值,不能賦值,但可以取地址。左值引用就是給左值的引用,取別名
int* p = new int(0);
int b = 1;
const int c = 2;//以下都是左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
什么是右值,什么是右值引用
右值也是一個表示數據的表達式,如:字面常量、表達式返回值、函數返回值(這個不能是左值引用返回)等待,右值可以出現在賦值符號的右邊,不能出現在左邊,右值不能取地址。右值引用就是對右值的引用,給右值取別名
double x = 1.1, y = 2.2;
//以下是常見的右值
10;
x + y;
fmin(x, y);//以下幾個都是右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);//編譯會報錯,error c2106, "=":左操作數必須為左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
6.2 左值引用和右值引用比較
左值引用總結:
1.左值引用只能引用左值,不能引用右值,但是const左值引用既可以引用左值,也可以引用右值
2.右值引用不能引用左值,move的可以
//左值引用不能給右值取別名,const左值可以
int& r1 = 10;
const int& r2 = 10;
//右值引用不能給左值取別名,move可以
int i = 10;
int&& rr3 = i;
int&& rr4 = move(i);
6.3 左值引用使用場景和意義
左值做參和返回值都可以提高效率,減少了拷貝
#pragma once
#include <string>
#include <iostream>
#include <assert.h>class string
{
public:typedef char* iterator;iterator begin(){return _str;}iterator end(){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);}// s1.swap(s2)void swap(string& s){::std::swap(_str, s._str);::std::swap(_size, s._size);::std::swap(_capacity, s._capacity);}// 拷貝構造string(const string& s):_str(nullptr){std::cout << "string(const string& s) -- 深拷貝" << std::endl;string tmp(s._str);swap(tmp);}// 賦值重載string& operator=(const string& s){std::cout << "string& operator=(string s) -- 深拷貝" << std::endl;string tmp(s);swap(tmp);return *this;}~string(){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];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)string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}
private:char* _str;size_t _size;size_t _capacity; // 不包含最后做標識的\0
};
左值引用的短板
當函數返回對象是一個局部變量,除了函數作用域就不存在了,不能使用左值引用返回,只能傳值返回。例如:string to_string(int value)函數中可以看到,這里只能傳值返回,傳值返回會導致至少一次拷貝構造(如果舊一點的編譯器可能是兩次拷貝構造)
string to_string(int value)
{string ret;while (value){int x = value % 10;value /= 10;ret += '0' + x;}std::reverse(ret.begin(), ret.end());return ret;
}
舊編譯器會產生兩次拷貝構造,返回的ret對象拷貝一次,賦值的時候也會調用一次賦值重載。既然ret已經是一個要銷毀的對象了,多次拷貝就會造成資源的浪費。下面第一個是連續的構造和拷貝構造都可以優化一次拷貝構造
下面這個無法優化
右值引用和移動語義解決上述問題
右值可以分為:
1.純右值,內置類型右值
2.將亡值,自定義的右值
移動構造本質是將參數右值的資源竊取過來,占為己有,不做深拷貝,叫它移動構造,就是竊取別人的資源構造自己。所以可以實現拷貝和賦值的移動版本
編譯器會選擇最匹配的調用,to_string返回的是右值,如果既有拷貝又有右值,就會匹配移動構造
//移動拷貝
string(string&& s)
{std::cout << "string(string&& s) -- 移動語義" << std::endl;swap(s);
}
//移動賦值
string& operator=(string&& s)
{std::cout << "string& operator=(string s) -- 移動語義" << std::endl;swap(s);return *this;
}
s和ret的字符串是同一個
運行后調用了一次移動構造和移動賦值,因為如果用一個已經存在的對象接收,編譯器沒辦法優化,to_string函數中先用str生成構造一個臨時對象,但是可以看到,編譯器把str識別成了右值,調用了移動構造,然后把臨時對象作為to_string函數調用的 返回值賦值給ret1,調用的移動賦值
stl容器都增加了移動構造和移動賦值
string s1("hello");
string s2 = s1;
string s3 = std::move(s1);
6.4 右值引用左值及一些深入的使用場景
按照語法,右值引用只能引用右值,但右值引用一定不能引用左值嗎?有些場景下,需要用右值去引用左值實現移動語義。當需要右值引用左值時,可以通過move函數將左值轉化為右值,c++11中,std::move()函數位于頭文件中,該函數名字具有迷惑性,并不搬移任何東西,唯一的功能是將一個左值強制轉換為右值使用,實現移動語義
template<class _Ty>inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT{// forward _Arg as movablereturn ((typename remove_reference<_Ty>::type&&)_Arg);}
//move會改為右值,s1的資源置空,轉移給了s3
string s1("hello");
string s2 = s1;
string s3 = std::move(s1);
//move的返回值是右值,并不改變變量本身
string s4 = s1;
stl容器也加入了右值引用版本
std::list<string> l1;
string s1 = "hello";
//左值
l1.push_back(s1);
//右值
l1.push_back(to_string(1234));運行結果:
// string(const string& s) -- 深拷貝
// string(string&& s) -- 移動語義
修改list
在前面的list中加入右值插入的版本,先把pushback函數加入右值,這時還是會有深拷貝
右值被右值引用以后得屬性是左值,編譯器設計因為右值引用需要被修改
這里x傳入下層又變為了左值,需要傳給inert的右值版本move后的,insert函數里創建節點也需要再次move
__list_node(T&& x): _prev(nullptr), _next(nullptr), _data(std::move(x))
{}void push_back(T&& x)
{Insert(end(), std::move(x));
}void Insert(iterator pos, T&& x)
{node* new_node = new node(std::move(x));//記錄前后節點node* pre = pos.node_iterator->_prev;node* cur = pos.node_iterator;//連接pre->_next = new_node;new_node->_prev = pre;new_node->_next = cur;cur->_prev = new_node;}list<string> l1;
string s1 = "hello";
l1.push_back(s1);
l1.push_back(to_string(1234));
6.5 完美轉發
模板中的&&萬能引用
void fun(int& x) { std::cout << "左值引用" << std::endl; };
void fun(const int& x) { std::cout << "const 左值引用" << std::endl; };
void fun(int&& x) { std::cout << "右值引用" << std::endl; };
void fun(const int&& x) { std::cout << "const 右值引用" << std::endl; };// 模板的&&不是右值引用,是萬能引用,既能接收左值,也能右值
// 引用類型的唯一作用是限制了接收的類型,后續使用都退化成了左值
// 想要保持左值和右值的屬性,要使用完美轉發
template <typename T>
void PerfectForward(T&& t)
{fun(t);
};PerfectForward(10); //右值
int a;
PerfectForward(a); //左值
PerfectForward(std::move(a)); //右值
const int b = 8;
PerfectForward(b); //左值
PerfectForward(std::move(b)); //右值
上面的t后續都退化成了左值,想要保持傳入的屬性,就要加入std::forward保留屬性
fun(std::forward<T>(t));
使用場景
容器的插入等可以直接使用完美轉發,代替左值和右值兩個版本
7. 新的類功能
默認成員函數
原來c++類中,有6個默認成員函數:
1.構造函數
2.析構函數
3.拷貝構造函數
4.拷貝賦值重載
5.取地址重載
6.const取地址重載
最后重要的是前4個,后兩個用處不大。默認成員函數就是我們不寫編譯器會生成一個默認的
c++11新增了兩個:移動構造函數和移動賦值運算符重載
針對移動構造函數和移動賦值運算符重載有一些需要注意的點如下:
- 如果沒有實現移動構造函數,且沒有實現析構函數、拷貝構造、拷貝賦值重載中的任意一個。那么編譯器會自動生成一個默認移動構造。默認生成的移動構造函數,對于內置類型成員匯之星逐成員按字節拷貝,自定義類型成員,則需要看這個成員是否實現移動構造,如果實現了就調用移動構造,沒有實現就調用拷貝構造
- 如果沒有實現移動賦值重載,且沒有實現析構函數、拷貝構造、拷貝賦值重載中的任意一個。那么編譯器會自動生成一個默認移動賦值。默認生成的移動構造函數,對于內置類型成員匯之星逐成員按字節拷貝,自定義類型成員,則需要看這個成員是否實現移動賦值,如果實現了就調用移動,沒有實現就調用拷貝構造
- 如果提供了移動構造或者移動賦值,編譯器就不會自動提供拷貝構造和拷貝賦值
類成員變量初始化
c++11允許在類定義時給成員變量初始缺省值,默認生成構造函數會使用這些缺省值初始化
強制生成默認函數的關鍵字default
c++11可以更好的控制要使用的默認函數,假設要使用某個默認的函數,但因為一些原因沒有默認生成,比如提供了拷貝構造,就不會生成移動構造,可以適用default關鍵字顯示指定移動構造生成
class Person{public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p):_name(p._name),_age(p._age){}Person(Person&& p) = default;private:bit::string _name;int _age;};int main(){Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;}
禁止生成默認函數的關鍵字delete
如果想要限制某些默認函數的生成,在c++98中,是該函數設置成private,并且只聲明補丁,止癢只要其他人想要調用就會報錯。在c++11中更簡單,只需在函數聲明上加上=delete即可,指示編譯器不生成對應函數的默認版本,修飾的為刪除函數
class Person{public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p) = delete;private:bit::string _name;int _age;};int main(){Person s1;Person s2 = s1;Person s3 = std::move(s1);}
繼承和多態中的final與override關鍵字
final修飾類或虛函數,表示不可被繼承或重寫。override檢測虛函數是否完成重寫
8. 可變參數模板
c++11的新特性可變參數模板能夠創建可以接收可變參數的函數模板和類模板,相比c++98/03,類模板和函數模板中只能含固定數量的模板參數,可變模板參數無疑是一個巨大的改進。然而由于可變模板參數比較抽象,使用起來需要一定的技巧,所以這塊還是比較晦澀的。掌握一些基礎的可變模板參數特性就可以了
// Args是一個模板參數包,args是一個函數形參參數包
// 聲明一個參數包Args...args,這個參數包中可以包含0到任意個模板參數。
template <class ...Args>void ShowList(Args... args){}
上面的參數args前面有省略號,所以它就是一個可變模板參數,把帶省略號的參數稱為“參數包”,里面包含了0到N(N>0)個模板參數。無法直接獲取參數包args中的每個參數,只能通過展開參數包的方式獲取每個參數,這時使用可變模板參數的一個主要特點,也是最大的難點,如何展開可變模板參數。由于語法不支持使用args[i]這樣方式獲取可變參數,所以我們用一些特殊方式一一獲取參數包
遞歸函數展開參數包
// 遞歸終止函數
void _ShowList()
{std::cout << std::endl;
}
//展開函數
template <class T, class ...Args>
void _ShowList(const T& value, Args ...args)
{std::cout << value << " ";_ShowList(args...);
}template <class ...Args>
void ShowList(Args ...args)
{_ShowList(args...);
}int main()
{ShowList(1, 2, 'x');ShowList(1, 2, 3.5);return 0;
}
逗號表達式展開
這種方式展開參數包,不需要通過遞歸終止函數,是直接在expand函數體中展開的,printarg不是一個遞歸終止函數,指示一個處理參數包每一個參數的函數。這種就地展開參數包的方式實現的關鍵是逗號表達式。逗號表達式會按順序執行,返回最后一個
expand函數中的逗號表達式也(printarg(args),0),也是按照這個執行順序,限制性printarg(args),在得到逗號表達式的結果0,同時還用到了c++11的另外一個特性–初始化列表,通過初始化列表來初始化一個變長數組,{(printarg(args), 0}將會展開成(printarg(arg1), 0),(printarg(arg2), 0), (printarg(arg3), 0), etc…),最終會創建一個元素值都為0的數組Int arr[sizeof…(args)]。由于是逗號表達式,在創建數組的過程中回顯執行逗號表達式前面的部分printarg(args)打印出參數,也就是說在構造int數組的過程中就將參數包展開了,這個參數的目的純粹是為了在數組構造的過程展開參數包
template <class T>void PrintArg(T t){cout << t << " ";}//展開函數
template <class ...Args>void ShowList(Args... args){int arr[] = { (PrintArg(args), 0)... };cout << endl;}}int main(){ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));
stl容器中empalce相關接口
template <class... Args>void emplace_back (Args&&... args);
emplace系列接口,支持模板的可變參數,并且萬能引用。那么相對insert優勢在哪里
int main(){std::list< std::pair<int, char> > mylist;// emplace_back支持可變參數,拿到構建pair對象的參數后自己去創建對象
// 那么在這里我們可以看到除了用法上,和push_back沒什么太大的區別
mylist.emplace_back(10, 'a');mylist.emplace_back(20, 'b');mylist.emplace_back(make_pair(30, 'c'));mylist.push_back(make_pair(40, 'd'));mylist.push_back({ 50, 'e' });for (auto e : mylist)cout << e.first << ":" << e.second << endl;return 0;}
emplace是由模板參數包直接傳入參數,不會拷貝一個臨時對象。而pushback需要拷貝構造或移動構造。移動構造的消耗也不是很高
int main(){// 下面我們試一下帶有拷貝構造和移動構造的bit::string,再試試呢
// 我們會發現其實差別也不到,emplace_back是直接構造了,push_back// 是先構造,再移動構造,其實也還好。
std::list< std::pair<int, bit::string> > mylist;mylist.emplace_back(10, "sort");mylist.emplace_back(make_pair(20, "sort"));mylist.push_back(make_pair(30, "sort"));mylist.push_back({ 40, "sort"});return 0;}