C++_函数
函数
函数是一个命名的代码块,通过调用执行。
函数可以:
- 有 0 个或多个参数
- 通常返回一个结果
- 支持重载:同名、不同参数列表
函数基础
一个典型函数由以下部分组成:
- 返回类型
- 函数名
- 形参列表
- 函数体
例如:
1 | int add(int a, int b) { |
函数调用
通过调用运算符 () 调用函数:
1 | int sum = add(1, 2); |
调用函数时会发生两件事:
- 用实参初始化形参
- 控制权转移到被调函数
函数执行结束后,再返回主调函数继续执行。
形参和实参
- 形参:定义在函数中的参数
- 实参:调用函数时提供的值
1 | int add(int a, int b); // a、b 是形参 |
说明:
- 实参数量必须与形参数量匹配
- 实参类型要与形参类型匹配,或能转换
- 函数实参的求值顺序一般未规定
空形参列表
没有参数的函数也必须写圆括号:
1 | void f1() { } |
两种写法都表示:无参数
形参声明规则
每个形参都必须写出类型:
1 | int f(int a, int b); // 正确 |
其他规则:
- 同一函数中形参名不能重复
- 函数最外层作用域中,局部变量不能与形参同名
返回类型
函数返回类型表示调用表达式的结果类型。
例如:
1 | int f(); // 返回 int |
注意:
- 函数不能返回数组类型
- 函数不能返回函数类型
- 但可以返回:
- 数组的指针
- 数组的引用
- 函数指针
局部对象
函数体内部定义的对象,以及形参,都属于局部对象。
局部对象有两个重要概念:
- 作用域:名字在哪一段代码中可见
- 生命周期:对象在程序运行中存在多久
自动对象
普通局部变量属于自动对象:
1 | void f() { |
特点:
- 进入块时创建
- 离开块时销毁
形参也属于自动对象。
局部静态对象
用 static 定义的局部变量称为局部静态对象:
1 | void count_calls() { |
特点:
- 第一次执行到定义语句时初始化
- 程序结束时才销毁
- 多次调用函数时会保留上一次的值
若未显式初始化:
- 内置类型会初始化为
0
函数声明
函数在使用前必须先声明。
函数可以:
- 声明多次
- 定义一次
函数声明也叫函数原型。
1 | int add(int, int); // 声明 |
函数声明只需写:
- 返回类型
- 函数名
- 形参类型
不必写形参名。
声明与定义分离
通常:
- 在头文件中声明函数
- 在源文件中定义函数
这样便于多个文件共享接口。
参数传递
函数调用时,形参与实参如何交互,取决于形参类型。
分两种主要方式:
- 值传递
- 引用传递
传值参数
如果形参不是引用,则实参的值会被拷贝给形参。
1 | void f(int x) { |
特点:
- 改变形参,不影响实参
- 会发生一次拷贝
指针形参
指针也是值传递:拷贝的是指针的值
1 | void reset(int *p) { |
说明:
- 改变
p本身,不影响实参指针 - 通过
*p可以修改指向的对象
例如:
1 | int i = 42; |
现代 C++ 中,若只是想操作某个对象,通常更推荐用引用形参。
传引用参数
若形参是引用,则它绑定到实参上,形参就是实参的别名。
1 | void reset(int &i) { |
调用:
1 | int j = 42; |
特点:
- 不发生拷贝
- 可以直接修改实参
- 常用于大对象,避免拷贝开销
用引用返回额外结果
函数只能直接 return 一个值,但可以通过引用形参返回更多信息。
1 | void find(int x, int &pos, bool &ok) { |
const 形参
如果函数不需要修改参数,应优先使用 const 引用:
1 | void print(const string &s) { |
优点:
- 避免拷贝
- 保证不会修改实参
- 能接收常量对象和临时对象
const 与实参
顶层 const 会被忽略,但底层 const 不能随意丢掉。
例如:
1 | void f(const int &x); |
可以传入:
- 非常量
int - 常量
int
数组形参
数组有两个重要特点:
- 不能拷贝
- 使用时通常会退化为指针
因此,数组作为函数参数时,实际上传递的是首元素指针。
1 | void print(const int arr[], int size); |
数组参数要防止越界
因为传进来的只是指针,函数本身通常不知道数组长度,所以要额外传长度,或传首尾指针。
例如:
1 | void print(const int *beg, const int *end) { |
数组引用形参
如果想让函数接收“固定大小的数组本身”,可以用数组引用:
1 | void f(int (&arr)[10]); |
含义:
arr是一个引用- 绑定到“含 10 个
int的数组”
注意区分:
1 | // void f(int &arr[10]); // 错误:引用数组 |
main 的参数
main 常见写法:
1 | int main(int argc, char *argv[]) { } |
也可写成:
1 | int main(int argc, char **argv) { } |
其中:
argc:命令行参数个数argv:参数数组,每个元素都是 C 风格字符串指针
说明:
argv[^0]通常是程序名- 用户输入参数从
argv[^1]开始
可变形参
当函数参数个数不固定时,可以使用:
initializer_list- 可变参数模板
- 省略符
...
常用、推荐的是 initializer_list。
initializer_list
适用于:参数个数不固定,但类型相同
1 |
|
调用:
1 | sum({1, 2, 3, 4}); |
常用操作
| 写法 | 含义 |
|---|---|
initializer_list<T> lst; |
空列表 |
initializer_list<T> lst{a, b, c}; |
用元素初始化 |
lst.size() |
元素个数 |
lst.begin() |
首元素指针 |
lst.end() |
尾后位置 |
说明:
- 元素是
const - 拷贝
initializer_list不会拷贝元素本身
省略符形参
形式:
1 | void foo(...); |
它主要用于与 C 兼容的场景,不够类型安全,一般不推荐使用。
return 语句
return 用于:
- 结束当前函数
- 将控制权交回调用处
- 如果有返回值,则把结果返回
无返回值函数
返回类型为 void 的函数可以写:
1 | return; |
或不写 return,执行到函数末尾自动结束。
有返回值函数
返回类型不是 void 时,每条可能执行到的路径都应该返回一个值。
1 | int f(int x) { |
要求:
- 返回值类型要与函数返回类型一致
- 或能隐式转换
不要返回局部对象的引用或指针
错误示例:
1 | const string& bad() { |
因为局部对象在函数结束时被销毁,返回其引用或指针会悬空。
返回值类别
- 返回引用的函数,调用结果是左值
- 返回非引用类型,调用结果通常是右值
例如:
1 | int &get(int &x) { return x; } |
可以写:
1 | get(i) = 10; |
列表初始化返回值
C++11 允许返回花括号列表:
1 | vector<int> f() { |
main 的返回值
main 返回值表示程序执行状态:
0:成功- 非
0:失败
也可以使用 <cstdlib> 中的宏:
1 |
|
递归
如果一个函数直接或间接调用自己,就称为递归函数。
例如:
1 | int fact(int n) { |
递归必须有:
- 递归调用
- 终止条件
否则会无限递归,最终导致栈溢出。
返回数组指针
函数不能返回数组本身,但可以返回:
- 数组指针
- 数组引用
普通写法
1 | int (*func(int i))[10]; |
含义:
func是函数- 接收一个
int - 返回一个指针
- 该指针指向“含 10 个
int的数组”
尾置返回类型
C++11 可以写得更清楚:
1 | auto func(int i) -> int(*)[10]; |
使用 decltype
如果已有数组对象,可借助 decltype:
1 | int odd[] = {1, 3, 5, 7, 9}; |
注意:
decltype(odd)的结果是数组类型- 若想返回“数组指针”,还要再加
*
函数重载
在同一作用域中,函数名相同但形参列表不同,称为重载
例如:
1 | void print(int); |
重载要求
重载函数必须在以下方面不同:
- 参数个数不同
- 参数类型不同
- 参数顺序不同(类型不同的前提下)
不能只靠返回类型区分重载:
1 | int f(int); |
顶层 const 不影响重载
下面两个函数其实相同:
1 | int func(int); |
因为传值参数的顶层 const 会被忽略。
const 引用/指针与重载
底层 const 可以构成不同重载:
1 | void f(int &); |
调用时:
- 非常量对象优先匹配非常量版本
- 常量对象只能匹配常量版本
调用重载函数
编译器在一组同名函数中选择最合适的那个过程叫:
- 函数匹配
- 重载确定
可能有 3 种结果:
- 找到最佳匹配,调用成功
- 没有可匹配函数,报错
- 有多个都能匹配,但没有最佳者,产生二义性
二义性示例
1 | void f(long); |
因为 10 转成 long 或 double 都可行。
重载与作用域
函数重载只发生在同一作用域中。
不同作用域里的同名函数,不是重载关系,而可能发生名字隐藏。
并且:
C++ 先做名字查找,再做类型检查
默认实参
默认实参就是给参数提供默认值,调用时可以省略。
1 | string screen(int h, int w, char c = ' '); |
调用:
1 | screen(24, 80); // c 使用默认值 |
规则
- 默认实参必须从右向左连续设置
- 一旦某个参数有默认值,它右边的参数都必须有默认值
正确:
1 | void f(int a, int b = 0, int c = 0); |
错误:
1 | // void f(int a = 0, int b, int c); // 错误 |
默认实参的补充声明
同一作用域中,一个参数只能被赋默认值一次。
后续声明只能给之前没有默认值的参数补默认值。
1 | string screen(int, int, char = ' '); |
默认实参的值
默认实参在函数调用时使用。
通常不能用局部变量作为默认实参。
内联函数
在函数前加 inline,可请求编译器把函数在调用处展开。
1 | inline int add(int a, int b) { |
适合:
- 代码短小
- 逻辑简单
- 调用频繁
说明:
inline只是建议,不保证一定展开- 内联函数通常定义在头文件中
constexpr 函数
constexpr 函数可以用于常量表达式。
1 | constexpr int new_sz() { |
使用:
1 | constexpr int foo = new_sz(); |
要求
传统规则下,constexpr 函数通常要求:
- 返回类型是字面值类型
- 形参类型是字面值类型
- 返回的结果在需要时能构成常量表达式
注意:
constexpr函数不一定每次都返回常量表达式- 只有在传入常量表达式实参时,结果才可能是常量表达式
与 inline
很多 constexpr 函数也会被隐式视作内联函数。
通常也定义在头文件中。
调试帮助
常见调试工具:
assertNDEBUG
assert
assert 是预处理宏,定义在:
1 |
用法:
1 | assert(expr); |
含义:
- 若
expr为真,什么都不做 - 若
expr为假,输出错误信息并终止程序
例如:
1 | assert(i > 0); |
NDEBUG
assert 是否生效与 NDEBUG 有关。
- 未定义
NDEBUG:assert生效 - 定义了
NDEBUG:assert被关闭
也可用它控制自定义调试代码:
1 |
|
函数指针
函数指针是“指向函数的指针”。
例如有函数:
1 | int func(int); |
则对应的函数指针写法为:
1 | int (*pf)(int); |
含义:
pf是指针- 指向一个参数为
int、返回int的函数
赋值与调用
函数名在需要时会自动转换成函数指针:
1 | pf = func; |
调用函数指针时:
1 | int a = pf(1); |
两种写法等价。
空函数指针
函数指针可赋值为:
1 | pf = nullptr; |
表示不指向任何函数。
返回函数指针
直接写比较复杂,通常建议用类型别名。
1 | using F = int(int*, int); // 函数类型 |
返回函数指针的函数:
1 | PF f1(int); |
直接声明
也可以直接写:
1 | int (*f1(int))(int*, int); |
含义:
f1是函数- 接收一个
int - 返回一个函数指针
- 该函数指针指向的函数:参数为
(int*, int),返回int
尾置返回类型
更清晰的写法:
1 | auto f1(int) -> int(*)(int*, int); |
易错点总结
实参求值顺序通常未规定
不要写依赖实参求值顺序的代码。
数组作参数时会退化为指针
1 | void f(int arr[]); // 等价于 void f(int *arr); |
不要返回局部变量的引用或指针
函数结束后局部对象已销毁。
重载不能只靠返回类型区分
1 | int f(int); |
默认实参必须从右往左连续给出
修改实参时优先考虑引用参数
不修改时优先考虑 const 引用。
initializer_list 元素是常量
不能修改其中元素。
main 的用户参数从 argv[^1] 开始
argv[^0] 通常是程序名。




