第六章 函数
函数是一个命名了的代码块,我们通过调用函数执行相应的代码,函数可以有0个或多个参数,而且(通常会产生一个结果)。可以重载函数,也就是说,同一个名字可以对应几个不同的函数。
6.1 函数基础
一个典型的函数(function)定义包括一下部分:返回类型(return type)、函数名字、由0个或多个形参(parameter)组成的列表以及函数体。
函数的形参列表
函数的形参列表可以为空,但不能省略。要想定义一个不带形参的函数,最常用的办法是写一个空的形参列表。不过为了与C语言兼容,也可以使用关键字void表示函数没有形参。
函数返回类型
大多数类型都是能用作函数的返回类型。一种特殊的返回类型是void,它表示函数 不返回任何值。函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
6.1.1 局部对象
形参和函数体内部定义的变量统称为局部变量(local variable)。它们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏(hide)在外层作用域中同名的其他所有声明中。
自动对象
对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义的语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象(automatic object)。当块的执行结束后,块中创建的自动对象的值就变成了未定义的了。
局部静态对象
某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有什么影响。
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 2 3 4 5 6 7 8 9 10
控制流第一次经过ctr的定义之前,ctr被创建并初始化为0。每次调用将ctr加1并返回新值。
size_t count_calls(size_t idx){
if(idx != 0){
cout << "count_calls " << ctr << endl; // 错误ctr未声明
}
static size_t ctr = 0; // 调用结束后,这个值仍然有效
return ++ ctr;
}
int main(){
for(size_t i = 0; i != 10; ++ i){
cout << count_calls(i) << endl;
}
return 0;
}
6.1.2 函数声明
和其它名字一样,函数的名字也必须在使用之前声明。类似于变量,函数只能定义一次,但可以声明多次。如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作**函数原型(**function prototype)。
6.1.3 分离式编译
C++语言支持所谓的分离式编译(separate compilation)。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。
编译和链接多个文件
假设fact函数的定义位于一个名为fact.cc的文件中,它的声明位于名为Chapter6.h头文件中。显然与其它所有用到fact函数的文件一样,fact.cc应该包含Chapter6.h头文件。另外,我们在名为factMain.cc的文件中创建main函数,main函数将调用fact函数。要生成可执行文件(executable file),必须告诉编译器我们用到的代码在哪里。
$ CC factMain.cc fact.cc # generates factMain.exe or a.out
$ CC factMain.cc fact.cc -o main # generates main or main.exe
如果我们修改了其中一个源文件,那么只需重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(Windows)或.o(UNIX)的文件。后缀名的含义是该文件包含对象代码(object code)
$ CC -c factMain.cc #generates factMain.o
$ CC -c fact.cc #generates fact.o
$ CC factMain.o fact.o #generates factMain.exe or a.out
$ CC factMain.o fact.o -o main # generates main or main.exe
6.2 参数传递
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。形参初始化的机理与变量初始化一样。
当形参是引用类型时,我们说他对应的实参被引用传递(passed by reference)或者函数被**传引用调用(**called by reference)。和其他引用一样,引用形参也是它绑定的对象的别名:也就是说,引用形参是它对应的实参的别名。
当实参的值拷贝给形参时,形参和实参是两个互相独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用(called by value)。
6.2.1 传值参数
当初始化一个非引用类型的变量时,初始值被拷贝给对象。此时,对变量的改动不会影响初始值。
指针形参
指针的行为和其它非引用类型一样,当执行拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。
熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参代替指针
6.2.2 传引用参数
通过使用引用形参,允许函数改变一个或多个实参的值。
void reset(int &i){
i = 0; // 改变了i所引对象的值
}
使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型对象。
// 比较两个string对象的长度
bool isShorter(const string &s1, const string &s2){
return s1.size() < s2.size();
}
如果函数无需改变引用形参的值,最好将其声明为常量引用。
使用引用形参返回额外信息
一个函数只能返回一个值,然而有时函数需要同时访问多个值,引用形参为我们一次返回多个值提供了有效的途径。
6.2.3 const和形参实参
顶层const作用于对象本身:
const int ci = 42; // 不能改变ci,const是顶层的
int i = ci; // 正确:当拷贝ci时,忽略了它的顶层const
int * const p = &i; // const是顶层的,不能给p赋值
*p = 0; // 正确:通过p改变对象的内容是允许的,现在i变成了0
和其它初始化过程一样,当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的:
void fcn(const int i); // fcn能读取i,但是不能向i写值
调用fcn函数时,既可以传入const int,也可以传入int。忽略掉形参的顶层const可能产生意想不到的结果:
void fcn(const int i); // fcn能读取i,但是不能向i写值
void fcn(int i); // 错误:重复定义了fcn(int)
指针或引用形参与const
我们可以使用非常量初始哈一个底层const对象,但反过来不行,同时一个普通的引用必须用同类型的对象初始化。
int i = 42;
const int *cp = &i; // 正确:但是cp不能改变i
const int &r = i; // 正确:但是r不能改变
const int &r2 = 42; // 正确
int *p = cp; // 错误:p的类型和cp类型不匹配
int &r3 = r; // 错误:r3的类型和r的类型不匹配
int &r4 = 42; // 错误:不能用字面值初始化一个非常量引用
尽量使用常量引用
把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制所能接受的实参类型。
// 不良设计:第一个形参的类型应该是const string&
string::size_type find_char(string &s, char c, string::size_type &occurs);
只能将find_char作用于string对象。类似下面这样的调用
find_char("Hello World", 'c', ctr);
将在编译时发生错误。
还有一种更难察觉的问题,假如其它函数(正确地)将它们形参定义成常量引用,那么第二个版本的find_char无法在此类函数中正常使用。
bool is_sentence(const string &s){
// 如果在s的末尾有