Wiki
函式指標
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
1.函式指標
閱讀提示: 本章前半部分只使用已經學過的函式、指標和類,需要按順序掌握。後半部分會擴充套件到
nullptr、using、Lambda 和std::function等現代 C++ 特性。擴充套件部分會提前解釋新語法和 API 的基本用法,但需要學完後面的“現代 C++”章節才能完全看懂。初次學習時可以先了解結論,暫時跳過擴充套件部分。
1.1.函式指標的基本概念
前面已經學習過:普通指標可以儲存變數的地址,通過指標能夠間接訪問變數。
函式編譯後也存放在記憶體中,因此函式同樣有地址。函式指標就是儲存函式地址的指標變數。通過函式指標,可以間接呼叫它所指向的函式。
函式指標常用於:
- 把一個函式傳給另一個函式,也就是回撥函式
- 在程式執行過程中選擇不同的處理函式
- 把多個形式相同的函式放進表中統一管理
- 儲存並呼叫類的成員函式
假設有下面的函式:
int add(int a, int b);
指向它的函式指標可以宣告為:
int (*function_pointer)(int, int);
這條宣告可以從變數名向外閱讀:
function_pointer是變數名。*function_pointer說明它是一個指標。(int, int)說明它指向的函式接收兩個int引數。- 最前面的
int說明該函式返回一個int。
函式指標的基本語法為:
返回值类型 (*函数指针变量名)(参数类型列表);
變數名兩側的括號不能省略:
int (*operation)(int, int); // operation 是函数指针
int *operation(int, int); // operation 是函数,返回值是 int*
總結:函式指標型別不僅包含返回值型別,還包含完整的引數型別列表。
1.2.示例1:儲存函式地址並呼叫函式
函式名在大多數表示式中會自動轉換為函式地址,因此可以直接把 add 賦給函式指標。也可以寫成 &add,其中 & 表示獲取函式地址,兩種寫法效果相同。
通過函式指標呼叫函式也有兩種寫法:
function_pointer(参数); // 推荐写法
(*function_pointer)(参数); // 先解引用再调用
示例:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int main()
{
int (*function_pointer)(int, int) = add;
std::cout << function_pointer(3, 5) << "\n";
std::cout << (*function_pointer)(10, 20) << "\n";
function_pointer = &add;
std::cout << function_pointer(7, 8) << "\n";
return 0;
}
執行結果:
8
30
15
總結:通常直接用函式名賦值,並使用
function_pointer(...)呼叫,程式碼更簡潔。
1.3.示例2:函式指標型別必須匹配
函式指標只能指向型別相容的函式。返回值型別和每個引數的型別都要匹配。
示例:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
double divide(double a, double b)
{
return a / b;
}
int main()
{
int (*integer_operation)(int, int) = add;
double (*decimal_operation)(double, double) = divide;
std::cout << integer_operation(4, 6) << "\n";
std::cout << decimal_operation(7.0, 2.0) << "\n";
// integer_operation = divide;
// 错误:divide 的类型是 double(double, double),类型不匹配
return 0;
}
執行結果:
10
3.5
注意:不要依賴引數呼叫時可能發生的隱式型別轉換。函式本身的型別必須與函式指標型別相容。
1.4.示例3:執行時切換不同的函式
只要函式型別相同,一個函式指標就可以在不同時間指向不同函式。這可以把“選擇執行哪個操作”和“執行操作”分開。
示例:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int main()
{
int (*operation)(int, int) = add;
std::cout << "add: " << operation(10, 4) << "\n";
operation = subtract;
std::cout << "subtract: " << operation(10, 4) << "\n";
return 0;
}
執行結果:
add: 14
subtract: 6
1.5.示例4:把函式指標作為引數——回撥函式
函式指標也可以作為另一個函式的引數。被傳入並由其他程式碼呼叫的函式通常稱為回撥函式。
回撥函式不是一種新的函式型別。“回撥”描述的是函式的使用方式:呼叫者把函式地址交給另一段程式碼,另一段程式碼在需要時呼叫這個函式。
下面的 calculate 不關心具體執行加法還是乘法,只要求傳入的函式能夠接收兩個 int 並返回 int。
示例:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int multiply(int a, int b)
{
return a * b;
}
int calculate(int a, int b, int (*operation)(int, int))
{
return operation(a, b);
}
int main()
{
std::cout << calculate(6, 4, add) << "\n";
std::cout << calculate(6, 4, multiply) << "\n";
return 0;
}
執行結果:
10
24
總結:回撥可以讓
calculate保持不變,只通過傳入不同函式來改變計算行為。
1.6.示例5:函式指標作為引數時也可以省略引數名
函式宣告只需要告訴編譯器引數型別,不一定要寫引數名。函式指標引數也遵守這個規則。
下面兩條函式宣告含義相同:
int calculate(int a, int b, int (*operation)(int, int));
int calculate(int, int, int (*)(int, int));
第一種寫法更容易閱讀,通常建議在教程、標頭檔案和函式定義中保留有意義的引數名。
示例:
#include <iostream>
void print_number(int value)
{
std::cout << value << "\n";
}
void run_twice(int value, void (*callback)(int))
{
callback(value);
callback(value);
}
int main()
{
run_twice(42, print_number);
return 0;
}
執行結果:
42
42
1.7.示例6:函式指標陣列
如果多個函式擁有相同型別,就可以把它們的地址儲存在同一個陣列中,再通過陣列下標選擇函式。
函式指標陣列的基本語法為:
返回值类型 (*数组名[元素个数])(参数类型列表);
陣列下標必須在有效範圍內。下面的陣列有 3 個元素,因此有效下標是 0、1 和 2。
示例:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int multiply(int a, int b)
{
return a * b;
}
int main()
{
int (*operations[3])(int, int) = {add, subtract, multiply};
std::cout << operations[0](8, 2) << "\n";
std::cout << operations[1](8, 2) << "\n";
std::cout << operations[2](8, 2) << "\n";
return 0;
}
執行結果:
10
6
16
這種根據下標選擇行為的寫法稱為表驅動。它可以減少很長的 if...else 或 switch,但使用下標前必須檢查範圍。
1.8.示例7:函式過載與函式指標
前面已經學習過函式過載:多個函式可以使用同一個名字,只要引數列表不同。
當過載函式名賦給型別明確的函式指標時,編譯器會根據函式指標的型別選擇匹配的過載版本。
示例:
#include <iostream>
int square(int value)
{
return value * value;
}
double square(double value)
{
return value * value;
}
int main()
{
int (*integer_square)(int) = square;
double (*decimal_square)(double) = square;
std::cout << integer_square(5) << "\n";
std::cout << decimal_square(1.5) << "\n";
return 0;
}
執行結果:
25
2.25
如果沒有足夠的型別資訊,單獨寫一個過載函式名可能產生二義性。現代 C++ 擴充套件部分會介紹如何使用顯式型別轉換進一步指定過載版本。
2.成員函式指標
2.1.普通函式指標與成員函式指標的區別
非靜態成員函式必須依附某個物件呼叫,因為函式內部可能訪問該物件的資料成員和 this 指標。因此,非靜態成員函式不能儲存在普通函式指標中,需要使用成員函式指標。
成員函式指標的型別中必須寫出它所屬的類:
返回值类型 (类名::*指针变量名)(参数类型列表);
取得成員函式地址時必須寫 &类名::函数名。呼叫時還需要提供一個物件:
(对象.*成员函数指针)(参数);
(对象指针->*成员函数指针)(参数);
2.2.示例8:靜態成員函式使用普通函式指標
靜態成員函式屬於類,但不依附某個具體物件,也沒有 this 指標。因此,可以使用普通函式指標儲存靜態成員函式的地址。
示例:
#include <iostream>
class Calculator
{
public:
static int add(int a, int b)
{
return a + b;
}
};
int main()
{
int (*operation)(int, int) = &Calculator::add;
std::cout << operation(6, 9) << "\n";
return 0;
}
執行結果:
15
2.3.示例9:通過物件呼叫非靜態成員函式指標
下面的 scale 是非靜態成員函式,會訪問當前物件的 factor_,因此呼叫時必須指定一個 Multiplier 物件。
運算子 .* 表示“通過物件使用成員指標”。呼叫表示式外層的括號不能省略,因為函式呼叫運算子 () 的優先順序高於 .*。
示例:
#include <iostream>
class Multiplier
{
private:
int factor_;
public:
Multiplier(int factor)
: factor_(factor)
{
}
int scale(int value)
{
return value * factor_;
}
};
int main()
{
int (Multiplier::*operation)(int) = &Multiplier::scale;
Multiplier twice(2);
Multiplier triple(3);
std::cout << (twice.*operation)(5) << "\n";
std::cout << (triple.*operation)(5) << "\n";
return 0;
}
執行結果:
10
15
同一個成員函式指標配合不同物件呼叫時,會訪問各個物件自己的資料。
2.4.示例10:通過物件指標呼叫成員函式指標
如果手中儲存的是物件指標,需要使用 ->* 運算子。
(对象指针->*成员函数指针)(参数);
示例:
#include <iostream>
class Counter
{
private:
int value_;
public:
Counter()
: value_(0)
{
}
void add(int amount)
{
value_ += amount;
}
int value()
{
return value_;
}
};
int main()
{
void (Counter::*action)(int) = &Counter::add;
Counter counter;
Counter* object_pointer = &counter;
(object_pointer->*action)(5);
(object_pointer->*action)(7);
std::cout << counter.value() << "\n";
return 0;
}
執行結果:
12
總結:物件使用
.*,物件指標使用->*。
2.5.示例11:const 成員函式指標
成員函式引數列表後的 const 表示該成員函式不會修改物件的普通資料成員,可以在 const 物件上呼叫。
const 是成員函式型別的一部分。因此,指向 const 成員函式時,函式指標宣告中也必須帶有 const。
示例:
#include <iostream>
class Sensor
{
private:
int value_;
public:
Sensor(int value)
: value_(value)
{
}
int read() const
{
return value_;
}
};
int main()
{
int (Sensor::*reader)() const = &Sensor::read;
const Sensor sensor(42);
std::cout << (sensor.*reader)() << "\n";
return 0;
}
執行結果:
42
下面兩個成員函式指標型別並不相同:
int (Sensor::*reader1)(); // 指向非 const 成员函数
int (Sensor::*reader2)() const; // 指向 const 成员函数
3.現代 C++ 擴充套件(可選讀)
以下內容用於說明函式指標與現代 C++ 特性的聯絡。這裡會解釋示例所需的基本語法,但完整理解仍需要學習後面的“現代 C++”章節。初次學習可以先閱讀結論,以後再回來複習。
3.1.擴充套件示例1:使用 nullptr 表示空函式指標
需要的標準版本: C++11。
nullptr 是 C++11 引入的空指標字面量,表示“沒有指向任何物件或函式”。它比舊式的 NULL 和數字 0 型別更明確。
函式指標可以放進 if 條件中檢查:非空指標轉換為 true,空指標轉換為 false。呼叫空函式指標會產生未定義行為,因此可能為空時必須先檢查。
示例:
#include <iostream>
void greet()
{
std::cout << "hello\n";
}
int main()
{
void (*task)() = nullptr;
if (task)
{
task();
}
else
{
std::cout << "task is empty\n";
}
task = greet;
if (task != nullptr)
{
task();
}
return 0;
}
執行結果:
task is empty
hello
注意:函式指標只儲存函式地址,不負責管理記憶體,不需要也不能對它使用
delete。nullptr的完整用法見後面的nullptr章節。
3.2.擴充套件示例2:使用 using 給函式指標型別起別名
需要的標準版本: C++11。
直接書寫函式指標型別比較困難。C++11 可以使用 using 給已有型別起別名:
using 新名字 = 已有类型;
下面的語句表示 Operation 是 int (*)(int, int) 的別名:
using Operation = int (*)(int, int);
using 不會建立新的型別,也沒有執行時開銷,只是讓型別更容易閱讀。
示例:
#include <iostream>
using Operation = int (*)(int, int);
int add(int a, int b)
{
return a + b;
}
int multiply(int a, int b)
{
return a * b;
}
int calculate(int a, int b, Operation operation)
{
return operation(a, b);
}
int main()
{
Operation operation = add;
std::cout << calculate(3, 8, operation) << "\n";
operation = multiply;
std::cout << calculate(3, 8, operation) << "\n";
return 0;
}
執行結果:
11
24
舊程式碼中也可能看到 typedef 寫法,兩者含義相同:
typedef int (*Operation1)(int, int);
using Operation2 = int (*)(int, int);
現代 C++ 通常優先使用更容易閱讀的 using。完整用法見後面的 using 章節。
3.3.擴充套件示例3:從函式返回函式指標
有了類型別名,函式也可以方便地返回一個函式指標。下面的 select_operation 根據字元返回對應函式的地址。如果字元無法識別,則返回 nullptr。
示例:
#include <iostream>
using Operation = int (*)(int, int);
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
Operation select_operation(char symbol)
{
if (symbol == '+')
{
return add;
}
if (symbol == '-')
{
return subtract;
}
return nullptr;
}
int main()
{
Operation operation = select_operation('-');
if (operation)
{
std::cout << operation(20, 7) << "\n";
}
else
{
std::cout << "unknown operation\n";
}
return 0;
}
執行結果:
13
總結:複雜的函式指標型別應先使用
using起別名,再作為引數型別或返回值型別。
3.4.擴充套件示例4:無捕獲 Lambda 轉換為函式指標
需要的標準版本: C++11。
Lambda 表示式可以在使用位置定義一個小型匿名函式。基本語法如下:
[捕获列表](参数列表) -> 返回值类型
{
函数体
};
返回值型別通常可以由編譯器推導,因此 -> 返回值类型 經常省略。
當捕獲列表為空,也就是寫成 [] 時,Lambda 可以轉換為簽名相容的普通函式指標。
示例:
#include <iostream>
using Operation = int (*)(int, int);
int calculate(int a, int b, Operation operation)
{
return operation(a, b);
}
int main()
{
Operation maximum = [](int a, int b) {
return a > b ? a : b;
};
std::cout << calculate(12, 9, maximum) << "\n";
std::cout << calculate(4, 7, [](int a, int b) {
return a < b ? a : b;
}) << "\n";
return 0;
}
執行結果:
12
4
有捕獲的 Lambda 需要在自身物件中儲存捕獲的資料,因此不能轉換為普通函式指標:
int offset = 10;
// Operation operation = [offset](int a, int b) {
// return a + b + offset;
// }; // 错误:有捕获的 Lambda 不能转换为普通函数指针
有關捕獲列表和 Lambda 型別的完整解釋,見後面的 Lambda 表示式章節。
3.5.擴充套件示例5:使用 static_cast 選擇過載函式
static_cast<目标类型>(表达式) 是 C++ 的顯式型別轉換語法。取得過載函式地址時,可以用它明確告訴編譯器需要哪個過載版本。
auto 會讓編譯器根據右側表示式推導變數型別。下面的 decimal_square 實際型別仍然是 double (*)(double)。
示例:
#include <iostream>
int square(int value)
{
return value * value;
}
double square(double value)
{
return value * value;
}
int main()
{
int (*integer_square)(int) = square;
auto decimal_square = static_cast<double (*)(double)>(square);
std::cout << integer_square(5) << "\n";
std::cout << decimal_square(1.5) << "\n";
return 0;
}
執行結果:
25
2.25
總結:優先使用明確的函式指標型別選擇過載;上下文仍不明確時,再使用
static_cast。完整轉換規則見後面的 C++ 型別轉換章節。
3.6.擴充套件示例6:使用 std::function 儲存不同的可呼叫物件
需要的標準版本: C++11。
std::function 是標準庫 <functional> 標頭檔案提供的通用可呼叫物件包裝器。它的基本語法是:
std::function<返回值类型(参数类型列表)>
例如,std::function<int(int, int)> 可以儲存一個“接收兩個 int 並返回 int”的可呼叫物件,包括普通函式、函式指標、匹配的 Lambda 和函式物件。
示例:
#include <functional>
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int main()
{
std::function<int(int, int)> operation = add;
std::cout << operation(3, 4) << "\n";
int offset = 100;
operation = [offset](int a, int b) {
return a + b + offset;
};
std::cout << operation(3, 4) << "\n";
return 0;
}
執行結果:
7
107
普通函式指標不能儲存有捕獲的 Lambda,std::function 可以。但 std::function 使用型別擦除實現統一包裝,存在額外的間接呼叫開銷,某些情況下還可能發生動態記憶體分配。因此,不應在所有場景中無條件用它替代函式指標。
完整用法和更多示例見後面的 std::function 章節。
3.7.函式指標、Lambda 與 std::function 的選擇
| 需求 | 推薦方式 | 說明 |
|---|---|---|
| 儲存普通函式或無捕獲 Lambda | 函式指標 | 型別簡單,沒有型別擦除開銷 |
| 儲存有捕獲 Lambda | Lambda 自身型別或 std::function |
普通函式指標無法儲存捕獲的資料 |
| 接收任意型別並且隻立即呼叫 | 模板引數 | 通常能保留具體型別並被編譯器最佳化 |
| 在成員變數中統一儲存多種回撥 | std::function |
可以統一包裝多種可呼叫物件 |
| 呼叫指定物件的非靜態成員函式 | 成員函式指標 | 呼叫時必須同時提供物件 |
這裡的“模板引數”和“Lambda 自身型別”需要結合後面的模板及 Lambda 章節理解。初次學習只需記住:函式指標適合儲存普通函式和無捕獲 Lambda;需求更復雜時,再考慮其他工具。
4.常見錯誤
4.1.錯誤1:忘記變數名兩側的括號
int (*operation)(int, int); // 正确:函数指针
int *operation(int, int); // 这是返回 int* 的函数声明
4.2.錯誤2:函式型別不匹配
void print(int value);
// int (*operation)(int, int) = print; // 错误:返回值和参数列表不匹配
4.3.錯誤3:呼叫空函式指標
void (*task)() = nullptr;
// task(); // 错误:产生未定义行为
if (task)
{
task(); // 正确:先检查再调用
}
4.4.錯誤4:用普通函式指標儲存非靜態成員函式
class Robot
{
public:
void run();
};
// void (*task)() = &Robot::run; // 错误
void (Robot::*task)() = &Robot::run; // 正确
4.5.錯誤5:呼叫成員函式指標時沒有提供物件
Robot robot;
void (Robot::*task)() = &Robot::run;
// task(); // 错误:没有对象
(robot.*task)(); // 正确
4.6.錯誤6:把有捕獲的 Lambda 轉換為函式指標
using Operation = int (*)(int);
int offset = 10;
// Operation operation = [offset](int value) {
// return value + offset;
// }; // 错误
5.總結
- 函式指標儲存函式地址,型別中包含函式的返回值和引數列表。
- 函式名通常可以直接賦給簽名相容的函式指標。
- 函式指標可以用於回撥、執行時選擇函式和函式表。
- 靜態成員函式可以使用普通函式指標。
- 非靜態成員函式需要成員函式指標,並且呼叫時必須提供物件。
- 物件使用
.*,物件指標使用->*。 const是成員函式指標型別的一部分。- 可能為空的函式指標必須在呼叫前檢查。
- 現代 C++ 可以使用
using簡化型別,用無捕獲 Lambda 建立函式指標。 - 需要統一儲存多種可呼叫物件時,可以使用
std::function。