C++11新特性学习
什么是C+11
C++11標準為C++編程語言的第三個官方標準,正式名叫ISO/IEC 14882:2011 - Information technology -- Programming languages -- C++。在正式標準發布前,原名C++0x。它將取代C++標準第二版ISO/IEC 14882:2003 - Programming languages -- C++成為C++語言新標準。
C++11是對目前C++語言的擴展和修正, C++11不僅包含核心語言的新機能,而且擴展了C++的標準程序庫(STL) ,并入了大部分的C++ Technical Report 1(TR1) 程序庫(數學的特殊函數除外)。
C++11包括大量的新特性:包括lambda表達式,類型推導關鍵字auto、 decltype,和模板的大量改進。
?
類型推導:auto
auto的自動類型推導,用于從初始化表達式中推斷出變量的數據類型。從這個意義上講,auto并非一種“類型”聲明,而是一個類型聲明時的“占位符”,編譯器在編譯時期會將auto替換為變量實際的類型。
通過auto的自動類型推導,可以大大簡化我們的編程工作:
#include <iostream> #include <vector> #include <string> using namespace std;double foo() {}void func(vector<string> & tmp) {for (auto i = tmp.begin(); i < tmp.end(); i++){// 一些代碼} }int main() {auto x = 1; // x的類型為intauto y = foo(); // y的類型為doublestruct m { int i; }str;auto str1 = str; // str1的類型是struct mauto z; // err, 無法推導,無法通過編譯z = x;return 0; }注意點:
void fun(auto x =1) {} // 1: auto函數參數,有些編譯器無法通過編譯struct str {auto var = 10; // 2: auto非靜態成員變量,無法通過編譯 };int main() {char x[3];auto y = x;auto z[3] = x; // 3: auto數組,無法通過編譯// 4: auto模板參數(實例化時),無法通過編譯vector<auto> x = {1};return 0; }decltype
decltype實際上有點像auto的反函數, auto可以讓你聲明一個變量,而decltype則可以從一個變量或表達式中得到其類型,如下:
#include <typeinfo> #include <iostream> #include <vector> using namespace std;int main() {int i;decltype(i) j = 0;cout << typeid(j).name() << endl; // 打印出"i", g++表示integerfloat a;double b;decltype(a + b) c;cout << typeid(c).name() << endl; // 打印出"d", g++表示doublevector<int> vec;typedef decltype(vec.begin()) vectype; // decltype(vec.begin()) 改名為 vectypevectype k; // 這是auto無法做到的//decltype(vec.begin()) k; // 這是auto無法做到的for (k = vec.begin(); k < vec.end(); k++){// 做一些事情}enum {Ok, Error, Warning}flag; // 匿名的枚舉變量decltype(flag) tmp = Ok;return 0; }追蹤返回類型
返回類型后置:在函數名和參數列表后面指定返回類型。
int func(int, int); auto func2(int, int) -> int;template<typename T1, typename T2> auto sum(const T1 & t1, const T2 & t2) -> decltype(t1 + t2) {return t1 + t2; }template <typename T1, typename T2> auto mul(const T1 & t1, const T2 & t2) -> decltype(t1 * t2) {return t1 * t2; }int main() {auto a = 3;auto b = 4L;auto pi = 3.14;auto c = mul( sum(a, b), pi );cout << c << endl; // 21.98return 0; }?
初始化
類內成員初始化
class Mem { public:Mem(int i): m(i){} //初始化列表給m初始化int m; }; class Group { public:Group(){}private:int data = 1; // 使用"="初始化非靜態普通成員,也可以 int data{1};Mem mem{2}; // 對象成員,創建對象時,可以使用{}來調用構造函數string name{"mike"}; };列表初始化
C++11引入了一個新的初始化方式,稱為初始化列表(List Initialize),具體的初始化方式如下:
int a[]{1, 3, 5}; int i = {1}; int j{3};初始化列表可以用于初始化結構體類型,例如:
struct Person { std::string name; int age; }; int main() { Person p = {"Frank", 25}; std::cout << p.name << " : " << p.age << std::endl; }防止類型收窄
類型收窄指的是導致數據內容發生變化或者精度丟失的隱式類型轉換。使用列表初始化可以防止類型收窄。
int main(void) {const int x = 1024;const int y = 10;char a = x; // 收窄,但可以通過編譯char* b = new char(1024); // 收窄,但可以通過編譯char c = { x }; // err, 收窄,無法通過編譯char d = { y }; // 可以通過編譯unsigned char e{ -1 }; // err,收窄,無法通過編譯float f{ 7 }; // 可以通過編譯int g{ 2.0f }; // err,收窄,無法通過編譯float * h = new float{ 1e48 }; // err,收窄,無法通過編譯float i = 1.2l; // 可以通過編譯return 0; }基于范圍的for循環
在C++中for循環可以使用基于范圍的for循環,示例代碼如下
int a[] = { 1, 2, 3, 4, 5 };int n = sizeof(a) / sizeof(*a); //元素個數for (int i = 0; i < n; ++i){int tmp = a[i];cout << tmp << ", ";}cout << endl;for (int tmp : a){cout << tmp << ", ";}cout << endl;for (int i = 0; i < n; ++i){int &tmp = a[i];tmp = 2 * tmp;cout << tmp << ", ";}cout << endl;for (int &tmp : a){tmp = 2 * tmp;cout << tmp << ", ";}cout << endl;使用基于范圍的for循環,其for循環迭代的范圍必須是可確定的:
int func(int a[])//形參中數組是指針變量,無法確定元素個數 {for(auto e: a) // err, 編譯失敗{cout << e;} }int main() {int a[] = {1, 2, 3, 4, 5};func(a);return 0; }靜態斷言
?
C/C++提供了調試工具assert,這是一個宏,用于在運行階段對斷言進行檢查,如果條件為真,執行程序,否則調用abort()。
int main() {bool flag = false;//如果條件為真,程序正常執行,如果為假,終止程序,提示錯誤assert(flag == true); //#include <cassert>或#include <assert.h>cout << "Hello World!" << endl;return 0; }C++ 11新增了關鍵字static_assert,可用于在編譯階段對斷言進行測試。
靜態斷言的好處:
- 更早的報告錯誤,我們知道構建是早于運行的,更早的錯誤報告意味著開發成本的降低
- 減少運行時開銷,靜態斷言是編譯期檢測的,減少了運行時開銷
語法如下:
static_assert(常量表達式,提示字符串)注意:只能是常量表達式,不能是變量 int main() {//該static_assert用來確保編譯僅在32位的平臺上進行,不支持64位的平臺static_assert( sizeof(void *)== 4, "64-bit code generation is not supported."); cout << "Hello World!" << endl;return 0; }noexcept修飾符、nullptr、原生字符串字面值
noexcept修飾符
void func3() throw(int, char) //只能夠拋出 int 和char類型的異常 {//C++11已經棄用這個聲明throw 0; }void BlockThrow() throw() //代表此函數不能拋出異常,如果拋出,就會異常 {throw 1; }//代表此函數不能拋出異常,如果拋出,就會異常 //C++11 使用noexcept替代throw() void BlockThrowPro() noexcept {throw 2; }nullptr
nullptr是為了解決原來C++中NULL的二義性問題而引進的一種新的類型,因為NULL實際上代表的是0。
void func(int a) {cout << __LINE__ << " a = " << a <<endl; }void func(int *p) {cout << __LINE__ << " p = " << p <<endl; }int main() {int *p1 = nullptr;int *p2 = NULL;if(p1 == p2){cout << "equal\n";}//int a = nullptr; //err, 編譯失敗,nullptr不能轉型為intfunc(0); //調用func(int), 就算寫NULL,也是調用這個func(nullptr);return 0; }原生字符串字面值
int main(void) {cout << R"(hello, \n world)" << endl;cout << "(hello, \n world)" << endl;string str = R"(helo \4 \r abc, mikehello\n)";cout << endl;cout << str << endl;return 0; }強類型枚舉
C++ 11引入了一種新的枚舉類型,即“枚舉類”,又稱“強類型枚舉”。聲明請類型枚舉非常簡單,只需要在enum后加上使用class或struct。如:
enum Old{Yes, No}; // old style enum class New{Yes, No}; // new style enum struct New2{Yes, No}; // new style“傳統”的C++枚舉類型有一些缺點:它會在一個代碼區間中拋出枚舉類型成員(如果在相同的代碼域中的兩個枚舉類型具有相同名字的枚舉成員,這會導致命名沖突),它們會被隱式轉換為整型,并且不可以指定枚舉的底層數據類型。
int main() {enum Status{Ok, Error};//enum Status2{Ok, Error};//err, 導致命名沖突, Status已經有成員叫Ok, Errorreturn 0; }在C++11中,強類型枚舉解決了這些問題:
int main() {enum class Status {Ok, Error};enum struct Status2{Ok, Error};//Status flag1 = 10; // err,無法隱式轉換為int類型//Status flag2 = Ok; // err,必須使用強類型名稱Status flag3 = Status::Ok;enum class C : char { C1 = 1, C2 = 2};//指定枚舉的底層數據類型enum class D : unsigned int { D1 = 1, D2 = 2, Dbig = 0xFFFFFFF0U };cout << sizeof(C::C1) << endl; // 1cout << (unsigned int)D::Dbig << endl; // 編譯器輸出一致,4294967280cout << sizeof(D::D1) << endl; // 4cout << sizeof(D::Dbig) << endl; // 4return 0; }常量表達式
?
常量表達式主要是允許一些計算發生在編譯時,即發生在代碼編譯而不是運行的時候。
這是很大的優化:假如有些事情可以在編譯時做,它將只做一次,而不是每次程序運行時都計算。
使用constexpr,你可以創建一個編譯時的函數:
constexpr int GetConst() {return 3; }int main() {int arr[ GetConst() ] = {0};enum { e1 = GetConst(), e2 };constexpr int num = GetConst();return 0; }constexpr函數的限制:
- 函數中只能有一個return語句(有極少特例)
- 函數必須返回值(不能是void函數)
- 在使用前必須已有定義
- return返回語句表達式中不能使用非常量表達式的函數、全局數據,且必須是一個常量表達式
常量表達式的構造函數有以下限制:
- 函數體必須為空
- 初始化列表只能由常量表達式來賦值
用戶定義字面量
用戶自定義字面值,或者叫“自定義后綴”更直觀些,主要作用是簡化代碼的讀寫
long double operator"" _mm(long double x) { return x / 1000; } long double operator"" _m(long double x) { return x; } long double operator"" _km(long double x) { return x * 1000; }int main() {cout << 1.0_mm << endl; //0.001cout << 1.0_m << endl; //1cout << 1.0_km << endl; //1000return 0; }?
根據 C++ 11 標準,只有下面參數列表才是合法的
char const * unsigned long long long double char const *, size_t wchar_t const *, size_t char16_t const *, size_t char32_t const *, size_t最后四個對于字符串相當有用,因為第二個參數會自動推斷為字符串的長度。例如:
size_t operator"" _len(char const * str, size_t size) {return size; }int main() {cout << "mike"_len <<endl; //結果為4return 0; }對于參數char const *,應該被稱為原始字面量 raw literal 操作符。例如:
char const * operator"" _r(char const* str) {return str; }int main() {cout << 250_r <<endl; //結果為250return 0; }類的改進
繼承構造
C++ 11允許派生類繼承基類的構造函數(默認構造函數、復制構造函數、移動構造函數除外)。
#include <iostream> using namespace std;//基類 class A { public:A(int x, int y){a = x;b = y;}protected:int a;int b; };//派生類 class B:public A { public: #if 0//通過參數列表給基類構造函數傳參B(int x, int y): A(x, y){} #endif//繼承構造using A::A;void display(){cout << "a = " << a << ", b = " << b << endl;}//沒有增加新的成員變量int tmp; };int main() {//派生類對象B obj(10, 20);obj.display();return 0; }注意:
- 繼承的構造函數只能初始化基類中的成員變量,不能初始化派生類的成員變量
- 如果基類的構造函數被聲明為私有,或者派生類是從基類中虛繼承,那么不能繼承構造函數
- 一旦使用繼承構造函數,編譯器不會再為派生類生成默認構造函數
繼承控制:final和override
C++11之前,一直沒有繼承控制關鍵字,禁用一個類的進一步衍生比較麻煩。
C++ 11添加了兩個繼承控制關鍵字:final和override。
final阻止類的進一步派生和虛函數的進一步重寫:
//final阻止類的進一步派生,虛函數的進一步重寫 #if 0 class A1 final //加上final,指定A1不能派生 {int a; };class A2: public A1 //err, 基類不能再派生了 {}; #endif//基類 class B1 { public:virtual void func() final {} //這是最終版本的虛函數,不能再重寫};//派生類重寫基類的虛函數 class B2: public B1 { public://virtual void func() {} //err, 基類中的虛函數是最終版本,不能再重寫 };override確保在派生類中聲明的函數跟基類的虛函數有相同的簽名:
class A1 { public://這是第一個虛函數,沒有重寫,不能用override修飾virtual int func(int a){} };class A2:public A1 { public://在重寫虛函數地方,加上override, 要求重寫的虛函數和基類一模一樣virtual int func(int b) override{} };defaulted 和 deleted 函數
defaulted 函數
背景問題
C++ 的類有四類特殊成員函數,它們分別是:默認構造函數、析構函數、拷貝構造函數以及拷貝賦值運算符。這些類的特殊成員函數負責創建、初始化、銷毀,或者拷貝類的對象。如果程序員沒有顯式地為一個類定義某個特殊成員函數,而又需要用到該特殊成員函數時,則編譯器會隱式的為這個類生成一個默認的特殊成員函數。
示例 1:
class X{ private: int a; }; X obj;在示例 1 中
該自動生成的默認構造函數沒有參數,包含一個空的函數體,即 X::X(){ }。雖然自動生成的默認構造函數僅有一個空函數體,但是它仍可用來成功創建類 X 的對象 obj,示例 1 也可以編譯通過。
但是,如果程序員為類 X 顯式的自定義了非默認構造函數,卻沒有定義默認構造函數的時候,示例 2 將會出現編譯錯誤:
示例 2:
class X { public: X(int i){ a = i; } private: int a; }; X obj; // 錯誤 , 默認構造函數 X::X() 不存在示例 2 編譯出錯的原因在于:類 X 已經有了用戶自定義的構造函數,所以編譯器將不再會為它隱式的生成默認構造函數。
如果需要用到默認構造函數來創建類的對象時,程序員必須自己顯式的定義默認構造函數。例如:
示例 3:
class X { public: X(){}; // 手動定義默認構造函數X(int i){ a = i; } private: int a; }; X obj; // 正確,默認構造函數 X::X() 存在從示例 3 可以看出,原本期望編譯器自動生成的默認構造函數卻需要程序員手動編寫了,即程序員的工作量加大了。此外,手動編寫的默認構造函數的代碼執行效率比編譯器自動生成的默認構造函數低。
類的其它幾類特殊成員函數也和默認構造函數一樣,當存在用戶自定義的特殊成員函數時,編譯器將不會隱式的自動生成默認特殊成員函數,而需要程序員手動編寫,加大了程序員的工作量。類似的,手動編寫的特殊成員函數的代碼執行效率比編譯器自動生成的特殊成員函數低。
defaulted 函數的提出
為了解決如示例 3 所示的兩個問題:
1. 減輕程序員的編程工作量;
2. 獲得編譯器自動生成的默認特殊成員函數的高的代碼執行效率。
C++11 標準引入了一個新特性:defaulted 函數。程序員只需在函數聲明后加上“=default;”,就可將該函數聲明為 defaulted 函數,編譯器將為顯式聲明的 defaulted 函數自動生成函數體。例如:
示例 4:
class X { public: X()= default; X(int i){ a = i; } private: int a; }; X obj;在示例 4 中,編譯器會自動生成默認構造函數 X::X(){},該函數可以比用戶自己定義的默認構造函數獲得更高的代碼效率。
defaulted 函數的用法及示例
defaulted 函數特性僅適用于類的特殊成員函數,且該特殊成員函數沒有默認參數。例如:
示例 5:
class X{ public: int f() = default; // 錯誤 , 函數 f() 非類 X 的特殊成員函數X(int, int) = default; // 錯誤 , 構造函數 X(int, int) 非 X 的特殊成員函數X(int = 1) = default; // 錯誤 , 默認構造函數 X(int=1) 含有默認參數};defaulted 函數既可以在類體里(inline)定義,也可以在類體外(out-of-line)定義。例如:
示例 6:
class X { public: X() = default; //Inline defaulted 默認構造函數X(const X&); X& operator = (const X&); ~X() = default; //Inline defaulted 析構函數}; X::X(const X&) = default; //Out-of-line defaulted 拷貝構造函數X& X::operator = (const X&) = default; //Out-of-line defaulted 拷貝賦值操作符在 C++ 代碼編譯過程中,如果程序員沒有為類 X 定義析構函數,但是在銷毀類 X 對象的時候又需要調用類 X 的析構函數時,編譯器會自動隱式的為該類生成一個析構函數。該自動生成的析構函數沒有參數,包含一個空的函數體,即 X::~X(){ }。例如:
示例 7:
class X { private:int x; };class Y : public X { public:Y(){p = new int;cout << "Y構造函數\n";}~Y(){delete p;cout << "Y析構函數\n";}private:int *p; };int main() {X *p = new Y;delete p;return 0; }在示例 7 中,程序員沒有為基類 X 定義析構函數,當在主函數內 delete 基類指針 p 的時候,需要調用基類的析構函數。于是,編譯器會隱式自動的為類 X 生成一個析構函數,從而可以成功的銷毀 p 指向的派生類對象中的基類子對象。
但是,這段代碼存在內存泄露的問題,當利用 delete 語句刪除指向派生類對象的指針 p 時,系統調用的是基類的析構函數,而非派生類 Y 類的析構函數,因此,編譯器無法析構派生類的已經在堆區分配空間 p 。
因此,一般情況下我們需要將基類的析構函數定義為虛函數,當利用 delete 語句刪除指向派生類對象的基類指針時,系統會調用相應的派生類的析構函數(實現多態性),從而避免內存泄露。
但是編譯器隱式自動生成的析構函數都是非虛函數,這就需要由程序員手動的為基類 X 定義虛析構函數,例如:
示例 8:
class X { public: virtual ~X(){}; // 手動定義虛析構函數 private:int x; };class Y : public X { public:Y(){p = new int;cout << "Y構造函數\n";}~Y(){delete p;cout << "Y析構函數\n";}private:int *p; };int main() {X *p = new Y;delete p;return 0; }在示例 8 中,由于程序員手動為基類 X 定義了虛析構函數,當利用 delete 語句刪除指向派生類對象的基類指針 p 時,系統會調用相應的派生類 Y 的析構函數以及基類 X 的析構函數,從而將派生類對象完整的銷毀,可以避免內存泄露。
但是,在示例 8 中,程序員需要手動的編寫基類的虛構函數的定義(哪怕函數體是空的),增加了程序員的編程工作量。更值得一提的是,手動定義的析構函數的代碼執行效率要低于編譯器自動生成的析構函數。
為了解決上述問題,我們可以將基類的虛析構函數聲明為 defaulted 函數,這樣就可以顯式的指定編譯器為該函數自動生成函數體。例如:
示例 9:
class X { public: virtual ~X()= defaulted; // 編譯器自動生成 defaulted 函數定義體 private:int x; };class Y : public X { public:Y(){p = new int;cout << "Y構造函數\n";}~Y(){delete p;cout << "Y析構函數\n";}private:int *p; };int main() {X *p = new Y;delete p;return 0; }在示例 9 中,編譯器會自動生成虛析構函數 virtual X::X(){},該函數比用戶自己定義的虛析構函數具有更高的代碼執行效率。
deleted 函數
為了能夠讓程序員顯式的禁用某個函數,C++11 標準引入了一個新特性:deleted 函數。程序員只需在函數聲明后上“=delete;”,就可將該函數禁用。
例如,我們可以將類 X 的拷貝構造函數以及拷貝賦值操作符聲明為 deleted 函數,就可以禁止類 X 對象之間的拷貝和賦值。
示例 11:
class X { public: X(); X(const X&) = delete; // 聲明拷貝構造函數為 deleted 函數X& operator = (const X &) = delete; // 聲明拷貝賦值操作符為 deleted 函數 }; int main(){ X obj1; X obj2=obj1; // 錯誤,拷貝構造函數被禁用X obj3; obj3=obj1; // 錯誤,拷貝賦值操作符被禁用}在示例 11 中,雖然只顯式的禁用了一個拷貝構造函數和一個拷貝賦值操作符,但是由于編譯器檢測到類 X 存在用戶自定義的拷貝構造函數和拷貝賦值操作符的聲明,所以不會再隱式的生成其它參數類型的拷貝構造函數或拷貝賦值操作符,也就相當于類 X 沒有任何拷貝構造函數和拷貝賦值操作符,所以對象間的拷貝和賦值被完全禁止了。
deleted 函數的用法及示例
class X { public:X(double);X(int) = delete; };int main() {X obj1(1.2);X obj2(2); // 錯誤,參數為整數 int 類型的轉換構造函數被禁用 }deleted 函數特性還可以用來禁用某些用戶自定義的類的 new 操作符,從而避免在自由存儲區創建類的對象。例如:
示例 13:
class X { public:void *operator new(size_t) = delete;void *operator new[](size_t) = delete; };int main() {X *pa = new X; // 錯誤,new 操作符被禁用X *pb = new X[10]; // 錯誤,new[] 操作符被禁用 }必須在函數第一次聲明的時候將其聲明為 deleted 函數,否則編譯器會報錯。即對于類的成員函數而言,deleted 函數必須在類體里(inline)定義,而不能在類體外(out-of-line)定義。例如:
示例 14:
int add(int, int) = delete;int main() {int a, b;add(a, b); // 錯誤,函數 add(int, int) 被禁用 }值得一提的是,在示例 15 中,雖然 add(int, int)函數被禁用了,但是禁用的僅是函數的定義,即該函數不能被調用。但是函數標示符 add 仍是有效的,在名字查找和函數重載解析時仍會查找到該函數標示符。如果編譯器在解析重載函數時,解析結果為 deleted 函數,則會出現編譯錯誤。例如:
示例 16:
int add(int, int) = delete; double add(double a, double b) {return a + b; } int main() {cout << add(1, 3) << endl; // 錯誤,調用了 deleted 函數 add(int, int) cout << add(1.2, 1.3) << endl;return 0; }?
lambda基礎使用
lambda 表達式(lambda expression)是一個匿名函數,lambda表達式基于數學中的 λ 演算得名。
C++11中的lambda表達式用于定義并創建匿名的函數對象,以簡化編程工作。
lambda表達式的基本構成:
① 函數對象參數
[],標識一個lambda的開始,這部分必須存在,不能省略。函數對象參數是傳遞給編譯器自動生成的函數對象類的構造函數的。函數對象參數只能使用那些到定義lambda為止時lambda所在作用范圍內可見的局部變量(包括lambda所在類的this)。函數對象參數有以下形式:
- 空。沒有使用任何函數對象參數。
- =。函數體內可以使用lambda所在作用范圍內所有可見的局部變量(包括lambda所在類的this),并且是值傳遞方式(相當于編譯器自動為我們按值傳遞了所有局部變量)。
- 。函數體內可以使用lambda所在作用范圍內所有可見的局部變量(包括lambda所在類的this),并且是引用傳遞方式(相當于編譯器自動為我們按引用傳遞了所有局部變量)。
- this。函數體內可以使用lambda所在類中的成員變量。
- a。將a按值進行傳遞。按值進行傳遞時,函數體內不能修改傳遞進來的a的拷貝,因為默認情況下函數是const的。要修改傳遞進來的a的拷貝,可以添加mutable修飾符。
- &a。將a按引用進行傳遞。
- a, &b。將a按值進行傳遞,b按引用進行傳遞。
- =,&a, &b。除a和b按引用進行傳遞外,其他參數都按值進行傳遞。
- &, a, b。除a和b按值進行傳遞外,其他參數都按引用進行傳遞。
?
② 操作符重載函數參數
標識重載的()操作符的參數,沒有參數時,這部分可以省略。參數可以通過按值(如:(a,b))和按引用(如:(&a,&b))兩種方式進行傳遞。
③ 可修改標示符
mutable聲明,這部分可以省略。按值傳遞函數對象參數時,加上mutable修飾符后,可以修改按值傳遞進來的拷貝(注意是能修改拷貝,而不是值本身)
④ 錯誤拋出標示符
exception聲明,這部分也可以省略。exception聲明用于指定函數拋出的異常,如拋出整數類型的異常,可以使用throw(int)
⑤ 函數返回值
->返回值類型,標識函數返回值的類型,當返回值為void,或者函數體中只有一處return的地方(此時編譯器可以自動推斷出返回值類型)時,這部分可以省略。
⑥ 是函數體
{},標識函數的實現,這部分不能省略,但函數體可以為空。
值傳遞和引用傳遞區別:
#include <iostream> using namespace std;int main() {int j = 12;auto by_val_lambda = [=] { return j + 1;};auto by_ref_lambda = [&] { return j + 1;};cout << "by_val_lambda: " << by_val_lambda() << endl;cout << "by_ref_lambda: " << by_ref_lambda() << endl;j++;cout << "by_val_lambda: " << by_val_lambda() << endl;cout << "by_ref_lambda: " << by_ref_lambda() << endl;return 0; }第3次調用結果還是13,原因是由于by_val_lambda中,j被視為了一個常量,一旦初始化后不會再改變。
lambda與仿函數
#include <iostream> using namespace std;class MyFunctor { public:MyFunctor(int tmp) : round(tmp) {}int operator()(int tmp) { return tmp + round; }private:int round; };int main() {//仿函數int round = 2;MyFunctor f1(round);//調用構造函數cout << "result1 = " << f1(1) << endl; //operator()(int tmp)//lambda表達式auto f2 = [=](int tmp) -> int { return tmp + round; } ;cout << "result2 = " << f2(1) << endl;return 0; }?
通過上面的例子,我們看到,仿函數以round初始化類,而lambda函數也捕獲了round變量,其它的,如果在參數傳遞上,兩者保持一致。
除去在語法層面上的不同,lambda和仿函數有著相同的內涵——都可以捕獲一些變量作為初始化狀態,并接受參數進行運行。
而事實上,仿函數是編譯器實現lambda的一種方式,通過編譯器都是把lambda表達式轉化為一個仿函數對象。因此,在C++11中,lambda可以視為仿函數的一種等價形式。
模板的改進
右尖括號>改進
在C++98/03的泛型編程中,模板實例化有一個很繁瑣的地方,就是連續兩個右尖括號(>>)會被編譯解釋成右移操作符,而不是模板參數表的形式,需要一個空格進行分割,以避免發生編譯時的錯誤。
template <int i> class X{}; template <class T> class Y{};int main() {Y<X<1> > x1; // ok, 編譯成功Y<X<2>> x2; // err, 編譯失敗return 0; };在實例化模板時會出現連續兩個右尖括號,同樣static_cast、dynamic_cast、reinterpret_cast、const_cast表達式轉換時也會遇到相同的情況。C++98標準是讓程序員在>>之間填上一個空格,在C++11中,這種限制被取消了。在C++11標準中,要求編譯器對模板的右尖括號做單獨處理,使編譯器能夠正確判斷出”>>”是一個右移操作符還是模板參數表的結束標記。
?
模板的別名
#include <iostream> #include <type_traits> //std::is_same using namespace std;using uint = unsigned int; typedef unsigned int UINT; using sint = int;int main() {//std::is_same 判斷類型是否一致//這個結構體作用很簡單,就是兩個一樣的類型會返回truecout << is_same<uint, UINT>::value << endl; // 1return 0; }左值引用、右值引用
?
左值引用是對一個左值進行引用的類型,右值引用則是對一個右值進行引用的類型。
左值引用和右值引用都是屬于引用類型。無論是聲明一個左值引用還是右值引用,都必須立即進行初始化。而其原因可以理解為是引用類型本身自己并不擁有所綁定對象的內存,只是該對象的一個別名。
左值引用是具名變量值的別名,而右值引用則是不具名(匿名)變量的別名。
左值引用
int &a = 2; // 左值引用綁定到右值,編譯失敗, err int b = 2; // 非常量左值 const int &c = b; // 常量左值引用綁定到非常量左值,編譯通過, ok const int d = 2; // 常量左值 const int &e = c; // 常量左值引用綁定到常量左值,編譯通過, ok const int &b = 2; // 常量左值引用綁定到右值,編程通過, ok“const 類型 &”為 “萬能”的引用類型,它可以接受非常量左值、常量左值、右值對其進行初始化
右值引用,使用&&表示:
int && r1 = 22; int x = 5; int y = 8; int && r2 = x + y; T && a = ReturnRvalue();測試示例:
#include <iostream> using namespace std;void process_value(int & i) //參數為左值引用 {cout << "LValue processed: " << i << endl; }void process_value(int && i) //參數為右值引用 {cout << "RValue processed: " << i << endl; }int main() {int a = 0;process_value(a); //LValue processed: 0process_value(1); //RValue processed: 1return 0; }移動語義
#include <iostream> using namespace std;class Test { public:Test(int a = 0){//普通構造函數d = new int(a);cout << "構造函數\n";}Test(const Test & tmp){//拷貝構造函數d = new int;*d = *(tmp.d);cout << "拷貝構造函數\n";}~Test(){//析構函數if(d != NULL){delete d;cout << "delete d\n";}cout << "析構函數\n";}int * d; };Test GetTmp() {Test h;cout << "Resource from " << __func__ << ": " << (void *)h.d << endl;return h; }int main() {Test obj = GetTmp();cout << "Resource from " << __func__ << ": " << (void *)obj.d << endl;return 0; }編譯器會對返回值進行優化,簡稱RVO,是編譯器的一項優化技術,它涉及(功能是)消除為保存函數返回值而創建的臨時對象。
-fno-elide-constructors,此選項作用是,在 g++ 上編譯時關閉 RVO。
通過上面的例子看到,臨時對象的維護 ( 創建和銷毀 ) 對性能有嚴重影響。
右值引用是用來支持轉移語義的。轉移語義可以將資源 ( 堆,系統對象等 ) 從一個對象轉移到另一個對象,這樣能夠減少不必要的臨時對象的創建、拷貝以及銷毀,能夠大幅度提高 C++ 應用程序的性能。
轉移語義是和拷貝語義相對的,可以類比文件的剪切與拷貝,當我們將文件從一個目錄拷貝到另一個目錄時,速度比剪切慢很多。
通過轉移語義,臨時對象中的資源能夠轉移其它的對象里。
移動語義定義
在現有的 C++ 機制中,我們可以定義拷貝構造函數和賦值函數。要實現轉移語義,需要定義轉移構造函數,還可以定義轉移賦值操作符。對于右值的拷貝和賦值會調用轉移構造函數和轉移賦值操作符。
如果轉移構造函數和轉移拷貝操作符沒有定義,那么就遵循現有的機制,拷貝構造函數和賦值操作符會被調用。
普通的函數和操作符也可以利用右值引用操作符實現轉移語義
轉移構造函數
?
?
總結
以上是生活随笔為你收集整理的C++11新特性学习的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux下的进程概论与编程二(进程控制
- 下一篇: 设计模式C++实现(3)——装饰模式