函数是一个命名了的代码块,我们通过调用函数执行相应的代码。函数可以有0个或多个参数,而且(通常)会产生一个结果。可以重载函数,也就是说,同一个名字可以对应几个不同的函数。
6.1 函数基础
一个典型的函数(function)定义包括以下部分:返回类型(return type)、函数名字、由0个或多个形参(parameter)组成的列表以及函数体。我们通过调用运算符(call opertaor)来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是一个用逗号隔开的实参(argument)列表。
- 函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。
- 执行函数的第一步是(隐式地)定义并初始化它的形参。当遇到一条return语句时函数结束执行过程。和调用函数一样,return语句也完成两项工作:一是返回return语句中的值(如果有的话),二是将控制权从被调函数转移回主调函数。
- 函数返回类型:大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何值。函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
- 自动对象:形参是一种的自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
- 局部静态对象:某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。
- 函数声明:因为函数的声明不包含函数体,所以也就无须形参的名字。事实上,在函数的声明中经常省略形参的名字。尽管如此,写上形参的名字还是有用处的,它可以帮助使用者更好的理解函数的功能。
- 函数也应该在头文件中声明而在源文件中定义。定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。
6.2 参数传递
当形参是引用类型时,我们说它对应的实参被引用传递(passed by reference);当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)。
NOTE:熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参替代指针。
- 传引用参数:拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问类型的对象。
12345//比较两个string对象的长度bool isShorter (const string &s1, const string &s2){return s1.size() < s2.size();}
NOTE:如果函数无须改变引用形参的值,最好将其声明为常亮引用。
- 尽量使用常亮引用:把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外使用引用而非常量引用也会极大地限制函数所能接受的实参类型。
- 数组形参:数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。如果我们传给函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。
- main:处理命令行选项:
1int main(int argc, char *argv[]) {...}
第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:
1int main (int argc, char **argv) {...}
其中argv指向char*。当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素一次传递命令行提供的实参。 - 含有可变形参的函数:为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板,关于它的细节将在16.4节介绍。C++还有一种特殊的形参类型(即省略符),可以用它传递可变的实参。
- initializer_list形参:initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。
和vector一样,initializer_list也是一种模板类型,定义initializer_list对象时,必须说明列表中所含元素的类型;个vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。
1 2 3 4 5 6 7 8 9 10 11 12 |
void error_msg (initializer_list<string> il) { for(auto beg = il.begin(); beg != il.end(); ++beg) cout << *beg << " " ; cout << endl; } //expected和actual是string对象 if (expected != actual) error_msg({"functionX", expected, actual}); else error_msg({"functionX", "okay"}); |
上面的代码中我们调用了同一个函数error_msg,但是两次调用传递的参数数量不同。
- 省略符形参:省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准功能。
12void foo (param_list, ...);void foo (...);
WARNING:省略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
6.3 返回类型和return语句
return语句有两种形式:
1 2 |
return; return expression; |
- 无返回值函数:没有返回值的return语句只能用在返回类型是void的函数中。
- 有返回值函数:return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。
- 不要返回局部对象的引用或指针:函数完成后,它所占用的内存空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域。
- 返回类类型的函数和调用运算符:调用运算符的优先级和点运算符和箭头运算符相同,并且也符合左结合律。因此,如果函数返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员。
12//调用string对象的size成员,该string对象是由shorterString函数返回的auto sz = shorterString(s1, s2).size(); - 引用返回左值:函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。
123456789101112char &get_val (string &str, string::size_type ix){return str[ix]; //get_val假定索引值是有效的}int main(){string s("a value");cout << s <<endl; //输出a valueget_val(s, 0) = 'A'; //将s[0]的值改为Acout << s << endl; 输出A valuereturn 0;}
如果返回类型是常量引用,我们不能给调用的结果赋值。 - 列表返回值:C++11新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。
1234567891011vector<string> process(){//...//expected和actual是string对象if(expected.empty())return {}; //返回一个空vector对象else if(expected == actual)return {"functionX", "okay"}; //返回列表初始化的vector对象elsereturn {"functionX", expected, actual};} - 主函数main的返回值:之前介绍过,如果函数的返回类型不是void,那么它必须返回一个值,但是这条规则有一个例外:我们允许main函数没有return语句直接结束。main函数的返回值可以看做是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,我们可以使用这两个变量分别表示成功与失败:
1234567int main(){if (some_failure)return EXIT_FAILURE; //定义在cstdlib头文件中elsereturn EXIT_SUCCESS; //定义在cstdlib头文件中}
因为它们是预处理变量,所以既不能在前面加上std::,也不能在using声明中出现。 - 递归:如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。
1234567//计算val的阶乘,即1*2*3...*valint factorial(int val){if (val > 1)return factorial(val - 1) * val;return 1;} - 返回数组指针:因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。
- 声明一个返回数组指针的函数:Type (*function(parameter_list)) [dimension] ,其中Type表示元素的类型,dimension表示数组的大小。(*function(parameter_list))两端的括号必须存在,如果没有这对括号,函数的返回类型将是指针的数组。
- 使用尾置返回类型:C++11新标准中还有一种可以简化上述func声明的方法,就是使用尾置返回类型(trailing return type)。尾置返回类型跟在形参列表后面并以->符号开头
12//func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组auto func (int i) -> int (*)[10];
- 使用decltype:还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。
1234567int odd[] = {1,3,5,7,9};int even[] = {0,2,4,6,8};//返回一个指针,该指针指向含有5个整数的数组decltype(odd) *arrPtr(int i){return (i%2) ? &odd : &even; //返回一个指向数组的指针}
arrPtr使用关键字decltype表示它的返回类型是个指针,并且该指针所指对象与odd的类型一致。
6.4函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数。
- 重载和const形参:顶层const不影响chuanru函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:
12345Record lookup (Phone);Record lookuo (const Phone); //重复声明了Record lookup(Phone)Record lookup (Phone*);Record lookuo (Phone *const); //重复声明了Record lookup(Phone*)
另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:
1234567//对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同//定义了4个独立的重载函数Record lookup (Account&); //函数作用于Account的引用Record lookup (const Account&); //新函数,作用于常量引用Record lookup (Account*); //新函数,作用于指向Account的指针Record lookup (const Account); //新函数,作用于指向常量的指针
const_cast和重载:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//比较两个string对象的长度,返回较短的那个引用 const string &shorterString (const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1:s2; } //重载为非const版本 string &shorterString (string &s1, string &s2) { auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); return const_cast<string&>(r); } |
第二个版本,首先将它的实参强制转换成对const的引用,然后调用了shorterString函数的const版本。const版本返回对const string 的引用,然后我们再将其转换回一个普通的string&,这显然是安全的。
6.5 特殊用途语言特性
本节介绍三种函数相关的语言特性,这些特性对大多数程序都有用,它们分别是:默认实参、内联函数和constexpr函数。
- 默认实参(default argument):
12typedef string::size_type sz;string screen (sz ht = 24, sz wid = 80, char backgrnd = ' ');
其中我们为每一个形参都提供了默认实参,默认实参作为形参的初始值出现在形参列表中。我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。 - 默认实参初始值:局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。
- 内联函数:上述的shorterString函数也存在一个潜在的缺点:调用函数一般比求等价表达式的值要慢一些。在大多数的机器上,一次函数调用包含一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置基础执行。而,内联函数(inline)可避免函数调用的开销。
123456789//声明为内联函数inline const string &shorterString (const string &s1, const string &s2){return s1.size() <= s2.size() ? s1:s2;}cout << shorterString(s1,s2) << endl;//在编译过程中展开成类似于下面的形式,从而消除了函数运行时的开销cout << (s1.size() <= s2.size() ? s1:s2) << endl;
NOTE:内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。 - constexpr函数:constexpr function是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:
12constexpr int new_sz() {return 42;}constexpr int foo = new_sz(); //正确:foo是一个常量表达式
为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
6.7 函数指针
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型 由它的返回类型和形参类型共同决定,与函数名无关。
1 2 3 4 5 |
//比较两个string对象的长度。该函数的类型是bool(const string&,const string&)。 bool lengthCompare(const string &, cnost string &); //要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可 //pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型 bool (*pf) (const string&,const string&); //未初始化 |
- 使用函数指针:当我们把函数名作为一个值使用时,该函数自动地转换成指针
12pf = lengthCompare; //pf指向名为lengthCompare的函数pf = &lengthCompare; //等价的赋值语句:取地址符是可选的
此外,我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针:
123bool b1 = pf("hello", "goodbye"); //调用lengthCompare函数bool b2 = (*pf)("hello", "goodbye"); //一个等价的调用bool b3 = lengthCompare("hello", "goodbye"); //另一个等价的调用 - 函数指针形参:
1234567//第三个形参是函数类型,它会自动地转换成指向函数的指针void useBigger(const string &s1, const string &s2,bool pf(const string &, const string &));//等价的声明:显示地将形参定义成指向函数的指针void useBigger(const string &s1, const string &s2,bool (*pf)(const string &, const string &));
我们可以直接把函数作为实参使用,此时它会自动转换成指针:
12//自动将函数lengthCompare转换成指向该函数的指针useBigger(s1,s2,lengthCompare);
1.返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,我们可以使用这两个变量分别表示成功与失败 :EXIT_FAILURE,EXIT_SUCCESS
2.Type (*function(parameter_list)) [dimension] ,其中Type表示元素的类型,dimension表示数组的大小。(*function(parameter_list))两端的括号必须存在,如果没有这对括号,函数的返回类型将是指针的数组。类似于,char* argv[ ] ,没有括号是指向指针的数组。
3.函数指针有点难理解