C++primer笔记——第六章【函数】

【第六章】 函数


6.1 函数基础
1、一个典型的函数定义包括:返回类型,函数名,由0个或多个形参组成的列表,函数体
其中,形参以逗号隔开,形参列表位于一对圆括号内。函数执行的操作在语句块中说明,该语句块称为函数体。

2、通过 调用运算符 来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;
圆括号之内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。

3、T fun(TYPE val){...}
函数的名字是fun,它作用于一个TYPE类型的参数,返回一个T类型的值。

调用函数:
4、函数的调用完成两个工作:
1)用实参初始化函数对应的形参
2)将控制权转移给被调用函数。此时,主调函数的执行被暂时中断,被调函数开始执行。
执行函数的第一步是隐式地定义并初始化它的形参。因此,调用函数fun时,首先创建一个名为val的TYPE型变量,然后将它初始化为调用时所用的形参

5、当遇到一条return语句时函数结束执行过程。和函数调用一样,return语句也完成两项工作:
1)返回return语句中的值(如果有的话)
2)将控制权从被调函数转移回主调函数。函数的返回值用于初始化调用表达式的结果,之后继续完成调用所在表达式的剩余部分

形参和实参:
6、实参是形参的初始值。

7、实参的类型必须与对应的形参类型匹配!!

函数的形参列表:
8、函数的形参列表可以为空,但是不能省略。要想定义一个不带形参的函数,最常用的办法是书写一个空的形参列表。也可以使用关键字void表示函数没有形参。
void f1(){...} // 隐式地定义空形参列表
void f2(void){...} // 显式地定义空形参列表

9、形参列表中的形参用逗号隔开,其中每个形参都是含有一个声明符的声明。即使两个形参的类型一样,也必须把两个类型都写出来。

10、任意两个形参不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。

11、形参名是可选的。但是由于我们无法使用未命名的形参,所以形参一般都应该有个名字。

函数返回类型:
12、大多数类型都能用作函数的返回类型。特殊地,返回类型void,表示函数不返回任何值。

13、函数的返回类型不能是 数组类型 或 函数类型,但可以是指向数组或指向函数的指针

// 练习6.1 形参和实参的区别是什么?
【解析】
1)形参出现在 函数定义 的地方,形参列表可以包含0个或多个形参,以逗号分隔。形参规定了一个函数所接受数据的 类型 和 数量。
2)实参出现在 函数调用 的地方,实参数量与形参一致。实参的作用主要是初始化形参,并且这种初始化过程是一一对应的。实参类型必须与形参类型匹配。

6.1.1 局部对象
1、C++中,名字有作用域,对象有生命周期。
1)名字的作用域是程序文本的一部分,名字在其中可见。
2)对象的生命周期是程序执行过程中该对象存在的一段时间。

2、函数体是一个语句块。块构成一个新的作用域,可以在其中定义变量。
形参和函数体内部定义的变量统称为 局部变量。它们对函数而言是局部的,仅在函数的作用域内可见,同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。

3、在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束时才销毁。局部变量的生命周期依赖于定义的方式。


自动对象:
4、对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。
我们把只存在于块执行期间的对象称为自动对象。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。

5、形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。

局部静态对象:
6、有时有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。
局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并指导程序终止时才销毁,在此期间即使对象所在的函数结束执行也对它没有影响。
size_t count_calls()
{
static size_t ctr = 0;
return ++ctr;
}
当控制流第一次经过ctr的定义之前,ctr被创建并初始化为0.每次调用将ctr加1并返回新值。每次执行count_calls函数时,变量ctr的值都已经存在并且等于函数上一次退出时ctr的值。

7、如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0.

// 练习6.6 说明形参、局部变量以及局部静态变量的区别。
【解析】
形参 和 定义在函数体内部的变量 统称为局部变量,它们对函数而言是局部的,仅在函数的作用域内可见。
函数体内的局部变量又分为 普通局部变量 和 静态局部变量,对于形参和普通变量来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁。
把只存在于块执行期间的对象称为 自动对象。这几个概念的区别是:
1)形参是一种自动对象,函数开始时就为其申请存储空间,用调用函数时提供的实参初始化形参对应的自动对象。
2)普通变量对应的自动变量是在定义该变量的语句处创建自动对象,如果定义语句时提供了初始值,则用该值初始化。否则执行默认初始化。当该变量所在的块结束后它被销毁
3)局部静态变量的生命周期贯穿函数调用及之后的时间。局部静态变量对应的对象称为局部静态对象,它的生命周期从定义语句处开始,直到程序结束为止。

6.1.2 函数声明
1、函数的声明和函数的定义非常类似,唯一的区别是函数声明无需函数体,用一个分号代替即可

2、因为函数的声明不包含函数体,所以也就无需形参的名字。

3、函数的三要素(返回类型,函数名,形参类型)描述了函数的接口,说明了调用该函数所需要的全部信息。函数声明也称作函数原型。

在头文件中进行函数声明:
4、建议变量在头文件中声明,在源文件中定义。类似地,函数也应该在头文件中声明而在源文件中定义。

5、定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。

6.1.3 分离式编译
分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

6.2 参数传递
1、每次调用函数时,都会重新创建它的形参,并用传入的实参对形参进行初始化。

2、形参的类型决定了形参和实参交互的方式。
如果形参是引用类型,它将绑定到对应的实参上。否则,将实参的值拷贝后赋给形参。

3、当形参是引用类型时,它对应的实参被 引用传递 或者函数 被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名,即对应的实参的别名。

4、当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。则说这样的实参被 值传递 或者函数被 传值调用

6.2.1 传值参数
5、当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。
也就是说,尽管函数改变了形参的值,但是这个改动不会影响传入该函数的实参。

指针形参:
6、指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。
因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。
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的地址,此时i=0


// 练习6.10 编写一个函数,使用指针形参交换两个整数的值。在代码中调用该函数。
【解析】
void MySwap(int *p, int *q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
int a = 5,b = 10;
int *r = &a, *s = &b;
调用:MySwap(&a, &b) 或MySwap(r, s)


而下面的函数无法满足这个要求:
void MySwap(int *p, int *q)
{
// 在函数体内部交换了两个形参指针本身的值,未能影响实参
int *t = p; // t是一个指针
p = q;
q = t;
}


6.2.2 传引用参数
修改一下reset函数,使其接受的参数是引用类型而非指针:
// 该函数接受一个int对象的引用,然后将对象的值置为0
void reset(int &i) // i是传给reset函数的对象的另一个名字
{
i = 0; // 改变了i所印对象的值
}
和其他引用一样,引用形参绑定初始化它的对象。当调用这个reset函数时,i绑定我们传给函数的int对象,此时改变i就是改变i所引对象的值。
int j = 42;
reset(j); // j采用传引用方式,它的值此时变为0

使用引用避免拷贝:
7、拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。

8、比如写一个函数要比较两个string对象的长度。因为string对象可能很长,所以应避免直接拷贝它们,此时应该使用 引用形参。
又因为无需改变string对象的内容,所以把形参定义成对常量的引用。
bool isShorter(const string &s1,const string &s2)
{
return s1.size()<s2.size();
}

使用引用形参返回额外信息:
9、一个函数只能返回一个值,然而有时候函数需要返回多个值,引用形参为我们一次返回多个结果提供了有效途径。而不用定义一个新的包含多个返回成员的数据类型。

// 练习6.12 使用引用而非指针交换两个整数的值,那种方法更容易使用呢
【解析】
与使用指针相比,使用引用交换变量的内容从形式上更简洁一些,并且无需声明额外的指针变量,也避免了拷贝指针的值。


// 练习6.13 比较两个函数声明的区别 void f(T), void f(&T)
【解析】
1)void f(T)的形参采用的是传值方式,实参的值被拷贝给形参,形参和实参是两个相互独立的变量,在函数f内部对形参所做的任何操作都不会影响实参的值
2)void f(&T)的形参采用传引用方式,此时形参是对应的实参的别名,形参绑定到初始化它的对象。如果改变了形参的值,就是改变了对应实参的值。


// 练习6.14 说说传引用方式的优势
【解析】
与值传递相比,引用传递的优势主要在三方面:
1)可以直接操作引用形参所引的对象
2)使用引用形参可以避免拷贝大的类类型对象或容器类型对象
3)帮助我们从函数中返回多个值


6.2.3 const形参和实参
1、顶层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)
void fcn(int i) // 错误,重复定义了fcn
C++中,允许定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。
因为顶层const被忽略了,所以上面的代码中传入两个fcn函数的参数可以完全一样。所以第二个fcn是错误的,只是形式上有差异

指针或引用形参与const
2、可以使用非常量初始化一个底层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是无符号类型

尽量使用常量引用:
3、把函数不会改变的形参定义成普通的引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。
此外,使用引用而非常量引用也会极大限制函数所能接受的实参类型。就像上面的例子,不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参!!!

6.2.4 数组形参
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响:
1)不允许拷贝数组 2)使用数组时(通常)会将其转换成指针
因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但是我们可以把形参携程类似数组的形式:
// 虽然形式不同,但是三个print函数等价。每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]); // 可以看出,函数的意图是作用于一个数组
void print(const int[10]); // 这里的维度表示我们期望数组含有多少元素,实际不一定
当编译器处理对print函数的调用时,只检查传入的参数是否是const int*类型:
int i = 0, j[2] = {0,1};
print(&i); // 正确,&i的类型是int*
print(j); // 正确,j转换成int*并指向j[0]
如果传给print函数的是一个数组,则实参自动转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。

4、因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术:
1)使用标记指定数组长度:
要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串。C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数处理C风格字符串时遇到空字符停止
void print(const char *cp)
{
if(cp) // 若cp不是一个空指针
while(*cp) // 只要指针所指的字符不是空字符
cout<< *cp++; // 输出当前字符并将指针向前移动一个位置
}

2)使用标准库规范:
传递指向数组首元素和尾后元素的指针。
void print(const int *beg, const int *end)
{
while(beg != end)
cout<< *beg++ <<endl;
}
调用时,print(begin(j), end(j));

3)显式传递一个表示数组大小的形参:
专门定义一个表示数组大小的形参。
// const int ia[]等价于const int *ia
// size表示数组的大小,将它显式的传给函数用于控制对ia元素的访问
void print(const int ia[], size_t size)
{
for(size_t i=0;i!=size;++i)
cout<< ia[i] <<endl;
}
调用时,print(j, end(j)-begin(j));

数组形参和const:
上面的三个print函数都把数组形参定义成了指向const的指针。当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。
只有当函数确实需要改变元素值的时候,才把形参定义成指向非常量的指针。

数组引用形参:
C++允许将变量定义成数组的引用。同样地,形参也可以是数组的引用。此时引用形参绑定到对应的实参上,也就是绑定到数组上。
void print(int (&arr)[10])
{
for(auto elem:arr)
cout<< elem <<endl;
}
因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心使用数组。但是,这限制了print函数的可用性,只能将函数作用于大小为10的数组:
int i = 0, j[2] = {0, 1};
int k = {1,2,3,4,5,6,7,8,9,0};
print(&i); // 错误,实参不是含有10个整数的数组
print(j); // 错误,实参不是含有10个整数的数组
print(k); // 正确

传递多维数组:
和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。
因为我们处理的是数组的数组,所以首元素本身就是一个指向数组的指针。数组第二维(及后面)的大小都是数组类型的一部分,不能省略。
// matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize){...}
该语句将matrix声明成指向含有10个整数的数组的指针
也可以使用数组的语法定义函数,
void print(int matrix[][10], int rowSize){...}
matrix的声明看起来像是一个二维数组,实际上形参是指向含有10个整数的数组的指针。

6.2.5 main:处理命令行选项
命令行选项通过两个(可选的)形参传递给main函数:
int main(int argc, char *argv[]){...}
第二个形参argv是一个数组,它的元素是指向C风格字符串的指针。第一个形参argc表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:
int main(int argc, char **argv){...}
其中argv指向char*
当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的形参。最后一个指针之后的元素值保证为0.
所以,当输入prog -d -o ofle data0的命令行后,argc等于5.
其中argv[0]="prog"; // 或者argv[0]也可以指向一个空字符串
argv[5]=0;
当使用argv中的实参时,一定要记得可选的实参从argv[1]开始,argv[0]保存程序的名字,而非用户输入。

6.2.6 含有可变形参的函数
有时我们无法预知应该向函数传递几个实参。为了编写能处理不同数量实参的函数,C++提供了两种方法:
1)如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型
2)如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板
3)还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参

initializer_list形参:
如果函数的实参数量未知但是全部实参的类型相同,可以使用initializer_list类型的形参。它是一种标准库类型,用于表示某种特定类型的值的数组。
提供的操作:
initializer_list<T> lst; // 默认初始化:T类型元素的空列表
initializer_list<T> lst{a,b,c...}; // lst的元素和初始值的一样多,lst的元素是对应初始值的副本,列表中的元素是const
lst2(lst); // 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素,拷贝后,原始列表和副本共享元素
lst2 = lst; // 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素,拷贝后,原始列表和副本共享元素
lst.size(); // 列表中的元素数量
lst.begin(); // 返回指向lst中首元素的指针
lst.end(); // 返回指向lst中尾元素下一位置的指针

和vector一样,initializer_list也是一种模板类型。定义initializer_list对象时,必须说明列表中所含元素的类型。
initializer_list<string> ls; // initializer_list中的元素类型是string

区别在于,initializer_list对象中的元素永远是常量值,无法改变initializer_list对象中元素的值。
void err_msg(initializer_list<string> il)
{
for(auto beg = il.begin(); beg != il.end(); ++beg)
cout<< *beg <<endl;
}
如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:
// a,b 都是string对象
err_msg({"fun", a, b});
err_msg({"fun", "ok"});
两次传递的参数数量不同。

省略符形参:
省略符形参只能出现在形参列表的最后一个位置,它的形式只有两种:
void foo(parm_list, ...);
void foo(...);
第一种指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查
省略符形参所对应的实参无需类型检查。在第一种形式中,形参声明后面的逗号是可选的。

// 练习6.29 在范围for循环中使用initializer_list对象时,应该将循环控制变量声明成引用类型吗?
【解析】
引用类型的主要优势是可以直接操作所引用的对象以及避免拷贝较为复杂的类类型对象和容器对象。因为initializer_list对象的元素永远都是常量值,
所以我们不能通过设定引用类型来更改循环控制变量的内容。只有当initializer_list对象的元素类型是类类型或者容器类型(比如string),才有必要把范围for循环的循环控制变量设为引用类型。
void err_msg(initializer_list<string> il)
{
for(const auto &elem: il)
cout<< elem <<endl;
}

6.3 返回类型和return语句
1、return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。有两种形式:
return; 
return expression;

6.3.1 无返回值函数
2、没有返回值的return语句只能用在返回类型是void的函数中。返回void的函数不要求非要return语句,因为这类函数最后一句后面会隐式地执行return。

3、void函数如果想在它的中间位置提前退出,可以使用return语句。

6.3.2 有返回值函数
4、只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值。return语句返回值的类型必须与函数的返回类型相同,或能隐式地转换成函数的返回类型。

值是如何被返回的:
5、某具有返回类型为string的函数,意味着返回值将被拷贝到调用点。因此,该函数将返回返回值的副本或者一个未命名的临时string对象。

6、同其他引用类型一样,如果函数返回引用,则该引用仅是它所引对象的一个别名。
const string &shorterString(const string &s1,const string &s2)
{
return s1.size() <= s2.size()? s1:s2;
}
其中形参和返回类型都是const string的引用,不管是调用函数还是返回结果都不会真正拷贝string对象。

不要返回局部对象的引用或指针:
7、函数完成后,它所占用的存储空间也随之被释放。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域:
// 错误,这个函数试图返回局部对象的引用
const string &manip()
{
string ret;
if(!ret.empty())
return ret; // 错误,返回局部对象的引用
else
return "Empty"; // 错误,该字符串时一个局部临时量
}
这两条return语句都将返回未定义的值。第一条明显返回的是局部对象的引用。
第二条return语句中,字符串字面值转换成一个局部临时string对象,对于manip来说,该对象和ret一样是局部的。
当函数结束临时对象所占用的空间也就被随之释放了,所以两条return语句都指向了不再可用的内存空间。
如前所述,返回局部对象的引用时错误的。同样,返回局部对象的指针也是错误的。一旦函数完成,局部对象被释放,指针将指向一个不存在的对象。

返回类类型的函数和调用运算符:
8、和其他运算符一样,调用运算符也有优先级和结合律。
调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律。因此,如果函数返回指针、引用或类的对象,就能使用函数调用的结果访问结果对象的成员
auto sz = shorterString(s1, s2).size();

引用返回左值:
9、调用返回一个引用的函数得到左值,其他返回类型得到右值!!
可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量的引用的函数的结果赋值,注意是非常量。
char &get_val(string &str, string::size_type ix)
{
return str[ix];
}

调用时,string s("hello");
get_val(s, 0) = 'A'; // 将s[0]的值改为A
如果返回类型是常量引用,就不能给调用的结果赋值了。

列表初始化返回值:
10、C++11规定函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。
如果列表为空,临时量执行值初始化。否则,返回的值由函数的返回类型决定。
vector<string> process()
{
if(!ret.empty())
return {}; // 返回一个空vector对象
else
return {"Empty","ok"}; // 返回列表初始化的vector对象
}

6.3.3 返回数组指针
因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。

声明一个返回数组指针的函数:
要想在声明fun时不使用类型别名,必须牢记被定义的名字后面数组的维度:
int arr[10]; // arr是一个含有10个整数的数组
int *p1[10]; // p1是一个含有10个指针的数组
int (*p2)[10]=&arr; // p2是一个指针,它指向含有10个整数的数组
11、和这些声明一样,如果想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且应该先于数组的维度。
所以,返回数组指针的函数形式如下:
Type (*function(parameter_list))[dimension]
Type表示元素的类型,dimension表示数组的大小,(*function(parameter_list))两端的括号必须存在,不然函数返回的类型将是指针的数组。
如:int (*fun(int i))[10];
1)fun(int i)表示调用fun函数需要一个int类型的实参
2)(*fun(int i))表示可以对函数调用的结果执行解引用操作
3)(*fun(int i))[10]表示解引用fun的调用得到一个大小是10的数组
4)int (*fun(int i))[10]表示数组中的元素是int类型

使用尾置返回类型:
简化上述fun声明的方法,就是使用尾置返回类型。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或数组的引用
尾置返回类型跟在形参列表后面并以->开头。为表示函数真正的返回类型跟在形参列表之后,在本该出现返回类型的地方放置一个auto
// fun接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto fun(int i) -> int (*)[10];

使用decltype:
如果我们知道函数返回的指针将指向哪一个数组,就可以使用decltype关键字声明返回类型。
例如,下面的函数返回一个指针,该指针根据参数i的不同指向两个已知数组中的某一个:
int odd[]={1,3,5,7,9};
int even[]={2,4,6,8,0};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even; // 返回一个指向数组的指针
}
arrPtr使用关键字decltype表示它的返回类型是个指针,并且该指针所指的对象与odd的类型一致。因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。
有个要注意:decltype不负责把数组类型转换成对应指针!所以decltype的结果是数组,要想表示arrPtr返回指针还必须在函数声明时加一个*

// 练习6.37
【解析】
所以返回数组的引用有以下几种方式:
1)直接返回数组的引用
string (&fun())[10];
2)使用类型别名
typedef string arr[10];
arr& fun();
3)使用尾置返回类型
auto fun() -> string(&)[10];
4)使用decltype关键字
string str[10];
decltype(str) *fun();

6.4 函数重载
1、如果同意作用域内的几个函数名字相同 但形参列表不同(形参数量和形参类型),称为重载函数。
这些函数的特点是,它们接受的形参类型不一样,但是执行的操作非常相似。当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数

2、main函数不能重载

定义重载函数:
3、不允许两个函数除了返回类型外其他所有元素都相同

重载和const形参:
4、顶层成onst不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来
// example1:
Record lookup(Phone);
Record lookup(const Phone); // 重复声明了
// example2:
Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明了
另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:
// 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
// 定义了4个独立的重载函数
Record lookup(Phone&); // 函数作用于Phone的引用
Record lookup(const Phone&); // 新函数,作用于常量引用

Record lookup(Phone*); // 新函数,作用于Phone的指针
Record lookup(const Phone*); // 新函数,作用于指向常量的指针
在上面的例子中,编译器可以通过实参是否是常量来判断应该调用哪一个函数,因为const不能转换成其他类型,所以我们只能把const对象传递给const形参。
相反地,因为非常量可以转换为const,所以上面的4个函数都能作用于非常量对象或者指向非常量的指针。只是编译器会优先选用非常量版本的函数。

const_cast和重载:
5、const_cast在重载函数的情景中最有用。比如之前的函数:
const string &shorterString(const string &s1,const string &s2)
{
return s1.size() <= s2.size()? s1:s2;
}
该函数的返回类型和参数都是const string的引用。我们可以对两个非常量的string实参调用这个函数,但返回的结果仍然是const string的引用。
我们可以对两个非常量的string实参调用这个函数,但返回的结果仍然是const string
    因此,我们需要一种新的函数,当它的实参不是常量时,得到的结果是一个普通的引用,使用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 string的引用。
这个引用事实上绑定在了某个初始的非常量实参上,因此,我们可以再将其转换回一个普通的string&

调用重载的函数:
6、在定义了一组重载函数之后,需要合理的实参调用它们。函数匹配是指一个过程,在这过程中我们把函数调用与一组重载函数的某一个关联起来,函数匹配也叫 重载确定。

7、调用重载函数时有三种可能的结果:
1)编译器找到一个与实参 最佳匹配 的函数,并生成调用该函数的代码
2)找不到任何一个函数与调用的实参匹配,此时编译器发出 无匹配 的错误信息
3)有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。这称为二义性调用。

6.5 特殊用途语言特性


6.5.1 默认实参
1、某些函数有这样一种实参,在函数的很多次调用中它们都被赋予一个相同的值,此时把这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可包含/省略该实参


2、作用是为了是函数既能接纳默认值,也能接受用户指定的值
string fun(int a = 10, int b = 8, char c = ' ');

3、默认实参作为形参的初始值出现在形参列表中,可以为一个或多个形参定义默认值。

4、一旦某个形参被赋予了初始值,它后面所有形参都必须有初始值!!

使用默认实参调用函数:
5、如果想使用默认实参,只要在调用函数的时候省略该实参就可以了。但只能省略尾部的实参。

6、设计含有默认实参的函数时,要合理设置形参的顺序,让不怎么使用默认值的形参出现在前面,经常使用默认值的形参出现在后面

默认实参声明:
7、对函数的声明来说,通常习惯是将其放在头文件中,并且一个函数只声明一次。在给定的作用域中一个形参只能被赋予一次默认实参。

默认实参初始值:
8、局部变量不能作为默认实参。初次之外,只要表达式的类型能转换成形参所需的类型,该表达式就能做默认实参

6.5.2 内联函数和constexpr函数

内联函数可避免函数调用的开销:
9、将函数指定为内联函数,通常就是将它在每个调用点上“内联地”展开。
一次函数调用包含着:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。
这样,内联函数就消除了原函数运行时开销。形式是 在原函数的返回类型前面加上关键字 inline就能将其声明成内联函数了。

10、一般地,内联机制用于规模较小,流程直接,频繁调用的函数。

constexpr函数:
11、constexpr函数是指能用于常量表达式的函数。

12、定义constexpr函数要遵循几个规定:
1)函数的返回类型及所有的实参都要是字面值类型!!!
2)函数体中必须有且只有一条return语句:
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz(); // 正确,foo是一个常量表达式
编译器能在编译时验证new_sz函数返回的是常量表达式,所以可以用new_sz函数初始化constexpr类型的变量foo
执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。

13、constexpr函数体内也能包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,可以有空语句,类型别名以及using声明。

14、把内联函数和constexpr函数放在头文件内

// 练习6.46
显然isShorter函数不符合constexpr函数的几个规定,它虽然只有一条return语句,但是返回的结果调用了标准库string类的size函数和<比较符,无法构成常量表达式。

6.5.3 调试帮助
C++程序员有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。
基本思想:
程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。
当应用程序编写完成准备发布时,要先屏蔽掉调试代码。
这种方法用到两项预处理功能:assert和NDEBUG

assert预处理宏:
assert是一种预处理宏。所谓预处理宏其实是一个预处理变量,它的行为类似于内联函数。assert宏使用一个表达式作为它的条件:
assert(expr);
首先对expr求值,如果表达式为假,assert输出信息并终止程序执行。如果为真,assert什么也不做。

15、assert宏定义在cassert头文件中。预处理名字由预处理器而非编译器管理,因此可以直接使用预处理名字而无需提供using声明。
也就是说,不用std::assert,也不用为assert提供using声明

16、和预处理变量一样,宏名字在程序内必须唯一。含有cassert头文件的程序不能再定义名为assert的变量、函数或其他实体。

17、assert宏常用于检查不能发生的事件。

NDEBUG预处理变量:
assert的行为依赖于一个 名为NDEBUG的预处理变量的状态。
18、如果定义了NDEBUG,assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。

19、我们可以使用一个#define 语句定义NDEBUG,从而关闭调试状态。

20、除了用于assert外,还可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,则执行#ifndef和#endif之间的代码。如果定义了NDEBUG,这些代码将被忽略。

6.6 函数匹配


确定候选函数和可行函数:
1、函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。

2、候选函数具备两个特征:(同名且可见就为候选函数)
1)与被调用的函数同名
2)其声明在调用点可见

3、函数匹配的第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为 可行函数

4、可行函数也有两个特征:(形参实参数量相等,且类型相同或实参能转换成形参的类型)
1)其形参数量与本次调用提供的实参数量相等
2)每个实参的类型与对应的形参类型相同,或者能转换成形参的类型

5、如果函数含有默认实参,则我们在调用该函数时传入的实参数量可能少于它实际使用的实参数量。

6、函数匹配的第三步,在使用实参数量初步判别了候选函数之后,接下来考察实参的类型是否与形参匹配。
实参与形参匹配的含义可能是它们具有相同的类型,也可能是实参类型和形参类型满足转换规则。

7、如果没找到可行函数,编译器将报告无匹配函数的错误。如果有,则寻找最佳匹配。

6.7 函数指针
1、函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。
函数的类型由它的返回类型和形参共同决定,与函数名无关。

2、要想声明一个可以指向某函数的指针,只需要用指针替换函数名即可:
bool (*pf)(const string &, const string &); // 未初始化,pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
这样来观察:
pf前面有个*,因此pf是指针;右侧是形参列表,表示pf指向的是函数;再观察左侧,发现函数的返回类型是布尔值。
*pf两端的括号必不可少。如果不写,则pf是一个返回值为bool指针的函数

使用函数指针:
3、当我们把函数名作为一个值使用时,该函数自动地转换成指针。例如,按照如下形式可以将lengthCompare的地址赋给pf:
pf = lengthCompare; // pf指向名为lengthCompare的函数
pf = &lengthCompare; // 等价的赋值语句,取地址符是可选的

4、此外,还能直接使用指向函数的指针调用该函数,无需提前解引用指针:
bool b1 = pf("hello", "ok"); // 调用lengthCompare函数
bool b2 = (*pf)("hello","ok"); // 一个等价的调用
bool b3 = lengthCompare("hello","ok");

5、在指向不同函数类型的指针之间不存在转换规则!!
但是,可以为函数指针赋值一个nullptr或者值为0的整型常量表达式,表示该指针没有指向任何一个函数
如,有一个函数
string::size_type sumLength(const string &, const string &);
bool cstringCompare(const char*, const char*);

pf = 0; // 正确:pf不指向任何函数
pf = sumLength; // 错误,返回类型不匹配
pf = cstringCompare;// 错误,形参类型不匹配
pf = lengthCompare;// 正确,函数和指针的类型精确匹配

重载函数的指针:
6、当我们使用重载函数时,上下文必须清晰界定到底应该选用哪个函数。如果定义了指向重载函数的指针
void ff(int*);
void ff(unsigned int);

void (*pf1)(unsigned int) = ff; // pf1指向ff(unsigned)
只要看返回类型和形参列表是否匹配就可以了,编译器通过指针类型决定选哪个函数,指针类型必须与重载函数中的某一个精确匹配:
void (*pf2)(int) = ff; // 错误,没有任何一个ff与该形参列表匹配
double (*pf3)(int*) = ff; // 错误,ff和pf3的返回类型不匹配

函数指针形参:
7、和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针:
// 第三个形参是函数类型,它会自动转换成指向函数的指针
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 &));
可以直接把函数作为实参使用,此时它会自动转换成指针:
调用:useBigger(s1, s2, lengthCompare);

返回指向函数的指针:
8、和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。
然而,必须把返回类型写成指针形式,编译器不会自动将函数返回类型当成对应的指针类型处理。
要想声明一个返回函数指针的函数,最简单的办法是使用类型别名:
using F = int(int*, int); // F是函数类型,不是指针
using PF = int(*)(int*, int); // PF是指针类型

定义以后,要时刻注意,和函数类型的形参不一样,返回类型不会自动地转换成指针!!!
必须显式地将返回类型指定为指针!!
PF f1(int); // 正确,PF是指向函数的指针,f1返回指向函数的指针
F f1(int); // 错误,F是函数类型,f1不能返回一个函数
F *f1(int); // 正确,显式地指定返回类型是指向函数的指针。

当然,也能用下面的形式直接声明f1:
int (*f1(int))(int*, int);
这样观察,看到f1有形参表,所以f1是个函数;f1前面有*,所以f1返回一个指针;指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是int

还可以使用尾置返回类型来声明一个返回函数指针的函数:
auto f1(int) -> int(*)(int*, int);

将auto和decltype用于函数指针类型:
如果明确的知道返回的函数是哪一个,就能用decltype简化书写函数指针返回类型的过程。使第三个函数返回一个指针,该指针指向前两个函数中的一个。
string::size_type sumLength(const string &, const string &);
string::size_type largerLength(const string &, const string &);
// 根据形参的取值,getFcn函数返回指向两个函数其中一个的指针
decltype(sumLength) *getFcn(const string &);
要注意的地方是当我们将decltype作用于某个函数时,它返回函数类型而非指针类型。因此要显式地加上*来表明我们需要返回指针,而非函数本身。

// 练习6.54 编写函数声明,令其接受两个int形参并返回类型也是int;然后声明一个vector对象,令其元素是指向该函数的指针
【解析】
int fun(int ,int );


vector<decltype(fun) *> vF;


// 练习6.55 编写4个函数,分别对两个int值执行加减运算,在上一题创建的vector对象中保存指向这些函数的指针。调用上述vector对象中的每个元素并输出结果
int fun1(int a, int b)
{
return a+b;
}


int fun2(int a, int b)
{
return a-b;
}


void compute(int a, int b, int (*pf)(int ,int))
{
cout<< pf(a, b);
}


int main()
{
decltype(fun1) *pf1 = fun1;
decltype(fun1) *pf1 = fun2;

vector<decltype(fun1) *> vF = {pf1, pf2};

for(auto i : vF)
{
compute(a, b, i);
}

return 0;
}














猜你喜欢

转载自blog.csdn.net/CSDN_dzh/article/details/81044868