您好,登錄后才能下訂單哦!
C++11的特性有哪些?很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
一、列表初始化
1.1 C++98中,標準允許使用花括號{}對數組元素進行統一的列表初始值設定。
int array1[] = {1,2,3,4,5}; int array2[] = {0};
對對于一些自定義類型,卻不行.
vector<int> v{1,2,3,4,5};
在C++98中這樣無法通過編譯,因此需要定義vector之后,在使用循環進行初始賦值。
C++11擴大了用初始化列表的使用范圍,讓其適用于所有的內置類型和自定義類型,而且使用時,=可以不寫
// 內置類型 int x1 = {10}; int x2{10} // 數組 int arr1[5] {1,2,3,4,5} int arr2[]{1,2,3,4,5}; // 標準容器 vector<int> v{1,2,3} map<int,int> m{{1,1},{2,2}} // 自定義類型 class Point { int x; int y; } Power p{1,2};
1.2 多個對象的列表初始化
給類(模板類)添加一個帶有initializer_list類型參數的構造函數即可支持多個對象的,列表初始化.
#include<initializer_list> template<class T> class Vector{ public: Vecto(initializer_list<T> l) :_capacity(l.size()) ,_size(0){ _array = new T[_capacity]; for(auto e : l) _array[_size++] = 3; } private; T* _array; size_t _capacity; size_t _size; };
二、變量類型推導
2.1 auto
在C++中,可以使用auto來根據變量初始化表達式類型推導變量的實際類型,簡化程序的書寫
// 不使用auto需要寫很長的迭代器的類型 map<string,string> m; map<string,string>::iterator it1 = m.begin(); // 使用auto就很簡單 auto it2 = m.begin();
2.1 decltype 類型推導
auto使用時,必須對auto聲明的類型進行初始化,否則編譯器不能推導出auto的實際類型。
但是有些場景可能需要根據表達式運行后的結果進行類型推導。因為編譯時,代碼不會運行,auto也就…
template<class T1,class T2> T1 Add(const T1& a,const T2& b){ return a + b; }
如果用加完后的結果作為函數的返回類型,可能會出錯,這就需要程序運行完后才能知道結果的實際類型,即RTTI(運行時類型識別)
decltype可以根據表達式的實際類型推演出定義變量時所用的類型
// 推演表達式作為變量的定義類型 int a = 1,b=2; decltype(a+b) c; cout<<typeid(c).name()<<endl; // 推演函數的返回值類型 void GetMemory(size_t size){ return malloc(size); } cout<<typeid(decltype(GetMemory)).name()<<endl;
三、基于范圍for的循環
vector<int> v{1,2,3,4,5}; for(const auto& e : v) cout<<e<<' '; cout<<endl;
四、final和override
在我的多態的文章中有介紹:https://www.jb51.net/article/162078.htm
五、委派構造函數
委派構造函數可以通過委派其它構造函數,使多構造函數的類編寫更加容易
class Info { public; Info() :_type(0) ,_name('s') {} Info(int type) :_type(type) ,_name('a') {} Info(char a) :_type(0) ,_name(a) {} pirvate; int _type; char _name; };
上面的構造函數除了初始化列表不同之外,其它部分都是類似的,代碼重復,可以使用委派構造函數
委派構造函數就是把構造的任務委派給目標構造函數來完成類的構造
class Info { // 目標構造函數 public: Info() :_type(0) ,_a('a') {} // 委派構造函數 Info(int type) :Info() { _type = type; } private; int _type = 0; char _a = 'a'; };
在初始化列表中調用“基準版本”的構造函數稱為委派構造函數,而被調用的“基準版本”則稱為目標構造函數
六、默認函數控制
在C++中對于空類,編譯器會生成一些默認的成員函數,如果在類中顯式定義了,編譯器就不會重新生成默認版本。但是如果在一個類中聲明了帶參的構造函數,如果又需要定義不帶參的實例化無參的對象。這時候編譯器是有時生成,有時不生成,就會造成混亂,C++11可以讓程序員自己控制是否需要編譯器生成。
6.1 顯式缺省函數
在C++11中,可以在默認函數定義或聲明時加上=default,來讓編譯器生成該函數的默認版本。
class A { public: A(int a) :_a(a) {} A() = default; // 顯式缺省構造函數 A& operator=(const A& a); // 在類中聲明,在類外定義時,讓編譯器生成默認賦值運算符重載 private: int _a; }; A& A::operator=(const A& a) = default;
6.2 刪除默認函數
要想限制一些默認函數的生成,在C++98中,可以把該函數設為私有,不定義,這樣,如果有人調用就會報錯。在C++11中,可以給該函數聲明加上=delete就可以。
class A { A(int a) :_a(a) {} A(constA&) = delete; // 禁止編譯器生成默認的拷貝構造函數 private: int _a; };
七、右值引用
7.1 移動語義
class String { public: String(char* str = '") { if(str == nullptr) _str = ""; _str = new char[strlen(str)+1]; strcpy(_str,str); } String(const String& s) :_str(new char[strlen(c._str)+1]) { strcpy(_str,s._str); } ~String() { if(_str) delete[] _str; } private: char* _str; }; String GetString(char* pStr) { String strTemp(pStr); return strTemp; } int main() { String s1("hello"); String s2(GetString("world")); return 0; }
在上面的代碼中,GetString函數返回的臨時對象,將s2拷貝成功之后,立馬銷毀了(臨時對象
的空間被釋放);而s2拷貝構造的時,又需要分配空間,一個剛釋放,一個又申請,有點多此一舉,那能否把GetString返回的臨時對象的空間直接交給s2呢?這樣s2也不需要重新開辟空間了。
移動語義:將一個對象資源移動到另一個對象中的方式,在C++中要實現移動語義,必須使用右值引用.
7.2 C++中的右值
右值引用,顧名思義就是對右值的引用。在C++中右值由純右值和將亡值構成。
7.3 右值引用
格式:類型&& 應用變量名字 = 實體;
使用場景:
1、與移動語義相結合,減少必須要的資源的開辟,提高運行效率
String&& GetString(char* pStr) { String strTemp(pStr); return strTemp; } int main() { String s1("hello"); String s2(GetString("world")); return 0; }
2、給一個匿名對象取別名,延長匿名對象的生命周期
String GetString(char* pStr) { return String(pStr); } int main() { String&& s = GetString("hello"); return 0; }
注意:
int a = 10; int&& a1; // 未初始化,編譯失敗 int&& a2 = a; // 編譯失敗,a是一個左值 // 左值是可以改變的值
7.4 std::move()
C++11中,std::move()函數位于頭文件中,它可以把一個左值強制轉化為右值引用,通過右值引用使用該值,實現移動語義。該轉化不會對左值產生影響.
注意:其更多用在生命周期即將結束的對象上。
7.5 移動語義中要注意的問題
1、在C++11中,無參構造函數/拷貝構造函數/移動構造函數實際上有三個版本
Object() Object(const T&) Object(T&&)
2、如果將移動構造函數聲明為常右值引用或者返回右值的函數聲明為常量,都會導致移動語義無法實現
String(const String&&); const String GetString();
3、C++11默認成員函數,默認情況下,編譯器會隱士生成一個移動構造函數,是按照位拷貝來進行。因此在涉及到資源管理時,最好自己定義移動構造函數。
class String { public: String(char* str = "") { if(str == nullptr) str = ""; _str = new char[strlen(str)+1]; strcpy(_str,str); } // 拷貝構造 // String s(左值對象) String(const String& s) :_str(new char[strlen(s._str) + 1]) { strcpy(_str,s_str); } // 移動構造 // String s(將亡值對象) String(String&& s) :_str(nullptr) { swap(_str,s._str); } // 賦值 String& operator=(const String& s) { if(this != &s) { char* tmp = new char[strlen(s._str)+1]; stcpy(tmp,s._str); delete[] _str; _str = tmp; } return *this; } // 移動賦值 String& operator=(String&& s) { swap(_str,s._str); return *this; } ~String() { if(_str) delete[] _str; } // s1 += s2 體現左值引用,傳參和傳值的位置減少拷貝 String& operator+=(const String& s) { // this->Append(s.c_str()); return *thisl } // s1 + s2 String operator+(const String& s) { String tmp(*this); // tmp.Append(s.c_str()); return tmp; } const char* c_str() { return _str; } private: char* _str; }; int main() { String s1("hello"); // 實例化s1時會調用移動構造 String s2("world"); String ret ret = s1 + s2 // +返回的是臨時對象,這里會調用移動構造和移動賦值,減少拷貝 vector<String> v; String str("world"); v.push_back(str); // 這里調用拷貝構造函數 v.push_back(move(str)); // 這里調用移動構造,減少一次拷貝 return 0; }
總結:
左值:可以改變的值;
右值: 不可以改變的值(常量,表達式返回值,臨時對象)
左值引用: int& aa = a; 在傳參和傳值的位置使用,減少拷貝,提高效率
右值引用: int&& bb = 10; 在傳值返回和將亡值傳參時,通過調用移動構造和移動賦值,減少拷貝,提高效率。
const 左值引用可以引用右值
右值引用可以應用move后的左值
7.6 完美轉發
完美轉發是指在函數模板中,完全按照模板的參數的類型,將參數傳遞給函數模板中調用的另外一個函數
void Func(int x) { // ...... } template<typename T> void PerfectForward(T t) { Fun(t); }
PerfectForward為完美轉發的模板函數,Func為實際目標函數,但上面的轉發還不夠完美,完美轉發是目標函數希望將參數按照傳遞給轉發函數的實際類型轉給目標函數,而不產生額外開銷,就好像沒有轉發者一樣.
所謂完美:函數模板在向其他函數傳遞自身形參時,如果相應實參是左值,就轉發左值;如果是右值,就轉發右值。(這樣是為了保留在其他函數針對轉發而來的參數的左右值屬性進行不同處理,比如參數為左值時實施拷貝語義、參數為右值時實施移動語義)
在C++11中,通過forward函數來實現完美轉發。
void Fun(int &x){cout << "lvalue ref" << endl;} void Fun(int &&x){cout << "rvalue ref" << endl;} void Fun(const int &x){cout << "const lvalue ref" << endl;} void Fun(const int &&x){cout << "const rvalue ref" << endl;} template<typename T> void PerfectForward(T &&t){Fun(std::forward<T>(t));} int main() { PerfectForward(10); // rvalue ref int a; PerfectForward(a); // lvalue ref PerfectForward(std::move(a)); // rvalue ref const int b = 8; PerfectForward(b); // const lvalue ref PerfectForward(std::move(b)); // const rvalue ref return 0; }
八、lambda表達式
在C++98中,如果想對一個數據集合中的元素進行排序,可以使用std::sort()方法,但其默認按照小于比較,如果想排降序,則要傳入第三個參數,可以使用std::greater()的比較方法,
vector<int> v{1,4,3,2,7,6,5}; // 默認按照小于比較,結果是升序 sort(v.begin(),v.end()); // 傳入第三個模板參數std::greater<T>(),按照大于比較,默認是降序 sort(v.begin(), v.end(),greater<int>());
但是該方法只支持內置類型,對于用于自定義的類型就無能為力了,這是就需要用于自定義排序時的規則。目前我們可以通過函數指針,仿函數,lambda來解決。
1、lambda 表達式語法
[捕捉列表](參數列表)mutable->返回值類型{函數體}
捕捉列表:該列表出現在lambda函數的開始位置,編譯器根據[]來判斷接下來的代碼是否為lambda函數,捕捉列表可以捕捉上下文中的變量供lambda函數使用
參數列表:與普通函數的參數列表一致。則可以連同()一起省略
mutable:默認情況下,lambda函數總是一個const函數,mutable可以取消其常量性。使用該修飾符,參數列表不可以省略(即使參數列表為空)
->返回值類型。用于追蹤返回值類型。沒有返回值時可以省略。返回值類型明確的情況下,也可以省略
{函數體}:在該函數體,除了可以使用參數外,也可以使用捕捉到的所有變量
!!!在lambda函數定義中,參數列表和返回值類型都是可選部分,而捕捉列表和函數體可以為空。
int main() { // 最簡單的lambda表達式 []{}; // 省略參數列表和返回值類型,返回值類型有編譯器推演為int int a=3,b=4; [=]{return a+3;}; // 省略返回值類型 auto fun1 = [&](int c){b = a + c;}; // 各部分完整的lambda函數 auto fun2 = [=,&b](int c)->int(return += a + c;); // 復制捕捉x int x = 10; auto add_x = [x](int a)mutable{x *= 2; return a + x;}; return 0; }
2、捕獲列表說明
捕獲列表描述了上下文中那些數據可以被lambda使用,以及使用的方式傳值還是引用
a [var]:表示值傳遞方式捕獲變量var b [=]:表示值傳遞方式捕獲所有父作用域中的變量(包括this) c [&var]:表示引用傳遞變量var d [&]:表示引用傳遞捕獲所有父作用域中的變量(this) e [this]:表示值傳遞方式捕獲當前的this指針
!!!:
a 父作用域包含lambda函數的語句塊 b 語法上捕獲列表可由多個捕獲項組成,并以逗號分隔 比如:[=,&a,&b]:以引用傳遞的方式捕獲變量a 和 b,值傳遞的方式捕獲其它所有變量. [&,a,this];值傳遞的方式捕獲變量a和this,引用方式捕獲其它變量。 捕捉列表不允許變量重復傳遞,否則會導致編譯錯誤。比如:[=,a]以傳值的方式捕獲了所有變量,又重復捕捉a c 塊作用域以外的lambda函數捕捉列表必須為空 e 在塊作用域中的lambda函數僅能捕捉父作用域中局部變量,捕捉任何非此作用域或者非局部變量都會導致編譯報錯 f lambda表達式之間不能相互賦值,即使看起來類型相同.
void (*PF)(); int main() { auto f1 = []{cout<<"hello world"<<endl;}; auto f2 = []{cout<<"hello world"<<endl;}; f1= f2; // 這里會編譯失敗,提示找不到operator=() auto f3(f2); // 允許使用一個lambda表達式拷貝一個新的福分 PF = f2; // 可以將lambda表達式賦值給相同類型的指針 return 0; }
3、lambda表達式與函數指針、仿函數
typedef bool (*GTF) (int, int); bool greater_func1(int l, int r) { return l > r; } struct greater_func2 { bool operator()(int l, int r) { return l > r; } }; int main() { // 函數指針 GTF f1 = greater_func1; // typedef 定義 // bool (*f1) (int, int) = greater_func1; // 不typedef ,直接原生寫法,可讀性差 cout<< f1(1,2)<<endl; // 仿函數 greater_func2 f2; cout<< f2(1,2)<<endl; // lamba表達式 auto f3 = [] (int l, int r) ->bool{return l > r;}; cout<< f3(1,2)<<endl; int a[] = {1,2,4,5,3,7,6,9,8}; sort(a,a+sizeof(a)/sizeof(a[0]),f1); sort(a,a+sizeof(a)/sizeof(a[0]),f2); sort(a,a+sizeof(a)/sizeof(a[0]),f3); // sort函數第三個模板參數能接受函數指針,仿函數、lambda表達式,是因為其第三個參數是一個模板custom (2) template <class RandomAccessIterator, class Compare> void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp); return 0; }
函數指針,仿函數,lambda用法上是一樣的,但函數指針類型定義很難理解,仿函數需要實現運算符的重載,必須先定義一個類,而且一個類只能實現一個()operator的重載。(ep:對商品的不同屬性實現比較就需要實現不同的類),要先定義好才能使用。而lambda可以定義好直接使用.
struct Goods { string _name; double _price; double _appraise; }; int main() { Goods gds[] = { { "蘋果", 2.1, 10 }, { "相交", 3, 8 }, { "橙子", 2.2, 7 }, { "菠蘿", 1.5, 10 } }; sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool {return g1._price > g2._price; }); sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool {return g1._appraise > g2._appraise; }); return 0; }
上面的例子就體現了其現做現用的特性。
4、lambda表達式的底層
class Rate { public: Rate(double rate) : _rate(rate) {} double operator()(double money, int year) { return money * _rate * year; } private: double _rate; }; int main() { // 函數對象 double rate = 0.49; Rate r1(rate); r1(10000, 2); // 仿函數 auto r2 = [=](double monty, int year)->double{return monty*rate*year; }; r2(10000, 2); return 0; }
函數對象將rate作為其成員變量,在定義對象時候給出初始值即可,lambda表達式通過捕獲列表直接捕獲該變量.
通過上面的圖可以看出,實際在底層編譯器對于處理lambda表達式的處理方式,完全就是按照函數對象的方式處理的,即:如果定義了一個lambda表達式,編譯器會自動生成一個類,在該類中重載了operator();
并且編譯器是通過lambda_+uuid來唯一辨識一個lambda表達式的
九、線程庫
C++11中引入了線程庫,使得在C++在并行編程時可以不需要依賴第三方庫,而且在原子操作中引入了原子類的概念。
要使用標準庫中的線程,必須包含頭文件
#include<iostream> #include<thread> void fun() { std::cout << "A new thread!" << std::endl; } int main() { std::thread t(fun); t.join(); std::cout << "Main thread!" << std::endl; system("pause"); return 0; }
9.1 線程的啟動
C++線程庫通過構造一個線程對象來啟動一個線程,該線程對象中包含了線程運行時的上下文環境,如:線程函數、線程棧、線程其實狀態、以及線程ID等,把所有操作全部封裝在一起,在同一傳遞給_beginthreadex()創建線程函數來實現(_beginthreadex是windows中創建線程的底層c函數)
std::thread()創建一個新的線程可以接受任意的可調用對象類型,包括lambda表達式,函數,函數對象,函數指針
// 使用lambda表達式作為線程函數創建線程 int main() { int n1 = 1; int n2 = 2; std::thread t([&](int addNum){n1 += addNum; n2 += addNum; }, 3); t.join(); std::cout << n1 << " " << n2 << std:: endl; system("pause"); return 0; }
9.1 線程的結束
啟動一個線程后,當線程執行完畢時,如果護手線程使用的資源,thread庫提供了兩種選擇。
1、join()
join():會主動等待線程終止,在調用進程中join(),當新的線程終止時,join()會清理相關的資源,然后返回,調用線程在繼續向下執行。由于join()清理了線程的相關資源,thread對象與已銷毀的線程就沒有關系了,因此一個線程對象每次只能join()一次,如果多次調用join(),joinable()會返回false;
int main() { int n1 = 1; int n2 = 2; std::thread t([&](int addNum){n1 += addNum; n2 += addNum; }, 3); std::cout << "join before,joinable=" << t.joinable() << std::endl; t.join(); std::cout << "join after,joinable=" << t.joinable() << std::endl; system("pause"); return 0; } // 執行結果: join before,joinable=1 join after,joinable=0
2、detach()
detach:會從調用線程中分離出新的線程,之后不能再與新線程交互。這是調用joinable()會返回false。分離的線程會在后臺運行,其所有權和控制權會交給C++運行庫。C++運行庫會保證在線程退出時,其相關資源能正確回收。
int main() { int n1 = 1; int n2 = 2; std::thread t([&](int addNum){n1 += addNum; n2 += addNum; }, 3); std::cout << "join before,joinable=" << t.joinable() << std::endl; t.detach(); std::cout << "join after,joinable=" << t.joinable() << std::endl; system("pause"); return 0; }
注意,必須在thread對象銷毀之前作出選擇,因為線程在join()或detach()之前,就可能已經結束,如果之后在分離,線程可能會在thread對象銷毀之后繼續運行。
9.3 原子性操作庫
多線程最主要的問題是共享數據帶來的問題(線程安全)。如果數據都是只讀的,沒有問題,因為只讀不會影響數據,不會涉及數據的修改,所有線程都會獲得同樣的數據。但是,當多個線程要修改數據時,就會產生很多潛在的麻煩。
int sum = 0; void fun(size_t num) { for (size_t i = 0; i < num; i++) sum++; } int main() { std::cout << "before,sum=" << sum << std::endl; std::thread t1(fun, 100000000); std::thread t2(fun, 100000000); t1.join(); t2.join(); std::cout << "After,sum=" << sum << std::endl; system("pause"); return 0; }
當fun的參數比較大時,就會產生和預期不相符的結果.
在C++98中可以通過加鎖來保護共享數據。
int sum = 0; std::mutex m; void fun(size_t num) { for (size_t i = 0; i < num; i++) { m.lock(); sum++; m.unlock(); } }
雖然加鎖結果了這個問題:但是它有一個缺陷:只要有一個線程在對sum++的時候,其它線程就會阻塞,會影響程序運行的效率,而且鎖如果控制不好,或導致思索的問題。
因此在C++11中引入了原子操作。對應于內置的數據類型,原子數據類型都有一份對應的類型。
要使用以上的原子操作,需要添加頭文件
#include<thread> #include<mutex> #include<atomic> std::atomic_int sum{ 0 }; void fun(size_t num) { for (size_t i = 0; i < num; i++) { sum ++; // 原子的 } } int main() { std::cout << "before,sum=" << sum << std::endl; std::thread t1(fun, 10000000); std::thread t2(fun, 10000000); t1.join(); t2.join(); std::cout << "After,sum=" << sum << std::endl; system("pause"); return 0; }
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。