七八章主要围绕函数展开,七章介绍函数基础性知识,八章讲 C++ 新增的函数特性,与前面各种变量类型以及指针数组都有很大的联系,还有比较复杂冗余的一些概念,接下来进行一一的拆分总结。
第七章:函数——C++的编程模块
函数的基本
在 C++ 中使用函数,必须完成三方面的工作:提供函数定义、提供函数原型、调用函数。
在定义函数的环节,可以将函数分为两类:没有返回值的函数和有返回值的函数,其中没有返回值的函数被称为 void 函数,有返回值的函数对于返回值类型有着一定的限制:不能是数组。
提供函数原型的原因
函数原型不要求提供变量名,原型中的变量名相当于占位符,因此也不必与函数定义中的变量名相同。原型的存在确保了以下几点:
- 编译器正确处理函数返回值
- 编译器检查使用的参数数目是否正确
- 编译器检查使用的参数类型,并在不正确的情况下尝试转换为正确类型
总而言之,就是为了确保编译器的检查工作。
函数的传参
c++ 通常按值传递参数,传递给函数的值是实参(argument),函数中用于接收传递值的变量则是形参(parameter),形参是局部变量,会在函数结束时进行释放。
函数与数组
指针处理数组
C++ 在函数中使用指针来处理数组,将数组名视为指针,在数组名作为函数参数传入时,实际上传递的是地址。因此需要在函数原型声明中使用指针声明,这里需要结合例子说明一下:
1 | int cookies[Arsize];//定义一个数组 |
这里 sum_arr
的函数原型对应如下:
1 | int sum_arr(int* arr, int n); |
在这个函数原型中,int* arr 和 int arr[] 含义是相同的,都表明 arr 是一个 int 指针,而 int arr[] 还可以提醒用户 arr 指向的是数组第一个 int 元素。
另外,对于遍历数组而言,使用指针加法和数组下标是等效的,如下两个恒等式:
1 | arr[i] == *(arr+i); |
传递数组时的隐患
传递常规变量时,函数使用的是该变量的拷贝,不会影响原变量;而传递数组时,函数将使用原来的数组,因为传递的只是地址。这种特性使得原数组承担着可能被修改的风险,因此在某些不需要修改的条件下可以在函数声明时加入 const 来进行限定,这意味着不能在函数中进行数据修改,但原始数组本身并非常量:
1 | void show_array(const double ar[],int n); |
指针与 const
可以使用两种方式将 const 关键字用于指针,第一种方法是让指针指向一个常量对象,第二种方法是将指针本身声明为常量。c++ 禁止将 const 的地址赋给非 const 指针,同样常量数组的地址也不能赋给非常量指针。
在使用函数中尽可能使用 const 可以避免无意间的错误,也可以使得函数既能处理 const 实参也能处理非 const 实参,否则将只能接受非 const 数据。
函数和二维数组
函数在声明二维数组时若指定行或列为固定值,则此数组的行或列在函数运算过程中始终为固定值。
1 | ar2[r][c] == *(*(ar2 + r) + c); |
二维数组在声明的过程中无法使用 const,因为二维数组在使用指针表示时已经进入了二级间接关系,其本身的声明已经存在了指针。除非将其本身的声明中的指针规定为常量,否则无法使用 const。
函数和 C 风格字符串
将字符串作为参数传递给函数时,表示字符串的方式有三种:char 数组、用引号括起的字符串常量、被设置为字符串的地址的 char 指针。字符串有内置的结束字符,因此不必将字符串长度作为参数传递给函数,而函数可以使用循环来检查每个字符,直到遇到结尾的空值字符。
函数和结构
结构变量的行为更接近于基本的单值变量,可以像普通变量一样按值传递结构,也可以通过地址运算符 & 来传递结构地址进行操作。这样可以避免按值传递结构的缺点,即复制结构将增加系统内存要求,降低系统运行速度。
在将 cin>> 用作测试条件读取输入的情况下,对于不合法输入将设置一个错误条件,禁止进一步读取输入。如果在输入循环后还要进行输入,则必须使用 cin.clear() 重置输入,然后还可能需要通过读取不合法的输入来丢弃它们。示例如下:
1 | for(i=0;i<n;i++) |
传递结构的地址
若要修改函数使得函数传递的并非结构的值而是结构的地址,需要注意三点:调用函数时将结构地址(&xxx)而非结构本身(xxx)传递给函数,将形参声明为指向结构的指针类型,对于形参应该使用间接成员运算符(->)而非成员运算符(句点)。
函数和 string、array 对象
与数组相比,string 对象与结构更加相似,如果需要多个字符串,可以声明一个 string 对象数组,而不是二维 char 数组。
在 C++ 中类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类,即可按值传递也可以按指针传递。
模板 array 并非只能存储基本数据类型,还可以存储类对象。
递归
函数自己调用自己称为递归,通常的方法将递归调用放在 if 语句中。
在需要将一项工作不断分为两项较小的、类似的工作时,递归非常有用。需要注意,调用次数将呈几何级数增长。
以 void 类型的递归函数 recurs() 的代码为例:
1 | void recurs(argumentlist) |
在上述函数中,如果 recurs 被调用了五次,则 statements1 按照函数调用的顺序执行五次,然后 statements2 按照相反的顺序执行五次,相当于是“对称”的关系。
函数指针
将一个函数的地址作为参数传递给新的函数,与直接调用相比比较麻烦,但是它允许在不同的时间传递不同函数的地址,意味着可以在不同的时间使用不同的函数。
函数指针的使用有三个环节:获取函数的地址、声明一个函数指针、使用函数指针来调用函数。
- 获取函数地址:单纯的使用函数名就代表该函数的地址
- 声明函数指针:将函数声明中的函数部分用指针声明来代替,如:
double 函数名(int)
对应的函数指针声明为double (*pt)(int)
。之后只需要将指针pt
和函数名建立相等关系即可。 - 使用指针来调用函数:c++ 允许像使用函数名一样使用指针代表函数,即:
double y = (*pt)(5)
和double y = pt(5)
所表示的功能是一样的,但前一种更能表示代码正在使用函数指针。
函数指针的意义在于函数可以作为参数传递给函数,增加了函数的适用性,一个模板函数可以自由的选择不同的算法。
深入探讨函数指针
需要注意,函数指针声明时的括号必不可缺。const double * f1()
和 const double (*f1)()
代表的意义完全不同,前者表示函数 f1 返回值是指向 double 类型的指针,后者表示的才是函数指针。若要表示返回值是 double 类型的函数指针,则声明应该为 const double * (*f1)()
。
函数指针数组则在以上基础上再做添加,假设 f1 是一个包含三个元素的函数指针数组,则该数组声明如下所示:const double *(*f1[3])()
。对于函数指针可以直接用 auto 进行自动类型推断,而对于函数指针数组则不能,因为 auto 只能用于单值初始化,而不能用于初始化列表。
除此以外还可以用 typedef 创建类型别名来进行简化,通过减少输入量来让程序更容易理解。
第八章 函数探幽——面向更高级应用的函数相关功能
内联函数(inline)
常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中,如果代码执行时间很短,则内联调用可以节省非内联调用的大部分时间。
要使用此特性,必须保证在函数声明前加上关键字 inline 或者在函数定义前加上关键字 inline。通常的做法是省略原型,将整个定义放在本应该提供原型的地方。
inline 是 C++ 新增的特性,C 语言中使用预处理器语句 #define 来提供宏,这相当于是内联代码的原始实现,但是宏不能按值传递,只是实现了文本替换。因此,如果使用了宏来执行类似函数的功能,应该考虑转换为内联函数。
引用变量(&)
& 可以用来指示变量的地址,也可以用来进行声明引用。
引用看上去很像伪装表示的指针,但是引用和指针的差别在于必须在声明引用时将其初始化,不可以像指针一样先声明再初始化。不论是直接还是间接,引用关系一经确定就不得修改。
引用被用作函数参数
引用最常被用作函数参数,使函数中的变量名成为调用程序中变量的别名,这种传参的方法被称为引用传递。引用传递相比于值传递来说差别体现在可以修改原变量的值上。同时,按引用传递和按值传递在应用程序中看起来是一致的,两者的区分只能通过原型或者函数定义才能知道。
引用的属性和特别之处
如果想让函数使用传递给它的信息,但又不修改信息内容,就应该使用常量引用(const),此种情况一般出现在数据比较大的情况下。(值传递需要拷贝副本,浪费时间空间)
对于形参为 const 引用的函数,如果实参不匹配,则对应行为类似于值传递,为确保原始数据不被修改,将使用临时变量来存储值。
使用 const 的三个理由:
- 使用 const 可以避免无意中修改数据的编程错误
- 使用 const 使得函数能够正确处理 const 和非 const 实参
- 使用 const 使函数能够正确生成并使用临时变量
引用与结构和类
引用非常适用于结构和类,其主要就是为了这些类型而提出,而非基本的内置类型,因为引用节省了时间和内存。
而在使用引用的函数中,也包含了返回引用的情况,返回引用最重要的一点在于应该避免返回函数终止时不再存在的内存单元引用(不能返回临时变量的引用,也应该避免返回指向临时变量的指针),最简单避免这种问题的方法就是返回一个作为参数传递给函数的引用。
另一种方法是通过 new 的方式分配新的存储空间,但是函数往往隐藏了对 new 的调用,使得很容易忘记使用 delete 来释放内存。
返回引用时可以将 const 用于引用返回类型,这是为了防止出现反复赋值、内容覆盖。如 re_ex 函数是一个返回引用的函数,如果不用 const 进行返回类型限定,会有以下情况发生:
1 | re_ex(dup,five) = four; |
引用参数的适用情况
使用引用参数的主要原因有两个,其一在于使得程序员能够修改调用函数中的数据对象,其二在于提高了程序的运行速度。
不对使用参数进行修改的函数:
- 数据对象很小,按值传递
- 数据对象为数组,使用指针,并将指针声明为指向 const
- 数据对象为较大结构,使用 const 引用
- 数据对象为类,使用 const 引用,传递类对象的标准方式是按引用传递
修改使用参数的函数:
- 内置数据类型用指针
- 数组只能用指针
- 结构使用指针或引用
- 类对象使用引用
ostream 对象的格式化方法
方法 setf()
可以设置各种状态,setf(ios_base::fixed)
将对象置于使用定点表示法的模式,setf(ios_base::showpoint)
将对象置于显示小数点的模式,即使小数部分为零。
方法 precision()
指定显示多少位的小数。
方法 width()
设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值的时候有效,然后将恢复到默认设置。
默认参数
默认参数是指当函数调用中省略了实参时自动使用的一个值,设置方法是将值赋给原型中的参数,对于带参数列表的函数,必须从右向左添加默认值。
1 | int hatpo (int n,int m = 4,int j = 5); |
需要注意的是只有原型指定默认参数,函数定义和没有默认参数时完全相同。
函数重载
函数多态(函数重载)能够使用多个同名函数,区分在于参数列表的不同。
函数重载的关键在于函数的参数列表(也成为函数特征标),C++允许定义名称相同的函数,条件是它们的特征标不同,如果参数数目或参数类型不同,则特征标也不同。
如果在调用的过程中,参数类型没有与原型匹配,C++ 会尝试使用标准类型转换强制进行匹配,如果还是不行将会拒绝调用,视为错误。
为了避免引用和类型之间产生混乱,检查函数特征标时将会把类型引用和类型本身视为同一个特征标。重载时,返回类型可以不同,但是特征标也必须不同。
一般情况下,仅当函数基本上执行相同任务,但使用不同形式的数据时,才应用函数重载。
函数模板
函数模板是通用的函数描述,使用泛型来定义函数,其中泛型可以用具体的类型替换,通过将类型传递给模板,可以使得编译器生成该类型的函数。
函数模板格式
要建立一个模板并将类型命名为 AnyType,关键字 template 和 typename 是必需的,并且在函数原型和函数声明处都要出现,关键字使用 typename 或者 class 均可。
1 | template <typename T> |
函数模板不能缩短可执行程序,最终的代码不包含任何模板,而只包含了为程序生成的实际函数。更常见的情形是将模板放在头文件中,并在需要使用模板的文件中包含头文件。
模板重载以及局限性
并非所有的模板参数都必须是模板参数类型,同时模板可以重载。
模板的局限性在于假设模板定义了乘法运算符,但对应的类型为数组、指针或者结构时,这种假设就不再成立了。对这种情况有两种解决方案,一种是为特定类型提供具体化的模板定义,另一种则是重载运算符,使其能够应用于特定的结构或者类。
显式具体化、实例化和具体化
显式具体化
前面提到的函数模板局限性的解决方案中有一点是提供具体化的模板定义,简言之就是特殊情况特殊处理方式,即显式具体化。
- 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本
- 显式具体化的原型和定义应该以
template<>
打头,并通过名称指定类型 - 具体化优先于常规模板,而非模板函数优先于具体化和常规模板
以下示例展现了交换 job 结构的非模板函数、模板函数和具体化的原型:
1 | void swap(job &,job &); |
实例化和具体化
包含函数模板本身并不会生成函数定义,而使用模板生成函数定义是,得到的是模板实例,这种实例化方法成为隐式实例化。
c++ 现在还允许显式实例化,意味着可以直接命令编译器创建特定的实例,其语法是声明所需要使用的种类————用<>符号指示类型,并在声明前加上关键字 template。
1 | template void Swap<int>(int,int); |
需要注意的是,这里的 template 没有 <>,与其不同的是显示具体化使用如下等价声明:
1 | template <> void Swap<int>(int &,int &); |
显式具体化表达的是不要使用模板生成定义,使用专门的显式定义函数去定义;而显示实例化则表示使用模板生成某种类型定义。
隐式实例化、显式实例化和显示具体化统称为具体化。