目錄
🌈 前言🌈
📁 C++11介紹
📁 統一初始化列表
📁 聲明
?📂 auto
?📂 decltype
?📂 返回類型后置
?📂 范圍for
?📂 模板別名
?📂 nullptr
📁 智能指針
📁 右值引用
?📂左值,右值的概念
?📂?左值引用和右值引用的比較
?📂 右值引用的使用場景
?📂 萬能引用和完美轉發
📁 新的類功能
?📂 強制生成默認函數的關鍵字default
?📂 禁止生成默認函數的關鍵字delete
?📂 final和override關鍵字
📁 可變模板參數
?📂 STL容器emplace相關接口函數
📁 lambda表達式
?📂 lambda表達式語法
?📂 函數對象和lambda表達式
📁 包裝器
?📂 function包裝器
?📂 bind包裝器
📁 線程庫
📂 thread 類介紹
📂 線程函數參數
📂 原子性操作庫
📂 互斥鎖
📂 條件變量condition_variable
📁 總結
🌈 前言🌈? ? ? ?
????????本期【C++雜貨鋪】介紹的是C++11新特性,其中如目錄所示的所有內容,包含了概念定義,如何使用等方面,快速入手C++11帶來的新內容。
? ? ? ? 此外,像一些比較重要的內容,受制于文章篇幅限制,不能完全介紹,需要搭配其他文章進行學習,例如智能指針,線程庫等,本文只能介紹使用,不詳細介紹底層,其他日常頻繁使用,較為重要,簡單易懂,能快速掌握的本文將會詳細介紹。
? ? ? ? C++11作為C++最常使用的標準,掌握C++11是非常有必要的。
????????本文參考書籍:《C++ Primer Plus》(第六版 ) 中文版
📁 C++11介紹
????????在2003年C++標準委員會曾經提交了一份技術勘誤表(簡稱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/03中孕育出的一種新語言。相比較而言, C++11能更好地用于系統開發和庫開發、語法更加泛華和簡單化、更加穩定和安全,不僅功能更強大,而且能提升程序員的開發效率,公司實際項目開發中也用得比較多,所以我們要作為一個重點去學習。C++11增加的語法特性非常篇幅非常多,我們這里沒辦法一 一講解,所以本節課程主要講解實際中比較實用的語法。
?
📁 統一初始化列表
? ? ? ? 在C++98中,{}可以用來對數組或結構體進行統一的列表初始值設定。如:
struct Point()
{int _x;int _y;
};int main()
{int arr1[5] = {1,2,3};int arr2[] = { 0 };Point p = {1,2};return 0;
}
? ? ? ? C++11擴大了{}的使用范圍,使其可用于所有的內置類型和用戶自定義的類型。使用列表初始化時,可以添加 = ,也可以不添加。
class A
{
public:A(int x,int y):_x(x),_y(y){}
private:int _x;int _y;
};int main()
{int i = { 1 };double d{ 1.1 };A a = { 1,2 };A b{ 2,3 };return 0;
}
? ? ? ? 此外,列表初始化也可以用于new 表達式中:
int* arr1 = new int[5]{ 1 }; // 1 0 0 0 0int* arr2 = new int[5] { 1,1,1,1,1 }; //1 1 1 1 1
1. 縮窄
? ? ? ? 初始化列表可以防止縮窄,即禁止將數值賦值給無法存儲它的數值變量。但是允許轉為更寬的類型。
char c = {1.1} not allweddouble d = { 1 } allowed
2. std::initializer_list
? ? ? ? C++11提供了對模板類initializer_list的支持。這個類包含成員函數begin()和end(),可用于獲悉列表的范圍。
????????可以用于構造函數,也可以用于operator=進行賦值
map<int, string> m({ { 1,"a" }, { 2,"b" } });vector<int> v = { 1,2,3 };v = { 3,2,1 };
????????還可以將initializer_list用于常規函數的參數:
void func(const initializer_list<int>& list)
{for (auto i = list.begin(); i != list.end(); ++i){cout << *i << endl;}
}
int main()
{func({ 1,2,3,4,5 });return 0;
}
📁 聲明
?📂 auto
? ? ? ? auto 是一個存儲類型說明符,C++11將其用于實現自動類型推斷。這就要求顯示實例化,讓編譯器能夠將變量的類型設置為初始值的類型。
int main()
{int a = 10;auto ca = a;auto pa = &a;return 0;
}
? ? ? ? 此外,auto還可以用于簡化模板聲明
int main()
{set<int> s = { 1,2,3,4,5 };for (auto it = s.begin(); it != s.end(); ++it){cout << *it << endl;}return 0;
}
? ? ? ? 此外,auto還可以配合引用
int main()
{int a = 10;auto& ra = a;auto& rra = ra;//ra rra 都是a的別名auto b = ra;//b是一個新的int變量,用ra的值進行初始化
}
?📂 decltype
? ? ? ? decltype將變量的類型聲明為表達式指定的類型。
int main()
{//y -> intdecltype(1 * 10) y;cout << typeid(y).name() << endl;//pa -> int*int a = 10;decltype(&a) pa;cout << typeid(pa).name() << endl;
}
? ? ? ? 這在定義模板時特別有用,因為只有等模板被實例化時才能確定具體的類型:
template<class T1, class T2>
void func(T1 t1, T2 t2)
{decltype(t1 * t2) t;cout << typeid(t).name() << endl;
}int main()
{func(10, 20);return 0;
}
? ? ? ? decltype的工作原理比auto更復雜,根據使用的表達式,指定的類型可以為引用和const
int main()
{//func(10, 20);int n = 10;int& rn = n;const int& cra = n;decltype(n) n1; //intdecltype((n)) n2 = n; //int&decltype(rn) n3 = n; //int&decltype(cra) n4 = n; //const int&n = 20;return 0;
}
?📂 返回類型后置
? ? ? ? 在函數名和參數列表后面指定返回類型
auto f(int x) -> int
? ? ? ? 就可讀性而言,這個語法好像是倒退了,但是您能夠使用decltype來指定模板函數的返回類型。
template<class T1,class T2>
auto f(T1 t1, T2 t2) -> decltype(t1* t2)
{return t1 * t2;
}int main()
{cout << f(3, 2) << endl;return 0;
}
?📂 范圍for
? ? ? ? 對于一個有范圍的集合而言,程序員來說明循環的范圍是多余的,有事還容易犯錯誤,因此C++11引入了基于范圍的for循環。
? ? ? ? for循環的括號由 冒號 :?分為兩部分,第一部分是范圍內用于迭代的變量,第二部分則表示被迭代的范圍。
int arr[] = {1,2,3};
for(auto e : arr)
{cout << it << endl;
}
????????除此之外,范圍for和普通for循環沒有任何區別,都可以在內內部進行break 和 continue。
? ? ? ? 范圍for的使用條件:
? ? ? ? 1. for循環迭代的范圍必須是確定的。對于數組而言,就是數組第一個元素和最后一個元素的范圍。對于類而言,應該提供begin和end方法,begin和end就是for循環迭代的范圍。
? ? ? ? 2. 迭代對象要實現++和==等操作。
?📂 模板別名
? ? ? ? 對于冗長且復雜的標識符,如果能創建其別名就很方便,為此,C++提供了typedef。
typedef std::vector<int>::iterator itType
? ? ? ? C++11提供了另一種用于模板部分具體化,但是typedef不能
template<class T>using arr12 = std::array<T,12>;std::array<double,12> a1 |v
arr12<double> a2;
?📂 nullptr
? ? ? ? 空指針是不會指向有效數據的指針。以前,C++在源代碼中使用0表示這種指針,但內部表示可能不同。這帶來一些問題,因為這使得0既能表示指針常量,也能表示整形常量。所以C++11引入新的關鍵字nullptr,用于表示空指針。nullptr是指針類型,不能轉化為整形常量。但為了向后兼容,C++11仍允許0表示空指針,因此表達式0 == nullptr 為true,但使用nullptr更為安全。
📁 智能指針
? ? ? ? 這里我們智能指針只做出大概講解,在《C++雜貨鋪》專欄中,會出一篇關于智能指針的文章,詳細介紹概念,使用以及底層原理等。
? ? ? ? 如果程序中使用new從堆分配內存,等到不需要時,應使用delete將其釋放。C++引入了智能指針auto_ptr,來幫助自動完成這個過程。隨后的編程(尤其是STL時)表明,需要更精致的機制。基于程序員的編程體驗和BOOST庫提供的解決方案,C++11摒棄了auto_ptr,并新增了三種智能指針:unique_ptr,shared_ptr 和 weak_ptr。
? ? ? ? 所有新增的智能指針都能與STL容器和移動語義協同工作。
📁 右值引用
?📂左值,右值的概念
? ? ? ? 左值是一個表示數據的表達式(如變量名或解引用的指針),我們可以獲取它的地址+可以對它賦值,左值可以出現在賦值符號的左邊,右值不能出現在賦值符號的左邊。但是const 修飾的左值不能賦值,但也可以取地址。總體來說,能取到地址的就是左值,不能取到地址的就是右值
? ? ? ? 左值/右值引用,都是給對象取別名。
? ? ? ? 傳統C++引用(現稱為左值引用)使得標識符關聯到左值。左值是一個表示數據的表達式,程序可以獲取到地址。
? ? ? ? C++11新增了右值引用,使用&&表示。右值引用可以關聯到右值,即出現在賦值表達式右邊,但不能取地址。右值包含字面常量,臨時對象,匿名對象,函數返回值,x+y表達式等。
int x = 10;
int y = 20;int&& r1 = 13;
int&& r2 = x + y;
int&& r3 = std::sort(1);
? ? ? ? r2關聯的是當時計算x+y得到的結果,也就是r2關聯的是33,即使后來修改了x或y,也不會影響r2。
? ? ? ? 需要注意的是右值不能取地址,但是給右值取別名后,會導致右值被存儲到特定的位置,且可以獲得該位置的地址。(即右值引用可以取到地址)也就是說不能將&運算符用于13,但是可以將其存放到右值引用中,通過數據與特定的地址關聯,使得可以通過右值訪問該數據。但是也可以使用const去引用
? ? ? ? 但是右值引用的使用場景并不在此,這個也不重要。
?📂?左值引用和右值引用的比較
左值引用:
? ? ? ? 1. 左值引用只能引用左值,不能引用右值。
? ? ? ? 2. const 左值引用 既可以引用左值,也可以引用右值。
int a = 10;
int& ra1 = a;const int& ra2 = 10;
const int& ra3 = a;
右值引用:
????????1. 右值引用只能右值,不能引用左值。
????????2. 但是右值引用可以引用move以后的左值。
int&& ra = 10;int a = 10;int&& ra1 = a; // errorint&& ra2 = std::move(ra);
?📂 右值引用的使用場景
? ? ? ? 左值引用既可以引用左值,又可以引用右值,那右值引用有什么作用呢?我們先來看看左值引用的短板。
? ? ? ? 左值引用可以作為參數和函數返回值,可以提高效率。但是當函數的返回對象是一個局部變量是,出了函數作用域就不存在了,就不能使用左值引用返回,只能傳值返回。傳值返回會導致知道1次的拷貝構造(舊的編譯器可能會造成兩次拷貝構造)
? ? ? ? 右值引用和移動語義解決了上述問題,C++11中引入了移動語義(移動賦值,移動構造)的概念,移動構造的本質就是將參數右值的資源竊取過來,占為己有,那么就不用深拷貝了,所以它叫做移動構造,就是竊取別人的資源來構造自己。
? ? ? ? 移動構造沒有開辟新的空間,拷貝數據,所以效率就提高了。
? ? ? ? 不僅有移動構造,還有移動賦值
// 移動賦值
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移動語義" << endl;swap(s);return *this;
}
int main()
{bit::string ret1;ret1 = bit::to_string(1234);return 0;
}
// 運行結果:
// string(string&& s) -- 移動語義
// string& operator=(string&& s) -- 移動語義
? ? ? ? 有些場景下,可能需要右值引用去引用左值來實現移動語義。當需要右值yinyong引用一個左值時可以通過move函數將左值轉化為右值。在C++11中,std::move()位于頭文件,它并不搬運任何東西,唯一的功能就是將一個左值強制轉換為右值引用,然后實現移動語義。
????????
?📂 萬能引用和完美轉發
? ? ? ? 模板中&&不代表右值引用,而是萬能引用,既可以接受左值,也能接受右值。模板的萬能引用只是提供了能同時接受左值和右值的能力。
????????但是引用的類型的唯一作用就是限制了接受的類型,后續使用中都會退化為左值。這里需要重點理解,右值引用本身就是一個左值,可以被賦值,可以取到地址。但如果右值引用作為參數傳遞,編譯器會認為這是一個左值,右值引用和左值引用同時存在,匹配左值。
? ? ? ? 因此,如果我們希望能在傳遞過程中保持它的左值或者右值屬性,就需要我們學習完美轉發
void Fun(int &x)
{ cout << "左值引用" << endl; }void Fun(const int &x)
{ cout << "const 左值引用" << endl; }void Fun(int &&x)
{ cout << "右值引用" << endl; }void Fun(const int &&x)
{ cout << "const 右值引用" << endl; }// 模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。
// 模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力,
// 但是引用類型的唯一作用就是限制了接收的類型,后續使用中都退化成了左值,
// 我們希望能夠在傳遞過程中保持它的左值或者右值的屬性, 就需要用我們下面學習的完美轉發
template<typename T>
void PerfectForward(T&& t)
{Fun(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;
}
? ? ? ? 如果,右值引用再去調用函數,形參都是左值接受。
void Func(int& x)
{cout << "func:左值引用" << endl;
}void Func(const int& x)
{cout << "func:const 左值引用" << endl;
}void Func(int&& x)
{cout << "func:右值引用" << endl;
}void Func(const int&& x)
{cout << "func:const 右值引用" << endl;
}void Fun(int& x)
{cout << endl;Func(x);cout << endl;cout << "左值引用" << endl;
}void Fun(const int& x)
{cout << endl;Func(x);cout << endl;cout << "const 左值引用" << endl;
}void Fun(int&& x)
{cout << endl;Func(x);cout << endl;cout << "右值引用" << endl;
}void Fun(const int&& x)
{cout << endl;Func(x);cout << endl;cout << "const 右值引用" << endl;
}// 模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。
// 模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力,
// 但是引用類型的唯一作用就是限制了接收的類型,后續使用中都退化成了左值,
// 我們希望能夠在傳遞過程中保持它的左值或者右值的屬性, 就需要用我們下面學習的完美轉發
template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}int main()
{PerfectForward(10); // 右值cout << endl;int a;PerfectForward(a); // 左值cout << endl;PerfectForward(std::move(a)); // 右值cout << endl;const int b = 8;PerfectForward(b); // const 左值cout << endl;PerfectForward(std::move(b)); // const 右值cout << endl;return 0;
}
? ? ? ? std::forward完美轉發在傳參過程中保留對象原生類型屬性
template<typename T>
void PerfectForward(T&& t)
{Fun(std::forward<T>(t));
}
📁 新的類功能
?📂 強制生成默認函數的關鍵字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:string _name;int _age;
};
?📂 禁止生成默認函數的關鍵字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;
};
?📂 final和override關鍵字
? ? ? ? final :?修飾虛函數,表示該虛函數不能被重寫。
????????override:檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯。
? ? ? ? 這兩個關鍵字在下面這篇文章詳細介紹了。
????????【C++雜貨鋪】多態-CSDN博客
📁 可變模板參數
????????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]這樣方式獲取可變 參數,所以我們的用一些奇招來一一獲取參數包的值。
1. 遞歸方式展開參數包
// 遞歸終止函數
template <class T>
void ShowList(const T& t)
{cout << t << endl;
}
// 展開函數
template <class T, class ...Args>
void ShowList(T value, Args... args)
{cout << value <<" ";ShowList(args...);
}
int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}
2. 逗號表達式展開參數包
????????我們知道逗號表達式會按順序執行逗號前面的表達式。最終會創建一個元素值都為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"));return 0;
}
?📂 STL容器emplace相關接口函數
template <class... Args>
void emplace_back (Args&&... args);
????????首先我們看到的emplace系列的接口,支持模板的可變參數,并且萬能引用。那么相對insert和 emplace系列接口的優勢到底在哪里呢?
? ? ? ? 在使用方面,和push沒有什么區別:
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系列接口,支持傳遞參數:
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;
}
📁 lambda表達式
? ? ? ? 我們有一個類,想要進行排序,需要為此寫一個仿函數,但是如果這個類包含了很多屬性,就需要書寫很多仿函數,仿函數寫起來也是比較麻煩的。因此C++11引入了lambda表達式。lambda表達式的實際就是一個匿名函數。
struct Goods
{string _name; ?// 名字double _price; // 價格int _evaluate; // 評價Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};int main()
{vector<Goods> v = { { "蘋果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠蘿", 1.5, 4 } };sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._price < g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._price > g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate < g2._evaluate; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate > g2._evaluate; });
}
?📂 lambda表達式語法
[capture-list] (parameters) mutable -> return-type { statement }
????????[capture-list] : 捕捉列表,該列表總是出現在lambda函數的開始位置,編譯器根據[]來 判斷接下來的代碼是否為lambda函數,捕捉列表能夠捕捉上下文中的變量供lambda 函數使用。
????????(parameters):參數列表。與普通函數的參數列表一致,如果不需要參數傳遞,則可以 連同()一起省略
????????mutable:默認情況下,lambda函數總是一個const函數(形參不能被修改),mutable可以取消其常量性。使用該修飾符時,參數列表不可省略(即使參數為空)。
????????->returntype:返回值類型。用追蹤返回類型形式聲明函數的返回值類型,沒有返回 值時此部分可省略。返回值類型明確情況下,也可省略,由編譯器對返回類型進行推導。
????????{statement}:函數體。在該函數體內,除了可以使用其參數外,還可以使用所有捕獲 到的變量。
????????注意:參數列表和返回值類型都是可選部分,而捕捉列表和函數體可以為空。因此C++11中最簡單的lambda函數為:[]{}; 該lambda函數不能做任何事情。
捕獲列表說明
????????捕捉列表描述了上下文中那些數據可以被lambda使用,以及使用的方式傳值還是傳引用。
[var]:表示值傳遞方式捕捉變量var
[=]:表示值傳遞方式捕獲所有父作用域中的變量(包括this) [&var]:表示引用傳遞捕捉變量var
[&]:表示引用傳遞捕捉所有父作用域中的變量(包括this)
[this]:表示值傳遞方式捕捉當前的this指針
注意:
1. 父作用域包含lambda函數的語句塊
2. 語法上捕捉列表可以由多個捕捉項組成,并以逗號分割。
????????[=, &a, &b]:以引用傳遞的方式捕捉變量a和b,值傳遞方式捕捉其他所有變量
3. 捕捉列表不允許變量重復捕捉
????????比如:[=, a]:=已經以值傳遞方式捕捉了所有變量,捕捉a重復
4.?在塊作用域以外的lambda函數捕捉列表必須為空。
5.?在塊作用域中的lambda函數僅能捕捉父作用域中局部變量,捕捉任何非此作用域或者非局部變量都會導致編譯錯誤。
6.?lambda表達式之間不能相互賦值,即使看起來類型相同
void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
// 此處先不解釋原因,等lambda表達式底層實現原理看完后,大家就清楚了
//f1 = f2; // 編譯失敗--->提示找不到operator=()
// 允許使用一個lambda表達式拷貝構造一個新的副本
auto f3(f2);
f3();
// 可以將lambda表達式賦值給相同類型的函數指針
PF = f2;
PF();
return 0;
}
?📂 函數對象和lambda表達式
????????函數對象,又稱為仿函數,即可以想函數一樣使用的對象,就是在類中重載了operator()運算符的類對象。從使用方式上來看,函數對象與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);// lamberauto r2 = [=](double monty, int year)->double {return monty * rate * year;};r2(10000, 2);return 0;
}
????????函數對象將rate作為其成員變量,在定義對象時給出初始值即可,lambda表達式通過捕獲列表可以直接將該變量捕獲到。
????????實際在底層編譯器對于lambda表達式的處理方式,完全就是按照函數對象的方式處理的,即:如果定義了一個lambda表達式,編譯器會自動生成一個類,在該類中重載了operator()。
📁 包裝器
?📂 function包裝器
????????function包裝器 也叫作適配器。C++中的function本質是一個類模板,也是一個包裝器。那么我們來看看,我們為什么需要function呢?
//上面func可能是什么呢?那么func可能是函數名?函數指針?函數對象(仿函數對象)?也有可能
//是lamber表達式對象?所以這些都是可調用的類型!如此豐富的類型,可能會導致模板的效率低下!
//為什么呢?我們繼續往下看
template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函數名cout << useF(f, 11.11) << endl;// 函數對象cout << useF(Functor(), 11.11) << endl;// lamber表達式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}
????????通過上面的程序驗證,我們會發現useF函數模板實例化了三份。包裝器可以很好的解決上面的問題
std::function在頭文件<functional>
// 類模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板參數說明:
Ret: 被調用函數的返回類型
Args…:被調用函數的形參
// 使用方法如下:
#include <functional>
int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};
class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};
int main()
{// 函數名(函數指針)std::function<int(int, int)> func1 = f;cout << func1(1, 2) << endl;// 函數對象std::function<int(int, int)> func2 = Functor();cout << func2(1, 2) << endl;// lamber表達式std::function<int(int, int)> func3 = [](const int a, const int b){return a + b; };cout << func3(1, 2) << endl;// 類的成員函數std::function<int(int, int)> func4 = &Plus::plusi;cout << func4(1, 2) << endl;std::function<double(Plus, double, double)> func5 = &Plus::plusd;cout << func5(Plus(), 1.1, 2.2) << endl;return 0;
}
? ? ? ? function就類似于武林盟主,統一了可執行對象的類型,可以接受函數指針,仿函數,lambda表達式,只需要給出返回值,形參即可,統一標識為function包裝器,方便統一進行操作。
?📂 bind包裝器
????????std::bind函數定義在頭文件中,是一個函數模板,它就像一個函數包裝器(適配器),接受一個可調用對象(callable object),生成一個新的可調用對象來“適應”原對象的參數列表。一般而言,我們用它可以把一個原本接收N個參數的函數fn,通過綁定一些參數,返回一個接收M個(通常M<N,M可以大于N,但這么做沒什么意義)參數的新函數。同時,使用std::bind函數還可以實現參數順序調整等操作
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
可以將bind函數看作是一個通用的函數適配器,它接受一個可調用對象,生成一個新的可調用對象來“適應”原對象的參數列表。
int Sub(int a, int b)
{return a - b;
}int main()
{auto f1 = Sub;cout << f1(10, 5) << endl;auto f2 = bind(Sub, placeholders::_2, placeholders::_1);cout << f2(10, 5) << endl;return 0;
}
? ? ? ? bind包裝器返回的是一個function適配器,如果想要將形參個數不一致的函數統一進行處理,利用bind將參數個數調整為一致的。? ? ? ??
// 使用舉例
#include <functional>
int Plus(int a, int b)
{return a + b;
}
class Sub
{
public:int sub(int a, int b){return a - b;}
};
int main()
{//表示綁定函數plus 參數分別由調用 func1 的第一,二個參數指定std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,placeholders::_2);//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);//func2的類型為 function<void(int, int, int)> 與func1類型一樣//表示綁定函數 plus 的第一,二為: 1, 2auto func2 = std::bind(Plus, 1, 2);cout << func1(1, 2) << endl;cout << func2() << endl;Sub s;// 綁定成員函數std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,placeholders::_1, placeholders::_2);// 參數調換順序std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,placeholders::_2, placeholders::_1);cout << func3(1, 2) << endl;cout << func4(1, 2) << endl;return 0;
}
📁 線程庫
📂 thread 類介紹
????????在C++11之前,涉及到多線程問題,都是和平臺相關的,比如windows和linux下各有自己的接口,這使得代碼的可移植性比較差。C++11中最重要的特性就是對線程進行支持了,使得C++在并行編程時不需要依賴第三方庫,而且在原子操作中還引入了原子類的概念。要使用標準庫中的線程,必須包含< thread >頭文件。
http://www.cplusplus.com/reference/thread/thread/?kw=thread
thread()
構造一個線程對象,沒有關聯任何線程函數,即沒有啟動任何線程thread(fn,args1, args2,...)
構造一個線程對象,并關聯線程函數fn,args1,args2,...為線程函數參數get_id()
獲取線程idjionable()
線程是否還在執行,joinable代表的是一個正在執行中的線程。jion()
該函數調用后會阻塞住線程,當該線程結束后,主線程繼續執行detach()
在創建線程對象后馬上調用,用于把被創建線程與線程對象分離開,分離
的線程變為后臺線程,創建的線程的"死活"就與主線程無關
void func()
{cout << "hello world" << endl;
}int main()
{thread t1(func);cout << t1.get_id() <<endl;cout << this_thread::get_id() <<endl; // get_id 還存放在this_thread 類想要調用主線程的tid,就要使用this_thread里的t1.join(); //線程必須等待return 0;
}
????????線程是操作系統中的一個概念,線程對象可以關聯一個線程,用來控制線程以及獲取線程的狀態。
????????當創建一個線程對象后,沒有提供線程函數,該對象實際沒有對應任何線程
????????get_id()的返回值類型為id類型,id類型實際為std::thread命名空間下封裝的一個類,該類中包含了一個結構體:
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t
????????當創建一個線程對象后,并且給線程關聯線程函數,該線程就被啟動,與主線程一起運行。
????????線程函數一般情況下可按照以下三種方式提供:
? ? ? ? 1. 函數指針
? ? ? ? 2. lambda表達式
? ? ? ? 3. 函數對象(仿函數)
????????thread類是防拷貝的,不允許拷貝構造以及賦值,但是可以移動構造和移動賦值,即將一個
線程對象關聯線程的狀態轉移給其他線程對象,轉移期間不意向線程的執行。
????????可以通過jionable()函數判斷線程是否是有效的,如果是以下任意情況,則線程無效
????????采用無參構造函數構造的線程對象
????????線程對象的狀態已經轉移給其他線程對象
????????線程已經調用jion或者detach結束
📂 線程函數參數
????????線程函數的參數是以值拷貝的方式拷貝到線程棧空間中的,因此:即使線程參數為引用類型,在線程中修改后也不能修改外部實參,因為其實際引用的是線程棧中的拷貝,而不是外部實參。
?
#include <thread>
void ThreadFunc1(int& x)
{x += 10;
}
void ThreadFunc2(int* x)
{*x += 10;
}
int main()
{int a = 10;// 在線程函數中對a修改,不會影響外部實參,因為:線程函數參數雖然是引用方式,但其實際//引用的是線程棧中的拷貝thread t1(ThreadFunc1, a);t1.join();cout << a << endl;// 如果想要通過形參改變外部實參時,必須借助std::ref()函數thread t2(ThreadFunc1, std::ref(a);t2.join();cout << a << endl;// 地址的拷貝thread t3(ThreadFunc2, &a);t3.join();cout << a << endl;return 0;
}
? ? ? ? 注意:如果是類成員函數作為線程參數時,必須將this作為線程函數參數。
📂 原子性操作庫
????????多線程最主要的問題是共享數據帶來的問題(即線程安全)。如果共享數據都是只讀的,那么沒問題,因為只讀操作不會影響到數據,更不會涉及對數據的修改,所以所有線程都會獲得同樣的數據。但是,當一個或多個線程要修改共享數據時,就會產生很多潛在的麻煩。比如:
?
#include <iostream>
using namespace std;
#include <thread>
unsigned long sum = 0L;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum++;
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}
????????傳統的解決方式:可以對共享修改的數據可以加鎖保護。雖然加鎖可以解決,但是加鎖有一個缺陷就是:只要一個線程在對sum++時,其他線程就會被阻塞,會影響程序運行的效率,而且鎖如果控制不好,還容易造成死鎖。
????????因此C++11中引入了原子操作。所謂原子操作:即不可被中斷的一個或一系列操作,C++11引入的原子操作類型,使得線程間數據的同步變得非常高效。
????????在C++11中,程序員不需要對原子類型變量進行加鎖解鎖操作,線程能夠對原子類型變量互斥的訪問。更為普遍的,程序員可以使用atomic類模板,定義出需要的任意原子類型。
atomic<T> t; // 聲明一個類型為T的原子類型變量t
📂 互斥鎖
????????在多線程環境下,如果想要保證某個變量的安全性,只要將其設置成對應的原子類型即可,即高效又不容易出現死鎖問題。但是有些情況下,我們可能需要保證一段代碼的安全性,那么就只能通過鎖的方式來進行控制。
????????鎖控制不好時,可能會造成死鎖,最常見的比如在鎖中間代碼返回,或者在鎖的范圍內拋異常。因此:C++11采用RAII的方式對鎖進行了封裝,即lock_guard和unique_lock。
? ? ? ??lock_guard和unique_lock 封裝了mutex,實例化時加鎖,出作用域銷毀時,釋放鎖,即調用構造函數成功上鎖,調用析構函數自動解鎖,可以有效避免死鎖問題。
????????unique_lock 和 lock_guard 的唯一區別是,unique_lock 可以手動操作。
鎖mutex的類型:
1. std::mutex
????????C++11提供的最基本的互斥量,該類的對象之間不能拷貝,也不能進行移動。mutex最常用的三個函數
lock() | 上鎖:鎖住互斥量 |
unlock() | 解鎖:釋放對互斥量的所有權 |
try_lock() | 嘗試鎖住互斥量,如果互斥量被其他線程占有,則當前線程也不會被阻塞 |
2. std::recursive_mutex
????????其允許同一個線程對互斥量多次上鎖(即遞歸上鎖),來獲得對互斥量對象的多層所有權,釋放互斥量時需要調用與該鎖層次深度相同次數的 unlock(),除此之外, std::recursive_mutex 的特性和 std::mutex 大致相同。
3. std::timed_mutex
比 std::mutex 多了兩個成員函數,try_lock_for(),try_lock_until()
? ? try_lock_for():
????????接受一個時間范圍,表示在這一段時間范圍之內線程如果沒有獲得鎖則被阻塞住(與std::mutex 的 try_lock() 不同,try_lock 如果被調用時沒有獲得鎖則直接返回false),如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false。
? ? try_lock_until():
????????接受一個時間點作為參數,在指定時間點未到來之前線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false。
4. std::recursive_timed_mutex
📂 條件變量condition_variable
????????condition_variable - C++ Reference (cplusplus.com)
//wait:
void wait (unique_lock<mutex>& lck);template <class Predicate>void wait (unique_lock<mutex>& lck, Predicate pred);
// 第二個參數為 函數對象,返回false 阻塞 釋放鎖 ; true 不阻塞,往下執行
📁 總結
? ? ? ? 以上就是本期【C++雜貨鋪】的主要內容了,包含了C++11帶來的最主要,常用的新特性,講解了基本的使用方法,以及概念等內容。
? ? ? ? 如果感覺本期內容對你有幫助,歡迎點贊,收藏,關注Thanks?(・ω・)ノ