一、C++11簡介
在2003年C++標(biāo)準(zhǔn)委員會曾經(jīng)提交了一份技術(shù)勘誤表(簡稱TC1),使得C++03這個(gè)名字已經(jīng)取代了C++98稱為C++11之前的最新C++標(biāo)準(zhǔn)名稱。不過由于TC1主要是對C++98標(biāo)準(zhǔn)中的漏洞進(jìn)行修復(fù),語言的核心部分則沒有改動(dòng),因此人們習(xí)慣性的把兩個(gè)標(biāo)準(zhǔn)合并稱為C++98/03標(biāo)準(zhǔn)。從C++0x到C++11,C++標(biāo)準(zhǔn)10年磨一劍,第二個(gè)真正意義上的標(biāo)準(zhǔn)珊珊來遲。相比于C++98/03,C++11則帶來了數(shù)量可觀的變化,其中包含了約140個(gè)新特性,以及對C++03標(biāo)準(zhǔn)中約600個(gè)缺陷的修正,這使得C++11更像是從C++98/03中孕育出的一種新語言。相比較而言,C++11能更好地用于系統(tǒng)開發(fā)和庫開發(fā)、語法更加泛華和簡單化、更加穩(wěn)定和安全,不僅功能更強(qiáng)大,而且能提升程序員的開發(fā)效率,公司實(shí)際項(xiàng)目開發(fā)中也用得比較多,所以很值得我們作為一個(gè)重點(diǎn)學(xué)習(xí)。C++11增加的語法特性篇幅非常多,沒辦法一一講解,所以在我的文章中只講比較實(shí)用的語法。
大家可以看一下C++11的官方網(wǎng)站,在讀這篇文章之前了解一下
C++11官方網(wǎng)站
其實(shí),關(guān)于C++11還有一個(gè)小故事。1998年是C++標(biāo)準(zhǔn)委員會成立的第一年,本來計(jì)劃以后每五年視實(shí)際需要更新一次標(biāo)準(zhǔn),C++國際標(biāo)準(zhǔn)委員會在研究C++03的下一個(gè)版本的時(shí)候,一開始計(jì)劃是2007年發(fā)布,所以最初這個(gè)版本標(biāo)準(zhǔn)較C++07.但是到06年的時(shí)候,官方覺得2007年肯定完不成C++07,而且官方覺得2008年可能也完不成。最后干脆叫C++0x了。x的意思是不知道到底能在07還是08還是09年完成。結(jié)果2010年的時(shí)候也沒完成,最后再2011年終于萬和城呢個(gè)了C++標(biāo)準(zhǔn),所以最終定名為C++11。
二、列表初始化
2.1 C++98中{}的初始化問題
在C++98中,標(biāo)準(zhǔn)允許使用花括號{}對數(shù)組元素進(jìn)行統(tǒng)一的列表初始值設(shè)定。比如:
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
對于一些自定義的類型,卻無法使用這樣的初始化。比如:
vector<int> v{1,2,3,4,5};
就無法通過編譯,導(dǎo)致每次定義vector時(shí),都需要先把vector定義出來,然后使用循環(huán)對其賦初始值,非常不方便。C++11擴(kuò)大了用大括號括起的列表(初始化列表)的使用范圍,使其可用于所有的內(nèi)置類型和用戶自定義的類型,使用初始化列表時(shí),可添加等號(=),也可不添加。
2.2 C++11中的列表初始化
C++11不同于C++98的變量初始化方法就體現(xiàn)在列表初始化上面,也就是說在C++11中,任何變量都可以直接用列表,在其創(chuàng)建是直接初始化,如下:
對于內(nèi)置類型:
這么看來,好像也沒厲害到那兒去,有一種脫了褲子放屁的感覺。
其實(shí),C++11設(shè)著這個(gè)東西主要是為了在自定義類型的初始化中起作用。如下:
在C++98的標(biāo)準(zhǔn)中,對于自定義類型的變量,用動(dòng)態(tài)管理的方式為它申請空間之后才能賦值。但在C++11標(biāo)準(zhǔn)中,可以直接用列表初始化。
注意,Point* p2 = new Point[2]{ {1,1},{2,2} };這種方式在VS2013中行不通,最后創(chuàng)建出來的變量還是沒有初始化,可能是編譯器的一個(gè)bug。要使用更高版本的編譯器。
另外,C++11還可支持多個(gè)對象的列表初始化。
多個(gè)對象想要支持列表初始化,需給該類(模板類)添加一個(gè)帶有initializer_list類型參數(shù)的構(gòu)造函數(shù)即可。注意:initializer_list是系統(tǒng)自定義的類模板,該類模板中主要有三個(gè)方法:begin()、end()迭代器以及獲取區(qū)間中元素個(gè)數(shù)的方法size()。如下:
舉個(gè)例子:
int main()
{
auto li = { 1,2,3,4,5 };
cout << typeid(li).name() << endl;
return 0;
}
其實(shí),可以理解為,花括號中的東西是一個(gè)常量數(shù)組,是存在于常量區(qū)的。然后編譯器會將其中的值一一賦值給li。賦值的方式也是調(diào)用迭代器。
于是乎,就可以定義以下變量:
三、各種小語法
3.1 auto
在C++98中auto是一個(gè)存儲類型的說明符,表明變量是局部自動(dòng)存儲類型,但是局部域中定義局部的變量默認(rèn)就是自動(dòng)存儲類型,所以auto就沒什么價(jià)值了。C++11中廢棄auto原來的用法,將其用于實(shí)現(xiàn)自動(dòng)類型腿斷。這樣要求必須進(jìn)行顯示初始化,讓編譯器將定義對象的類型設(shè)置為初始化值的類型。
如下:
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
3.2 decltype
上文中出現(xiàn)的typeid只能輸出變量的類型,卻不能用它再定義一個(gè)變量,而decltype卻可以。如下:
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的類型是double
decltype(&x) p;// p的類型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}
但是這個(gè)東西真的不太常用(至少我還沒用過)
3.3 nullptr
由于C++中NULL被定義成字面量0,這樣就可能回帶來一些問題,因?yàn)?既能指針常量,又能表示整形常量。所以出于清晰和安全的角度考慮,C++11中新增了nullptr,用于表示空指針。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
3.4 范圍for
范圍for的底層是一個(gè)迭代器,也是用來遍歷一個(gè)容器(或者容器適配器),其用法比較簡單,如下:
四、STL中的一些變化
C++11中增加了一些新容器,如下:
對于第一個(gè)array,本質(zhì)上就是一個(gè)數(shù)組,與vector的區(qū)別是,vector是動(dòng)態(tài)的,可以隨時(shí)擴(kuò)容。
array與vector和普通的數(shù)組最大的區(qū)別在于,它的越界訪問機(jī)制非常嚴(yán)格,越界讀和越界寫都會被檢查出來。而vector和普通數(shù)組對于越界讀不做檢查,對于越界寫則采用抽查的方式。
(除此之外,array就沒有什么太大的用處了)
讀者也可以自己去cplusplus網(wǎng)站看一下它的用法,這里不再贅述。
下一個(gè)是forward_list,這是一個(gè)單向的鏈表。而list則是一個(gè)帶頭的雙向循環(huán)鏈表。
相較于list,這個(gè)容器增加了頭插和頭刪操作:
但是注意,它并不支持尾插尾刪操作,因?yàn)檫@需要遍歷找到尾結(jié)點(diǎn),會導(dǎo)致效率大大降低。
對于另外的兩個(gè),感興趣的讀者可以自己去網(wǎng)站上看一看,這里不多解釋了。另外,對于以上兩個(gè)容器的介紹也不完整,大家也可以看一下。C++網(wǎng)站
五、左/右值引用和移動(dòng)語義(本篇重點(diǎn))
5.1 做值引用和右值引用
傳統(tǒng)的C++語法中就有引用的語法,而C++11中新增了的右值引用語法特性,所以從現(xiàn)在開始我們之前學(xué)習(xí)的引用就叫做左值引用。無論左值引用還是右值引用,都是給對象取別名。
什么是左值,什么是左值引用?
左值是一個(gè)表示數(shù)據(jù)的表達(dá)式(如變量名或解引用的指針),我們可以獲取它的地址+可以對它賦值,左值可以出現(xiàn)賦值符號的左邊,右值不能出現(xiàn)在賦值符號左邊。定義時(shí)const修飾符后的左值,不能給他賦值,但是可以取它的地址。左值引用就是給左值的引用,給左值取別名。
如下:
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下幾個(gè)是對上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
什么是右值,什么是右值引用?
右值也是一個(gè)表示數(shù)據(jù)的表達(dá)式,如:字面常量、表達(dá)式返回值,函數(shù)返回值(這個(gè)不能是左值引用返回)等等,右值可以出現(xiàn)在賦值符號的右邊,但是不能出現(xiàn)出現(xiàn)在賦值符號的左邊,右值不能取地址。右值引用就是對右值的引用,給右值取別名。
如下:
int main()
{
double x = 1.1, y = 2.2;
// 以下幾個(gè)都是常見的右值
10;
x + y;
fmin(x, y);
// 以下幾個(gè)都是對右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 這里編譯會報(bào)錯(cuò):error C2106: “=”: 左操作數(shù)必須為左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
需要注意的是右值是不能取地址的,但是給右值取別名后,會導(dǎo)致右值被存儲到特定位置,且可以取到該位置的地址,也就是說例如:不能取字面量10的地址,但是rr1引用后,可以對rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 報(bào)錯(cuò)
return 0;
}
5.2 左值引用與右值引用比較
左值引用總結(jié):
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra為a的別名
//int& ra2 = 10; // 編譯失敗,因?yàn)?0是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用總結(jié):
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 無法從“int”轉(zhuǎn)換為“int &&”
// message : 無法將左值綁定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
對于上面代碼中的move,下文中做出解釋。
5.3 右值引用使用場景和意義
先來說一下引用的意義:
在函數(shù)傳參和函數(shù)傳返回值時(shí),使用引用,可以達(dá)到減少拷貝的效果。
現(xiàn)在應(yīng)該明白上面所說的引用,其實(shí)指的是左值引用。
左值引用有沒有徹底解決問題?
答案是沒有,要不然就沒有右值引用什么事了。當(dāng)某一個(gè)變量/對象出了作用域就不存在了,這種情況就不能用引用返回了,這是眾所周知的。
而當(dāng)需要返回的變量/對象是一個(gè)特別復(fù)雜的結(jié)構(gòu),就必須要傳值返回,就要進(jìn)行深拷貝,效率將大大降低。
而右值引用就將解決這個(gè)問題。
為了解釋它解決問題的原理,需要先自己實(shí)現(xiàn)一個(gè)string類,以便觀察它解決的具體過程。string類代碼如下:
namespace sny
{
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)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷貝構(gòu)造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷貝" << endl;
string tmp(s._str);
swap(tmp);
}
// 賦值重載
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷貝" << 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 = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做標(biāo)識的\0
};
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
sny::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
現(xiàn)在假設(shè)讀者對上面類中的所有函數(shù)都已經(jīng)理解了,然后再看下面這行代碼:
int main()
{
sny::string ret = sny::to_string(-1234);
return 0;
}
這里注意,本來調(diào)用to_string函數(shù)最后返回值的時(shí)候,str到ret應(yīng)該是兩次拷貝----str出了作用域要銷毀,所以會產(chǎn)生一個(gè)臨時(shí)變量,然后再用這個(gè)變量拷貝給ret。但是編譯器對于這種連續(xù)的拷貝作了優(yōu)化,所以最后只會有一次拷貝。
另外,一定是連續(xù)的拷貝才會被優(yōu)化,否則不會優(yōu)化,如下:
注意,這里出現(xiàn)三次而不是兩次拷貝構(gòu)造,是因?yàn)樯厦娲a中的拷貝構(gòu)造使用的是現(xiàn)代寫法實(shí)現(xiàn)的,里面多了一次拷貝。
但是,就算是作了優(yōu)化,但最后還是有一次拷貝,C++11的解決方法是什么呢?
答案是移動(dòng)構(gòu)造:
//移動(dòng)構(gòu)造
string(string&& s)
{
cout << "string(string&& s) -- 移動(dòng)拷貝" << endl;
swap(s);
}
這段代碼要放在上面的string類中。
這里再補(bǔ)充一點(diǎn):
C++11中的右值又分為純右值和將亡值。
純右值一般指的是內(nèi)置類型表達(dá)式的值;將亡值一般指的是自定義類型表達(dá)式的值。
顧名思義,將亡值就是快要嗝兒屁的值。既然都要嗝兒屁了,就沒必要再將其內(nèi)容拷貝一遍了,直接將其搶過來就行。(但這里的搶不是單純地?fù)?,而是將自己和它做交換)
所以,上面例子中,編譯器做出的優(yōu)化可以分為兩部分----兩次連續(xù)的構(gòu)造合并為一次,以及返回的str被識別為右值。所以,就調(diào)用了移動(dòng)構(gòu)造。
但是這個(gè)東西要慎用,因?yàn)樽约旱臇|西也會被換給它,然后被它一起帶到墳?zāi)估?。如下?br>
同樣的,賦值構(gòu)造也可以采用類似的做法:
// 移動(dòng)賦值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移動(dòng)賦值" << endl;
swap(s);
return *this;
}
注意?。?!右值引用的工作機(jī)制是借助于移動(dòng)語義(移動(dòng)構(gòu)造或移動(dòng)賦值),進(jìn)行資源轉(zhuǎn)移,而不是延長將亡值的生命周期!(有一些文章中說延長生命周期毫無疑問是錯(cuò)的)
5.4 萬能引用
雖然右值引用很厲害,但是左值引用只能引用左值,右值引用只能引用右值。當(dāng)兩個(gè)變量/類型需要完成相同的功能,但差別僅僅是一個(gè)是左值,一個(gè)是右值時(shí),就不得不寫兩個(gè)版本的功能函數(shù),造成了代碼冗余。
所以,C++11又增加了一個(gè)萬能引用,可以接收左值和右值,如下:
template<typename T>
void PerfectForward(T&& t)
{
//
}
int main()
{
PerfectForward(10);// 右值
int a;
PerfectForward(a);// 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b);// const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
但是注意,如果傳過去的參數(shù)屬性為const,則模板內(nèi)不能對其更改;若沒有const屬性的參數(shù),則可以修改,如下:
5.5 完美轉(zhuǎn)發(fā)
如果對于上面的模板中接收到的值,再一次傳參,會怎么樣呢?
首先,鋪墊一個(gè)小知識點(diǎn),任何右值引用的本質(zhì)都是左值,比如:
int&& rr=10;
rr本身就是一個(gè)左值。因?yàn)?0作為一個(gè)常量本來沒有為其準(zhǔn)備存儲的地址,但是一旦被右值引用,就必須在內(nèi)存中為它開一個(gè)空間進(jìn)行存儲,這時(shí)rr就成了左值。
所以,在上面的代碼中,所有的t本質(zhì)都是左值,如下:
如果將其move一下,就全都變成右值了。那到底如何將其以原來的屬性傳參呢?
這個(gè)時(shí)候就要用到完美轉(zhuǎn)發(fā)了,它可以發(fā)在傳參的過程中保留對象原生類型屬性,如下:
完美轉(zhuǎn)發(fā)原理如下:文章來源:http://www.zghlxwxcb.cn/news/detail-413498.html
//完美轉(zhuǎn)發(fā)原型
T&& forward(T&& t) { return static_cast<T&&>(t); }
// 用法: template<typename T>
void func1(T && val) { func2(std::forward<T>(val)); }
// 當(dāng)傳入左值引用
void func1(T& && val) { func2(static_cast<T& &&>(val)); }
// 引用折疊后:
void func1(T& val) { func2(static_cast<T&>(val)); }
// 當(dāng)傳入右值引用
void func1(T&& && val) { func2(static_cast<T&& &&>(val)); }
// 引用折疊后:
void func1(T&& val) { func2(static_cast<T&&>(val)); }
本篇完,青山不改,綠水長流!文章來源地址http://www.zghlxwxcb.cn/news/detail-413498.html
到了這里,關(guān)于【C++11那些事兒(一)】的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!