这一章主要介绍函数的定义和声明、函数重载和函数指针。
函数其实就是一个命名了的代码块,我们通过调用函数执行相应的代码。
6.1 函数基础
一个典型的函数 (function) 定义包含以下部分:返回类型 (return type)、函数名字、形参 (parameter)以及函数体。形参以逗号隔开,形参的列表位于一对圆括号内。函数执行的操作语句块中说明,该语句块成为函数体。
函数的调用完成两项工作:传递实参初始化函数对应的形参、控制权转移给被调函数。
随后被调函数(隐式)定义并初始化它的形参,然后执行。
当遇到return
时,函数执行结束,返回return
语句的值(如果有的话),将控制权转移给主调函数
实参是形参的初始值,实参依据形参列表顺序依次初始化形参,注意 实参的求值顺序由编译器决定,和形参列表顺序无关, 实参必须与形参的类型相匹配,数量一致,类型不同时,会尝试隐式转换类型,失败则无法调用。
形参列表可以为空,但是不能省略;形参通常以逗号隔开,每个形参都是一个声明,形参不允许同名
void f1(){}
void f2(void){} //void兼容C语言
int f3(int v1,int v2){}
char* f4(char* v1){}
函数的返回类型可以是void
,它表示不返回任何值。返回类型不能是数组或者函数类型,但可以是指向数组或者函数的指针。
6.1.1 局部对象
C++中,名字有 作用域,对象有 生命周期(lifetime)
- 名字的作用域时程序文本的一部分,名字在其中可见
- 对象的生命周期是函数执行过称中该对象存在的一段时间
函数体是一个语句块,块内构成新的作用域。形参和函数内部定义的变量统称为 局部变量(local variable),仅在函数的作用域里可见。
在所有函数体外定义的对象存在于程序的整个执行过程中,启动时创建,结束时销毁。 局部变量的生命周期依赖于定义的方式
只存在于块执行期间的对象成为 自动对象(automatic object),块执行结束之后,块中创建的自动对象的就变成未定义了。
形参是一种自动对象,函数开始时为形参申请空间,结束时销毁。
- 局部静态对象
某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间,可以将局部变量定义为
static
类型。 局部静态对象(local static object)在函数首次经过对象定义语句时初始化,程序终止后才销毁,如
size_t count_calls(){
static size_t ctr = 0;
return ++ctr;
}
int main(){
for (size_t i = 0;i != 10; ++i){
cout<< count_calls() << endl;
}
return 0;
}
会输出1-10的数字
如果局部静态变量没有显式的初始值,他将执行值初始化,内置类型的局部静态变量初始化为0
6.1.2 函数声明
函数的名字必须在使用之前声明,类似于变量。函数只能定义一次,但能声明多次 唯一例外是如虚函数。函数声明如:
void print(vector<int>::const_iterator beg
vector<int>::const_iterator end);
函数三要素(返回类型,函数名,形参类型)描述了函数的接口,说明了调用该函数所需要的所有信息。函数声明也称作 函数原型(function prototype)
建议变量、函数在头文件里声明
6.1.3 分离式编译
为了允许编写程序时按照逻辑关系划分,C++支持 分离式编译,允许我们把程序分割到几个文件中,每个文件单独编译。
如果我们修改了其中一个源文件,那么只需要重新编译那个改动了的文件。大部分编译器编译时通常会产生后缀名为.obj
(Windows)或.o
(UNIX)的文件,后缀名的含义时该文件包含对象代码(object code)。随后编译器将对象文件链接在一起形成可执行文件。编译过程如下:
$ CC -c factMain.cc #生成 factMain.o
$ CC -c fact.cc #生成 fact.o
$ CC factMain.o fact.o -o main #生成 main 或者 main.exe
6.2 参数传递
函数在被调用时,都会重新创建形参,并用传入的实参进行初始化。
- 当形参是引用类型时,我们说他对应的实参是被 引用传递 或函数被 传引用调用,引用形参时它对应的实参的别名
- 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被 值传递 或者函数被 传值调用
6.2.1 传值参数
当初始化一个非引用类型的变量时,初始值被拷贝给对象,此时,对变量的改动不会影响初始值。
- 指针形参拷贝的是指针的值,拷贝之后是两个不同的指针,通过指针,可以改变指针指向的对象的值,而实参本身不变
6.2.2 传引用参数
引用的操作实际上是作用在引用所引的对象上,如
int n = 0, i = 42;
int &r = n; //r绑定了n,相当于别名
r = 42; //现在n的值是42
r = n;
i = r;
void reset(int &i){ // i是传给reset函数的对象的另一个名字
i = 0; //改变了i所引对象的值
}
此时,被改变的对象是传入的实参,在reset
内部对i
的使用即是对实参的使用
- 使用引用避免拷贝 - 拷贝大的类类型对象或者容器对象比较低效,甚至有些类类型根本不支持拷贝(如IO类型) 如果函数无需改变引用参数的值,最好将其声明为常量引用
- 使用引用形参返回额外信息 - 当函数需要返回两个及以上的值时,可以给函数传入一个额外的引用形参,保存数据
6.2.3 const 形参和实参
顶层const
作用于对象本身,当实参初始化形参时会忽略掉顶层const
。当形参有顶层const
时,传递常量或者非常量都是可以的,但是某些时候会产生意想不到的结果
- 形参初始化方式和变量初始化方式是一样的。我们可以使用非常量初始化一个底层
const
对象,但反过来不行。同时 一个普通的引用必须用同类型的对象初始化 - 尽量使用常量引用 把函数不会改变的形参定义为(普通)引用会造成误导,即函数可以改变它的实参的值。而且使用引用而非常量引用会限制函数所能接受的实参类型,如
string::size_type find_char(string &s){};
调用时如find_char("hello")
会出错
数组形参
数组不允许拷贝,因此通常是传递数组首元素的指针。虽然不能以值传递的方式传递数组,但我们可以把形参写成类似数组的方式
//尽管形式不同,但这3个print函数是等价的
//每个函数都有一个const int* 的形参
void print(const int*);
void print(const int[]);
void print(const int[10]); //实际只是const int*,只用于描述,实际‘10’并无意义
当编译器处理print
调用时,只检查传入参数是否是const int*
类型
在处理数组长度时,需要提供额外信息
- 使用标记指定数组长度 - 在尾部添加明显标记
- 使用标准库规范 - 传递尾指针
- 显式传递长度
数组引用形参
C++允许将变量定义成数组的引用,所以,形参也可以时数组的引用。此时引用形参绑定到对应的实参上,即绑定到数组上
void print(int (&arr)[10]){ //arr是具有10个整数的整型数组的引用
for (auto elem : arr){
cout << elem << endl;
}
}
// ()必不可少
// f(int &arr[10]); 错误,arr成了引用的数组
传递多维数组
C++实际上并没有真正的多维数组,多维数组其实是数组的数组 所以传递的参数依然是数组首元素地址
void print(int (*matrix)[10],int rowsize); //指向含有10个整数的数组的指针
// ()不可或缺,缺少的话就成了指针数组
等价定义
void print(int matrix[][10],int rowsize);
6.2.5 main: 处理命令行选项
main函数如
int main(int argc,char *argv[]){...}
//argc为数组中字符串数量,argv是数组
注意 argv[0]保存的是程序的名字,并非用户输入
6.2.6 含有可变形参的函数
有时我们无法提前预知应该像函数传递几个实参,参见可变形参模板
- 省略符形参
省略符形参是为了便于和C代码而设置的,使用了
varargs
的C标准库功能。省略符不应用于其他目的。省略符形参只能出现在形参列表的最后一个位置,如
void foo(pam_list,...);
void foo(...);
6.3 返回类型和return语句
return
语句终止当前正在执行的函数并将控制权返回给调用该函数的地方。
6.3.1 无返回值函数
没有返回值的return
语句只能用于返回类型是void
的函数。可以省略,函数会隐式调用return;
6.3.2 有返回值函数
有返回值的return
语句只能用于返回类型对应的值,允许隐形转换。
返回一个值的方式和初始化一个变量的方式完全一样。
如果函数返回引用,则该引用仅是它引对象的一个别名,不会真正拷贝对象。
- 不要返回局部对象的引用或指针
- 引用返回左值
返回值是左值,因此调用是个左值,和其他左值也能出现在赋值运算符的左侧
char &get_val(string &str,string::size_type ix){ return str[ix]; } int main(){ string s("a value"); get_val(s,0)='A'; //s变为"A value" return 0; }
6.3.3 返回数组指针
int arr[10]; //arr是一个含有10个整数的数组 int *p1[10]; //p1是一个含有10个指针的数组 int (*p2)[10]; //p2是一个指针,指向含有10个整数的数组
6.4 函数重载
如果统一作用域内的几个函数名字相同但形参列表不同,我们称之为 重载函数,调用时,编译器会根据实参进行推断调用的是哪个函数
6.5 特殊用途语言特性
6.6 函数匹配
6.7 函数指针
小结