天天看點

C++11部分特性

初識C++的時候,覺得會個STL就差不多了,後來發現了C++11這個東西,以及C++14,C++17QAQ,看了一下,好高深不學,emmmm真香= =

這裡就隻講一下對ACM寫代碼有很高幫助的部分特性,因為大部分OJ和比賽隻支援11,是以14和17就不講了,然後還有C++11增加的元組tuple和幾個容器另談。

一、nullptr

在之前版本的c++中,NULL的值其實就是0,因為其實就是#define NULL 0,有些時候是((void *) 0)emmm,不說那些廢話了。是以這些就會遇到一個問題,

例如下面這兩個函數

1 int fun(char* ch){}
2 int fun(int num){}      

當char ch = NULL,這樣一個值去代入函數時,編譯會調用下面那個函數,而不是第一個。而nullptr類型是nullptr_t,就是專門為了差別空指針和0,是以以後寫代碼nullptr代替NULL就能行了。

二、constexpr

constexpr變量必須是一個常量,必須用常量表達式來初始化,例如下面代碼

1 const int a = 10;
2 int b = 10;
3 constexpr int d = 10;  //正确
4 constexpr int c = a + 10;  //正确
5 constexpr int e = b;  //錯誤      

 值得一提的是如果成員函數标記為constexpr,則預設其是内聯函數,如果變量聲明為constexpr,則預設其是const,這個會在構造函數中用到。

三、類型推導

auto

其實以前的c++中就有auto,但是emmm,廢話不說,直接說内容吧,有了auto就很友善了。比如下面這個代碼

1 map<int,int>::iterator it = mp.begin();
2 auto it = mp.begin();      

誰友善一眼便知道了吧,auto可以把自動推導成變量或者函數。

比如

1 int fun(int i){
2     cout << i << endl;   
3 }
4 
5 function<int(int)> f;
6 auto a = fun;      

這裡auto其實就和自動推導成了function<int(int)>類型。值得注意的時,定義auto類型變量必須賦初值,不然則會編譯錯誤。還有就是函數的傳回值不能直接用auto代替,例如

auto fun(int i){
}      

這是會報錯的,但是不是函數的傳回值就不能是auto類型了呢,其實是可以的,但隻是不能直接這樣定義,下面我們會講到如何把函數傳回值定義為auto。

decltype

顧名思義,就是推導一個變量的類型是啥,和sizeof用法類似,它的出現就是為了彌補auto的缺陷。

auto a = 1,b = 2;
    decltype(a+b) z;      

拖尾傳回類型

上面不是說怎麼把函數值弄為auto嗎,其實在以前得c++中也可以用typename,比如

1 template<typename R, typename T, typename U>
2 R add(T x, U y) {
3     return x+y
4 }      

 當有多個這種函數的時候,就會顯得代碼很冗長,很不人性化qwq,是以就出現了拖尾傳回類型,例如下面這個代碼

1 auto fun(int i) -> bool{
2     return i&1;
3 }      

這個代碼編譯是沒有問題的,後面的->bool就是說明函數的傳回類型是布爾類型,是以就可以使用auto作為函數類型,當然也可以和decltype連用寫出下面的代碼。

1 template<typename T>
2 auto fun(T i) -> decltype(i*i){
3     return i*i;
4 }      

簡潔易懂,而且看着特别舒服有木有。

四、區間疊代

在其他語言裡面經常可以看到for(a : b)這樣的循環用法,而在c++裡面,你要是想對一個容器進行區間疊代,你得這樣寫

1 for(vector<int>::iterator iter = vec.begin(); iter != vec.end(); iter++)      

雖然看上去很整潔,但也太不人性化了吧,是以11就引用了:進行區間疊代,現在你可以直接用auto和:寫出下面的代碼

1 for(auto &iter : vec)      

這真的很coool

五、尖括号'>'

當多個容器疊加的時候,我們都是用空格将>>區分開,因為在以前版本的c++中>>是會看成右移操作符号的,但是11更新後,你完全不用擔心這個問題,例如下面代碼,在11中編譯是沒有任何問題的

1 vector<vector<int>> q;      

六、初始值

在以前的c++版本中,當我們需要對一個結構體或者類賦初值,我們得寫一個函數,例如

1 struct node{
2     int a,b;
3     node(int _a,int _b){this->a = _a,this->b = _b;}
4     node(int _a,int _b):a(_a),b(_b){}
5 };      

但現在,c++11有五種不同的方式對各種變量初始化,你可以直接用大括号指派,下面提供了幾種不同的結構體指派方法。

1 struct node{
2     int a,b;
3 };
4 
5 node asd{1,2};
6 node as = {1,2};      

對于其他變量和容器也适用。

七、類型别名以及預設别名

類型别名

typedef大家肯定使用過,一般将一個類或者結構體做别名,但對于容器,比如下面用法

1 template<typename T>
2 typedef vector<T> qwq;      

是不合法的,是以c++11引入了using,和typedef功能相同。

1 template<typename T>
2 using asd = vector<T>;      

當需要使用vector時候,就直接調用即可。

1 asd<long long> vec;      

預設别名

對于某個函數

1 template<typename T1,typename T2>
2 auto fun(T1 a,T2 b)->bool{
3     return a < b;
4 }      

但是如果你想制定預設模闆參數類型怎麼辦,c++11提供了一種便利,可以直接指定預設參數

1 template<typename T1 = int,typename T2 = int>
2 auto fun(T1 a,T2 b)->bool{
3     return a < b;
4 };      

八、lambda表達式

Lambda表達式是用來建立匿名函數的。什麼叫做匿名函數,就是沒有名字qwq,那麼為什麼要用到匿名函數呢,舉個例子,比如sort的第三個參數,謂詞函數,一般我們會寫一個比較函數,但這樣的後果就是,如果有多個sort你就得,每個函數去找他的比較函數,實屬麻煩。各位看下面這個例子

1 sort(a.begin(),a.end(),[](int a,int b)->bool{return a < b;});      

就算你不懂什麼是lambda表達式,但你也能猜到這個sort就是按照a<b比較吧。那麼lambda表達式是什麼樣子的呢。大緻就是

1 [capture](parameters)opt->return-type{body}      

emmm 後面那一截和普通函數沒什麼差別,參數,傳回值類型,函數體。和函數最大的不同就是匿名函數是臨時函數,沒有函數名,及拿及用,當然你也可以用function儲存一個匿名函數。

至于opt是個什麼東西= =下面說,這個一般不會用上。

其中:

1.傳回值類型->return-type可以省略,由語言自動推導,但隻限于lambda表達式語句簡單。

2.引入lambda表達式的前導符是一對方括号,叫做lambda引入符。lambda表達式可以使用與其相同範圍内的變量,一共有下面這麼幾種方式捕獲變量

[]//不捕獲任何外部變量

[=]//以值形式捕獲所有外部變量

[&]//以引用方式捕獲所有外部變量

[x,&y]//x以值形式捕獲,y以引用形式捕獲

[=,&x]//x以引用形式捕獲,其餘變量以值捕獲

[&,x]//x以值形式捕獲,其餘變量以引用形式捕獲

[this]捕獲目前類的指針。捕獲this的目的是可以在lambda中使用目前類的成員函數和成員變量。

對于[=]和[&],lambda可以直接使用this指針,但是對于[]的形式,如果要使用this指針,必須顯式傳入

3.opt函數選項

可以選填mutable,exception,attribute。

mutable說明lambda表達式體内的代碼可以修改被捕獲的變量,并且可以通路被捕獲的對象的non-const方法。

exception說明lambda表達式是否抛出異常以及何種異常

attribute用來聲明屬性

4.lambda表達式不能直接被指派,閉包類型禁用了指派操作符号,但是可以用lambda表達式去初始化另一個lambda表達式,例如

1 auto a = []{cout <<1 << endl;};
2 auto b = a;        

也可以吧lambda表達式指派給相對應的函數指針,例如

1 function<int(int)> fun = [](int a){return a;};      

那麼就可以很友善的利用lambda表達式填充各種謂詞函數了,例如下面便是一個斐波那契數列

1     array<int,10> a;
2     auto f0 = 0, f1 = 1;
3     generate(a.begin(),a.end(),[&f0,&f1]()->int{int v = f1;f1 += f0, f0 = v;return v;});
4     for_each(a.begin(),a.end(),[](int v){cout << v << " ";});
5     cout << endl;;      

代碼一眼看上去就能知道意思,無需定義額外函數。大部分的STL算法,都可以搭配lambda表達式來實作想要的效果。

九、右值引用和move

什麼是右值引用,先看一個例子。

1 string a(x);
 2 string b(x+y);
 3 string c(fun());
 4 
 5 //如果使用以下拷貝構造函數
 6 string(const string& str){
 7     size_t size = strlen(str.data)+1;
 8     data = new char[size];
 9     memcpy(data,str.data,size);
10 }      

則隻有第一行的x深度拷貝有必要,因為其他地方還可能會用到x,x就是一個左值。但第二行和第三行的參數則是右值,因為表達式産生的匿名string對象,之後沒法再用。

c++11引入了一種機制“右值引用”,用&&來表示右值引用,以便我們通過重載直接使用右值參數,例如下面這個構造函數:

1 string(string&& that){
2     data = that.data;
3     that.data = 0;
4 }      

我們沒有深度拷貝堆記憶體中的資料,而是僅僅複制了指針,并把源對象的指針置空。事實上,我們“偷取”了屬于源對象的記憶體資料。由于源對象是一個右值,不會再被使用,是以客戶并不會覺察到源對象被改變了。在這裡,我們并沒有真正的複制,是以我們把這個構造函數叫做“轉移構造函數”,他的工作就是把資源從一個對象轉移到另一個對象,而不是複制他們。

那麼指派操作符就可以寫成

1 string& operator=(string that){
2     std::swap(data, that.data);
3     return *this;
4 }      

注意到我們是直接對參數that傳值,是以that會像其他任何對象一樣被初始化,那麼确切的說,that是怎樣被初始化的呢?對于C++ 98,答案是複制構造函數,但是對于C++ 11,編譯器會依據參數是左值還是右值在複制構造函數和轉移構造函數間進行選擇。

如果是a=b,這樣就會調用複制構造函數來初始化that(因為b是左值),指派操作符會與新建立的對象交換資料,深度拷貝。這就是copy and swap 慣用法的定義:構造一個副本,與副本交換資料,并讓副本在作用域内自動銷毀。這裡也一樣。

如果是a = x + y,這樣就會調用轉移構造函數來初始化that(因為x+y是右值),是以這裡沒有深度拷貝,隻有高效的資料轉移。相對于參數,that依然是一個獨立的對象,但是他的構造函數是無用的(trivial),是以堆中的資料沒有必要複制,而僅僅是轉移。沒有必要複制他,因為x+y是右值,再次,從右值指向的對象中轉移是沒有問題的。

轉移左值是十分危險的,但是轉移右值卻是很安全的。如果C++能從語言級别支援區分左值和右值參數,我就可以完全杜絕對左值轉移,或者把轉移左值在調用的時候暴露出來,以使我們不會不經意的轉移左值。

複制構造函數執行的是深度拷貝,因為源對象本身必須不能被改變。而轉移構造函數卻可以複制指針,把源對象的指針置空,這種形式下,這是安全的,因為使用者不可能再使用這個對象了。

有時候,我們可能想轉移左值,也就是說,有時候我們想讓編譯器把左值當作右值對待,以便能使用轉移構造函數,即便這有點不安全。出于這個目的,C++ 11在标準庫的頭檔案< utility >中提供了一個模闆函數std::move。實際上,std::move僅僅是簡單地将左值轉換為右值,它本身并沒有轉移任何東西。它僅僅是讓對象可以轉移。

1 unique_ptr<Shape> a(new Triangle);
2 unique_ptr<Shape> b(a);              //false
3 unique_ptr<Shape> c(move(a));   //true      

請注意,第三行之後,a不再擁有Triangle對象。不過這沒有關系,因為通過明确的寫出move(a),我們很清楚我們的意圖:親愛的轉移構造函數,你可以對a做任何想要做的事情來初始化c;我不再需要a了,對于a,您請自便。

當然,如果你在使用了mova(a)之後,還繼續使用a,那無疑是搬起石頭砸自己的腳,還是會導緻嚴重的運作錯誤。

總之,move(val)将左值轉換為右值(可以了解為一種類型轉換),使接下來的轉移成為可能。

十、正規表達式

正規表達式描述了一種字元串比對的模式。一般使用正規表達式主要是實作下面三個需求:

1) 檢查一個串是否包含某種形式的子串;

2) 将比對的子串替換;

3) 從某個串中取出符合條件的子串。

C++11 提供的正規表達式庫操作 string 對象,對模式 std::regex (本質是 basic_regex)進行初始化,通過 std::regex_match 進行比對,進而産生 smatch (本質是 match_results 對象)。

我們通過一個簡單的例子來簡單介紹這個庫的使用。考慮下面的正規表達式:

[a-z]+.txt: 在這個正規表達式中, [a-z] 表示比對一個小寫字母, + 可以使前面的表達式比對多次,是以 [a-z]+

能夠比對一個及以上小寫字母組成的字元串。在正規表達式中一個 . 表示比對任意字元,而 . 轉義後則表示比對字元 . ,最後的 txt

表示嚴格比對 txt 這三個字母。是以這個正規表達式的所要比對的内容就是檔案名為純小寫字母的文本檔案。

regex_match 用于比對字元串和正規表達式,有很多不同的重載形式。最簡單的一個形式就是傳入string 以及一個 regex 進行比對,當比對成功時,會傳回 true,否則傳回 false。例如:

1 string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};//`\` 會被作為字元串内的轉義符,需要對 `\` 進行二次轉義,進而有 `\\.`
2 regex txt_regex("[a-z]+\\.txt");
3 for (const auto &fname: fnames)
4     cout << fname << ": " << regex_match(fname, txt_regex) << endl;      

另一種常用的形式就是依次傳入 string/smatch/regex 三個參數,其中 smatch 的本質其實是 match_results,在标準庫中, smatch 被定義為了 match_results,也就是一個子串疊代器類型的 match_results。使用 smatch 可以友善的對比對的結果進行擷取,例如:

1 regex base_regex("([a-z]+)\\.txt");
 2 smatch base_match;
 3 for(const auto &fname: fnames) {
 4     if (regex_match(fname, base_match, base_regex)) {
 5         // sub_match 的第一個元素比對整個字元串
 6         // sub_match 的第二個元素比對了第一個括号表達式
 7         if (base_match.size() == 2) {
 8             string base = base_match[1].str();
 9             cout << "sub-match[0]: " << base_match[0].str() << endl;
10             cout << fname << " sub-match[1]: " << base << endl;
11         }
12     }
13 }      

代碼運作結果為

foo.txt: 1
bar.txt: 1
test: 0
a0.txt: 0
AAA.txt: 0
sub-match[0]: foo.txt
foo.txt sub-match[1]: foo
sub-match[0]: bar.txt
bar.txt sub-match[1]: bar      

十一、構造函數&繼承&修飾符

C++11 引入了委托構造的概念,這使得構造函數可以在同一個類中一個構造函數調用另一個構造函數,進而達到簡化代碼的目的:

委托構造

1 class Base {
 2 public:
 3     int value1;
 4     int value2;
 5     Base() {
 6         value1 = 1;
 7     }
 8     Base(int value) : Base() {  // 委托 Base() 構造函數
 9         value2 = 2;
10     }
11 };      

繼承構造

在繼承體系中,如果派生類想要使用基類的構造函數,需要在構造函數中顯式聲明。

假若基類擁有為數衆多的不同版本的構造函數,這樣,在派生類中得寫很多對應的“透傳”構造函數。如下:

1 struct A
 2 {
 3   A(int i) {}
 4   A(double d,int i){}
 5   A(float f,int i,const char* c){}
 6   //...等等系列的構造函數版本
 7 };
 8 struct B:A
 9 {
10   B(int i):A(i){}
11   B(double d,int i):A(d,i){}
12   B(folat f,int i,const char* c):A(f,i,e){}
13   //......等等好多個和基類構造函數對應的構造函數
14 };      

constexpr構造

如果想要使得函數擁有編譯時計算的能力,則使用關鍵字constexpr

1 class Square {
 2 public:
 3     constexpr Square(int e) : edge(e){};
 4     constexpr int getArea() {return edge * edge;}
 5 private:
 6     int edge;
 7 };
 8  
 9 int main() {
10     Square s(10);
11     cout << s.getArea() << endl;
12     return 0;
13 }      

十二、删除的函數以及新增的函數

字元串和數值類型轉換

itoa等等字元串和數值類型的轉換成為曆史。

提供了to_string和stox方法,将字元串和數值自由轉換;

1 //數值轉字元串
 2 std::string to_string(int value);
 3 std::string to_string(long int value);
 4 std::string to_string(long long int value);
 5 std::string to_string(unsigned int value);
 6 std::string to_string(unsigned long long int value);
 7 std::string to_string(float value);
 8 std::string to_string(double value);
 9 std::wstring to_wstring(int value);
10 std::wstring to_wstring(long int value);
11 std::wstring to_wstring(long long int value);
12 std::wstring to_wstring(unsigned int value);
13 std::wstring to_wstring(unsigned long long int value);
14 std::wstring to_wstring(float value);
15 std::wstring to_wstring(double value);
16 
17 //字元串轉數值
18 std::string str = "1000";
19 int val = std::stoi(str);
20 long val = std::stol(str);
21 float val = std::stof(str);      

c++11還提供了字元串(char*)轉換為整數和浮點類型的方法:

1 atoi: 将字元串轉換為 int
2 atol: 将字元串轉換為long
3 atoll:将字元串轉換為 long long
4 atof: 将字元串轉換為浮點數      

随機數函數

生成随機數,免去了以前需要自行調用srand初始化種子的步驟,因為有時候忘記初始化導緻結果錯誤。

std::random_device rd;

int randint = rd();

std::chrono

擷取時間函數,比以前友善許多。

1 std::chrono::duration<double> duration //時間間隔
 2 
 3 std::this_thread::sleep_for(duration); //sleep
 4 
 5 LOG(INFO) << "duration is " << duration.count() << std::endl;
 6 
 7 std::chrono::microseconds  //微秒
 8 
 9 std::chrono::seconds //秒
10 
11 end = std::chrono::system_clock::now(); //擷取目前時間      

all_of(),any_of(),none_of(),copy_n(),iota()

1 #include<algorithm>
 2 #include<numeric>
 3 
 4 all_of(first,first+n,ispositive());//false
 5 
 6 any_of(first,first+n,ispositive());//true
 7 
 8 none_of(first,first+n,ispositive());//false
 9 
10 int source[5]={0,12,34,50,80};
11 int target[5];
12 //從source拷貝5個元素到target
13 copy_n(source,5,target);
14 
15 //iota()算法可以用來建立遞增序列,它先把初值指派給 *first,然後用前置 ++ 操作符增長初值并指派到給下一個疊代器指向的元素,如下:
16 
17 inta[5]={0};
18 charc[3]={0};
19 iota(a,a+5,10);//{10,11,12,13,14}
20 iota(c,c+3,'a');//{'a','b','c'}      

原子變量和正規表達式

std::atomic<XXX>

用于多線程資源互斥操作,屬c++11重大提升,多線程原子操作簡單了許多。

C正則(regex.h)和boost成為曆史

hash進入STL

新增基于hash的無序容器。

對于容器的emplace

作用于容器,差別于push、insert等,如push_back是在容器尾部追加一個容器類型對象,emplace_back是構造1個新對象并追加在容器尾部

對于标準類型沒有變化,如std:;vector<int>,push_back和emplace_back效果一樣

如自定義類型class A,A的構造函數接收一個int型參數,

那麼對于push_back需要是:

std::vector<A> vec;
A a(10);
vec.push_back(a);      

對于emplace_back則是:

std::vector<A> vec;
vec.emplace_back(10);      

避免無用臨時變量。比如上面例子中的那個a變量。

對于容器的shrink_to_fit

這個改進還是有點意義的,日常程式應該能減少不少無意義的記憶體空間占用

push、insert這類操作會觸發容器的capacity,即預留記憶體的擴大,實際開發時往往這些擴大的區域并沒有用途

std::vector<int> v{1, 2, 3, 4, 5};
    v.push_back(1);
    std::cout << "before shrink_to_fit: " << v.capacity() << std::endl;
    v.shrink_to_fit();
    std::cout << "after shrink_to_fit: " << v.capacity() << std::endl;      

可以試一試,減少了很多。

十三、動态指針&智能指針

C++98标準庫中提供了一種唯一擁有性的智能指針std::auto_ptr,該類型在C++11中已被廢棄,因為其“複制”行為是危險的。

auto_ptr 的危險之處在于看上去應該是複制,但實際上确是轉移。調用被轉移過的auto_ptr 的成員函數将會導緻不可預知的後果。是以你必須非常謹慎的使用auto_ptr ,如果他被轉移過。

C++ 11中,std::auto_ptr< T >已經被std::unique_ptr< T >所取代,後者就是利用的右值引用。

十四、會使用的庫

<utility>

<unordered_map>

<unordered_set>

<random>

<tuple>

<array>

<numeric>

需要深入了解的話就自己去找資料了吧qwq,我覺得用的最多的可能還是lambda表達式和區間疊代了吧,越來越像python了,qwq希望有所幫助。

繼續閱讀