函数的形参列表
函数的形参列表可以为空,但是不能省略。要想定义一个不带形参的函数,最常用的办法是书写一个空的形参列表。不过为了与C语言兼容,也可以使用关键字void表示函数没有形参:
void f1(){ / * ...*/ } //隐式地定义空形参列表
void f2(void){/* ...*/ } //显式地定义空形参列表
形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明。即使两个形参的类型一样,也必须把两个类型都写出来:
int f3(int vl, v2) {/* ...*/ } //错误
int f4(int vl, int v2) {/* ...*/ } //正确
任意两个形参都不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。
函数返回类型
在C++语言中,名字有作用域,对象有生命周期。理解这两个概念非常重要。
自动对象
局部静态对象 ( 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到10(包括10在内)的数字。
如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。
在头文件中进行函数的声明
定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。
举个例子,假设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
当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。
指针形参
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:
int n= 0, i = 42;
int *p = &n, *q = &i ; //p指向n;q指向i
*p = 42; //n的值改变;p不变
p = q; //p现在指向了i;但是i和n的值都不变
指针形参的行为与之类似:
//该函数接受一个指针,然后将指针所指的值置为0
void reset(int *ip){
*ip = 0; //改变指针ip所指对象的值
ip = 0; //只改变了ip的局部拷贝,实参未被改变
}
调用reset函数之后,实参所指的对象被置为0,但是实参本身并没有改变:
int i = 42;
reset (&i); //改变i的值而非i的地址
cout << "i=:" << i <<endl; //输出i =0
熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C+语言中,建议使用引用类型的形参替代指针。
//该函数接受一个int对象的引用,然后将对象的值置为0
void reset(int &i) //i是传给reset函数的对象的另一个名字
{
i = 0; //改变了i所引对象的值
}
和其他引用一样,引用形参绑定初始化它的对象。当调用这一版本的reset函数时,i绑定我们传给函数的int对象,此时改变i也就是改变i所引对象的值。此例中,被改变的对象是传入reset的实参。
调用这一版本的reset函数时,我们直接传入对象而无须传递对象的地址:
int j = 42;
reset (j); //j采用传引用方式,它的值被改变
cout <<"j= "<< j << endl ; //输出j= 0
在上述调用过程中,形参i仅仅是j的又一个名字。在reset内部对i的使用即是对j的使用.
使用引用避免拷贝
//比较两个string 对象的长度
bool isshorter(const string &s1, const string &s2){
return sl.size() < s2.size() ;
}
使用引用形参返回额外信息
定义函数使得它能够既返回位置也返回出现次数?
//返回s中c第一次出现的位置索引
//引用形参occurs负责统计c出现的总次数
string::size_type find_char (const string &s, char c, string::size_type &occurs)
{
auto ret = s.size() ; //第一次出现的位置(如果有的话)
occurs = 0; //设置表示出现次数的形参的值
for (decltype(ret) i = 0; i != s.size(); ++i){
if (s[i] == c){
if (ret == s.size() )
ret = i ; //记录c第一次出现的位置
++oCcurs; //将出现的次数加1
}
}
return ret; //出现次数通过occurs隐式地返回
}
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)
在C++语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为顶层const被忽略掉了,所以在上面的代码中传入两个fcn函数的参数可以完全一样。
指针或引用形参与const
int i = 42;
const int *cp = &i; //正确:但是cp不能改变i
const int &r = i; //正确:但是r不能改变i
const int &r2 = 42; //正确
int *p = cp; //错误:p的类型和cp 的类型不匹配
int &r3 = r; //错误:r3的类型和r的类型不匹配
int &r4 = 42; //错误:不能用字面值初始化一个非常量引用
将同样的初始化规则应用到参数传递上可得如下形式:
int i = 0 ;
const int ci = i;
string::size_type ctr = 0;
reset(&i); //调用形参类型是int*的reset函数
reset(&ci); //错误:不能用指向const int对象的指针初始化int*
reset(i); //调用形参类型是int&的reset函数
reset(ci); //错误:不能把普通引用绑定到const对象ci上
reset(42); //错误:不能把普通应用绑定到字面值上
reset(ctr) ; //错误:类型不匹配,ctr是无符号类型
//正确:find_char的第一个形参是对常量的引用
find_char( "Hello world! " , 'o', ctr) ;
尽量使用常量引用
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:
因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术,
数组形参和const
数组引用形参
C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上:
//正确:形参是数组的引用,维度是类型的一部分
void print(int (&arr) [10])
{
for (auto elem : arr)
cout << elem << endl;
}
&arr两端的括号必不可少:
f(int &arr[10]) //错误:将arr声明成了引用的数组
f(int (&arr)[10]) //正确:arr是具有10个整数的整型数组的引用
有时我们确实需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。例如,假定main函数位于可执行文件prog之内,我们可以向程序传递下面的选项:
prog -d -o ofile data0
这些命令行选项通过两个(可选的)形参传递给main函数:
int main (int argc,char *argv[]) { ... }
第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:
int main(int argc, char**argv) { ... }
其中arav指向char*。
当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。
以上面提供的命令行为例,argc应该等于5,argv应该包含如下的C风格字符串:
argv[o] - "prog" ;//或者argv[0]也可以指向一个空字符串
argv[1] = "-d" ;
argv[2] = "-o" ;
argv[3] = "ofile" ;
argv[4] = "datao" ;
argv[5] = 0;
当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。
为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:
C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。本节将简要介绍省略符形参,不过需要注意的是,这种功能一般只用于与C函数交互的接口程序。
initializer_list形参
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。initializer_list类型定义在同名的头文件中,它提供的操作如下表所示。
和vector一样,initializer_list也是一种模板类型。定义initializer_list对象时,必须说明列表中所含元素的类型:
initializer_list<string> ls; // initializer_list的元素类型是string
initializer_list<int> li; // initializer_list的元素类型是int
和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。
我们使用如下的形式编写输出错误信息的函数,使其可以作用于可变数量的实参:
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); ++beg)
cout << *beg << " " ;
cout << endl ;
}
如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:
//expected和actual是string对象
if (expected != actual)
error_msg({"functionx" , expected, actual});
else
error_msg({ "functionx" ,"okay" }) ;
在上面的代码中我们调用了同一个函数error_msg,但是两次调用传递的参数数量不同:第一次调用传入了三个值,第二次调用只传入了两个。
省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。你的C编译器文档会描述如何使用varargs.
省略符形参应该仅仅用于C和C+通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:
void foo (parm_list, ...);
void foo(...);
第一种形式指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。省略符形参所对应的实参无须类型检查。在第一种形式中,形参声明后面的逗号是可选的。
略
不要返回局部对象的引用或指针
返回类类型的函数和调用运算符
如果函数返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员。
例如,我们可以通过如下形式得到较短string对象的长度:
//调用string对象的size成员,该string对象是由shorterstring函数返回的
auto sz = shorterString(s1,s2).size();
引用返回左值
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值:
char &get_val (string &str, string::size_type ix){
return str[ix]; //get_val 假定索引值是有效的
}
int main (){
string s("a value") ;
cout << s << endl ; //输出a value
get_val(s,0)= 'A'; //将s[0]的值改为A
cout << s << endl; //输出A value
return 0;
}
把函数调用放在赋值语句的左侧可能看起来有点奇怪,但其实这没什么特别的。返回值是引用,因此调用是个左值,和其他左值一样它也能出现在赋值运算符的左侧。
列表初始化返回值
C++11新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。
vector<string> process ()
{
// ...
//expected和actual是string对象
if(expected.empty())
return{}; //返回一个空vector对象
else if (expected == actual)
return { "functionX", "okay" } ; //返回列表初始化的vector对象
else
return { "functionx" , expected, actual } ;
}
typedef int arrT[10]; //arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT = int [10] ; //arrT的等价声明
arrT* func (int i) ; //func返回一个指向含有10个整数的数组的指针
其中 arrT是含有10个整数的数组的别名。因为我们无法返回数组,所以将返回类型定义成数组的指针。因此,func函数接受一个int实参,返回一个指向包含10个整数的数组的指针。
声明一个返回数组指针的函数
要想在声明func时不使用类型别名,我们必须牢记被定义的名字后面数组的维度:
int arr[10]; //arr是一个含有10个整数的数组
int *p1[10] ; //p1是一个含有10个指针的数组
int (*p2)[10] = &arr; // p2是一个指针,它指向含有10个整数的数组
和这些声明一样,如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。因此,返回数组指针的函数形式如下所示:
Type (*function(parameter_list)) [dimension]
类似于其他数组的声明,Type表示元素的类型,dimension表示数组的大小。(*function(parameter_list))两端的括号必须存在,就像我们定义p2时两端必须有括号一样。如果没有这对括号,函数的返回类型将是指针的数组。
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为 重载函数
void print(const char *cp) ;
void print(const int *beg, const int *end) ;
void print(const int ia[], size_t size) ;
Record lookup (const Account&);
bool lookup(const Account&); //错误:与上一个函数相比只有返回类型不同
重载和const 形参
顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:
Record lookup(Phone) ;
Record lookup(const Phone); //重复声明了Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone* const) ; //重复声明了 Record lookup (Phone* )
在这两组函数声明中,每一组的第二个声明和第一个声明是等价的。
另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:
//对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
//定义了4个独立的重载函数
Record lookup(Account&); //函数作用于Account的引用
Record lookup(const Account&) ; //新函数,作用于常量引用
Record lookup (Account* ) ; //新函数,作用于指向Account的指针
Record lookup (const Account* ); //新函数,作用于指向常量的指针
const_cast和重载
const_cast在重载函数的情景中最有用。举个例子,
//比较两个string对象的长度,返回较短的那个引用
const string &shorterstring (const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
这个函数的参数和返回类型都是 const string 的引用。我们可以对两个非常量的string 实参调用这个函数,但返回的结果仍然是const string 的引用。因此我们需要一种新的shorterstring 函数,当它的实参不是常量时,得到的结果是一个普通的引用,使用const_cast可以做到这一点:
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&,这显然是安全的。
我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
为了使得窗口函数既能接纳默认值,也能接受用户指定的值,我们把它定义成如下的形式:
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ' );
函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。例如,要想覆盖backgrnd的默认值,必须为ht和 wid提供实参:
string window ;
window = screen() ; //等价于screen (24,80,' ')
window = screen(66); //等价于screen (66,80,' ')
window = screen(66,256); //screen (66,256,' ')
window = screen(66,256,'#′); //screen (66,256,'#’)
window = screen(, , '?') ; //错误:只能省略尾部的实参
window = screen ('?'); //调用screen(' ?’, 80 , ' ')
默认实参声明
换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。假如给定:
//表示高度和宽度的形参没有默认值
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); //错误:重复声明, 不能修改一个已经存在的默认值
string screen(sz = 24,sz = 80, char); //正确:添加默认实参
函数调用一般比求等阶表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。
内联函数可避免函数调用的开销
在shorterString函数的返回类型前面加上关键字 inline ,这样就可以将它声明成内联函数了:
//内联版本:寻找两个string对象中较短的那个
inline const string & shorterstring(const string &s1,const string &s2){
return s1.size() <= s2.size() ? sl : s2;
}
//调用
cout << shorterstring(s1, s2) << endl;
//将在编译过程中展开成类似于下面的形式:
cout << (s1.size() < s2.size() ? sl : s2) <<endl;
constexpr函数
constexpr函数(constexpr function)是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:
constexpr int new_sz(){ return 42;}
constexpr int foo = new_sz(); //正确:foo是一个常量表达式
当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和NDEBUG。
assert 预处理宏
assert是一种预处理宏( preprocessor marco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:
assert (expr) ;
assert宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阙值。此时,程序可以包含一条如下所示的语句:
assert(word.size()> threshold) ;
NDEBUG预处理变量
$ CC -D NDEBUG main.C # use /D with the Microsoft compiler
这条命令的作用等价于在main.c文件的一开始写#define NDEBUG。
调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:
//比较两个string对象的长度
bool lengthCompare (const string &, const string &) ;
该函数的类型是 bool(const string&,const string&)。要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可:
//pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf)(const string &, const string &); //未初始化
从我们声明的名字开始观察,pf前面有个*,因此pf是指针;右侧是形参列表,表示pf指向的是函数;再观察左侧,发现函数的返回类型是布尔值。因此,pf 就是一个指向函数的指针,其中该函数的参数是两个const string 的引用,返回值是bool类型。
pf两端的括号必不可少。如果不写这对括号,则pf是一个返回值为bool指针的函数:
//声明一个名为pf的函数,该函教返回bool
bool *pf (const string &, const string &);