函数与指针:函数
核心要点速览
- 声明与定义:可多次声明,仅能一次定义;原型需匹配参数类型 / 个数(返回类型不影响)
- 参数传递:值传递(拷贝,不影响实参)、引用传递(别名,无拷贝)、指针传递(地址,支持 nullptr)
- 函数重载:同名 + 参数列表不同(类型 / 个数 / 顺序),返回类型不参与重载
- 内联函数:inline 修饰,建议编译器嵌入调用处(适合短函数,无循环 / 递归)
- 默认参数:遵循 “靠右原则”,不可跳过右侧参数省略左侧
- 返回值:禁止返回局部变量引用;大型对象返回有 RVO/NRVO 优化
一、函数的声明与定义
概念
- 声明:告知编译器函数的 “存在及原型”(返回类型、函数名、参数类型 / 个数),不包含实现;参数名可选(仅声明时)。
- 用途:解决 “调用在前、定义在后” 的编译错误,如
int add(int, int);
- 用途:解决 “调用在前、定义在后” 的编译错误,如
- 定义:包含声明 + 函数体(具体实现),编译器会分配内存生成指令。
规则
- 单定义原则(ODR):一个函数可在多个文件中声明,但仅能在一个文件中定义(否则链接报错 “重复定义”)。
- 原型匹配:声明与定义的参数类型、个数必须一致;返回类型可省略(默认 int,不推荐),但建议显式声明。
二、参数传递
参数传递决定函数与实参的交互方式,核心分三类,重点考察适用场景与区别。
1. 值传递
规则:形参是实参的拷贝,函数内修改形参不影响实参。
优点:安全(隔离实参)、逻辑简单。
缺点:大对象拷贝开销大(如大型结构体、类对象)。
示例:
1
2void swap(int a, int b) { int t=a; a=b; b=t; } // 仅交换拷贝,实参不变
int x=1, y=2; swap(x,y); // x=1, y=2(实参未变)
2. 引用传递(int&)
规则:形参是实参的别名,无拷贝开销;函数内修改形参直接影响实参。
优点:效率高(无拷贝)、语法简洁、可修改实参。
约束:引用必须绑定实参(不能传字面量,如
func(5)报错),绑定后不可更改指向。示例:
1
2void swap(int& a, int& b) { int t=a; a=b; b=t; }
int x=1, y=2; swap(x,y); // x=2, y=1(实参已交换)
3. 指针传递(int*)
规则:形参是实参的地址拷贝,通过解引用(
*p)间接修改实参。优点:可修改实参,支持传递
nullptr(空值语义)。缺点:需显式解引用(易漏写
*),可能出现野指针(未初始化指针)。示例:
1
2void swap(int* a, int* b) { int t=*a; *a=*b; *b=t; }
int x=1, y=2; swap(&x,&y); // x=2, y=1(通过地址修改)
三类传递方式对比表
| 对比维度 | 值传递 | 引用传递 | 指针传递 |
|---|---|---|---|
| 内存开销 | 拷贝实参(大对象开销大) | 无拷贝(仅别名) | 拷贝地址(4/8 字节,固定) |
| 实参修改 | 不可修改 | 可直接修改 | 可修改(需解引用) |
| 空值传递 | 不支持 | 不支持(必须绑定对象) | 支持(传nullptr) |
| 适用场景 | 小对象、无需修改实参 | 大对象、需修改实参 | 需 “空值” 语义的场景 |
| 语法复杂度 | 简单(直接传值) | 简洁(传变量名) | 复杂(传地址 + 解引用) |
三、返回值
1. 返回值类型与规则
- 基本类型(
int/double等):直接返回值拷贝,无额外风险。 - 自定义类型(类 / 结构体):返回对象副本,编译器可能触发返回值优化(RVO/NRVO) ,避免临时对象拷贝。
- 引用返回(
&):优点:无拷贝,效率高(适合大对象)。
禁忌:禁止返回局部变量的引用(局部变量生命周期随函数结束,引用变为 “悬垂引用”,访问即未定义行为)。
示例(错误):
1
2int& func() { int x=10; return x; } // x是局部变量,函数结束后销毁
int& res = func(); // 悬垂引用,访问res可能崩溃
2. 特殊返回值:void
- 表示 “无返回值”,函数体中可省略
return,或用return;提前结束。 - 限制:不能定义
void类型变量(void x;报错);可定义void*指针(指向任意类型,需强转后使用)。
3. 返回值优化(RVO/NRVO)
- 概念:编译器对 “返回大型对象” 的优化,直接在调用处构造对象,避免函数返回时的临时对象拷贝。
- 特点:无需手动修改代码,编译器自动触发(主流编译器均支持),显著提升大型对象返回效率。
四、函数重载
核心定义
同一作用域中,函数名相同但参数列表不同,用于实现 “同一功能的不同参数版本”。
重载的条件(缺一不可)
- 同一作用域(如同一命名空间、同一类)。
- 函数名完全相同。
- 参数列表不同(满足任一即可):
- 参数类型不同(如
add(int, int)vsadd(double, double)); - 参数个数不同(如
add(int)vsadd(int, int)); - 参数顺序不同(如
func(int, double)vsfunc(double, int))。
- 参数类型不同(如
- 补充场景(易忽略):
- 参数为引用 / 指针时,
const修饰可形成重载(如func(int&)vsfunc(const int&)); - 类的成员函数中,
const修饰this指针可形成重载(如void show()vsvoid show() const)。
- 参数为引用 / 指针时,
重载的排除条件
- 返回类型不同不能形成重载(如
int add(int)vsdouble add(int),编译报错); - 参数名不同不能形成重载(如
add(int a)vsadd(int b),视为同一函数)。
重载决议(编译器匹配逻辑)
编译器按 “实参与形参的匹配度” 选择最佳函数,匹配度从高到低:
- 精确匹配(类型完全一致,含
const匹配); - 提升转换(如
char→int、float→double); - 标准转换(如
int→double、int*→void*); - 用户定义转换(如类的构造函数、运算符重载)。
- 若匹配度相同(歧义),编译报错(需显式类型转换解决)。
五、内联函数(inline)
特性
- 作用:建议编译器将函数体 “嵌入” 调用处(而非生成函数调用指令),减少栈帧创建 / 销毁的开销。
- 建议性:
inline是编译器 “建议”,非强制 —— 若函数含循环、递归、复杂分支,编译器会忽略inline,按普通函数处理。 - 定义即声明:内联函数需在调用前定义(不能仅声明),通常放在头文件中(方便多个编译单元包含)。
- 避免重复定义:允许在多个编译单元中定义,但所有定义必须完全一致(否则链接报错)。
与宏(#define)的区别
| 对比维度 | 内联函数(inline) |
宏(#define) |
|---|---|---|
| 类型检查 | 有(编译期校验参数类型) | 无(文本替换,无类型校验) |
| 调试支持 | 支持(可打断点、查看变量) | 不支持(替换后无宏信息) |
| 复杂逻辑支持 | 支持(循环、分支等) | 不适合(易因优先级出错) |
| 作用域 | 受作用域限制(如局部内联) | 全局替换(无作用域) |
| 副作用风险 | 低(参数仅计算一次) | 高(参数可能多次计算) |
六、默认参数
规则
靠右原则:默认参数必须从右往左连续指定,不能跳过右侧参数省略左侧参数。
示例(正确 vs 错误):
1
2
3
4void func(int a, int b=2, int c=3); // 正确(从右往左指定)
func(1); // 等价于func(1,2,3)
func(1,4); // 等价于func(1,4,3)
// func(,4,5); // 错误(跳过左侧a,未指定默认值)声明与定义一致性:默认参数仅需在声明或定义中指定一次(建议在声明中指定,方便外部调用者查看)。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 肖恩的博客!
评论

