目錄
- 🍁統一的列表初始化 { }
- initializer_list
- 🍁decltype 推導表達式類型
- 🍁可變參數模板
- 解析可變參數包
- 方法一
- 方法二
- 🍁lambda 表達式
- 捕捉列表的使用
- 運用場景舉例
- lambda表達式 與 函數對象
🍁統一的列表初始化 { }
在 C++98 標準中,花括號 { } 可以對數組、結構體元素進行同一的初始化處理:
struct Point
{int _x;int _y;
};int main()
{int arr[] = { 1, 2, 3, 4, 5 }; //初始化數組char str[] = { "hello world" };Point p = { 1, 2 };return 0;
}
時間來到 C++11 的時候,就擴大了花括號 { } 列表的使用范圍。
花括號 { } 可以用來所有的內置類型 和 自定義類型;簡單的來說就是可以用花括號來初始化一切變量,并且可以省略賦值符號。
struct Point
{int _x;int _y;
};int main()
{int x1 = 10;int x2 = { 20 }; //初始化變量x2int array1[] { 1, 2, 3, 4, 5 }; //初始話array1數組省略賦值符號char str[] { "hello world" };Point p { 1, 2 };return 0;
}
列表初始化可以對 new 對象進行初始化:
int* pa = new int[5]{ 1, 2, 3, 4, 5 };
創建對象時使用列表初始化會調用該對象的構造函數:
class Date
{
public:Date(int year, int month, int day):_year(year), _month(month), _day(day){std::cout << "Date(int year, int month, int day)" << std::endl;}private:int _year;int _month;int _day;
};int main()
{Date d(2024, 1, 1); //調用構造函數//使用列表初始化Date d1 = { 2024, 1, 2 }; //構造+拷貝構造==>編譯優化為構造Date d2{ 2024 ,1, 3 };return 0;
}
列表初始化還可以運用在容器上:
#include <vector>
#include <list>int main()
{std::vector<int> v1 = { 1, 2, 3, 4, 5 }; //初始化vector容器std::list<int> lt1 = { 10, 20, 30, 40 }; //初始化list容器return 0;
}
注意:在初始化 vector 和 list 這樣的容器的時候,并不是直接去調用 vector 和 list 的構造函數。vector 和 list 的構造函數也不支持這么多參數的傳參。
initializer_list
花括號里面的初始化內容,C++會識別成 initializer_list
:
int main()
{auto i1 = { 1, 2, 3, 4, 5, 6 };auto i2 = { 10, 20, 30, 40, 50, 60 };std::cout << typeid(i1).name() << std::endl;std::cout << typeid(i2).name() << std::endl;return 0;
}
initializer_list
是一個類:
initializer_list
會構建一個類型,這個類型有兩個指針:第一個指針指向列表的開始,另一個指針指向列表結尾的下一個位置
int main()
{auto i1 = { 1, 2, 3, 4, 5, 6 };auto it1 = i1.begin();auto it2 = i1.end();std::cout << it1 << std::endl;std::cout << it2 << std::endl;std::cout << it2 - it1 << std::endl; return 0;
}
尾指針地址減去列表首元素的地址,得到就是列表元素個數
提示:列表中的內容是不能修改的,因為它們是被存放到常量區
int main()
{auto i1 = { 1, 2, 3, 4, 5, 6 };auto it = i1.begin();(*it)++; //報錯return 0;
}
當然,我們也可以使用這個類:
int main()
{std::initializer_list<int> il = { 1, 2, 3, 4, 5, 6 };for (auto& e : il){std::cout << e << " ";}std::cout << std::endl;return 0;
}
為什么 vector 和 list 容器能夠支持列表初始化呢?
C++11 標準出來后,像 vector 和 list 這樣的容器推出了這樣的構造函數:
vector(initializer_list<value_type> il, const allocator_type& alloc = allocator_type());
vector 和 list 通過 initializer_list
類去初始化列表,進而實現 vector 和 list 的構造初始化。
下面來實現一個簡單版的 vector 支持 initializer_list 的構造函數:
vector(std::initializer_list<T> il)
{reserve(il.size()); //檢查容量for (auto& e : il)push_back(e);
}
下面再來舉例幾個列表初始化的案例:
#include <map>
#include <set>
#include <vector>int main()
{std::vector<int> v1 = { 1, 2, 3, 4, 5 };std::vector<int> v2 = { 10, 20, 30, 40, 50 };std::vector<std::vector<int>> vv1 = { v1 ,v2 };//對象初始化std::vector<std::vector<int>> vv2 = { std::vector<int>{100, 200, 300}, v2 }; //匿名對象初始化std::vector<std::vector<int>> vv3 = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; //編譯器自動推導類型std::set<std::vector<int>> s = { {1, 2, 3}, {10, 20, 30} };std::map<std::string, int> m = { {"蘋果", 1}, {"香蕉", 2}, {"哈密瓜", 3} };return 0;
}
🍁decltype 推導表達式類型
decltype 是 C++11 中引入的一個新的關鍵字,主要用于 聲明和推導表達式的類型
int main()
{int x = 10;int y = 20;double a = 1.1;double b = 2.2;decltype (x + y) ret1;decltype (b - a) ret2;decltype (&x) ret3;std::cout << typeid(ret1).name() << std::endl;std::cout << typeid(ret2).name() << std::endl;std::cout << typeid(ret3).name() << std::endl;return 0;
}
decltype 可以用于 auto 推導不了類型的場景,例如模板的實例化傳參:
int main()
{double a = 1.1;double b = 2.2;//要求vector存儲與 a*b 表達式的返回值一致的類型std::vector<decltype(a * b)> v; //利用 decltype 推導表達式的類型return 0;
}
🍁可變參數模板
C++98/03中,類模板的和函數模板中只能含有固定數量的模板參數,比較局限。在 C++11 引入了可變參數模板,可以創建可變參數函數和類模板。
示例:
template <class ...Args> //...Args表示模板參數包
void ShowList(Args... args)
{}int main()
{ShowList();ShowList(1);ShowList(1, 'x'); //不會限制傳入的類型和參數的個數return 0;
}
- Args是一個模板參數包,args是一個函數形參參數包
在函數聲明一個形參參數包 Args... args
,表示這個參數包中可以被傳入的參數個數是 0 個甚至是多個參數
也可以用sizeof
來統計傳入參數的個數,如下:
#include <iostream>
using namespace std;template<typename ...Args>
void ShowList(Args... args)
{cout << sizeof...(args) << endl; //統計傳入參數的個數
}int main()
{ShowList();ShowList(1);ShowList(1, 'x'); //不會限制類型ShowList(1, 'x', "abc"); return 0;
}
解析可變參數包
可變參數的作用我們看到了,就是解決了傳參個數的限制。但是,當一個函數設置了參數包,那么在函數內部我們如何去獲取可變參數包參數變量呢?
方法一
C++11提供了一個遞歸的方式來解析可變參數包,在函數模板中多設置一個模板參數:
void ShowList()
{cout << endl;
}template<typename T, typename ...Args>
void ShowList(const T& val, Args... args)
{cout << val << " "; ShowList(args...);//傳入參數包,遞歸解析
}
int main()
{ShowList();ShowList(1);ShowList(1, 'x'); ShowList(1, 'x', "abc"); return 0;
}
當參數被傳入后,T 模板參數 val 會獲取首次傳入的參數。在之后都是獲取到可變參數包的參數內容,每獲取一次,可變參數個數遞歸傳給下一次 ShowList 的參數個數就會減少一次。至此,就達到解析可變參數包的效果。
由于在參數包中傳入的參數個數可以是 0 個,因此當參數包個數為 0 時也就是遞歸結束的條件!
方法二
通過調用函數的方式初始化數組來解析參數包:
template<typename T>
int PrintArg(const T& t)
{cout << t << " ";return 0;
}
template<typename ...Args>
void ShowList(Args... args)
{int arr[] = { PrintArg(args)... }; //初始化數組cout << endl;
}
int main()
{ShowList();ShowList(1);ShowList(1, 'x'); ShowList(1, 'x', "abc"); return 0;
}
以 ShowList(1, 'x', "abc");
為例子,編譯器在編譯階段會將上面代碼解析為下面這樣:
void ShowList(int a1, char a2, string a3)
{int arr[] = { PrintArg(a1), PrintArg(a2), PrintArg(a3) };cout << endl;
}
使用參數包會影響編譯器的效率,因為要推演函數參數的類型。對此,一般的參數包都會設計成下面這樣的情況:
這里拿 ShowList 模板函數為例子
template<typename ...Args>
void ShowList(Args&&... args) //這里的&&表示折疊引用
{int arr[] = { PrintArg(args)... }; //初始化數組cout << endl;
}
在參數包后加上 &&
,在推演參數類型時,傳遞的參數為左值 &&
就會折疊為左值;傳遞的參數為右值 &&
就沒有變化還是右值;因此,在參數包后加上 &&
,也被稱為萬能引用!
🍁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 } };return 0;
}
vector 容器中的 Goods 對象有這樣的屬性:價格、名稱 和 重量。
現在,如果想要將 vector 容器中的 Goods對象 按照價格進行降序排序,正常操會是這樣的:先定義一個對貨物的價格做比較的仿函數,再使用 sort 函數進行排序處理
struct ComparePriceGreater //定義仿函數
{bool operator()(const Goods& g1, const Goods& g2){return g1._price > g2._price;}
};int main()
{vector<Goods> v = { { "蘋果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠蘿", 1.5, 4 } };//降序sort(v.begin(), v.end(), ComparePriceGreater());return 0;
}
為了方便展示,這里直接使用了 VS 的監視窗口。
排序前:
排序后結果如下:
可以看到在 vector 容器中,各個物品都是按照價格降序的順序進行排列。
在使用仿函數時,需要定義一個類,實現對應的函數運算符重載。仿函數的名字,定義都是有些許的繁瑣和麻煩。
在 C++11引出了這么一個新的語法:lambda 表達式 可以代替仿函數而去使用 sort 函數。
下面就來介紹一下 lambda 表達式。
lambda 表達式由以下幾個部分構成:
[]
捕捉列表:編譯器會根據[]
來判斷代碼是否為 lambda 表達式。[]
捕捉列表 用于捕獲上下域中的變量提供給lambda 函數使用()
參數列表:與普通的函數參數列表一樣,當形參不存在時,()
可以省略不寫- mutable:通常情況下,lambda 表達式總是一個const 函數,mutable 關鍵字用于取消 lambda表達式的常性,可以省略
->
返回值類型:用追蹤返回類型形式聲明函數的返回值類型,返回值類型明確情況下可以省略不寫,由編譯器自動推導{}
函數體:實現 lambda 表達式的功能,函數體內部可以使用形參列表的內容,以及被捕獲的變量值
lambda 表達式的書寫格式:[] () mutable->return-type {}
注意:lambda 表達式返回值是一個對象
下面來舉個示例,實現兩個數相加的 lambda 表達式 :
int main()
{std::cout << [](int x, int y)->int { return x + y; }(1, 2) << std::endl;return 0;
}
但是這樣寫 lambda表達式很抽象,不利于代碼的閱讀。
lambda 表達式返回值是一個對象,可以將上面代碼改寫為下面這樣:
int main()
{auto add = [](int x, int y)->int { return x + y; }; //讓編譯器自動推導lambda表達式的類型std::cout << add(10, 20) << std::endl;return 0;
}
將 lambda 表達式返回,編譯器會自動推導 lambda表達式類型定義為add。實例化一個 add 匿名對象執行對應的相加功能,通過打印輸最后出到終端。
上面是通過傳參的方式實現兩個數相加的功能,下面用 lambda 表達式的捕獲方式來實現相加功能:
int main()
{int x = 10, y = 20;auto add = [x, y]()->int { return x + y; }; //用[]來捕獲x和y的值std::cout << add() << std::endl;return 0;
}
對 lambda 表達式有了一定了解后,我們再回過頭來看看先前舉的例子:
struct ComparePriceGreater //定義仿函數
{bool operator()(const Goods& g1, const Goods& g2){return g1._price > g2._price;}
};int main()
{vector<Goods> v = { { "蘋果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠蘿", 1.5, 4 } };//降序sort(v.begin(), v.end(), ComparePriceGreater());return 0;
}
寫仿函數的進行排序的方式是不是太過于有點麻煩了,我們可以將仿函數改寫成 lambda 表達式:
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) //使用lambda表達式{return g1._price > g2._price;});return 0;
}
排序前:
排序后:
使用lambda表達式進行排序的實現效果跟使用仿函數一樣
捕捉列表的使用
下面用 lambda 表達式實現 swap 函數:
可以通過參數列表來實現,不過在使用參數列表時,需要用引用參數來接收:
int main()
{int x = 10, y = 20;//引用傳入變量,正常值拷貝不會影響lambda表達式外的變量auto swap = [](int& x, int& y) {int tmp = x;x = y;y = tmp;};swap(x, y); //實例化swap匿名對象cout << x << " " << y << endl; return 0;
}
lambda 表達式內部作用域 與 當前使用 lambda表達式 的函數作用域是分開的。
也就是說:在當前函數中的變量,在 lambda 表達式內部是使用不了的。如果,想要在 lambda 表達式中使用當前函數的變量,有兩種方式:參數列表傳參 和 捕捉列表。
如上舉例,可以想象成函數傳參。
下面再來通過捕獲列表來實現 swap交換功能:
int main()
{int x = 10, y = 20;auto swap = [x, y]() //捕獲x,y變量{int tmp = x;x = y;y = tmp;};swap();cout << x << " " << y << endl;return 0;
}
但是,直接捕獲的變量,對其直接進行修改會報錯:
在 lambda表達式中 被捕獲的變量是按照拷貝的形式。
這里的 x 和 y 變量被捕獲到 lambda表達式后,是被 const 修飾過的,直接進行值修改會報錯 。
捕捉列表有兩種捕獲變量的方式:傳值捕捉 和 傳引用捕捉
傳值捕捉 和 傳引用捕捉 可以在捕捉列表中任意組合,下面來舉例幾個案例:
int x = 10, y = 20;
[&]() {};//全部傳引用捕捉
[=]() {};//全部傳值捕捉//混合捕捉
[&x, y]() {};
[&, x]() {};
[=, &y]() {};
捕捉列表不能重復捕捉同一個變量,下面這種情況編譯器會報錯:
int x = 10;
[=, x]() {}; //出錯
回到剛剛實現的 lambda 表達式。如果想要通過 傳值捕捉 實現 swap 交換的功能,就要在 lambda 表達式中就要加上 mutable
關鍵字:
int main()
{int x = 10, y = 20;auto swap = [x, y]() mutable //捕獲x,y變量,使用mutable關鍵字{int tmp = x;x = y;y = tmp;};swap();cout << x << " " << y << endl;return 0;
}
當然,也可以通過 傳引用捕捉 的方式實現對應的功能:
int main()
{int x = 10, y = 20;auto swap = [&x, &y]() //傳引用捕捉{int tmp = x;x = y;y = tmp;};swap();cout << x << " " << y << endl;return 0;
}
運用場景舉例
實現這樣的一個程序,在這個程序中創建線程池,使得每個線程都能夠執行打印特定數字的功能:
#include <thread>
#include <vector>using namespace std;int main()
{int n = 0;//創建n個線程cin >> n;vector<thread> thds(n); //創建n個默認構造線程for (int i = 0; i < n; i++){size_t m = 0;cin >> m; //輸入打印數的范圍//創建線程池,使每個線程執行打印功能thds[i] = thread([i, m]() //移動賦值,創建的匿名線程是將亡值{for (int j = 0; j < m; j++){//打印對應線程編號與數字cout << this_thread::get_id() << ":" << j << endl;}cout << endl;});}//等待線程池for (auto& th : thds) th.join(); //這里必須傳引用,線程沒有拷貝構造(沒有意義)return 0;
}
lambda表達式 與 函數對象
先來介紹一下函數對象:函數對象,又被稱為仿函數。實現一個類,在這個類中實現一個 operator() 運算符重載。實例化出這個類對象,在調用 operator() 時,就像調用函數那般。就被稱為仿函數。
下面實現一個仿函數 和 lambda 表達式,實現的功能都類似:
class Rate
{
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return _rate * money * year;}
private:double _rate;
};int main()
{//函數對象double rate = 0.49;Rate r1(rate);//構造r1(1000, 2);//lambda表達式auto r2 = [=](double money, int year)->double{return money * year * rate;};r2(1000, 2);return 0;
}
在VS2022調試下,查看反匯編:
下面再來計算一下仿函數的大小和 lambda表達式的大小,還是拿上面的例子:
class Rate
{
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return _rate * money * year;}
private:double _rate;
};int main()
{double rate = 0.49;auto r2 = [=](double money, int year)->double{return money * year * rate;};cout << sizeof Rate << endl; //查看Rate類的大小cout << sizeof r2 << endl; //查看r2的lambda表達式的大小return 0;
}
反觀底層,仿函數和lambda表達式都是類似的匯編調用方式。而且,如果 lambda表達式捕獲的變量 與 仿函數類中的成員一樣,那么計算的大小都是一樣的。
可以這樣說:在編譯器眼里,lambda表達式就是仿函數。只不過在用戶表面看來,兩個表達式方式是那么的不一樣。
lambda 表達式就介紹到這里。
C++11新增的語法當然還不止這些,如果對 C++11 還感興趣的老鐵,可以看看小編的另一篇文章:C++入門語法介紹