第七章:函数——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++ Primer Plus》为蓝本,逐章提炼必考知识点、易错点、面试高频考点,跳过冗余示例,直击语法本质与工程实践,帮你高效吃透 C++ 基础,夯实底层开发必备能力。

全部评论
收藏了
点赞 回复 分享
发布于 昨天 00:16 河北
问下秋招进度
点赞 回复 分享
发布于 04-01 23:12 河北
欢迎订阅专栏《C++/嵌入式开发-秋招面经》:https://www.nowcoder.com/creation/manager/columnDetail/MKaoll
点赞 回复 分享
发布于 03-31 22:42 河北

相关推荐

评论
4
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务