文章目录
1. 函数基础
函数是一个命名了的代码块,我们通过调用函数执行相应的代码。
- 函数定义包含以下几个部分:返回类型、函数名字、由0个或多个形参组成的列表以及函数体
- 我们通过调用运算符执行函数,调用运算符的形式是一对圆括号,它作用域一个表达式,表达式时函数或指向函数的指针;圆括号内为用逗号分割的实参,实参用于初始化函数的形参
函数的编写和调用
- 略
1.1 局部对象
形参和函数体内部定义的变量统称为局部变量,局部变量的生命周期依赖于创建方式。
在所有函数体之外定义的对象存在于程序的整个执行过程中,此类对象在程序启动时被创建,直到程序阶数才会销毁。
自动对象
-
函数的控制路径经过变量定义语句时创建该对象,到达定义所在的块末尾时销毁它。
-
这样只存在于块执行期间的对象称为自动对象
-
块结束后,块中创建的自动对象的值就变成未定义的了
-
形参是自动对象
-
通过实参初始化形参
-
如果变量定义有初始值,就用初始值进行初始化;否则,执行默认初始化
-
形参初始化示例
#include <iostream> using namespace std; void f1(int a = 20){ cout << a << endl; } void f2(int a){ cout << a << endl; } int main(){ f1(); f1(10); // f2(); // error: too few arguments to function 'void f2(int)' f2(10); return 0; }
输出:
20 10 10
-
-
内置类型的未初始化局部变量将有未定义的值
局部静态对象
-
有时候有必要另局部变量的生命周期贯穿调用及之后的时间(后续调用会用到相同的变量,要求其保留状态的情况)
-
将局部变量定义为
static
可以获得这样的对象 -
局部静态变量在程序的执行路径第一次经过对象定义时初始化,直到程序终止才销毁
在此器件即使对象所在的函数结束执行也不会对它有影响
-
例子:
#include <iostream> using namespace std; void f(){ static int a = 0; cout << a++ << endl; } int main(){ f(); f(); return 0; }
输出:
0 1
1.2 函数声明
-
函数必须在使用之前声明
-
只能定义一次,但可以声明多次
-
如果一个函数不会被我们用到,可以只声明(15.3节)
-
函数声明也称为函数原型
-
例子:
void print(vector<int>::const_iterator beg, vector<int>::const_iterator end);
-
建议尽量在头文件中进行声明,在源文件中进行定义。
1.3 分离式编译
- 略
2. 参数传递
形参的类型决定了形参和实参的交互方式
- 形参为引用:绑定到实参上(引用调用)
- 形参为值:实参的值拷贝后赋值给形参(传值调用)
2.1 传值参数
非引用类型
- 初始值被拷贝给形参
- 对变量的改动不会影响初始值
指针形参
- 指针的行为与非引用类型一样
- 指针拷贝时拷贝的是指针的值
- 拷贝后两个指针为不同指针,但是指向的对象相同
2.2 传引用参数
-
引用形参可以通过形参改变原来的实参
-
使用引用形参可以避免拷贝
-
使用形参可以返回额外信息
由于一个函数只能返回一个值,有时候函数需要返回多个值,可以通过引用形参来返回值
2.3 const形参和实参
- 实参初始化形参时会忽略掉顶层const,即常量对象或非常量对象都可以初始化具有顶层const的形参
- 可以用非常量初始化一个底层const对象,但是不可以用底层const对象初始化一个非常量,适用于形参中。
- 尽量使用常引用
2.4 数组形参
数组的特殊性质对作用在数组上的函数有影响:不允许拷贝数组以及使用数组时通常会将其转换为指针
-
传递数组实际上是传递指针
-
形参依然可以写为类似数组的形式
void print(const int *); void print(const int []); void print(const int [10]);
扩展:传递二维数组
// 方式一 void f(const int (*arr)[3]){ ... } // 方式二 void f(const int arr[][3]){ }
常用的传递方式
-
使用标记指定数组长度
C语言风格,读到空指针就停下
-
使用标准库规范
传递指向数组首元素和尾后元素的指针
-
显式传递一个表示数组大小的形参
-
数组引用形参
void f(int (&ref)[10]){ // 需要指定大小 ... }
2.5 main:处理命令行选项
int main(int argc, char *argv[])
main函数可以接收两个参数,第一个参数是数组argv中字符串的个数,第二个参数是指向C风格字符串数组的指针
例子:
prog -d -o ofile data0
argc = 5
argv[0] = "prog"
argv[1] = "-d"
argv[2] = "-o"
argv[3] = "ofile"
argv[4] = "data0"
argv[5] = "prog0
2.6 可变参数
有时候无法预知有多少个实参会被传递,这是通过C++11标准提供的方法:
- 传递的实参类型相同时,可以传递
initializer_list
的标准库类型 - 如果实参类型不同,可以编写特殊的可变参数模板(16.4节)
- 特殊形参:省略符
initializer_list形参
- 用法:略
- 含有initializer_list形参的函数也可以拥有其他形参
省略符形参
- 用于形参列表的最后一个位置
- 为了便于C++程序访问某些特殊的C代码而设置的
3. 返回类型和return语句
3.1 无返回值函数
- 无返回值的return语句只能用在返回类型为void的函数中
- 这样的函数中的return语句也可以省略
3.2 有返回值函数
-
有返回值的函数返回值的类型必须与返回类型一直
-
值是如何被返回的?
返回一个值的方式和初始化一个变量或者形参的方式一样:返回地值用于初始化调用点的一个临时量,该临时量就是函数调用的结果
-
不要返回局部对象的引用或指针
3.3 返回数组指针
可以通过以下集中方式返回一个数组指针
-
声明一个返回数组指针的函数
形式:
Type (*function(parameter_list)) [dimension]
例子:
int (*func(int i))[10];
-
拆解过程
func(int i)表示func函数接受一个int型实参
(*func(int i))表示func返回值为指针
(*func (int i))[10]表示func返回值为指向大小为10数组的指针
int (*func(int i))[10]表示这个指针直线的数组为int型数组
-
-
尾置返回值
C++11新标准,任何函数定义都可以使用尾置返回
但是这种形式对于返回类型比较复杂的函数最有效(例如返回类型为数组指针/引用)
尾置返回类型跟在形参列表后并以一个
->
符号开头为了表示函数真正的返回类型在形参列表之后,我们在原来的返回类型的地方放置一个auto
auto func(int i) -> int(*)[10];
-
使用decltype
例子:
int odd[] = {1, 3, 5, 7, 9}; int even[] = {0, 2, 5, 6, 8}; decltype(odd) *arrPtr(int i){ return (i % 2)? &odd : &even; // 返回一个指向数组的指针 }
注意的是decltype并不负责把数组类型转换成对应的指针,所以后面还需要加上
*
符号。
4. 函数重载
重载函数:在同一作用域内的几个函数名字相同但形参不同
定义重载函数
- 重载函数的形参数量或形参类型上必须有所不同
- 不允许两个函数除了返回类型外其他所有的要素都相同
重载和const形参
-
顶层const不影响传入参数
故相同顶层const参数的两个函数将无法重载
例子:
#include <iostream> using namespace std; void f(const int arr){ // 给成int const也不行 cout << 1 << endl; } void f(int *arr){ cout << 2 << endl; } int main(){ int s[] = {1,2,3,4,5,6,7,8,9}; f(s); return 0; }
一个拥有顶层const无法和没有顶层const的形参区分开来
-
对于底层const,函数可以区分它指向的是常量对象还是非常量对象区分开来
此时的const是底层的
例子:
#include <iostream> using namespace std; void f(const int *arr){ cout << 1 << endl; } void f(int *arr){ // g++9.2.0版本下默认会执行这个函数 cout << 2 << endl; } int main(){ int s[] = {1,2,3,4,5,6,7,8,9}; f(s); return 0; }
-
重载函数的原则:重载的几个函数实现的功能应该相同,即与它们的名字相符合
const_cast和重载
-
通过const_cast强制类型转换可以在不同重载函数之间相互调用
注意: const_cast 中的类型必须是指针、引用或指向对象类型成员的指针
const string& shorterString(const string& s1, const string& s2) { return s1.size() <= s2.size() ? s1 : s2; } string& shorterString(string& s1, string& s2) { const string& (r) = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); return const_cast<string&> (r); }
-
这样的意义是什么?
调用重载的函数
-
函数匹配:将函数调用与重载函数中的某一个关联起来(也称函数确定)
编译器将调用的实参与重载函数的形参列表进行比较,根据比较确定函数
重载与作用域
-
不同的作用域中无法重载函数
书上例子(6.4.1节)
5. 特殊语言用途
5.1 默认实参
函数调用的过程中,形参都被赋予了一个重复的值,这个反复出现的参数可以作为默认实参,调用含有默认实参的函数时,可以包含该参数,也可以不包含该参数。
默认实参声明:
-
函数通常只声明一次,但是声明多次也是合法的。
-
一个形参被赋予了默认值,则后面的形参也需要被赋予默认值。
-
声明多次包含默认实参的函数,只能为没有默认值的形参添加默认值。
例如:
string screen(sz, sz, char = '*'); string screen(sz, sz, char = '?'); // 错误:重复声明 string screen(sz, sz = 80, char); // 正确:添加默认实参表
默认实参初始值:
- 局部变量不能作为默认实参
5.2 内联函数
使用内联函数可以减少函数调用的开销
6. 函数匹配
确定候选函数和可行函数
- 集合中的函数称为候选函数,满足:
- 与被调用函数同名
- 声明在调用点可见
- 考察参数选出可行函数
- 形参数量与提供实参数量相等
- 实参类型与对应形参类型相同或者可以转换为形参类型
寻找最佳匹配(可能存在的)
- 逐一检查实参,寻找与形参最匹配的
6.1 实参类型转换
为了确定最佳匹配,将形参类型优先级进行划分:
-
精确匹配
类型相同
数组可以转换为指针
向实参添加顶层const(即视为常量)或向实参删除顶层const
-
通过const转换实现的匹配
-
通过类型提升实现的匹配
-
通过算术类型转换或指针转换实现的匹配
-
通过类类型转换实现的匹配
7. 函数指针
函数类型由返回类型和形参类型共同确定,而与函数名无关。
使用函数指针
-
函数名作为值使用时,自动转换为函数指针
即函数名相当于函数指针
例子:
bool lengthCompare(const string &, const string &); bool (*pf)(const string &, const string &); // 以下两种声明都可以 pf = lengthCompare; pf = &lengthCompare; // 以下两种使用都可以 bool b1 = pf(s1, s2); bool b2 = (*pf)(s1, s2);
重载函数的指针
- 要求重载函数与定义的函数指针精确匹配
函数指针形参
- 和数组类似,不可定义函数类型的形参,但可以定义指向函数的指针,此时指针可以当做函数来用