C/C++: C++可調用對象詳解


  C++中有幾種可調用對象:函數,函數指針,lambda表達式,bind創建的對象,以及重載了函數調用符的類。
1. 函數
  函數偏基礎的東西,在這里不再敘述。重點講下C++11的某些重要特性和函數指針。
 
   可變形參函數:
  C++11有兩種辦法傳遞可變形參(其實學了模板以后可以用模板的自動遞歸來展開所傳遞的參數,這個后面再說)。
  1.  第一種是initializer_list,這是一個標准庫類型(其實是個模板)。
std::initializer_list<T>{ };

  可以使用列表初始化來進行初始化,T表示的是參數類型,initializer_list可以被拷貝,但是一定要注意的是,它是一種引用拷貝,也就是說拷貝后新的list和被拷貝的list是的元素都是共享的。

 

  2. 第二種是省略符形參,熟悉C的人對這個也應該很熟悉了,就是printf和scanf這些函數所用的方法。

void print(int, ...);

  省略符形參其實是為了方便訪問特殊的C代碼而設計的,這些代碼其實是使用了標准庫varargs的功能(C標准庫)。

  如果我們使用這種方法傳遞省略形參,一定要注意,這種代碼只能用來處理C++和C通用版本的東西,對於C++內的對象,這種方法大多數情況下都是不行的。
 1 #include <stdarg.h>
 2 void method(int i, ...)
 3 {
 4     int v;
 5     va_list params;//聲明可變參數列表
 6     va_start(params, i); //依據可變參數前的參數獲得可變參數的起始地址
 7     do {
 8         v = va_arg(params, int); //從可變參數列表中獲得參數的值,類型為int
 9         printf("%d", v);
10     } while (v != 0);
11     va_end(params);
12 }
13 
14 void format(const char* format, ...)
15 {
16     char buf[1024];va_list params;
17     va_start(params, format);
18     vsnprintf_s(buf, 1024, 1024, format, params);
19     va_end(params);
20     printf("%s", buf);
21 }
  (注意省略符前面的逗號可以省略。)
 
  注意C++內,函數可以返回引用(但是不能返回局部對象的引用,類的成員函數還可以指定返回左值還是返回右值),另外C++11還支持列表初始化返回。
  C++可以返回數組,我們可以使用別名來返回數組(一定要知道數組的維度)。
typedef int arr[10];//typedef方法
using Arr = int[10];//別名用法
Arr *func0(int i);
int(*func1(int i))[10];
auto func2(int i) -> int(*)[10];
  
  另外C++還可以有尾置返回類型:
1 //尾置返回類型(明確知道函數返回的指針指向什么地方)
2 int odd[] = { 1,3,5,7,9 };
3 int even[] = { 0,2,4,6,8 };
4  
5 decltype(odd) *arrPtr(int i)
6 {
7     return (i % 2) ? &odd : &even;
8 }
 
  C++重要的特性之一:函數重載(main函數不能被重載),函數重載需要滿足形參列表不同的條件(但要注意編譯器無法區別頂層const,如果有兩個相同位置且相同類型的參數,編譯器並不會區分它們的頂層const的區別,但是底層const會區別)。如果我們傳遞一個非常量對象或者一個指向非常量對象的指針的時候,編譯器會優先選擇非常量對象。
 1 const std::string &shortString(const std::string &s1, const std::string &s2)
 2 { 
 3     return s1.size() <= s2.size() ? s1 : s2; 
 4 }
 5 std::string &shortString(std::string &s1, std::string &s2)
 6 {
 7     auto &r = shortString(const_cast<const std::string &>(s1),
 8         const_cast<const std::string &>(s2));//注意這里的shortSring必須要const_cast
 9                                              //因為在這里s1和s2都是非常連版本,而編譯器會優先選擇非常量版本進行調用,導致遞歸出錯
10     return const_cast<std::string &>(r);
11 }

const_cast和重載(注意自己一定要清楚const_cast之前的變量是不是const,如果是const,那么就會產生未定義的行為)。

  函數的默認實參,從左往右,從某個形參開始起,如果它被賦予了默認形參,那么其后面的所有形參都要賦予默認形參,默認形參在給定的作用域內只能被賦予一次(即使函數有可能被多次聲明,但是后續聲明不能為已經添加過默認形參的形參添加默認形參)。而且要注意,局部變量是不能去定義默認形參的。primer 5e上面的例子:
 1 size_t wd = 80;char def = ' ';
 2 size_t ht();
 3 string screen(size_t = ht(), size_t = wd, char = def);
 4 string window = screen();//調用screen(ht(), 80 ' ')
 5 
 6 void f2() 
 7 {
 8     def = '*';
 9     size_t wd = 100;
10     window = screen();//調用screen(ht(), 80 '*')
11 }
  
  內聯函數和constexpr函數:簡單來說,內聯函數可以在匯編的時候把函數體展開,條件就是函數體必須要短小,不過很多時候就算你指定了函數為內聯函數,編譯器並不會承認,在很多編譯器中,內聯inline關鍵字是一個可選選項,編譯器並不會強制執行iniline操作。
  對於constexpr函數,其是一個C++11用於之指定常量表達式的方法constexpr函數需要滿足:返回類型和所有形參類型都是字面值類型,而且函數體必須有且只有一條return語句,因為constexpr函數只有返回值,所以constexpr被隱式指定為內聯函數,constexpr函數也可以包含其他語句,只要這些語句不執行任何操作就可以了(比如可以有空語句,以及類型別名)。
1 constexpr size_t scale(size_t cnt) 
2 { 
3     using test = int;
4     return cnt * 2; 
5 }
 
要注意,C++允許constexpr函數返回一個非常量值,而且當constexpr函數的實參不是字面值常量的時候,返回值有可能不是常量表達式。
int t;
int arr[scale(100)];
auto ret = scale(t);

  scale(100)返回的是一個常量表達式,但是scale(t)則返回的就是size_t類型。

  內聯函數和constexpr函數都應該放在頭文件里面,內聯函數和constexpr函數與其他函數最大的不同就是,內聯函數和constexpr函數是可以多次定義的,但是這多次定義必須完全一致,所以內聯函數和constexpr函數一般都定義在頭文件內。
 
  函數匹配和二義性問題:由於C++由函數重載,所以函數匹配是一件很讓編譯器頭疼的事情,編譯器將實參類型到形參類型的轉換分為以下幾個等級:(從上到下優先級依次降低)。
  1. 精確匹配(實參類型和形參類型是一樣的,或者實參從數組類型或者函數類型轉換成對應的指針類型,向實參添加頂層const或者從實參中消除頂層const)(對於底層const,如果同時定義了非常量版本和常量版本,如果傳入非常量,那么就會調用非常量版本函數,如果傳入的實參為常量版本,則會調用常量版本的函數)。
  2. 通過const實現的匹配
  3. 通過類型提升來實現的匹配(比如整型提升)。
  4. 通過算術轉換或者指針轉換實現的匹配。
  5. 通過類類型轉來來匹配的轉換。
 
 
2. 函數指針
  1. 函數指針顧名思義就是指向函數的指針,類似於
int(*pf)(std::string s1);
  指針名的括號必不可少,不然會認為是返回int的指針,使用函數指針可以直接讓指針等於函數名,C++規定給函數名取地址和直接使用函數名都是可以得到函數的地址:
1 int test(std::string s1) { return 0; }
2 int(*pf)(std::string s1);
3 pf = test;
4 pf = &test;
5 test(std::string());
6 (*test)(std::string());
  上面的 兩種使用方法都是等價的。同時在C++11中,我們也可以使用尾置返回類型來聲明一個函數的返回值為一個函數指針(和數組指針類似)
auto foo() -> int(*)(std::string s1);
  也可以用decltype來指定返回的函數指針的類型(注意函數名的前面一定顯式添加星號表明返回的是一個函數指針而不是函數本身)
decltype(test) *getfcn();

 

 

3. lambda表達式(匿名函數對象,一個C++的語法糖)


  一個lambda表達式表示一個可調用的代碼單元,我們可以將其理解為有一個未命名的內聯函數,與任何函數類似,一個lambda具有一個返回類型,一個參數列表和一個函數體,並且這個函數體是可以定義在函數內部的。

  lambda必須使用尾置返回類型來指定返回類型,不能有默認形參,並且具有以下形式:
        [capture list](parameter list) -> return type{function body }
   lambda表達式可以忽略返回類型和形參,形如:
auto fcn = [] {return 42;};//定義了一個可調用對象fcn為一個lambda表達式
               //忽略參數列表,忽略返回類型(都為空)
  有一些標准庫的算法的謂詞,其形參參數可能不能滿足我們的需求,這個時候我們可以使用lambda表達式的參數捕獲來間接獲得更多的參數,比如標准庫的<algorithm>里面有find_if函數,它接受一個一元謂詞,可以在規定范圍內找到滿足條件的第一個迭代器,假設現在我們給定一個vector<string>,我們要找到在整個vector內不大於長度l的第一個迭代器,如果我們一定要使用find_if這個標准庫函數來實現這個功能,這個時候我們就要用到lambda的參數捕獲了。
  先來個例子:
 1 #include <algorithm>
 2 #include <iostream>
 3 #include <vector>
 4 
 5 using std::vector;
 6 using std::find_if;
 7 using std::for_each;
 8 using std::cout;
 9 using std::endl;
10 
11 void searchSegment(vector<int> nums, const int floorSize, const int ceilSize)
12 {
13     std::stable_sort(nums.begin(), nums.end(),
14         [](const int &a, const int &b) { return a < b;});//注意sort和stable_sort當comp(x,x)的時候一定要返回false
15     auto segFloorIndex = find_if(nums.begin(), nums.end(),
16         [&floorSize](const int &a) ->bool { return a >= floorSize; });
17     auto segCeilIndex = find_if(nums.begin(), nums.end(),
18         [&](const int &a) { return a >= ceilSize; });
19     cout << "The size of the segment is " << segCeilIndex - segFloorIndex << endl;
20 }

  捕獲分值捕獲和引用捕獲,引用捕獲就在變量前面加&就可以了,引用捕獲可以解決某些類不能被拷貝的問題(比如輸入輸出流),另外捕獲還可以是隱式的,比如我上面的算segCeilIndex的時候,就可以直接一個&就代表捕獲所有的引用參數了。

  另外捕獲可以混用(引用和值捕獲混用),這個時候捕獲列表的第一個元素必須是&(引用捕獲)或者=(值捕獲)(不能同時隱式值捕獲和隱式引用捕獲,只能是其中一種情況),如果才用了一種隱式捕獲情況,另一種捕獲必須顯示,比如采用了隱式引用捕獲,那么值捕獲的所有值都必須顯式捕獲。
  lambda表達式值捕獲的值也是可以變的,只是這個時候必須使用mutable關鍵字,(非常量引用捕獲理所當然可以修改,這個不用多說)比如:
int v;
auto f = [v]()mutable {return ++v; };
  有些時候lambda表達式的尾置返回類型不能被省略,最常見的就是if_else的情況發生(?:表達式是可以省略返回參數的),就例如上面的segFloorIndex加了個->bool。
  lambda表達式質上是一個重載了函數調用符的類(看5),很適合在一些函數短小而且只用寫幾次的地方,可以把代碼寫得很好看。
 
 
 
4. bind綁定的對象

  我們上面看到lambda表達式可以很好地解決標准庫有些函數的謂詞問題,現在我們想問的問題是,如果不想用lambda來實現謂詞的轉換,我只想用函數,那么怎么辦呢?標准庫提供了一個很好的辦法,那就是bind函數(C++11 Feature)。(定義在頭文件functional里面)。
  簡單的來說,bind可以把一些固定的參數和函數綁定,然后調用函數的時候相當於被綁定的參數就不用填了,相當於減少了傳入參數的數量,比如上面的例子我們可以這么改。
1 bool findMin(const int &a, const int &value) 
2 {
3     return a >= value; 
4 }
5 segFloorIndex = find_if(nums.begin(), nums.end(), bind(findMin, _1, floorSize));
6 segCeilIndex = find_if(nums.begin(), nums.end(), bind(findMin, _1, ceilSize));

  其中_1(類似的還有_2,_3....)這個東西為占位符(有點像匯編的那個,定義在std的placeholders的作用域),表示的是外面的可以傳入的參數,參數從左到右依次填入_1,_2...中(比如如果有調用對象G的定義為auto G= bind(f, _2, a, _1 ),調用G(x,y)實際上是f(Y,a,x ),用這個拿來交換參數位置)。

  有些參數bind不能直接綁定(比如萬惡的輸入輸出流不允許拷貝),那就用標准庫的ref函數來得到這個對象的引用就可以了(cref是得到常量引用,這兩個都在functional的頭文件中)。
 
 
 
5. 重載了函數調用符的類
  C++的類厲害的地方之一就是可以重載函數運算,就是retType operator( )(parameter...){ }的形式,這種重載了函數調用符的類可以直接讓我們用類名來實現函數的功能,比如:
1 class FunctonalClass
2 {
3 public:
4     bool operator()(const int &a, const int &value)
5     {
6         return a >= value;
7     }
8 };
  當然了上面的這個例子我是故意寫成這樣的,其實和lambda調用形式長得很像,其實lambda就是相當於重載了函數調用符的類去掉了類名而已,事實上lamba也是在做類的事情,比如在3的lambda其實是這樣展開的:
1 class AnonymityFunctional
2 {
3 public:
4     AnonymityFunctional(const int &v) :value(v) { }
5     bool operator()(const int &a) const { return a >= value; }
6 private:
7     const int &value;
8 };
  調用形式:
segFloorIndex = find_if(nums.begin(), nums.end(), AnonymityFunctional(floorSize));
segCeilIndex = find_if(nums.begin(), nums.end(), AnonymityFunctional(ceilSize));
  lambda表達式產生的類不含默認構造函數,賦值運算符和默認析構函數,它是否含有默認的拷貝和移動函數那就要視捕獲的數據成員來定。
  事實上這種函數對象再functional里面一大把,看看標准庫就知道了,標准庫的函數對象一般為類模板,比如greator<T>。
 
 
 
利用std::function創建可調用對象的集合

  上面說了5種可調用對象,有時候我們想實現某些設計模式的時候,我們想用一個統一接口來調用這些對象,實現可調用對象表,但是這些可調用對象本質上類型都是不一樣的,如何把他們統一起來呢?答案就是使用標准庫模板function<T>,
  最簡單的,我們可以把function的統一接口:
std::function<bool(const int &)>
  這個時候,可以添加形如返回值的bool,形參為const int&的對象進去了,無論是lambda還是函數指針還是類調用對象:
 1 bool fcn(const int &a){ return a; }
 2 
 3 class FunctionTest
 4 {
 5 public:
 6     FunctionTest() = default;
 7 
 8     bool getWhat(const int &a)const { return true; }
 9     bool getS()const { return true; }
10 };
11 
12 std::function<bool(const int &)> f1 = [](const int &a) { return a ; };
13 std::function<bool(const int &)> f2 = Test();
14 std::function<bool(const int &)> f3 = fcn;

  最常見的就是把一些函數放進map里面,那樣我們就可以根據關鍵字來調用對象了,非常方便。不過需要注意的是,如果函數是重載過的,那我們就不能直接塞函數名進去了(會產生二義性),這個時候就要自己創建要放入函數的函數指針,再創建function。

1 bool fcn() { return true; }
2 bool fcn(const int &a){ return a; }
3 
4 bool(*pfcn)(const int &) = fcn;
5 std::function<bool(const int &)> f3 = pfcn; 
  
   關於類成員函數指針轉換為可調用對象的問題:
  說實話這個指針方法很少用到(不過在Qt上可以用這個來實現信號和槽的綁定,寫的時候比用SIGNAL和SLOT好,當然這種寫法信號和槽不能有函數重載),類成員函數指針可以用來指向類的非靜態函數(也就是類成員指針使用的時候一定要綁定類對象),因為成員函數指針必須用*.或者是->*先給類解引用了才可以使用,所以本質上成員函數指針不是可調用對象,但是我們仍然可以用function或者bind來是它成為可調用對象。
  不過說實話這個東西確實有點偏,先扯一下用法把,假設一個類里面有一個類:
1 class FunctionTest
2 {
3 public:
4     FunctionTest() = default;
5 
6     bool getWhat(const int &a)const { return true; }
7     bool getS()const { return true; }
8 };
  然后我們這樣定義一個成員函數指針,讓它指向getWhat
1 FunctionTest haha, *phaha = &haha;
2 
3 bool (FunctionTest::*pf)(const int &) const;//先定義一個指針
4 pf = &FunctionTest::getWhat;//指針指向類的什么函數
5 
6 auto ret = (haha.*pf)(floorSize);
7 auto ret1 = (phaha->*pf)(ceilSize);
  *.和->*是用來定義指向成員的,*.給對象用,->*給指向對象的指針用,這個很好理解(這里我只是用來演示了一下找成員函數,其實找成員也是這樣找的)。
  當然了上面的寫法是為了應付有函數重載的情況,如果沒有函數重載,那么auto肯定是最好的拉~
auto pf = &FunctionTest::getWhat;
  好了扯了那么多那怎么用fcuntion來生成成員函數指針的可調用對象呢?很簡單,直接:
std::function<bool(const FunctionTest *, const int &)> f4(&FunctionTest::getWhat);

  這里我們發現了要生成成員函數的可調用對象必須帶對象,所以生成的可調用對象都必須帶類的對象,這很符合成員函數指針的要求——必須綁定對象,事實上,當我們使用這樣的可調用對象的時候,就相當於把this指針先傳給第一個參數了,所以這個function對象本質上是需要兩個參數的!不能用於find_if。

  當然了,生成一個這樣的的調用對象,除了用function,還可以用標准庫的另一個函數:mem_fn
auto f = std::mem_fn(&FunctionTest::getWhat);
  生成的可調用對象f和function類似,這個調用對象f也是不能用於find_if,因為他一定要傳this進去,而getWhat本身就是一元謂詞,f4直接變二元了。
 
 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM