C++11中的函式

shuxiaow發表於2019-04-24

函式包含兩個要素:函式簽名和函式體。

其中函式簽名確定了函式的型別;函式體確定了它的功能。

說到函數語言程式設計,核心就是我們可以把函式當做“一等公民”:可以宣告函式變數、可以賦值、可以當做引數傳遞給函式、也可以作為函式返回的型別。

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

  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中過載複製建構函式和賦值運算子。但是不要兩種方法同時用,會產生二義性,導致編譯失敗。

相關文章