目錄
1、{ }初始化
1.1 C++98的{ }
1.2 C++11的{ }
1.3?C++11中的std::initializer_list
總結一下:
2、lambda
2.1 lambda的語法
2.2 捕捉列表
2.3 lambda的應用
2.4 lambda的原理
3、包裝器
3.1 function
3.2 bind
1、{ }初始化
1.1 C++98的{ }
C++98中一般數組和結構體可以用{ }進行初始化。
struct Point
{int _x;int _y;
};int main()
{int array1[] = {1, 2, 3, 4, 5};int array2[5] = {0};Point p = {1, 2};return 0;
}
1.2 C++11的{ }
C++11 以后想統一初始化方式,試圖實現一切對象皆可用 { } 初始化,{ } 初始化也叫做列表初始化。
內置類型支持,自定義類型也支持,自定義類型本質是類型轉換,中間會產生臨時對象,最后優化
了以后變成直接構造。
{ } 初始化的過程中,可以省略?=?。
#include <iostream>
#include <vector>
using namespace std;struct Point
{int _x;int _y;
};class Date
{
public:Date(int year = 1, int month = 1, int day = 1): _year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}Date(const Date& d): _year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}private:int _year;int _month;int _day;
};int main()
{// C++98 支持的初始化方式int a1[] = {1, 2, 3, 4, 5};int a2[5] = {0};Point p = {1, 2};// C++11 支持的初始化方式// 內置類型支持int x1 = {2};// 自定義類型支持// 這里本質是用 {2025, 1, 1} 構造一個 Date 臨時對象// 臨時對象再去拷貝構造 d1,編譯器優化后合二為一變成 {2025, 1, 1} 直接構造初始化 d1// 運行一下,我們可以驗證上面的理論,發現是沒調用拷貝構造的Date d1 = {2025, 1, 1};// 這里 d2 引用的是 {2024, 7, 25} 構造的臨時對象const Date& d2 = {2024, 7, 25};// 需要注意的是 C++98 支持單參數時類型轉換,也可以不用 {}Date d3 = {2025};Date d4 = 2025;// 可以省略掉 =Point p1{1, 2};int x2{2};Date d6{2024, 7, 25};const Date& d7{2024, 7, 25};// 不支持,只有 {} 初始化,才能省略 =// Date d8 2025;vector<Date> v;v.push_back(d1);v.push_back(Date(2025, 1, 1));// 比起有名對象和匿名對象傳參,這里 {} 更有性價比v.push_back({2025, 1, 1});return 0;
}
1.3?C++11中的std::initializer_list
但是對象容器初始化還是不太方便,比如一個vector對象,我想用N個值去構造初始化,那么我們得實現很多個構造函數才能支持,
vector<int> v1 = {1, 2, 3};
vector<int> v2 = {1, 2, 3, 4, 5};
C++11 庫中提出了一個 std::initializer_list 的類,
auto il = { 10, 20, 30 }; // the type of il is an initializer_list
這個類的本質是底層開一個數組,將數據拷貝過來,std::initializer_list 內部有兩個指針分別指向數組的開始和結束,支持迭代器遍歷。
這是它的文檔:initializer_list - C++ Reference?。
容器支持一個 std::initializer_list 的構造函數,也就支持任意多個值構成的初始化。STL 中的容器支持用 {x1, x2, x3...} 進行初始化,就是通過 std::initializer_list 的構造函數支持的。
// 標準庫中的聲明示例// vector 的 initializer_list 構造函數
vector(initializer_list<value_type> il, const allocator_type& alloc = allocator_type());// list 的 initializer_list 構造函數
list(initializer_list<value_type> il, const allocator_type& alloc = allocator_type());// map 的 initializer_list 構造函數
map(initializer_list<value_type> il,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());// 自定義 vector 的簡化實現
template<class T>
class vector {
public:typedef T* iterator; // 迭代器類型// initializer_list 構造函數vector(initializer_list<T> il) {for (auto& e : il) { // 遍歷列表元素push_back(e); // 逐個插入容器}}private:iterator _start = nullptr; // 指向首元素iterator _finish = nullptr; // 指向最后一個元素的下一個位置iterator _endofstorage = nullptr; // 指向存儲空間的末尾
};
#include <iostream>
#include <vector>
#include <string>
#include <map>
using namespace std;int main() {// 1. 測試 initializer_list 的基本特性std::initializer_list<int> mylist;mylist = {10, 20, 30}; // 初始化列表賦值// 輸出 initializer_list 的大小(通常是固定大小,包含兩個指針)cout << "sizeof(mylist): " << sizeof(mylist) << endl;// 2. 驗證 initializer_list 的存儲位置int i = 0;cout << "mylist.begin(): " << mylist.begin() << endl; // 指向第一個元素cout << "mylist.end(): " << mylist.end() << endl; // 指向末尾后一位cout << "&i: " << &i << endl; // 對比棧地址// 3. 容器的兩種初始化方式對比vector<int> v1({1, 2, 3, 4, 5}); // 直接構造(顯式 initializer_list)vector<int> v2 = {1, 2, 3, 4, 5}; // 隱式轉換(可能觸發拷貝優化)const vector<int>& v3 = {1, 2, 3, 4, 5}; // 引用臨時對象(生命周期延長)// 4. map 的嵌套初始化(pair + initializer_list)map<string, string> dict = {{"sort", "排序"}, {"string", "字符串"}};// 5. initializer_list 賦值操作v1 = {10, 20, 30, 40, 50}; // 調用 operator=(initializer_list)return 0;
}
總結一下:
C++11,基本實現了一切對象皆可初始化。?
2、lambda
2.1 lambda的語法
lambda 表達式本質是一個匿名函數對象,跟普通函數不同的是它可以定義在函數內部。
lambda 表達式語法層面而言沒有類型,所以我們一般是用auto或者模板參數定義的對象去接收。
lambda 表達式的格式:
[capture-list] (parameters) -> return type { function body }
[capture-list]:捕捉列表,編譯器根據 [ ] 來判斷是否為 lambda 函數。捕捉列表能夠捕捉上下文中的變量供 lambda 函數使用,捕捉列表可以傳值和傳引用捕捉,具體細節在 2.2 中我們再細講。捕捉列表為空?[ ] 也不能省略。
(parameters):參數列表,與普通函數的參數列表功能類似,如果不需要參數傳遞,則可以連同( )一起省略。
-> return type:返回值類型,用追蹤返回類型形式(允許將函數的返回類型放在參數列表之后(用->引導))聲明函數的返回值類型,沒有返回值時此部分可省略。一般返回值類型明確情況下,也可省略,由編譯器對返回類型進行推導。
{ function body }:函數體,函數體內的實現跟普通函數完全類似,在該函數體內,除了可以使用其參數外,還可以使用所有捕獲到的變量,函數體為空{ }也不能省略。
#include <iostream>
using namespace std;int main() {// 1. 一個簡單的lambda表達式auto add1 = [](int x, int y)->int { return x + y; };cout << add1(1, 2) << endl; // 輸出: 3/* Lambda表達式特性說明:1. 捕捉列表為空也不能省略(必須寫[])2. 參數列表為空可以省略(如func1示例)3. 返回值可以省略,可以通過返回對象自動推導4. 函數體不能省略*/// 2. 無參數、無顯式返回類型的lambdaauto func1 = [] {cout << "hello bit" << endl;return 0;};func1(); // 輸出: hello bit// 3. 通過引用修改外部變量int a = 0, b = 1; auto swap1 = [](int& x, int& y) {int tmp = x;x = y;y = tmp;};swap1(a, b);cout << a << ":" << b << endl; // 輸出: 1:0return 0;
}
2.2 捕捉列表
Lambda 表達式的變量捕捉規則
在?lambda?表達式中,默認只能使用?lambda 函數體和參數中的變量。如果想使用外層作用域中的變量,就需要進行捕捉。
第一種捕捉方式:顯式捕捉
在捕捉列表中顯式指定?傳值捕捉?和?傳引用捕捉,多個變量用逗號分隔。
[x, y, &z] 表示 x 和 y?值捕捉,z?引用捕捉。
第二種捕捉方式:隱式捕捉
[=]?表示?隱式值捕捉,[&] 表示?隱式引用捕捉。
lambda 表達式中使用了哪些變量,編譯器就會自動捕捉哪些變量。即按需捕捉。
第三種捕捉方式:混合捕捉
在捕捉列表中?混合使用隱式捕捉和顯式捕捉:
[=, &x]?表示其他變量?隱式值捕捉,x?引用捕捉。
[&, x, y] 表示其他變量?隱式引用捕捉,x?和 y?值捕捉。
混合捕捉規則:
-
第一個元素必須是 &?或 =。
-
&?混合捕捉時,后面的捕捉變量必須是?值捕捉。
-
=?混合捕捉時,后面的捕捉變量必須是?引用捕捉。
Lambda 表達式的變量作用域規則
-
如果?lambda 表達式定義在函數局部作用域,它可以捕捉?lambda 位置之前定義的局部變量,但不能捕捉?靜態局部變量?和?全局變量(它們可以直接使用,無需捕捉)。
-
如果?lambda 表達式定義在全局位置,捕捉列表必須為空(不能捕捉任何變量)一般也不會定義在全局。
Lambda 的 const 性和 mutable 修飾
-
默認情況下,傳值捕捉是被?const ?修飾的,即?傳值捕捉的變量不能修改。引用捕捉沒有const修飾。
-
使用?mutable 修飾符(加在參數列表后面)可以取消?const 限制,使?傳值捕捉的變量可以修改(但修改的是形參,不影響實參)。
-
使用?mutable 后,參數列表()不可省略(即使參數為空)。
#include <iostream>
using namespace std;int x = 0; // 全局變量// 全局lambda的捕捉列表必須為空(全局變量可直接使用,無需捕捉)
auto func_global = []() {x++; // 直接修改全局變量
};int main() {// 局部變量int a = 0, b = 1, c = 2, d = 3;// 1. 顯式捕捉:值捕捉a,引用捕捉bauto func1 = [a, &b] {// a++; // 錯誤:值捕捉的變量默認const,不可修改b++; // 正確:引用捕捉可修改return a + b;};cout << func1() << endl;// 2. 隱式值捕捉(=):自動捕捉所有使用的局部變量(a,b,c)auto func2 = [=] {return a + b + c; // d未使用,不會被捕捉};cout << func2() << endl;// 3. 隱式引用捕捉(&):自動引用捕捉所有修改的變量auto func3 = [&] {a++; c++; d++; // 全部通過引用修改};func3();cout << a << " " << b << " " << c << " " << d << endl;// 4. 混合捕捉1:默認引用捕捉,顯式值捕捉a,bauto func4 = [&, a, b] {// a++; b++; // 錯誤:a,b是值捕捉c++; d++; // 正確:其他變量隱式引用捕捉return a + b + c + d;};func4();cout << a << " " << b << " " << c << " " << d << endl;// 5. 混合捕捉2:默認值捕捉,顯式引用捕捉a,bauto func5 = [=, &a, &b] {a++; b++; // 正確:a,b是引用捕捉// c++; d++; // 錯誤:其他變量隱式值捕捉return a + b + c + d;};func5();cout << a << " " << b << " " << c << " " << d << endl;// 6. 靜態/全局變量無需捕捉static int m = 0;auto func6 = [] {return x + m; // 直接使用全局和靜態變量};// 7. mutable修飾:允許修改值捕捉的副本(不影響外部變量)auto func7 = [=]() mutable {a++; b++; c++; d++; // 修改的是內部副本return a + b + c + d;};cout << func7() << endl; // 輸出副本修改后的值cout << a << " " << b << " " << c << " " << d << endl; // 原變量不變return 0;
}
2.3 lambda的應用
在學習 lambda 表達式之前,我們使用的可調用對象只有函數指針和仿函數對象,函數指針的
類型定義起來比較麻煩,仿函數要定義一個類,相對會比較麻煩。使用 lambda 去定義可調用對象,既簡單方便。
lambda 在很多其他地方用起來也很好用。比如線程中定義線程的執行函數邏輯,智能指針中定
制刪除器等,lambda 的應用還是很廣泛的,以后我們會不斷接觸到。
struct Goods
{string _name; // 名字double _price; // 價格int _evaluate; // 評價// ...Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};int main()
{vector<Goods> v = { { "蘋果", 2.1, 5 }, { "?蕉", 3, 4 }, { "橙?", 2.2, 3 }, { "菠蘿", 1.5, 4 } };// 類似這樣的場景,我們實現仿函數對象或者函數指針支持商品中// 不同項的?較,相對還是?較?煩的,那么這? lambda 就很好?了sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());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;});return 0;
}
2.4 lambda的原理
lambda 的原理和范圍 for 相似,編譯后從匯編指令層的角度看,壓根就沒有 Lambda 和范圍 for 這樣的東西。范圍 for 底層是迭代器,而 lambda 底層是仿函數對象,也就是說我們寫了一個 lambda 以后,編譯器會生成一個對應的仿函數的類。
仿函數的類名是編譯器按一定規則生成的,保證不同的 lambda 生成的類名不同。
lambda 參數 / 返回類型 / 函數體就是仿函數 operator () 的參數 / 返回類型 / 函數體。
lambda 的捕捉列表本質是初始化?仿函數類的成員變量,也就是說捕捉列表的變量都是 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;// lambda 表達式auto r2 = [rate](double money, int year) {return money * rate * year;};// 函數對象(仿函數)Rate r1(rate);r1(10000, 2); // 調用仿函數r2(10000, 2); // 調用 lambda// 無參 lambdaauto func1 = [] {cout << "hello world" << endl;};func1(); // 調用無參 lambdareturn 0;
}
// lambda 表達式
auto r2 = [rate](double money, int year) {return money * rate * year;
};// 捕捉列表的 `rate`,可以看到作為 `lambda_1` 類構造函數的參數傳遞了,
// 這樣在初始化成員變量后,才能在下面的 operator() 中使用
//
// 匯編代碼片段:
// 00D8295C lea eax,[rate]
// 00D8295F push eax
// 00D82960 lea ecx,[r2]
// 00D82963 call `main'::`2'::<lambda_1>::<lambda_1> (0D81F80h) // 函數對象(仿函數)
Rate r1(rate);
// 匯編代碼片段:
// 00D82968 sub esp,8
// 00D8296B movsd xmm0,mmword ptr [rate]
// 00D82970 movsd mmword ptr [esp],xmm0
// 00D82975 lea ecx,[r1]
// 00D82978 call Rate::Rate (0D81438h) r1(10000, 2);
// 匯編代碼片段:
// 00D8297D push 2
// 00D8297F sub esp,8
// 00D82982 movsd xmm0,mmword ptr [__real@40c3880000000000 (0D89B50h)]
// 00D8298A movsd mmword ptr [esp],xmm0
// 00D8298F lea ecx,[r1]
// 00D82992 call Rate::operator() (0D81212h) // 從匯編層可以看到,r2(lambda 對象)的調用本質還是調用 operator(),
// 其類型是 `lambda_1`,這個類型名的規則由編譯器自定義,確保不同的 lambda 不沖突
r2(10000, 2);
// 匯編代碼片段:
// 00D82999 push 2
// 00D8299B sub esp,8
// 00D8299E movsd xmm0,mmword ptr [__real@40c3880000000000 (0D89B50h)]
// 00D829A6 movsd mmword ptr [esp],xmm0
// 00D829AB lea ecx,[r2]
// 00D829AE call `main'::`2'::<lambda_1>::operator() (0D824C0h)
3、包裝器
3.1 function
1. std::function 是一個類模板,也是一個包裝器。
2. std::function 的實例對象可以包裝存儲其他的可調用對象,包括函數指針、仿函數、lambda 等,統一類型,存儲的可調用對象被稱為 std::function 的目標。若 std::function 不含目標,則稱它為空。調用空 std::function 的目標會導致拋出 std::bad_function_call 異常。
// 前置聲明(未定義的通用模板)
template <class T>
class function; // undefined base template// 特化版本:支持函數類型簽名(返回類型 + 參數列表)
template <class Ret, class... Args>
class function<Ret(Args...)> {// 實現內容(此處未展開)// 通常包含 operator() 以支持函數調用
};
以上是 std::function 的原型,它被定義在 <functional> 頭文件中。
官方文檔:std::function - cppreference.com
#include <functional>
#include <iostream>
using namespace std;// 普通函數
int f(int a, int b) {return a + b;
}// 仿函數(函數對象)
struct Functor {int operator()(int a, int b) {return a + b;}
};// 類(包含靜態和非靜態成員函數)
class Plus {
public:Plus(int n = 10) : _n(n) {}// 靜態成員函數static int plusi(int a, int b) {return a + b;}// 非靜態成員函數(依賴 this 指針)double plusd(double a, double b) {return (a + b) * _n;}private:int _n;
};int main() {// 1. 包裝普通函數function<int(int, int)> f1 = f;cout << f1(1, 1) << endl; // 輸出: 2// 2. 包裝仿函數對象function<int(int, int)> f2 = Functor();cout << f2(1, 1) << endl; // 輸出: 2// 3. 包裝 lambda 表達式function<int(int, int)> f3 = [](int a, int b) { return a + b; };cout << f3(1, 1) << endl; // 輸出: 2// 4. 包裝靜態成員函數(無需對象實例)function<int(int, int)> f4 = &Plus::plusi;cout << f4(1, 1) << endl; // 輸出: 2// 5. 包裝非靜態成員函數(需綁定對象)Plus pd(10); // _n = 10// 方式1:傳遞對象指針function<double(Plus*, double, double)> f5 = &Plus::plusd;cout << f5(&pd, 1.1, 1.1) << endl; // 輸出: (1.1 + 1.1) * 10 = 22// 方式2:傳遞對象值(拷貝)function<double(Plus, double, double)> f6 = &Plus::plusd;cout << f6(pd, 1.1, 1.1) << endl; // 輸出: 22// 方式3:傳遞右值引用function<double(Plus&&, double, double)> f7 = &Plus::plusd;cout << f7(move(pd), 1.1, 1.1) << endl; // 輸出: 22cout << f7(Plus(10), 1.1, 1.1) << endl; // 輸出: 22(臨時對象)return 0;
}
靜態成員函數不屬于對象,不需要傳this指針,非靜態成員函數需傳遞this指針,傳遞對象(會自行取對象地址)或對象地址。?
3.2 bind
1. 基本形式(自動推導返回類型)
template <class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);
2. 指定返回類型(C++14 起支持)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);
bind 是一個函數模板,它也是一個可調用對象的包裝器,可以把它看做一個函數適配器(?封裝函數/可調用對象),對接收 的 fn 可調用對象進行處理后返回一個可調用對象。
bind 可以用來調整參數個數和參數順序。bind 也在<functional>這個頭文件中。
調用 bind 的一般形式: auto newCallable = bind(callable,arg_list); 其中 newCallable 本身是一個可調用對象,arg_list 是一個逗號分隔的參數列表,對應給定的 callable 的 參數。當我們調用 newCallable 時,newCallable 會調用 callable,并傳給它 arg_list 中的參數。
arg_list 中的參數可能包含形如 _n 的名字,其中 n 是一個整數,表示 newCallable 的參數。數值 n 表示生成的可調用對象中參數的位置:_1 為 newCallable 的第一個參數(無所謂_1的位置在哪),_2 為第二個參數,以此類推。_1/_2/_3.... 這些占位符放在 placeholders 的一個命名空間中。
#include <iostream>
#include <functional>// 定義一個減法函數,返回 (a - b) * 10
int Sub(int a, int b) {return (a - b) * 10;
}// 定義一個減法函數,返回 (a - b - c) * 10
int SubX(int a, int b, int c) {return (a - b - c) * 10;
}// 定義一個 Plus 類,包含靜態和非靜態的加法函數
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::placeholders 命名空間中的占位符using namespace std::placeholders;// 綁定 Sub 函數,正常順序傳遞參數auto sub1 = std::bind(Sub, _1, _2);std::cout << sub1(10, 5) << std::endl;// 調整參數順序,交換 _1 和 _2 的位置auto sub2 = std::bind(Sub, _2, _1);std::cout << sub2(10, 5) << std::endl;// 調整參數個數,固定第一個參數為 100auto sub3 = std::bind(Sub, 100, _1);std::cout << sub3(5) << std::endl;// 調整參數個數,固定第二個參數為 100auto sub4 = std::bind(Sub, _1, 100);std::cout << sub4(5) << std::endl;// 分別綁死 SubX 函數的第 1、2、3 個參數auto sub5 = std::bind(SubX, 100, _1, _2);std::cout << sub5(5, 1) << std::endl;auto sub6 = std::bind(SubX, _1, 100, _2);std::cout << sub6(5, 1) << std::endl;auto sub7 = std::bind(SubX, _1, _2, 100);std::cout << sub7(5, 1) << std::endl;// 綁定 Plus 類的非靜態成員函數std::function<double(Plus&&, double, double)> f6 = &Plus::plusd;Plus pd;std::cout << f6(std::move(pd), 1.1, 1.1) << std::endl;std::cout << f6(Plus(), 1.1, 1.1) << std::endl;// 使用 std::bind 綁定 Plus 類的非靜態成員函數,固定對象std::function<double(double, double)> f7 = std::bind(&Plus::plusd, Plus(), _1, _2);std::cout << f7(1.1, 1.1) << std::endl;// 定義一個計算復利的 lambda 函數auto func1 = [](double rate, double money, int year) -> double {double ret = money;for (int i = 0; i < year; i++) {ret += ret * rate;}return ret - money;};// 綁死一些參數,實現支持不同年利率、不同金額和不同年份計算復利的結算利息std::function<double(double)> func3_1_5 = std::bind(func1, 0.015, _1, 3);std::function<double(double)> func5_1_5 = std::bind(func1, 0.015, _1, 5);std::function<double(double)> func10_2_5 = std::bind(func1, 0.025, _1, 10);std::function<double(double)> func20_3_5 = std::bind(func1, 0.035, _1, 30);std::cout << func3_1_5(1000000) << std::endl;std::cout << func5_1_5(1000000) << std::endl;std::cout << func10_2_5(1000000) << std::endl;std::cout << func20_3_5(1000000) << std::endl;return 0;
}