C++11中的函數

函數包含兩個要素:函數簽名和函數體。程序員

其中函數簽名肯定了函數的類型;函數體肯定了它的功能。編程

說到函數式編程,核心就是咱們能夠把函數當作「一等公民」:能夠聲明函數變量、能夠賦值、能夠當作參數傳遞給函數、也能夠做爲函數返回的類型。數組

1 函數和函數指針的定義

當咱們定義一個函數類型時,函數名、形參列表、返回值、函數體缺一不可。bash

當咱們聲明一個函數變量時,則不須要指定函數體,且以;結尾:ide

// 如下是一個函數定義
int func(int a, int b) {
    return a + b;
}

// 如下是函數聲明
int func(int a, int b);
複製代碼

C++中變量的類型包括:函數式編程

  • 基本類型(整型、浮點型、字符型)
  • 自定義結構體或類
  • 複合類型(數組)
  • 指針
  • 函數類型

對於函數的形參和返回值而言,它們能夠是除數組類型或函數類型以外其餘的任意類型。函數

那麼若是確實要返回數組類型或者函數類型怎麼辦呢?這就須要藉助到指針了:指向數組的指針,和指向函數的指針。ui

// 定義一個指向int[10]類型的數組的指針
int a[10];
int (*pa) [10] = a;

// 定義一個指向 int (int)類型的函數的指針
int (*pf) (int, int) = func;
複製代碼

使用數組指針訪問數組時,必須寫上解指針符號:this

(*pa)[0] = 1
複製代碼

使用函數指針調用函數時,能夠省略解指針符號:spa

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

  1. 上面那種定義返回函數指針的函數,用的仍是兼容C的寫法。在現代C++中,可使用尾置返回類型的方式來定義:
auto func2() -> int (*)(int, int);
複製代碼
  1. 可使用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返回的也是一個可調用對象。

callablenewCallable這兩個可調用對象的形參列表,以及實參的順序都是能夠隨意調整的。

在調用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中重載複製構造函數和賦值運算符。可是不要兩種方法同時用,會產生二義性,致使編譯失敗。

相關文章
相關標籤/搜索