函式包含兩個要素:函式簽名和函式體。
其中函式簽名確定了函式的型別;函式體確定了它的功能。
說到函數語言程式設計,核心就是我們可以把函式當做“一等公民”:可以宣告函式變數、可以賦值、可以當做引數傳遞給函式、也可以作為函式返回的型別。
1 函式和函式指標的定義
當我們定義一個函式型別時,函式名、形參列表、返回值、函式體缺一不可。
當我們宣告一個函式變數時,則不需要指定函式體,且以;
結尾:
// 以下是一個函式定義
int func(int a, int b)
{
return a + b;
}
// 以下是函式宣告
int func(int a, int b);
複製程式碼
C++中變數的型別包括:
- 基本型別(整型、浮點型、字元型)
- 自定義結構體或類
- 複合型別(陣列)
- 指標
- 函式型別
對於函式的形參和返回值而言,它們可以是除陣列型別或函式型別之外其他的任意型別。
那麼如果確實要返回陣列型別或者函式型別怎麼辦呢?這就需要藉助到指標了:指向陣列的指標,和指向函式的指標。
// 定義一個指向int[10]型別的陣列的指標
int a[10];
int (*pa) [10] = a;
// 定義一個指向 int (int)型別的函式的指標
int (*pf) (int, int) = func;
複製程式碼
使用陣列指標訪問陣列時,必須寫上解指標符號:
(*pa)[0] = 1
複製程式碼
使用函式指標呼叫函式時,可以省略解指標符號:
pf(a, b);
複製程式碼
接下來看看如何定義一個函式,返回一個陣列指標:
int (*func1(int val))[10]
{
int (*pa)[10] = (int(*)[10])(new(int)[10]);
for(auto i = 0; i < 10; ++i) {
(*pa)[i] = val + i;
}
return pa;
}
int main()
{
auto pa = func1(3);
// 因為func1是在堆上分配的陣列,所以需要delete它
delete (int *)pa;
}
複製程式碼
再看如何返回一個函式指標:
// func2 形參列表為空,然後返回一個函式指標:需要2個int形參,返回int
int (*func2())(int, int)
{
return func;
}
複製程式碼
當我們把一個函式名稱當做值使用時(即除了呼叫函式之外的其它用法),它會自動轉換成函式指標。
tips
- 上面那種定義返回函式指標的函式,用的還是相容C的寫法。在現代C++中,可以使用尾置返回型別的方式來定義:
auto func2() -> int (*)(int, int);
複製程式碼
- 可以使用
decltype
定義函式指標型別。但是decltype
一個函式名稱時,得到的是函式型別,而不是函式指標型別:
// 定義一個函式
int retfunc(const int& a, const int& b);
// 定義一個函式,返回指向int(const int&, const int&)函式型別的指標
// 以下兩種寫法等價
int(*getFunc(const int& x))(const int&, const int&);
decltype(retfunc)* getFunc(const int& x);
複製程式碼
2 lambda表示式
lambda表示式,就是傳說中的匿名函式:即沒有名字的“函式”。
int main()
{
int a = 10;
auto fl = [&a](int x) -> int { a++; return x > a ? a : x };
std::cout << a << " " << fl(3) << " " << a << std::endl;
return 0;
}
複製程式碼
例如,上例中,我們定義了一個lambda物件fl
:它按引用捕獲了呼叫它的函式的區域性變數a,需要傳入一個引數,並返回int值。
在lambda表示式中,僅能也是隻需要捕獲定義它的函式的自動區域性變數。對於靜態區域性變數或函式外部變數,不用捕獲也是可以訪問的。
對於在類的成員函式中定義的lambda表示式,除了可以捕獲區域性變數之外,還可以捕獲這個類的非靜態的成員變數(跟捕獲區域性變數一樣)。對成員變數,還有個額外的規則:如果捕獲了this
指標,那麼自動獲取所有成員變數的訪問許可權。
如果需要在lambda表示式中修改按值捕獲的變數,需要在引數列表和尾置返回型別之間加上mutable
關鍵字:
auto fl = [a](int x) mutable -> int {
return x + a;
}
複製程式碼
使用bind
繫結引數
auto newCallable = bind(callable, arg_list);
複製程式碼
bind
可以看做是從一個可呼叫物件到另外一個可呼叫物件的對映。跟lambda表示式一樣,bind
返回的也是一個可呼叫物件。
callable
和newCallable
這兩個可呼叫物件的形參列表,以及實參的順序都是可以隨意調整的。
在呼叫bind
時,我們在arg_list
中,不僅可以傳入任意具體的實參變數,也可以傳入形如_n
的“佔位符”。佔位符的作用,就是將呼叫newCallable
時的引數,對映到callable
時的引數:_1
就是對映成newCallable
的第一個引數,_2
就是第二個引數,依次類推。有多少個“佔位符”,就表示在呼叫newCallable
時需要傳入多少個引數。
舉個例子:
// 我們有個需要傳入2個引數的函式funcA
int funcA(int x, int y);
int a;
// 有一個佔位符,所以呼叫funcB時,需要傳入一個引數
auto funcB = bind(funcA, a, _1);
int b;
funcB(b); // 等價於 funcA(a, b)
複製程式碼
而且在arg_list
中,_n
的順序和位置是任意的,比如_2
可以在_1
前面:
int funcA(int x, int y, int z);
int a;
auto funcB = bind(funcA, _2, a, -1);
int b, c;
funcB(b, c); // 等價於 funcA(c, a, b);
複製程式碼
注:
_n
是定義在名字空間std::placeholders
中的,所以需要先using namespace std::placeholders
。
繫結引用引數
在使用bind
做函式對映時,對於那些不是佔位符的引數,是將其拷貝到bind
返回的可呼叫物件中的。如果某些引數不支援拷貝呢?比如ostream
。
可以使用標準庫裡的ref
函式返回一個變數的引用型別:
ostream& print(ostream& os, const string& s, char c);
ostream os;
auto f = bind(print, ref(os), _1, ' ');
f("hello, world");// 等價於 print(os, "hello, world", ' ');
複製程式碼
其實這沒有改變bind
的拷貝行為,因為ref()
返回的就是一個可拷貝的物件,只不過它的內部定義了一個原來引數的引用型別,並且保證拷貝後都引用同一個變數。
不信,我們可以自己實現一個類myref
(為了簡單起見,沒有實現成模板類,只能轉ostream
引用):
class myref {
public:
// 包含了引用型別的成員變數,只能在建構函式裡面顯式初始化
myref(ostream& os) : os_(os) {}
// 保證可以將它轉換成一個ostream引用型別
operator ostream& ()
{
return os_;
}
private:
ostream& os_;
};
複製程式碼
除了ref
之外,還可以用cref
返回變數的const
引用型別。
繫結類成員函式
bind針對成員函式,提供了特別的支援,只要你把指向類例項的指標作為第二個引數傳遞即可。
class Test {
public:
int func(int v);
};
Test t;
auto f = bind(&Test::func, &t, std::placeholders::_1);
複製程式碼
注意,對普通函式,當我們把函式名字當做值使用時,會自動轉換成函式指標;但是對於成員函式,我們必須顯式寫上取址符。
3 函式物件
如果一個類實現了函式呼叫運算子operator()
,那麼它的物件就是一個函式物件。如果這個類還定義了其它的成員變數,那麼它的物件就是一個有狀態的函式物件,比普通的函式擁有更強大的能力。
知識點:lambda表示式就是一個函式物件:
- 它定義了函式呼叫運算子
operator()
;- 如果它按值捕獲了外部變數,那麼它就定義了相應的成員變數,並在建構函式中初始化這些成員變數;
- 如果它按引用捕獲了外部變數,那麼編譯器會直接使用這些引用,而不會在類中建立相應的成員變數。所以需要程式設計師保證在
lambda
物件生存期間,它捕獲的引用變數要一直可訪問;- 預設
operator()
是const
的,如果它被定義成mutable
,那麼它的operator()
就不是const
的。
函式/函式指標、bind返回值、lambda表示式、函式物件等,這5種物件都有一個特點就是我們都可以對它執行函式呼叫。我們將其稱為“可呼叫物件”。
“可呼叫物件”的一個重要屬性,就是它的呼叫形式(或函式簽名):包括返回型別和一個實參型別列表。
雖然這5種可呼叫物件的型別是不一樣的,但是他們可能擁有相同的呼叫形式。
例如,以下物件都實現了相同的呼叫形式int (int, int)
:
// 普通函式和函式指標
int add(int a, int b) { return a + b; }
int (*padd)(int, int) = add;
// lambda表示式
auto mod = [](int a, int b) -> int { return a - b; }
// 函式物件
struct divide {
int operator()(int den, int div) {
return den / div;
}
};
複製程式碼
如果我們要把這些物件放進同一個容器呢?因為它們型別不同,是沒法做到的:
std::map<std::string,int(*)(int,int)> binops;
binops.insert(make_pair("add", add)); // OK
binops.insert(make_pair("mod", mod)); // 錯誤,型別不匹配
binops.insert(make_pair("divide", divide())); // 錯誤,型別不匹配
複製程式碼
我們需要有一種型別,所有這些可呼叫物件都能自動轉換成這種型別。標準庫提供的function
類就是啦!
function<int(int,int)> f;
f = add; // OK
f = mod; // OK
f = divide(); // OK
f = bind(add, _1, _2); // OK
複製程式碼
只要我們定義一個呼叫形式一樣的function
物件,就可以儲存所有呼叫形式一樣的可呼叫物件。
Q:如何實現將類A自動轉換成類B?
A: 有兩種方法:在類A中過載型別轉換運算子;在類B中過載複製建構函式和賦值運算子。但是不要兩種方法同時用,會產生二義性,導致編譯失敗。