函数是什么,如果从数学的角度理解,我们首先应该对函数有一个泛化的定义。首先来看两个函数的例子
int fact(int val)
{
int ret =1 ;
while(val > 1)
ret *=val--;
return ret;
}
int main()
{
int j = fact(5);
cout <<"5! is"<<j <<endl;
return 0;
}
先看第一个函数,函数的名字是fact,它作用于一个整型的参数,在函数内部也定义了一个ret变量,相互之间做了一些运算,得到的结果是个整数然后返回了。
main函数也是如此,但是main函数调用了fact函数。给fact函数传递了一个实参。main函数是程序的入口,所以叫主调函数,而fact叫被调函数。
观察发现,函数只不过是个结构,是个舞台,而在这个舞台占据位置的只不过是之前的变量而已,如参数, 返回值,这些在某些程度上都微妙的决定了函数的行为。
我们抽象以下函数的范式:
返回类型 函数名(形参列表)
{
语句快;
返回值;
}
函数调用有以下范式
变量 = 函数名(实参);
上面解释下:实参是形参的初始值,实参列表与形参列表应一一对应,实参到形参必须一致或者能够转化过去。 返回类型不能是数组类型或函数类型,但可以是指向数组类型或函数类型的指针。
我们本章要回答这么几个问题: 函数如何定义? 函数的名字,返回值,参数列表类型是如何影响函数的行为的? 函数的调用过程是怎样的,如何匹配到我们想用的函数的呢。函数内部的对象又有什么性质。
6.1 函数基础
函数也可以看作是一个“对象”,函数有声明,有定义,函数可以声明多次,只能定义一次,其实基本声明和定义就是这样一个关系,声明可以使得函数一次定义多地方使用。
函数的声明并不需要结构体,也无须形参的名字,写上也不错的。 那么剩下的函数的返回类型 ,函数名,形参类型三要素描述了函数的接口。函数的声明也叫函数原型
一般在头文件中进行声明。编译器负责验证函数的声明和定义是否匹配。
函数的声明有什么用呢,稍微扯远点,fact函数声明可以放到头文件中fact.h,而具体的实现在fact.cpp中,假设存放main函数的factMain.cpp这个文件调用了fact函数。我们可以分开编译 fact.cpp,factMain.cpp,然后链接起来,但是牵扯到模板就不行了,模板一般都写到头文件中。具体可以参考here
函数中的对象扮演很重要的角色,在c++中,名字有作用于,对象右生命周期
啥意思,作用于是指某个名字可见的一个范围。对象的声明周期是指程序运行过程中对象存在的一段生命的周期。
局部对象
函数体是一个语句快,一个语句块构成一个作用域。形参和函数体内部的变量通称为局部变量,他们對函数而言是局部的。函数体内部变量会隐藏外部作用域内的同名变量。
在所有函数题之外的对象存在于整个运行周期,知道程序结束才会消亡。局部变量的生命周期依赖于定义的方式。
自动对象
我们把只存在于块执行期间的对象称为自动对象。
局部静态对象
有些时候,有必要令局部对象的声明周期贯穿整个函数调用之后的时间,可以将局部变量定义成static变量,局部静态对象在程序执行第一次经过该对象时候进行初始化,知道程序终止才被销毁,此期间即使程序执行结束也不会有影响。
6.2 参数传递
形参的初始化机理与变量初始化一样。那么问题来了,变量初始化过程的一些列问题都会在这里出现。 实参是什么类型, 形参是什么类型。实参传递给形参会发生什么?好了,脑子里所有的变量类型都应该翻个遍,内置类型,类类型,复合类型诸如引用类型指针类型,再牵扯以下const。不过如此。
参数传递我们分为两种: 传值和传引用。
当实参被拷贝给形参的时候,形参和实参是两个相互独立的对象,这就是传值调用;独立的意思是,一个对象改变对另一个对象没有丝毫的影响。
需要注意的一点,指针类型的形参也是传值调用,你不管如何改变指针指向谁,实参指向的内存不变。建议,在函数内部去改变外部内存时候最好用引用,安全。
如果形参是引用类型时候,我们说它对应的实参被引用传递,也就是形参是实参一个别名,外号,英文名字,。。,但指代还是原对象。
所以阿,传递引用两个好处,第一个好处是避免拷贝。比如你需要用一个函数比较两个字符串,传递给函数如果拷贝两个字符串,那么开销很大,算了太明显的道理了,如果你不想改变字符串又不想拷贝,传一个常量引用。另一个好处可以返回额外信息,不多说。
别忘了还有个const限定符,也是参数类型的修饰。当const遇到形参和实参。
const 有顶层和底层之分。形参和实参都可能被const修饰,排列组合就有9种组合方式
两者都没有修饰的不在本个话题之内,前面讨论的内容就可以解决。两者都有了参数一致没什么好说的。底层const赋值给顶层const或者反过来,这种情况也太奇怪了吧。也不会出现,也不讨论,去掉了5种,还有四种要讨论。
先来看顶层const,1)顶层const的实参传给实参会忽略顶层const。仔细一想,这不就是一个拷贝么,原来对象不让改,但副本我可以改吧。合情合理。2)实参传递给一个顶层const的形参的时候,实参是不是顶层的conts是无关紧要的。也好理解,就说你能耐很大,上天入地无所不能,突然想让你收敛一些当然可以做到了。但你让一个傻蛋上天入地就有点困难了,就是这么个道理。这就意味者这两个参数不能区分函数的类型。
底层的const是个什么意思呢,就是一个自以为是的家伙,它自认为不能改变自己指向的那个值。 这就意味着可以用一个非常量初始化一个底层的const的对象,人家让你改,你也可以不该。但是反过来就不行,人家就不让你改,你偏要改,这就完蛋了,是不允许的。 底层const传递的是对某个固定对象的权限。
如果某个参数在函数的执行过程中值不会被改变,那么尽量声明成常量的引用。比如下面的不良设计:
string::size_type find_char(string &s , char c ,
string::size_type & occurs );
如果这样调用:
find_char(“Hello world” , ‘o’ , ctr);
在编译时会发生错误。是因为常量的底层const是不能初始化非常两引用的。同样下面的也会发生错误:
bool is_sentence(const string &s )
{
string::size_type ctr =0;
return find_char(s, ‘.’,ctr)==s.size()-1
}
不要忘了还有一种比较特殊的类型也可以作为形参的,那就是数组:
在这里我们强调数组的两个特性:不能拷贝数组,通常遇到数组时候会转换成指针。
。当我们为函数传递一个数组时,实际是传递指向数组元素的指针。
虽然不能传数组阿,我们仍然可以把参数写成类似数组的东西。比如:
//以下的三个函数是等价的。
void print(const int *);
void print(const int []);//函数的意图是传递一个数组
void print(const int[10])//纬度表示我们期望传递的数组元素个数,不一定的。
编译器在检查这几个参数的时候都是检查参数是否是const int *类型的。 这句话可是primer说的:如果我们给print传递的是一个数组,则实参自动转换成指向数组首元素的指针。数组的大小对函数的调用没有影响。函数执行保证数组不越界是程序员的责任。
那最起码你应该告诉我数组的尺寸吧,艾,对了,你可以传递一些额外的信息给函数,以参数的形式,或者你可以在数组中加个标记,吉祥字符型数组表示字符串那样。或者你直接遵循标准库的规范,把初始指针和最后一个元素下一个位置的指针告诉它。
函数定义提到const的指针了,const指针和引用一样,归于底层const的范畴。
c++允许将变量定义为数组的引用,基于同样的道理,形参也可以是数组的引用。。此时,引用形参绑定到对应的实参上,也就是绑定到数组上。
这样 print(int (&arr)[10])限制了print函数可用性,只能打印10个元素的数组。
传递多维数组:
我们可以定义指向数组的指针。 print(int (*matrix)[10] , int rowsize);或者等价定义的形式是print(int matrix[][10], int rowSize);看似这是一个二维数组的指针,实际是指向10个元素数组的指针。
我们说了很所的函数的参数类型,main函数有没有参数呢,有的
int main(int argc, char **argv){
}
如果编译生成了一个叫prog的可执行文件,那么 argv[0]=”prog”;argv[1]=”-d”;argv[2]=”-o”;
另一方面参数的个数是不是可以变得呢,答案是肯定的。如果所有的实参类型相同,可以传递一个名initializer_list 的标准库类型。如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板,关于细节后续介绍。
c++还有一种特殊的参数类型,省略符形参,一般只用于与c函数交互的接口程序。
对于initializer_list形参列表类似vector.
省略符形参只出现在形参列表的最后一个位置。
6.3 返回类型和return语句
return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。
一个函数可以有返回值,也可以没有返回值。特别强调,对有返回值的函数,return的返回值类型必须与函数的返回类型一致,或者可以隐式的转换成函数的返回类型。
那么值是如何被返回的呢。
返回值的方式和初始化一个变量的或形参的方式完全一样,返回值用于初始化调用点的一个临时量。该临时量就是调用的结果。函数可以返回引用,该引用仅仅是所引对象的一个别名。
但是记住了,不要返回局部对象的引用或指针,因为它所占的存储空间在函数完成后会被释放掉。这就说明函数结束后,指针不再指向邮箱的内存区域。
函数一旦返回了指针/引用/类的对象,我们能够使用函数的调用结果访问结果对象的成员。因为调用运算符优先级和点和箭头运算符的优先级一致并且满足左结合律。
调用一个返回引用的函数得到左值,可以放到赋值号的左边被赋值,其他返回类型返回右值。
此外列表也用来表示函数返回的临时量进行初始化。
main函数返回值是啥呢。0表示执行成功,其他表示失败,非0值由机器确定具体的含义。
递归不展开说。
还是说到一个特殊的返回类型那就是数组。函数可以返回数组的指针或引用. 那么如何声明一个返回数组指针的函数的呢。
要想在声明func时不使用类型别名,我们必须牢记被定义的名字后面数组的维度,同样的,如果我们想定义一个返回数组指针的函数,则数组的纬度必须跟在函数的名字之后。函数的形参列表也跟在函数名字后面,且参数列表应该先于数组的维度。因此返回数组指针的函数形式如下所示。
Type (*function(parameter_list))[dimension]
举个例子下面的这个声明没有使用类型别名。
int (*func(int i))[10];
可以利用下面的顺序来逐曾理解该声明的含义。mb++;
其实还有一种方式是使用别名:
typedef int arrT[10];
using arrT = int[10];
arrT * func(int i);
使用尾置返回类型
auto func(int i)->int(*)[10];
使用decltype
如果我们知道函数返回值类型指向哪个数组,就可以使用decltype关键字指名返回类型。
例如
int odd[] ={1 ,3,5,7,9};
int even[] ={0,2,4,6,8};
decltype(odd) *arrPtr(int i)
{
return (i%2)?&odd:&even;
}
decltype并不负责将数组类型转换成对应的指针。所以deltype是个数组。还必须要加一个*号。
6.4 函数重载
什么是函数重载:在同一作用域内的几个函数的名字相同但是列表不同。我们称之为重载函数,函数的名字让编译器知道它调用的是哪一个函数,而函数重载可以在一定程度上减轻程序员记名字,起名字的负担。
对于重载函数来说,他们应该在形参数量或形参类型上有所不同。返回类型病不能区分函数的类型。
那么如何正确的判断两个形参类型是否一样,有的时候两个形参列表看起来不一样,但实际上是相同的。就像下面的几个。
Record lookup(const Account &acct);
Record lookup(const Account&);
typedef Phone telno;
Record lookup(const Phone&);
Record lookup(const Telno);
我们说形参的类型和个数可以区分函数,参数的类型如果是const会发生什么情况呢。
我们说一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。另一方面如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数的重载。此时的const是底层的。当我们传递一个非常量的对象指针时候,编译器会优先选择使用非常量版本的函数。
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);
}
调用重载的函数
函数匹配也叫重载确定
编译器首先将调用的实参与重载的集合中每一函数的实参进行比较。然后根据比较的结果决定到底应该调用哪一个函数。现在我们需要掌握的是调用重载函数的三种结果。
编译器找到一个与实参最佳匹配的函数并生成代用该函数的代码。
找不到任何一个函数与调用的函数实参匹配,此时编译器发出无匹配的错误。
有多个函数可以匹配,但是每一个都不是明显的最佳选择。此时将发生错误。成为二义性调用。
重载与作用域的关系。
这一点一定要注意了。如果我们的内层作用域中声明的名字,它将隐藏外层作用域中的同名实体。在不同的作用域中无法重载函数名。primer 6.4.1一定要看一下,这一节很好的说明了隐藏是怎么回事,对于函数的隐藏只需要名字与外层作用域相同就可以了。在寻找匹配的函数的时候,只要内层有名字相同的函数旧验证是否匹配,如果不匹配就会报错,而不会到外层作用域继续寻找。
6.5 特殊用途语言特性
默认实参
有什么功能你该知道的。
对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只能声明一次,但是多次声明同一个函数也是合法的。不过有一点要注意,在给定的作用域中一个形参只能被赋予一次默认实参,换句话说,函数的后续声明只能为之前的那些没有默认值的形参添加默认实参,而且形参右侧所有实参都必须默认值。
默认实参的初始值
局部变量不能作为默认实参,除此之外,只要表达式的类型转换成形参所需的类型,该表达式就能作为默认实参。
内敛函数和constexpr 函数
调用函数一般比求等价表达式的值要慢一些。在大多数及其上,一次函数调用设计到一些列的工作,调用前要先保存寄存器。并在返回时回复;可能要拷贝实参。程序转向一个新的位置继续执行。
内敛函数可以避免函数调用的开销。
将函数指定为内敛函数的就是将它在每个调用点上内联的展开。 内敛只是向编译器发出请求,编译器可以完全忽略这个请求。
constexpr函数
constexpr函数是指能够用于常量表达式的函数。定义constexpr函数的方法与其他的函数类似,不过要遵循几项约定,函数的返回类型及所有形参的类型都是字面值类型。而且函数体中必须有且只有一条retrun语句。
例如:
constexpr int new_sz(){return 42;}就可以用这个函数初始化constexpr类型的变量
编译器把constexpr函数的调用替换成结果值,为了能在编译过程中随时展开。它被隐式的指定为内联函数。但是我们允许这种函数返回一个非常量的表达式。例如constexpr size_t scale(size_t cnt){return new_sz*cnt;}
当调用的参数不是一个常量的时候,返回的类型也不是一个常量。
强调一点,内联函数和constexpr函数可以在程序中多次定义。编译器想要展开函数,仅有函数声明是不够的。对于给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。内敛函数和constexpr函数通常定义在头文
件中。
调试帮助
assert是一种预处理宏。预处理宏由预处理器负责而不是编译器。assert的行为依赖于一个NDEBUG的变量。如果定义了NDEBUG,assert什么都不做。
还有几个变量我们可以注意:
func 当前调试函数的名字。
FIlE存放文件名的字符串字面值
LINE存放当前行号的整形字面值
DATE存放文件编译日期的字符串字面值
6.6 函数匹配
当某些函数的参数数量相等并且可以由其他类型的转换得来时,这项工作就没那么容易了。
1.首先选出候选函数,候选函数具备两个特征,一是与被调函数同名,另一个是声明在调用点可见,对于隐藏的就不可见,这点前边提过。
2.从候选中选出能被这组实参调用的函数,这些选出的函数叫可行函数。形参数量与本次调用提供的形参数量相等。二是每个实参的类型与对应的形参类型相同。或者能转换成形参的类型。
3.其次寻找最佳匹配,实参类型与形参类型越接近,他们匹配的越好。
4.如果含有多个参数,有且仅有一个函数满足下列条件,那么匹配成功。
该函数每个实参的匹配都步劣于其他可行函数需要的匹配
至少有一个实参匹配由于其他可行函数提供的匹配。
如果所有的函数没哟满足这两点的,编译器抛出错误。
但是函数设计时候尽量减少强制类型转换。
实参类型转换
编译器将实参类型到形参类型的转换分成几个等级,具体排序如下所示。
1.精确匹配
实参类型和形参类型相同
实参从数组类型或函数类型转换成指针类型。
向实参添加顶层const或者从实参中删除顶层const
2.通过const转换实现的匹配(非常量转化为常量)
3.通过提升实现的匹配
4.通过算术类型转换或者指针转换(数组指针,void *)
5.通过类类型转换实现的匹配。
如果重载函数的区别在于他们引用的类型是否是常量或者指针类型的形参是否指向const。当调用发生时编译器通过是否是常量来决定选择哪个函数。
6.7 函数指针
我们如何声明一个函数的指针呢,看个例子
bool (*pf)(const string & , const string &);
pf的括号必不可少
那么如何来使用函数指针:
我们把函数名作为一个值使用时,该函数自动转换位指针,比如存在一个compare的函数:
pf= compare;
指向不同类型的指针之间不存在转换规则。但是指针可以被赋予一个nullptr或者0的整形常量表达式。表示该指针没有指向任何一个函数。
问题来了,重载函数的指针,编译器也通过指针类型选用哪个函数。指针类型必须能够与重载函数中的某一个精确匹配。
还有问题,函数指针是不是也可以作为形参使用呢。
和数组类似,形参也可以是指向函数的指针。我们也可以把函数直接当成参数使用,只不过会自动转换成指向函数的指针。
decltype这个家伙又来了,说,我可以推断出函数的类型,但是是函数类型哦,要得到指针还要加个*。
那么都可以作为参数了,是不是可以作为返回值呢。当然可以。这时候返回的依然是指向函数的指针。
要想声明一个返回函数指针的函数,最简单的方法就是类型别名。
using F = int(int * , int);//F是函数
uisng PF= int ()(int ,int);//PF是指针
当然可以直接声明:
int (f1(int))(int ,int);
等价于 auto f1(int)->int()(int , int);
auto和decltype可以作用于函数类型。
最后,提醒自己,这些应该成为愉快顺手的技巧,在合适的地方选用他们,而不应该为了炫技。