第七章:函数——C++的编程模块 | C++ Primer Plus 重点带看
为什么需要函数原型
C++ 的编译是从上到下逐文件独立编译的。编译器在处理代码时,必须先知道函数长什么样(返回类型、参数列表),才能正确编译调用它的代码。
函数原型的作用:
1、让编译器在编译期检查参数匹配。如果让编译器自动查找函数实现,会导致编译效率降低,并且可能函数实现根本不在本文件里面;
2、支持跨文件调用。编译器一次只编译一个 .cpp 文件,不可能跨文件去"找"函数实现。
// math.h(头文件)
int add(int a, int b); // 函数原型
// main.cpp
#include "math.h"
int main() {
int result = add(3, 4); // 编译器只需看到原型就能编译
}
// math.cpp
#include "math.h"
int add(int a, int b) { // 函数定义在另一个文件
return a + b;
}
// 编译过程:
// main.cpp → 编译器只看到原型,不知道实现 → 生成目标文件 main.o
// math.cpp → 编译器编译函数实现 → 生成目标文件 math.o
// 链接器 → 把 main.o 和 math.o 链接在一起 → 可执行文件
3、提高编译效率。对于大型项目,如果一个函数的实现在文件末尾,或者跨文件,编译器每次都要"往后翻"去查找,效率很低。有了原型,编译器在当前位置就能做出判断。
// 如果不需要函数原型,编译器遇到函数调用时需要扫描整个文件甚至所有文件 add(3, 4); // 编译器:我得往下翻,找到 add 的定义才知道怎么处理 // 有了函数原型,编译器直接看原型就知道怎么处理 int add(int, int); // 一眼就知道:两个 int 参数,返回 int add(3, 4); // 直接编译,不需要往下找
头文件作用
头文件(.h / .hpp)的本质就是集中存放函数原型的地方。这和"先看说明书再使用"是一个道理:调用者只需要知道函数的签名(返回什么、需要什么参数),不需要看内部实现。
math.h → 函数原型(对外展示的"接口") math.cpp → 函数定义(内部实现) 其他文件 #include "math.h" → 只看接口,不关心实现
C++ 函数传参
C++ 函数参数传递本质上只有一种方式:值传递。但通过指针和引用,可以实现类似"传递本身"的效果。
- 值传递:形参是实参的副本,修改形参不影响实参。
void addTen(int x) {
x = x + 10; // 修改的是副本
}
int main() {
int a = 5;
addTen(a);
std::cout << a; // 输出 5,a 没变
}
调用前: 传参后:
┌─────┐ ┌─────┐ ┌─────┐
│ a=5 │ │ a=5 │ │ x=5 │ ← x 是 a 的副本
└─────┘ └─────┘ └─────┘
实参 形参(独立内存)
- 指针传递:传递的是实参的地址,通过地址可以修改实参。
void addTen(int* p) {
*p = *p + 10; // 通过地址修改实参
}
int main() {
int a = 5;
addTen(&a); // 传 a 的地址
std::cout << a; // 输出 15,a 被修改了
}
传参后:
┌─────┐ ┌─────┐
│ a=5 │ ───→ │ p │ ← p 存的是 a 的地址
└─────┘ │ &a │
└─────┘
通过 *p 就能修改 a
- 引用传递:形参是实参的别名,直接操作实参本身。(但注意按引用传参底层类似于 int * const,实际上还是指针,也需要占内存)
void addTen(int& ref) {
ref = ref + 10; // ref 就是 a 的别名,直接改 a
}
int main() {
int a = 5;
addTen(a); // 不需要取地址,直接传
std::cout << a; // 输出 15,a 被修改了
}
传参后:
┌─────────┐
│ a = 15 │ ← ref 和 a 是同一块内存的两个名字
│ ref=15 │
└─────────┘
没有副本,没有地址传递,ref 就是 a 本身
int *arr 与 int arr[] 作为函数参数的区别
只有在作为函数形参时,int* arr 和 int arr[] 完全等价,都代表数组第一个元素的地址。在其他场景下,它们是不同的东西。
但是 int arr[] 更能提示开发者当前指针是指向的数组第一个元素。另外,在函数内 sizeof(arr) 仍旧为地址的长度,并不是整个数组的长度。所以在传递数组的时候,除了要传递起始元素的地址,还要传递元素的个数。
向函数传递原始数组的局限性
在函数里面无论如何也无法得知原始数组的长度,只能由外部传入数组长度。
“超尾指针 ”的概念
超尾指针指向数组最后一个元素的下一个位置,不是最后一个元素本身。
C++ 统一采用半开区间 [begin, end) 表示范围,即包含 begin 指向的元素,不包含 end 指向的位置。这种约定的好处在于可以统一表示空范围(begin == end 时范围为空),并且元素个数可以通过 end - begin 直接计算。如果不使用超尾指针而是指向最后一个元素(闭区间),则无法表示空数组的情况。STL 的所有容器(vector、list、map 等)和算法(sort、find、fill 等)都遵循这一约定,begin() 指向首元素,end() 返回超尾位置。需要特别注意的是,超尾指针可以进行指针比较、算术运算和递减操作,但绝对不能解引用(*end 是未定义行为),因为它指向的位置没有有效的元素。
指针与 const
1、指针常量 const int *ptr,说明 ptr 指向的类型为 const int,所以不能通过 ptr 改内存地址中的值。另外,禁止将 const 的地址赋给非 const 的指针。
2、常量指针 int * const ptr,说明 ptr 本身是常量,所以不能改变 ptr 的指向。
int x; const int cx; int *ptr; const int *cptr; ptr = &x; // 合法 ptr = &cx // 不合法 cptr = &x // 合法 cptr = &cx // 合法
一维数组原型
int arr[4]——int *arr 或 int arr[]。(注意 arr 本身不是指针,只是可以退化为指向首元素的指针)
一维数组传参时,退化为指向首元素的指针,以下三种写法等价:
void print(int arr[4], int size) { } // 写法 1,4 被忽略
void print(int arr[], int size) { } // 写法 2,省略大小
void print(int* arr, int size) { } // 写法 3,直接用指针
二维数组原型
int arr[3][4]——int (*arr)[4] 或 int arr[][4];
arr[r][c] 等价于 *(*(arr +r) + c)。
二维数组传参时,退化为指向数组的指针,不能省略第二维的大小:
void print(int arr[3][4], int size) { } // 写法 1,3 被忽略,4 不能省
void print(int arr[][4], int size) { } // 写法 2,省略第一维,保留第二维
void print(int (*arr)[4], int size) { } // 写法 3,数组指针
函数指针
函数指针是指向函数的指针,可以通过它间接调用函数。函数名本身在大多数表达式中会退化为函数的地址,就像数组名退化为首元素地址一样。
int (*ptr)(int);注意必须加括号。
int (*ptr)(int); // 函数指针:指向 返回 int、参数 int 的函数 //→ (*ptr) 有括号 → ptr 先和 * 结合 → ptr 是指针 int *ptr(int); // 函数声明:声明了一个返回 int*、参数 int 的函数 //→ ptr 先和 () 结合 → ptr 是函数 → 返回 int*
调用的时候可以 (*ptr)(4) 或 ptr(4)。
int add(int a, int b) {
return a + b;
}
// 赋值
int (*ptr)(int, int) = add; // ✅ 函数名自动退化为函数地址
int (*ptr2)(int, int) = &add; // ✅ 显式取地址,等价
// 调用
int result = ptr(3, 4); // ✅ 方式 1:直接调用
int result2 = (*ptr)(3, 4); // ✅ 方式 2:解引用后调用,等价
函数指针最常见的用途是回调函数——把一个函数作为参数传给另一个函数:
void forEach(int arr[], int size, void (*callback)(int)) {
for (int i = 0; i < size; i++) {
callback(arr[i]); // 通过函数指针调用回调
}
}
void print(int x) { std::cout << x << " "; }
void doubleIt(int x) { std::cout << x * 2 << " "; }
int main() {
int arr[] = {1, 2, 3};
forEach(arr, 3, print); // 输出 1 2 3
forEach(arr, 3, doubleIt); // 输出 2 4 6
}
C++11 提供了 std::function,是函数指针的现代替代品:
#include <functional>
// 函数指针
int (*ptr)(int, int) = add; // 只能指向普通函数
// std::function
std::function<int(int, int)> f = add; // 指向普通函数
std::function<int(int, int)> f2 = [](int a, int b) { return a + b; }; // 指向 lambda
std::function<int(int, int)> f3 = MyClass::staticAdd; // 指向静态成员函数
函数指针数组原型
函数指针数组是一个数组,数组的每个元素都是函数指针。
int (*ptr[3])(int)
│ │ │ │
│ │ │ └── 参数列表:接收一个 int
│ │ └────── ptr[3] → ptr 是数组,有 3 个元素
│ └──────── * 表示每个元素是指针
└────────── 返回类型:int
int add(int x) { return x + 1; }
int sub(int x) { return x - 1; }
int mul(int x) { return x * 2; }
int main() {
// 声明函数指针数组
int (*ptr[3])(int) = {add, sub, mul};
// 调用
std::cout << ptr[0](5) << std::endl; // add(5) → 6
std::cout << ptr[1](5) << std::endl; // sub(5) → 4
std::cout << ptr[2](5) << std::endl; // mul(5) → 10
// 用循环遍历调用
for (int i = 0; i < 3; i++) {
std::cout << ptr[i](10) << " "; // 11 9 20
}
}
C++ Primer Plus 精读|从入门到面试,重点内容全程带看。 本专栏以《C++ Primer Plus》为蓝本,逐章提炼必考知识点、易错点、面试高频考点,跳过冗余示例,直击语法本质与工程实践,帮你高效吃透 C++ 基础,夯实底层开发必备能力。
查看14道真题和解析