6.1函数基础
一个典型的函数定义包括以下部分:返回类型、函数名、由0个或多个形参组成的列表以及函数体。形参以逗号分隔,形参的列表位于圆括号内。通过调用运算符()来执行函数。函数的调用完成两项工作一是实参初始化函数对应的形参,二是将控制权转义给被调用的函数。主调函数暂时中断,被掉函数开始执行。
形参和实参
实参是形参的初始值。没有规定实参的求值顺序,编译器能以任何可行的顺序对实参求值。
实参的类型必须与形参的类型匹配,函数有几个形参,必须提供相同数量的实参。
函数的形参列表
函数的形参列表可以为空,但是不能省略。可以为空也可以用void表示。
void f1()
void f2(void)
形参列表用逗号隔开,每个形参都含有一个声明符的声明.即使两个形参类型一样,也必须把类型写出来。任意两个形参不能同名。形参名可选,形参不被使用,也必须提供实参。
int f3(int v1,v2);//错误
int f4(int v1,int v2);//正确
函数的返回类型
函数的返回类型不能是数组或函数类型,但可以是指向数组或函数的指针。
6.1.1 局部对象
名字有作用域,对象有生命周期。名字的作用域是程序文本的一部分,名字在其中可见。对象的生命周期是程序执行过程中该对象存在的一段时间。
函数体是个语句块,形参和函数体内部的变量统称为局部变量,仅在函数的作用域内可见,同事局部变量还会隐藏在外层作用域中的同名的其他声明。
自动对象
把只存在于块执行期间的对象称为自动对象,当执行结束后,块中创建的自动对象就变成未定义的。
局部静态对象
有时候需要将局部变量的生命周期贯穿于函数调用及之后的时间,可以将局部变量定义为static类型从而获得这样的对象。局部静态对象在程序执行路径第一次经过定义语句时初始化,直到程序终止被销毁。
6.1.2函数声明
函数的名字必须在使用之前声明。函数只能定义一次,但是可以声明多次。函数的声明无需函数体,用一个分号替代即可。函数的声明没有函数体,也就无需形参的名字,写上还是有好处的方便理解。函数的声明也称为函数的原型。
在头文件中进行函数声明
函数也应该在头文件中声明在源文件中定义。定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。
6.1.3分离式编译
分离式编译允许我们把程序分割到几个文件中,每个文件独立编译。如果修改了其中一个源文件,只需要重新编译那个改动的文件。
6.2参数传递
形参初始化的机制和变量初始化一样。当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。当实参的值被拷贝给形参时,形参和实参是两个独立的对象,我们说这样的实参是值传递或者函数被传值调用。
6.2.1 传值参数
指针形参。当执行指针拷贝操作时,拷贝的是指针的值。两个指针是不同的指针。通过指针可以修改它所指向的对象。
6.2.2 传引用参数
通过使用引用形参,允许函数改变一个或多个实参的值。
void reset(int &i)
{
i = 0;
}
使用引用避免拷贝
拷贝大的类类型对象或容器对象效率较低,需要用引用形参访问。当函数无需修改引用形参的值时,最火爆使用常量引用。
使用引用形参返回额外信息
可以给函数传入一个额外的引用实参,令其返回值。
6.2.3 const形参和实参
用实参初始化形参时会忽略掉顶层const.换句话说形参顶层的const被忽略掉了。当形参有顶层const对象时,给他传递常量对象和非常量对象都可以。
void fcn(const int i );
void fcn(int i);
因为顶层const可以被忽略掉,上面的代码实际上是一个函数。
指针或引用形参与const
形参的初始化方式和变量的初始化方式一样。可以用非常量初始化一个底层const对象,反过来不行。函数的形参是常量引用,允许用字面值初始化。尽量使用常量引用
6.2.4数组形参
数组的两个性质(不允许拷贝和将其转化为指针),让在定义和使用数组的的函数上有影响。
尽管不能以传值的方式传递数组,但是可以把形参写成类似数组的形式。
void print( const int*);
void print(const int[]);
void print(const int[10]);//虽然有10表示维度,但不一定
上面三个函数等价。
因为数组以指针的形式传递给函数,函数并不知道数组大小,应该提供一些额外的信息。
1.使用标记指定数组长度
类似于C风格字符串。
2.使用标准库规范
传递指向数组首、尾后元素的指针。
3.显式传递一个表示数组大小的形参。
数组形参和const
当函数不需要对象数组元素执行写操作时,数组形参应该指向const指针。
数组引用形参
C++允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。
void print(int (&arr)[10])
{
}
数组的大小构成数组的一部分,只要不超过维度,函数体内就可以放心的使用数组。
int i = 0, j[2]={0,1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
print(&i); //错误实参不是含有10个整数的数组
print(j); //错误实参不是含有10个整形的数组
print(k);//正确含有10个元素
多维数组
void print(int (*matrix)[10],int rowsize)
首元素本身是一个数组,指针就是一个指向数组的指针。
6.2.5main处理命令行选项
int main(int argc, char * argv[])
第一个表示表示数组中的字符串数量。第二个是数字,他的元素指向C风格字符串的指针。argv第一个元素指向程序的名字或第一个空字符串,接下来元素一次传递命令行提供的实参。最后一个指针之后的元素保证为0
6.2.6含有可变形参的函数
C++ 11提供了两种方法,一是所有实参类型相同,可以传一个initializer_list标准库类型,如果类型不同可以用可变参数模板。还有一种特殊的形参(省略号)这种一般只与C函数交互的接口程序。
initializer_list形参
initializer_list定义在同名的文件中。
initializer_list<T> lst;
initializer_list<T> lst2{a,b,c};
lst2(lst)
lst2= lst
lst.size()
lst.begin()
lst.end()
initializer_list中的对象永远是常量值。无法更改initializer_list中的元素值。
void error_msg(initializer_liset<string> il)
{
for(auto beg=il.begin();beg != il.end(); ++ beg)
cout<<*beg<<" ";
}
想向initializer_list形参中传递一个值的序列,需要放到一对花括号内
error_msg({"aaa","bbb","cccc"});
6.3返回类型和return语句
6.3.1 无返回值函数
没有返回值的return语句只能用在返回类型为void的函数中。返回void不要求非得有return语句,函数的最后一句会隐藏的执行return.强行令void函数返回其他类型的表达式将产生编译错误。
6.3.2有返回值的函数
只要函数类型不是void,函数每条语句必须返回一个值。return语句的返回值类型必须与函数的返回类型相同,或者能隐式的转化为函数的返回类型。
返回一个值的方式和初始化一个变量或形参的方式完全一样。
不要返回局部对象的引用或指针
函数完成后,局部变量的引用或指针将不存在。
列表初始化返回值
C++11规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表页用来对表示函数返回的临时量进行初始化。
vector<string> procces(){
return {"function","okey"};
}
main的返回值
如果main函数的结尾处没有return语句,编译器将隐式的插入一条返回0的return.
cstdlib定义了两个预处理变量。EXIT_FAILURE、EXIT_SUCCESS。同时 main函数不能调用main函数。
6.3.3返回数组指针
数组不能拷贝,函数不能返回数组,但是可以返回数组的支付或引用。
方法是用类别名
typedef int arrt[10];
using arrt = int[10];
arrt* func(int i);
声明一个返回数组指针的函数
要想在声明func时不使用类型别名,数组的维度应跟随在要定义的数组名之后。
int arr[10]; //arr是一个含有10个整数的数组
int *p1[10];
int (*p2)[10] = &arr; //p2是一个指针,指向含有10这个整数的数组
Type(* function(parameter_list))[dimentsion];
int (*func(int i))[10];
使用尾指针返回类型
任何函数都可以用尾指针返回类型,但对复杂的函数有效。
auto func(int i) ->int(*)[10];
使用decltype
如果知道函数返回指针指向哪个数组,可以用decltype关键字声明
int odd[]={1,3,5,7,9};
int even[]={2,4,6,8,10};
decltype(odd) * arrPtr(int i)
6.4函数重载
如果同一作用域几个函数名字相同但是形参列表不同,我们称之为重载函数。
main函数不能重载
定义重载函数
不允许两个函数除了返回类型外其他所有的要素都相同,如果有两个函数,形参列表一样,但返回类型不同,则第二个函数的声明是错误的。
重载和const形参
顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另外一个没有顶层const的形参区分开来。另外一方面如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数的重载。此时的const是底层的。
Record lookup(Account&);
Record lookup(const Account&);
Record lookup(Account *);
Record lookup(const Account*);
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)
{
auto &r = shorterString(const_cast<const string &>(s1),const_cast<const string&>(s2));
return const_cast<string&>(r);
}
调用重载的函数
函数匹配是指一个过程,在这个过程中我们把函数调用与一组重载函数中的每个关联起来,函数匹配也叫做重载确定。编译器首先将调用的实参与重载集合中每个函数的形参进行比较,然后根据比较结果决定到底哪个函数。
6.4.1重载和作用域
在内存作用域中声明的名字,将隐藏外层作用域中声明的同名实体,在不同作用域中无法重载函数名。
string read();
void print(const string&);
void print(double);//重载
void fooBar(int ival)
{
bool read = false; //隐藏了外部的read
string s = read(); //报错,read 是bool
void print(int); // 隐藏了之前所有的print
print("Value:"); //错误,之前的声明被隐藏
print(ival);//调用当前的print(int)
print(3.14);//调用当前的print(int);之前的print(double)被隐藏
}
6.5特殊用途语言特性
6.5.1默认实参
在函数的很多次调用中他们都被赋予了一个相同的值,这个反复出现的值为默认实参。调用时候,可以包含实参,也可以忽略。一旦某个形参被赋予了默认值,它后面所有的形参都必须有默认值。
使用默认实参调用函数
默认实参声明
习惯一个函数声明一次,多次声明同一个函数也是合法的。但是在给定的作用域中,一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
string screen(sz,sz,char='');
不能修改一个已经存在的默认值
string screen(sz,sz,char = '*')//错误
但是可以添加如下默认实参
string screen(sz =24,sz = 80,char);//正确
通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
默认实参初始值
局部变量不能作为默认实参,除此之外只要表达式的类型能转化为形参的类型即可。
sz wd =80;
char def = '';
sz ht();
string screen(sz = ht(),sz = wd,char = def);
string window = screen();//调用screen(ht(),80,'');
void f2(){
def = '*';
sz wd = 100; //此wd和前面的wd无关
window = screen();//继续调用screen(ht(),80,'*');
}
6.5.2内联函数和constexpr函数
内联函数可避免函数调用的开销
将函数指定为内联函数,通常就是将它在每个调用点上“内联的”展开。从而消除运行开销。
在函数的返回类型前面加上关键字inline,就可以声明为内联函数。
内联函数常用于优化规模小,流程直接、频繁调用的函数。
constexpr函数
constexpr是指能用于常量表达式的函数。函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。
constexpr int new_sz(){return 42;}
constexpr int foo = new_sz();//正确
编译器把constexpr函数调用替换为其结果值。constexpr函数被隐式的指定为内联函数。
constexpr size_t scale(size_t cnt) { return new_sz()*cnt;};
当scale的实参是常量表达式,它的返回值也是常量表达式,反之不然。
int arr[scale(2)]; //正确,scale(2)是常量表达式
int i=2;
int a2[scale(i)]; //错误:scale(i)不是常量表达式
把内联函数和constexpr函数放在头文件内
内联函数和constexpr可以在程序中多次定义,内联函数和constexpr函数定义在头文件中。
6.5.3 调试帮助
assert预处理宏
assert(expr);
首先对expr求值,如果为假,assert输出信息并终止执行,如果为真,assert什么都不做。
assert宏定义在cassert中。不需要为assert提供using声明。含有cassert头文件不能再定义名为assert的变量、函数和其他。
NDEBUG预处理变量
assert行为依赖于一个名为NDEBUG的预处理变量状态。如果定义了NDEBUG则assert什么都不做,默认没有定义NDEBUG.
__FILE__存放文件名的字符串字面值
__LINE__存放当前行号的整型字面值
__TIME__存放文件编译时间的字符串字面值
__DATE__存放文件编译日期的字符串字面值
6.6函数匹配
确定候选函数和可行函数
函数匹配的第一步是选定本次调用对应重载函数集,集合中的函数称为候选函数。一个是与被调用的函数同名,一个是声明在调用点可见。
第二步是考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数。这些新选出的函数称为可行函数。两个特征:一个是形参数量与本次调用的实参数量相同;二是每个实参的类型与对应形参类型相同或者能够转化。
含有多个形参的函数匹配
选择可行函数的方法和只有一个实参时一样,每个函数实参的匹配都不劣于其他可行函数需要的匹配,至少有一个实参的匹配优于其他可行函数提供的匹配。
6.6.1 实参类型转换
为了最佳匹配,编译器将实参类型到形参类型转化分为几个阶段。
1.精准匹配
1.1 实参和形参类型相同。
1.2 实参从数组或函数类型转化为指针类型
1.3向实参添加顶层const或者从实参中删除掉顶层const
2.通过const转化实现的匹配
3.通过类型提升实现的匹配
4.通过算数类型转换或指针转换实现的匹配。
5.通过类类型实现的匹配。
需要类型提升和算数类型转换的匹配
void ff(int);
void ff(short);
ff('a');
一个接受int ,一个接受short,则只有当调用提供short值时,才会选了short
所有算数类型转换的级别都一样。例如从int向unsigned int转换并不比int 向double转换级别高。
void manip(long);
void manip(float);
manip(3.14); //错误:二义性调用
函数匹配和const实参
Record lookup(Account &);
Record lookup(const Account&);
const Account a;
Account b;
lookup(a); //调用lookup(const Account&)
lookup(b);//调用lookup(Account)
6.7函数指针
函数指针指向的是函数而非对象,函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
bool lengthcompare(const string &,const string &)
要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可。
bool (*pf)(const string&,const string&);
使用函数指针
当把函数名作为一个值使用时,函数自动转化为指针。
pf = lengthCompare;
pf = &lengthCompare; //等价的赋值函数。取地址符可选
可以直接使用指向函数的指针调用该函数,无需提前解引用。
bool b1 = pf("aa","bb");
bool b2 = (*pf)("aa","bb");//等价的调用
重载函数的指针
当使用重载函数时,必须清晰的界定到底应该选哪个函数。
void ff(int*);
void ff(unsigend int);
void(*pf1)(unsigend int) = ff; //pf1指向ff(unsigned)
指针类型必须与重载函数中的某个精准匹配
void(*pf2)(int) = ff;//错误,形参列表不匹配
double(*pf3)(int *) = ff //错误
函数指针形参
形参可以是指向函数的指针。
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 &));
类型别名能简化函数指针的代码。
typedef bool Func(const string&, const string &);
typedef decltype(lengthCompare) Func2; //等价类型
typedef bool(*FuncP)(const string &,const string &);
typedef decltype(lengthCompare) * FuncP2;
func 和func2是函数类型,FuncP和FuncP2是指针类型,
所以void useBigger(const string&,const string&,Func);
void useBigger(const string &,const string&,FuncP2);
返回指向函数的指针
能够返回指向函数的指针,用类型别名
using F = int(int*,int); //F是函数类型
useing PF=int(*)(int*,int); //PF是指针类型
必须显示的将返回类型指定为指针。
PF f1(int);
F * f1(int);
auto f1(int)->int(*)(int*,int);
将auto和decltype用于函数指针类型
如果明确知道函数是哪个,就能用decltype简化书写函数指针返回类型的过程
size_type sumLength(const string&,const string&);
size_type largerLength(const string&,const string&);
decltype(sumLength) * getFcn(const string&);
decltype作用于函数时,返回函数类型而非指针。需要显式的加上*.