C++函数
1. 函数基础
1.1 局部对象
-
对象的生命周期是指程序执行过程中该对象存在的一段时间。
-
局部变量是指形参和函数体内定义的变量。
-
代表对象的局部变量(包括内置类型)的生命周期依赖于相应对象的定义方式。可以分为自动对象和局部静态对象。
1.1.1 自动对象
- 只存在于块执行期间的对象称为自动对象。自动对象在变量定义语句处创建,在块末尾销毁
- 局部变量对应的自动对象有两种初始化情况
- 变量定义本身有初始值,使用此初始值进行初始化
- 变量定义没有初始值,执行默认初始化。根据默认初始化的规则,局部变量不会初始化,为未定义,无确定值。
- 形参作为局部变量,是一种自动对象,使用实参进行初始化。
1.1.2 局部静态对象
-
使用
static
关键字声明局部静态对象。 -
局部静态对象在第一次执行对象定义语句时创建对象,直到整个程序结束时销毁对象。
-
此种对象不受对象定义语句所在作用域的影响,且对象定义语句只执行一次。
-
类似自动对象,局部静态对象同样有两种初始化方式
- 提供显示的初始值进行初始化
- 没有提供显示的初始值,则采用值初始化(内置类型的局部静态变量初始化为0)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23int count(){
// 此处的num即为局部静态对象
static int num = 0;
return ++num;
}
int main(){
for(int i = 0;i<10;++i)
cout<< count() << endl;
return 0;
}
/* 运行结果
1
2
3
4
5
6
7
8
9
10
*/
1.2 函数声明
-
函数只能定义一次,但可以多次声明。如果一个函数永远不会被用到,那么就可以只有声明,没有定义(虚函数)
-
函数声明
1
2
3// 包含三要素:返回类型,函数名,形参类型
int Test(int,char);
// 形参的名字可以省略 -
函数定义
1
2
3
4// 有函数体
int Test(int a, char b){
.....
} -
函数应该在头文件中声明,在源文件中定义。源文件应当将含有函数声明的头文件包含进来
2. 参数传递
参数的传递即形参的初始化和变量的初始化是一样的。
根据形参的类型不同,可以分为两种参数传递方式:
-
形参是引用类型,进行引用传递,此时形参是对应实参的别名
-
形参不是引用类型,进行值传递,此时实参的值被拷贝给形参,实参和形参是两个相互独立的对象
2.1 值传递
- 非引用类型的形参进行值传递。实参的值拷贝给形参,二者相互独立不会互相影响
- 特别的,对于指针形参,此时拷贝的是指针的值。两个指针是不同的指针对象,但是两个指针指向同一个对象。可以通过指针对对象进行修改
2.2 引用传递
-
形参是引用类型,形参称为实参的别名。可以通过形参修改实参的值。
-
C++中推荐使用引用类型的形参代替指针来修改实参。
-
对于大的类类型对象或者容器对象,进行值传递比较低效,此时一般使用引用传递。如果不想对实参进行修改,最好将形参声明为常量引用
const int &a
-
引用传递还可用于通过形参返回额外信息
int Test(int &a)
,此时即可通过参数a
返回信息
2.3 const形参和实参
顶层const
作用于对象本身。顶层const
在拷贝/赋值过程中被忽略。
执行对象的拷贝操作时,拷入和拷出对象必须具有相对应的底层const
资格,或者两个对象的数据类型可以相互转换(非常量可以转换为常量)。
- 二者类型(指针、引用、其他)相同,则底层
const
资格需要相同 - 二者类型不同,
const int a = 1
本来为顶层const
,a
作为右值时具有底层const
注意:const int a = 1
中const
是顶层的。指针类型可以有两种const
层级,引用类型(常量引用)为底层const
,其余类型为顶层const
字面值不能用于初始化普通引用,但是可以用于初始化常量引用
实参的初始化过程和规则等同于变量的初始化
2.4 数组形参
数组具有两个特殊性质:数组无法直接拷贝或赋值;数组名通常被编译器当做指针处理。这些特性导致数组不能通过元素的拷贝进行传递,但是可以使用指针传递。
对应于参数传递的方式,可以分为值传递(指针传递)和引用传递
在函数定义中,将参数类型设置为对应的指针类型,就可进行指向数组首元素的指针的值传递
1 | int a[] = {1,2,3,4,5}; |
数组类型的形参有以下几种函数声明和对应定义方式,都相当于相应类型的指针
1 | //声明 |
数组类型也可使用引用传递,将形参设置为数组的引用类型
1 | int a[] = {1,2,3,4} |
2.5 多维数组形参
整体类似于普通数组形参的传递
值传递(指针传递)
1 | int a[3][4]; |
引用传递
1 | int a[3][4]; |
2.6 main函数的参数
main
函数有两个可选参数
int argvc
:整形参数,表示第二个数组参数中的字符串数量。由程序默认进行传入,无需手动传递实参。char*argv[]
:等价于char**argv
。数组,保存字符指针,用于传递多个字符串参数。其中argv[0]
默认z自动保存程序的名字或空字符串,从argv[1]
开始为用户传入的参数,argv[argc]
即argv
数组的最后一个元素自动设为0
main
函数的完整定义
1 | int main(int argc,char**argv){...} |
mian
函数参数的实际使用情况
1 |
|
1 | ./main.exe -d -o ofile data0 |
2.7 可变数量参数
有两种处理不同数量实参的函数参数定义方式
- 所有实参类型相同,使用
initializer_list
标准库类型 - 实参类型不同,使用可变参数模板
- 省略符形参,用于与
C
函数交互
initializer_list形参
函数实参数量未知但是类型相同时,使用initialzer_list
类型定义。initialzer_list
类型定义于initializer_list
头文件中,是一种模板类型,用于表示特定类型的数组。
initializer_list
操作,类似其他模板操作,但是其中元素始终为const
类型,无法修改
操作 | 说明 |
---|---|
initializer_list<T> lst; |
T类型元素的空列表,进行默认初始化 |
initializer_list<T> lst{a,b,c...}; initializer_list<T> lst = {a,b,c}; |
列表初始化 |
lst2(lst); lst2 = lst; |
拷贝\赋值对象,而不是列表元素。二者共享列表元素 |
lst.size(); |
元素数量 |
lst.begin(); |
指向首元素的迭代器 |
lst.end(); |
指向尾后元素的迭代器 |
initializer_list
类型,实参传递值的序列时,必须使用{}
将其括起来,并使用,
分隔。
1 | void error_msg(initializer_list<string> lst){...} |
可变参数模板
省略符形参
使用省略符...
表示任意数量和类型的参数。省略符形参只能出现在参数列表的最后一个位置
省略符形参仅用于C
和C++
通用的类型,对于省略符形参,编译器不进行类型检查
此种方式是C++
为了兼容C
所支持的一种变参方式,具有一定的安全隐患,只用于和C
程序的交互
参考资料
3. 函数返回
函数定义必须有return
语句,并且使用return
语句结束函数运行并返回值。无返回值函数和有返回值函数,分别对应不同格式的return
语句。
3.1 无返回值
当函数声明的返回值为void
类型时,即为无返回值函数。无返回值的return
语句有以下几种形式
1 | return; |
3.2 有返回值
返回值的原理
函数返回值的过程为:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
返回值的过程涉及到函数内部临时变量对外部接收变量的初始化,且函数返回过后,函数内所有变量都会被销毁。
引用或指针类型的初始化后只是指向原始对象的变量,而不含有原始对象本身。因此不能使用函数返回局部对象的引用或者指针。
1 | //错误示例 |
返回左值的引用
非常量引用必须是左值引用。函数可以使用非常量引用和常量引用返回左值,且非常量引用返回的左值可以修改。
1 |
|
返回列表
函数可以返回{}
包围的值的列表,并使用此列表对相关类型进行初始化或列表初始化。初始化过程满足普通初始化的规则。
1 |
|
使用{}
包含一个值,即可返回内置类型。此时相当于不用{}
进行返回。
1 | int test(){ |
main函数的返回
main
函数的返回值可以看作状态指示器。返回0
表示执行成功,返回其他值表示执行失败,且非0
值的含义依具体机器而定。cstdlib
头文件定义了机器无关的预处理变量来表示成功与失败的执行状态。
1 |
|
main
函数可以没有return
语句直接结束,此时编译器会在函数末尾隐式的插入一条return 0
语句。
1 | int main(){ |
3.3 返回数组
数组无法被拷贝,所以函数不能直接返回数组。但是数组是对象,因此可以返回数组的指针和引用。
传统返回方式
返回数组的指针和引用时,必须显示指出数组的维度。类似数组的声明,将其放置在函数名(包含形参列表)之后
1 | Type (*function(parameter_list))[dimension]{..} |
1 |
|
尾置返回
尾置返回即为使用尾置返回类型进行返回。尾置返回适用于任何函数的任何返回类型,但是这种方式更适合于返回复杂类型。尾置返回格式如下所示
1 | //将返回类型置于->符号之后,并在原来返回类型的位置放置auto关键字 |
1 |
|
使用类型别名或decltype
类型别名
1 | using arrT = int(*)[5]; |
decltype
1 | int arr[5] = {1,2,3,4,5}; |
4. 函数重载
函数名字和返回值类型相同,但是形参列表不同(类型、数量、顺序),则这一组函数称为重载函数。
如下情况的形参类型视为相同,不构成重载关系
-
形参名称不同
1
2
3//二者是同样的形参类型,不构成重载关系
int Test(int a);
int Test(int b); -
使用类型别名
1
2
3
4//二者是同样的形参类型,不构成重载关系
using INT = int;
int Test(int);
int Test(INT); -
拥有顶层
const
的形参和没有顶层const
的形参(底层const
不同可以构成重载关系)1
2
3//二者不够成重载关系
int Test(const int);
int Test(int);1
2
3
4
5
6//可以构成重载关系
int Test(const int *);
int Test(int *);
int Test(const int &);
int Test(int &);
4.1 重载确定
重载确定,也叫函数匹配,是指在调用重载函数时,在一组重载函数中匹配一个具体的函数的过程
在此过程中,编译器会将调用的实参和重载集合中每一个函数的形参进行比较,然后决定调用哪个函数。
调用函数重载时有三种可能结果
- 编译器找到最佳匹配函数并调用
- 编译器找不到任何匹配函数,产生无匹配错误
- 有多于一个函数可以匹配,但是都不是最佳选择,产生二义性调用错误
函数匹配具体规则查看[函数匹配详解](#6. 函数匹配)
4.2 重载与作用域
当内层作用域和外层作用域中声明有同名实体时,内层作用域实体将隐藏外层作用域同名实体。此时在内层作用域中只能访问内层实体,无法直接访问外层实体。此处的实体包含变量和函数。
在内层作用域声明函数的行为不被提倡,并且在不同作用域中无法重载函数
1 |
|
5. 特殊用途语言特性
5.1 默认实参
形参可以具有默认实参,即为相应参数的默认值。调用含有默认实参的函数时,可以包含该实参,也可以省略,省略后该实参的值即为默认实参。
默认实参只能声明在函数声明中,不能出现在函数定义中,并且具有默认实参的形参必须声明在形参列表的最后
不同的默认实参不能构成函数重载
1 |
|
默认实参的添加
默认实参存在于函数声明中,且常放置于头文件中
在给定作用域中,一个形参只能被赋予一个实参,后续不能通过重复声明修改默认实参,但是可以通过重复声明添加默认实参
1 | void Test(int,int =1); |
默认实参的初始值
默认实参可以是符合类型要求的变量和表达式,且不能是局部变量。
默认实参在函数声明所在的作用域内解析,但具体的求值过程发生在函数调用时。所以,绑定的变量无法被修改和覆盖,但是变量的值可以修改。
1 |
|
5.2 内联函数和constexpr函数
内联函数
调用函数一般比调用等价的表达式的开销要大,内联函数就是为了缓解这一问题。
内联函数常用于优化规模较小、调用频繁的函数
内联函数(inline
关键字)可以在每个调用点上展开函数代码,用空间换时间,从而加快程序运行速度
1 | inline void Test(int); |
1 | int main(){ |
以上两个代码块等价
关键字inline
必须和函数定义放在一起才能使函数成为内联函数,仅仅将其放在函数声明前是没有实际作用的
1 | inline void Test(int); |
constexpr函数
constexpr
函数是指可以作为常量表达式使用的函数。constexpr
函数需要满足以下几点要求:
- 函数的返回类型必须是字面值类型
- 函数的所有形参类型必须是字面值类型
- 函数体中只有一条
return
语句(只有这一条语句)或者除return
语句外其他的语句不执行任何实际操作
constexpr函数会在编译时被替换为其结果值,并且被隐式的定义为内联函数,以便在编译时随时展开
1 | constexpr int test(){ |
constexpr
函数不一定必须返回常量表达式,但是必须包含返回常量表达式的场景,并且当其在编译过程中展开并被检查到不返回常量时,编译器会报错(只有使用常量表达式变量接收constexpr
函数时,编译器才会验证此要求)
1 | constexpr int Test(){ |
constexpr函数的定义必须出现在调用位置之前,单纯的前置声明没有作用
1 |
|
定义位置
内联函数和constexpr
函数都可以在程序中多次定义,但是要保证多个定义完全一致。
constexpr
函数是特殊的内联函数,二者都会在编译过程中被展开、替换,此时必须能够找到相应的定义
基于以上原因,constexpr
函数和内联函数通常直接定义在头文件中
参考资料c++ 内联函数(一看就懂)_Ha-Ha-Interesting的博客-CSDN博客_c++内联函数是什么
5.3 调试帮助
用于程序调试结束后屏蔽调试代码
assert预处理宏
assert
为一个预处理宏,行为类似内联函数。assert
定义在头文件cassert
中
assert
使用表达式作为其条件
1 |
|
-
表达式结果为真,
assert
不做任何操作,程序继续执行1
2
3//输入a=1,输出如下
//a
//b -
表达式结果为假,
assert
终止程序运行1
2
3
4
5
6
7
8//输入a=0,输出如下
//a
//Assertion failed!
//Program: d:\Program\C++\Study\test.exe
//File: D:\Program\C++\Study\test.cpp, Line 8
//Expression: a>0
NDEBUG预处理变量
assert
的行为依赖于NDEBUG
预处理变量。当源文件定义了NDEBUG
变量时,assert
失效,不进行任何处理;未定义NDEBUG
变量(默认情况)时,则执行检查功能。
NDEBUG
预处理变量可以手动定义,也可以在编译时使用命令行添加
1 |
1 | g++ -D NDEBUG # 相当于在main函数所在文件最开头添加语句#define NDEBUG |
预定义变量
编译器和预处理器都定义了一些变量,用于调试
__func__
:由编译器定义,保存当前程序所在函数的名字
1 | int main(){ |
__FILE__
:由预处理器定义,存放执行程序的文件名的字符串字面值
1 | int main(){ |
__LINE__
:由预处理器定义,存放当前行号的整形字面值
1 | int main(){ |
__TIME__
:由预处理器定义,存放文件编译时间的字符串字面值
1 | int main(){ |
__DATE__
:由预处理器定义,存放文件编译日期的字符串字面值
1 | int main(){ |
6. 函数匹配
函数匹配过程可以分为如下几个步骤
- 确定候选函数
- 确定可行函数
- 寻找最佳匹配
以下解析过程以如下的重载函数集合为例
1 | void f(); //1 |
6.1 确定候选函数
候选函数即为本次调用的函数的重载函数集,需要满足以下两个标准
- 与被调用的函数构成重载关系
- 函数声明在调用点可见
上述示例中,候选函数为1,2,3,4
号函数
6.2 确定可行函数
考察函数调用提供的实参,从候选函数中选出可以相匹配的函数,这些函数即为可行函数。可行函数的选择有两条标准
- 必要的形参数量与函数调用提供的实参数量相等
- 每个实参的类型与对应的形参类型相同或者实参可以转换为形参的类型
上述示例中,可行函数为2,4
号函数
6.3 寻找最佳匹配
寻找规则
确定可行函数之后,只需要根据实参和形参的匹配程度选择相应的函数
对于单参数函数,只需要比较可行函数的单个形参和实参的匹配程度,选择最高匹配等级的函数
对于多参数函数,则分别确定每个参数的匹配程度,如果有一个函数满足如下条件,则为最佳匹配,否则触发二义性调用错误
- 该函数每个实参的匹配等级都大于等于其他可行函数的实参的匹配等级
- 至少一个实参的匹配等级优于其他可行函数提供的匹配
匹配等级
编译器将实参类型和形参类型的匹配划分为几个等级,具体排序如下,从上到下等级递减
- 精确匹配
- 实参和形参类型相同
- 实参从数组类型或函数类型(函数指针)转换为对应的指针类型
- 实参添加或删除顶层
const
后与形参匹配
- 通过
const
转换实现的匹配 - 通过类型提升实现的匹配
- 通过算数类型转换或指针转换实现的匹配
- 通过类类型转换实现的匹配
示例中的函数4
精确匹配,所以会被最终调用。如果将函数调用改为f(1,1.2);
,则3,4
都为可行函数并且无最佳匹配,此时编译器会报出二义性调用错误
7. 函数指针
函数指针即为指向函数的指针。可以将函数视为一种特殊的类型,其类型由其返回值和形参列表确定,和其名称无关,因此函数指针可以视为指向相应类型的指针。
函数指针的声明
1 | int (*fp)(int,int); //指针指向返回值为int,且参数列表为int,int的函数类型 |
7.1 函数指针的使用
将函数名作为一个值进行使用时(包括放在赋值语句右侧和作为实参),会自动的转换为相应类型的指针
1 | int test(int,int); |
使用函数指针调用原函数
1 | test(1,2); //常规函数调用 |
函数指针也是指针,可以使用0
,nullptr
进行初始化,但是使用其他值进行初始化时必须保证类型匹配
1 | fp = 0; |
7.2 重载函数指针
重载函数指针和普通函数指针一样,必须确定其函数类型,并且在调用时显式指出调用的指针。
重载函数指针实际上解除了函数的重载,在使用时需要明确指出调用的函数
1 | int test(int,int); |
7.3 函数指针形参
函数指针类型可以作为一个形参,用于接收一个相应类型的函数,并在一个函数内调用一个外部函数
1 |
|
可以使用类型别名和decltype
简化函数类型的声明
1 | //函数类型 |
7.4 函数指针返回值
函数无法返回,但是可以返回指向函数的指针。声明形式类似返回数组的函数
1 | //完整的声明方式 |
函数的返回类型不能自动转换为指针,因此必须显式声明为指针
1 | using fp = int (*)(int,int); |