10. lambda表達式【★】
例如在C++98
中,如果想要對一個數據集合中的元素進行排序,可以使用std::sort
方法,具體如下:
#include <algorithm>
#include <functional>
int main(){
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默認按照小於比較,排出來結果是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));
for (auto& e : array) {
cout << e << " ";
}
cout << endl;
// 如果需要降序,需要改變元素的比較規則
std::sort(array, array + sizeof(array)/sizeof(array[0]),greater<int>());
for (auto& e : array) {
cout << e << " ";
}
return 0;
}
輸出結果:
0 1 2 3 4 5 6 7 8 9
9 8 7 6 5 4 3 2 1 0
如果待排序元素爲自定義類型,需要用戶定義排序時的比較規則:
struct Goods{
string _name;
double _price;
};
struct Compare{ //仿函數
bool operator()(const Goods& gl, const Goods& gr){
return gl._price <= gr._price;
}
};
int main(){
Goods gds[] = { { "蘋果", 2.1 }, { "香蕉", 3 }, { "橙子", 2.2 }, {"菠蘿", 1.5} };
sort(gds, gds+sizeof(gds) / sizeof(gds[0]), Compare());
for (auto& e : gds) {
cout << e._name << ':' << e._price << " ";
}
cout << endl;
return 0;
}
輸出結果:
隨着C++
語法的發展,人們開始覺得上面的寫法太複雜了,每次爲了實現一個algorithm
算法, 都要重新去寫一個類 / 仿函數,如果每次比較的邏輯不一樣,還要去實現多個類,特別是相同類的命名,這些都給編程者帶來了極大的不便。因此,在C++11
語法中出現了Lambda
表達式:
int main(){
Goods gds[] = { { "蘋果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠蘿", 1.5} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const Goods& r){
return l._price < r._price;
}
);
return 0;
}
上述代碼就是使用C++11
中的lambda
表達式來解決,可以看出lambda
表達式實際是一個匿名函數。
語法
格式:
[capture-list] (parameters) mutable -> return-type { statement }
[capture-list]
: 捕捉列表。
該列表總是出現在lambda函數的開始位置,編譯器根據[]
來判斷接下來的代碼是否爲lambda函數,捕捉列表能夠捕捉上下文中的變量供lambda函數使用。(parameters)
:參數列表。
與普通函數的參數列表一致,如果不需要參數傳遞,則可以連同()
一起省略。mutable
:默認情況下,lambda函數總是一個const函數,mutable可以取消其常量性。使用該修飾符時,參數列表不可省略(即使參數爲空)。->return-type
:返回值類型。
用追蹤返回類型形式聲明函數的返回值類型,沒有返回值時此部分可省略。返回值類型明確情況下,也可省略,由編譯器對返回類型進行推導。{statement}
:函數體。
在該函數體內,除了可以使用其參數外,還可以使用所有捕獲到的變量。
注意: 在lambda函數定義中,參數列表和返回值類型都是可選部分,而捕捉列表和函數體可以爲空。
- 因此C++11中最簡單的lambda函數爲:
[]{};
,該lambda函數不能做任何事情。
int main(){
// 最簡單的lambda表達式, 該lambda表達式沒有任何意義
[]{};
// 省略參數列表和返回值類型,返回值類型由編譯器推導爲int
int a = 3, b = 4;
[=]{return a + 3; };
// 省略了返回值類型,無返回值類型
auto fun1 = [&](int c){b = a + c; }; //匿名函數在此體現
fun1(10);
cout << a << " " << b << endl;
// 各部分都很完善的lambda函數
auto fun2 = [=, &b](int c)->int{ return b += a + c; };
cout << fun2(10) << endl;
// 複製捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
輸出結果:
3 13
26
30
通過上述代碼可以看出,lambda
表達式實際上可以理解爲無名函數,該函數無法直接調用,如果想要直接調用,可藉助auto
將其賦值給一個變量。
捕獲列表
捕捉列表描述了上下文中那些數據可以被lambda使用,以及使用的方式傳值還是傳引用。
[var]
:表示值傳遞方式捕捉變量var[=]
:表示值傳遞方式捕獲所有父作用域中的變量(包括this
)[&var]
:表示引用傳遞捕捉變量var[&]
:表示引用傳遞捕捉所有父作用域中的變量(包括this
)[this]
:表示值傳遞方式捕捉當前的this
指針
【注】:
- 父作用域指包含
lambda
函數的語句塊 - 語法上捕捉列表可由多個捕捉項組成,並以逗號分割。
比如:[=, &a, &b]
:以引用傳遞的方式捕捉變量a
和b
,值傳遞方式捕捉其他所有變量
[&,a, this]
:值傳遞方式捕捉變量a
和this
,引用方式捕捉其他變量 - 捕捉列表不允許變量重複傳遞,否則就會導致編譯錯誤。
比如:[=, a]
:=
已經以值傳遞方式捕捉了所有變量,捕捉a
重複 - 在塊作用域以外的
lambda
函數捕捉列表必須爲空。 - 在塊作用域中的
lambda
函數僅能捕捉父作用域中局部變量,捕捉任何非此作用域或者非局部變量都會導致編譯報錯。 lambda
表達式之間不能相互賦值,即使看起來類型相同。
void (*PF)();
int main(){
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
f1 = f2; // 編譯失敗--->提示找不到operator=()
// 允許使用一個lambda表達式拷貝構造一個新的副本
auto f3(f2);
f3();
// 可以將lambda表達式賦值給相同類型的函數指針
PF = f2;
PF();
return 0;
}
函數對象與lambda表達式對比
- 函數對象,又稱爲仿函數,即可以像函數一樣使用的對象,是在類中重載了
operator()
運算符的類對象。
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);
// lambda
auto r2 = [=](double monty, int year)->double {return monty * rate*year; };
r2(10000, 2);
return 0;
}
從使用方式上來看,函數對象與lambda
表達式完全一樣。
函數對象將rate
作爲其成員變量,在定義對象時給出初始值即可,lambda
表達式通過捕獲列表可以直接將該變量捕獲到。
- 實際在底層編譯器對於
lambda
表達式的處理方式完全就是按照函數對象的方式處理的:
如果定義了一個lambda
表達式,編譯器會自動生成一個類,在該類中重載了operator()
。
11. 線程庫【★】
C++11
中很重要的特性之一就是對線程進行支持了,使得C++
在並行編程時不需要依賴第三方庫,而且在原子操作中還引入了原子類的概念。要使用標準庫中的線程,必須包含< thread >
頭文件,該頭文件聲明瞭std::thread
線程類。
官方文檔:【http://www.cplusplus.com/reference/thread/thread/?kw=thread】
演示:
#include <iostream>
#include <thread>
using namespace std;
void fun(){
cout << "A new thread!" << endl;
}
int main(){
thread t(fun);
t.join();
cout << "Main thread!" << endl;
return 0;
}
線程的啓動
C++
線程庫通過構造一個線程對象來啓動一個線程,該線程對象中就包含了線程運行時的上下文環境,比如:線程函數、線程棧、線程起始狀態等以及線程ID等,所有操作全部封裝在一起,最後在底層統一傳遞給_beginthreadex()
創建線程函數來實現
【注】:_beginthreadex
是windows
中創建線程的底層c
函數)。
std::thread()
創建一個新的線程可以接受任意的可調用對象類型(帶參數或者不帶參數),包括lambda
表達式(帶變量捕獲或者不帶),函數,函數對象,以及函數指針。
// 使用lambda表達式作爲線程函數創建線程
int main(){
int n1 = 500;
int n2 = 600;
thread t([&](int addNum){
n1 += addNum;
n2 += addNum;
}, 500);
t.join();
std::cout << n1 << ' ' << n2 << std::endl;
return 0;
}
線程的結束
啓動了一個線程後,當這個線程結束的時候,如何去回收線程所使用的資源呢?thread
庫給我們兩種選擇:
- 加入式:
join()
join()
:會主動地等待線程的終止。在調用進程中join()
,當新的線程終止時,join()
會清理相關的資源,然後返回,調用線程再繼續向下執行。由於join()
清理了線程的相關資源,thread
對象與已銷燬的線程就沒有關係了,因此一個線程的對象每次你只能使用一次join()
,當你調用的join()
之後joinable()
就將返回false
了。
- 分離式:
detach()
detach()
:會從調用線程中分離出新的線程,之後不能再與新線程交互。就像是和女朋友分手,那之後你們就不會再有聯繫(交互)了,而她的之後消費的各種資源也就不需要你去埋單了(清理資源)。此時調用joinable()
必然是返回false
。分離的線程會在後臺運行,其所有權和控制權將會交給C++
運行庫。同時,C++
運行庫保證,當線程退出時,其相關資源的能夠正確的回收。
【注】:必須在thread
對象銷燬之前做出選擇,這是因爲線程可能在你加入或分離線程之前,就已經結束了,之後如果再去分離它,線程可能會在thread
對象銷燬之後繼續運行下去。
原子性操作庫(atomic)
多線程最主要的問題是共享數據帶來的問題(即線程安全)。如果共享數據都是隻讀的,那麼沒問題,因爲只讀操作不會影響到數據,更不會涉及對數據的修改,所以所有線程都會獲得同樣的數據。
但是,當一個或多個線程要修改共享數據時,就會產生很多潛在的麻煩~ 如:
#include <iostream>
#include <thread>
using namespace std;
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
:11940842
,出現錯誤!!!因爲線程不安全!
C++98
中傳統的解決方式:可以對共享修改的數據可以加鎖保護。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
std::mutex m;
unsigned long sum = 0L;
void fun(size_t num){
for (size_t i = 0; i < num; ++i){
m.lock(); //加鎖
sum++;
m.unlock(); //解鎖
}
}
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
:20000000
雖然加鎖可以解決,但是加鎖有一個缺陷就是:
- 只要一個線程在對
sum++
時,其他線程就會被阻塞,會影響程序運行的效率,而且鎖如果控制不好,還容易造成死鎖。因此C++11
中引入了原子操作!
需要使用以上原子操作變量時,必須添加頭文件
<atomic>
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic_long sum{ 0 };
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, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
cout << "After joining, sum = " << sum << std::endl;
return 0;
}
輸出sum
:20000000
感謝您閱讀至此,C++11
專題就全部講解完了,相信您已對C++11
標準有了較爲全面的瞭解和認識,新特性的出現表示着語言藝術的迭代,感謝挖井人的不懈領航,希望互聯網行業可以生生不息,蓬勃發展~
【從零學C++11(上)】
列表初始化
、decltype
關鍵字、委派構造
等新特性
【https://blog.csdn.net/qq_42351880/article/details/100140163】
【從零學C++11(中)】
移動語義
、右值引用
、std::move()
、完美轉發
等新特性
【https://blog.csdn.net/qq_42351880/article/details/100144856】