导图社区 函数探幽思维导图
下图内容包括:内联函数、引用变量、默认参数、函数载重、函数模板等。
编辑于2021-01-02 19:17:27第8章 函数探幽
8.1 C++内联函数
(1)常规函数和内联函数的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需调到另一个位置执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如,如果程序在10个不同的地方调用同一个函数,则该程序将包含该函数代码的10个副本。
(2)要使用这项特性,必须采取下述措施之一: · 在函数声明前加上关键字inline · 在函数定义前加上关键字inline 通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方
(3)程序员请求将函数作为内联函数时,编译器不一定会满足这种要求。它可能认为该函数过大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数
8.2 引用变量
8.2.1 创建引用变量
将rodents作为rats的别名: int rats; int& rodents = rats; rats和rodents的地址都相同,实际上,上述代码是下述代码的伪表示: int* const pr = &rats; 其中,引用rodents扮演的角色与表达式*pr相同
8.2.2 将引用用作函数参数
8.2.3 引用的属性和特别之处
概述
引用的实参必须是变量: double refcube(double& ra); double z = refcube(x + 3.0); // should not compile 因为表达式x + 3.0不是变量。如,不能有以下赋值: x + 3.0 = 5.0; // nonsensical
左值介绍
左值最初指的是可出现在赋值语句左边的实体。但现在,常规变量和const常量都可以视为左值,因为都可以通过地址访问它们。 常规变量:可修改的左值 const变量:不可修改的左值
1. 临时变量
产生条件:参数为const 产生情况: (1)实参的类型正确,但不是左值 (2)实参的类型不正确,但可以转换为正确的类型
2. 引用参数
在早期的C++里,对于引用的限制比较宽松,因此即使引用的实参不为const,也可以产生临时变量: void swapr(int& a, int& b) { int temp; temp = a; a = b; b = temp; } 如果在早期C++较宽松的条件下,执行下列程序: long a = 3, b = 5; swapr(a, b); std::cout << "a = " << a << ", b = " << b << std::endl; 所得到的的结果: a = 3, b = 5 这是为什么呢? 因为这里类型不匹配,因此编译器将创建两个临时int变量,将它们初始化为3和5,然后交换临时变量的内容,但是这和a、b有什么关系呢?它们的值仍保持不变! 因此,我们需要在这种情况下加入const限定,来避免操作无效的问题,而只是传递参数值。现在的C++已然规定了这一点
3. 尽可能使用const
· 使用const可以避免无意修改数据的编程错误 · 使用const使函数能够处理const和非const实参,否则将只能接受非const数据 · 使用const引用使函数能够正确生成并使用临时变量
4. 右值引用
C++11新增了另一种引用——右值引用。这种引用可指向右值,使用&&声明: double&& rref = std::sqrt(36.00); // not allowed for double& double j = 15.0; double&& jref = 2.0 * j + 18.5; // not allowed for double& std::cout << rref << "\n"; // display 6.0 std::cout << jref << "\n"; // display 48.5 新增右值引用的目的是,让库设计人员能够提供有些操作的更有效实现。 以前的引用现在称为左值引用
8.2.4 将引用用于结构
1. 程序说明
部分程序: #include<iostream> ... struct free_throws { ... } ... void display(const free_throws& ft); free_throws& accumulate(free_throws& target, const free_throws& source) { ... return target; } int main() { free_throws one = ...; free_throws two = ...; free_throws three = ...; free_throws four = ...; free_throws five = ...; free_throws team = ...; ... accumulate(team, one); display(accumulate(team, two)); accumulate(accumulate(team, three), four); ... }
如果函数accumulate()的返回类型被声明为free_throws而不是free_throws&,则函数将返回target的拷贝。但返回类型为引用,这意味着返回的就是最初传递给accumulate()的team对象。 接下来,将accumulate()的返回值作为参数传递给了display(),这意味着将team传递给了display()。因此下述代码: display(accumulate(team, two)); 与下面的代码等效: accumulate(team, two); display(team); 上述逻辑也适用于如下语句: accumulate(accumulate(team, three), four); 因此,该语句与下面的语句等效: accumulate(team, three); accumulate(team, four);
2. 为何要返回引用
传统返回机制与按值传递参数类似:计算关键字return后面的表达式,并将结果返回给调用函数。从概念上来说,这个值被复制到另一个临时位置,而调用程序将使用这个值。请看下面的代码: double m = sqrt(16.0); cout << sqrt(25.00); 第一条语句中,值4.0被复制到一个临时位置,然后被赋值给m。第二条语句中,值5.0倍复制到一个临时位置,然后被传递给cout(实际上,编译器可能合并某些步骤) dup = accumulate(team, five); 如果accumulate()返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝复制给dup。但在返回值为引用时,将直接把team复制到dup,其效率更高
3. 返回引用时需要注意的问题
最重要的一点:应避免返回函数终止时不存在的内存单元引用。如: const free_throws& clone2(free_throws& ft) { free_throws newguy; newguy = ft; return newguy; } 该函数返回一个指向临时变量newguy的引用,函数运行完毕后它将不再存在(自动释放),因此出错。
解决方法: (1)返回作为参数传递给函数的引用。即,函数参数有哪些是引用,就返回哪些引用,这样保证引用始终存在。 (2)使用new来分配自由存储空间(不受程序的影响)。只需将上述函数代码第一行改为: free_throws newguy = new free_throws; 当然要注意用delete释放 (3)改用指针写函数
4. 为何将const用于引用类型
8.2.5 将引用用于类对象
8.2.6 对象、继承和引用
特征一: ostream是基类,而ofstream是派生类。派生类继承了基类的方法,意味着派生类(ofstream对象)可以使用基类(ostream)的特性,如格式化方法precision()和setf()
特征二: 基类引用可以指向派生类对象,而无需强制类型转换。因此,我们可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。
部分程序: ios_base::fmtflags initial = cout.setf(ios_base::fixed); cout.precision(0); cout << "Focal length of objective: " << 1800 << " mm\n"; cout.setf(ios::showpoint); cout.precision(1); cout.width(6); cout << "1234\n"; cout.width(6); cout << "1234" << "\n"; cout.width(3); cout << "1234\n"; cout.setf(initial); setf(ios_base::fixed):将对象置于使用定点表示法的模式,设置一直保持不变 setf():返回调用它之前有效的所有格式设置 ios_base::fmtflags:是存储这种信息所需的数据类型名称 因此下面的语句将cout的初始设置赋给了initial: ios_base::fmtflags initial = cout.setf(ios_base::fixed); setf(ios_base::showpoint):将对象置于显示小数点的模式,即使小数部分为零。设置一直保持不变 precision():指定显示多少位小数(假定对象处于定点状态下),设置一直保持不变 width():设置下一次输出操作使用的字段宽度,不够则无影响,多出部分则在开头补齐空格。设置只在显示下一个值时有效,然后恢复到默认设置
8.2.7 何时使用引用参数
使用原因
· 程序员能够修改调用函数中的数据对象 · 通过传递引用而不是整个数据对象,可以提高程序的运行速度
(1)对于使用传递的值而不做修改的函数
· 如果数据对象很小,如内置数据类型或小型结构,则按值传递 · 如果数据对象是数组,则使用指针,因为这是唯一选择,并将指针声明为指向const的指针 · 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序效率。 · 如果数据对象是类对象,则使用const引用
(2)对于修改调用函数中数据的函数
· 如果数据对象是内置数据类型,则使用指针。 · 如果数据对象是数组,则只能使用指针 · 如果数据对象是结构,则使用引用或指针 · 如果数据对象是类对象,则使用引用
8.5 函数模板(通用编程)
概述
函数模板允许以任意类型的方式来定义函数。例如,可以这样建立一个交换模板: template <typename AnyType> void Swap(AnyType& a, AnyType& b) { AnyType temp; temp = a; a = b; b = temp; } 第一行指出,要建立一个模板,并将类型命名为AnyType,可以使用class代替typename。类型名可以任意选择,一般使用T。 模板并不创建函数,只是告诉编译器如何定义函数。在需要交换int的函数时,编译器将按模板模式创建这样的函数,并用int代替AnyType。 通常将模板放在头文件中,并在需要使用模板的头文件中包含头文件。
8.5.1 重载的模板
和常规一样,被重载的模板的函数特征标必须不同,如: template <typename T> void Swap(T& a, T& b); template <typename T> void Swap(T& a, T& b, int n);
8.5.2 模板的局限性
编写的模板很可能不能处理某些类型。如,编写的Swap()函数就无法处理数组、结构等。为此,我们可以为特定类型提供具体化的模板定义。
8.5.3 显示具体化
定义结构: struct job { char name[40]; double salary; int floor; }
第三代具体化(IISO/ANSI C++标准): · 对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及它们的重载版本 · 显示具体化的原型和定义应以template<>打头,并通过名称来指出类型 · 具体化优先于常规模板,而非模板函数优先于具体化和常规模板 下面是用于交换job结构的非模板函数、模板函数和具体化的原型: // non template function prototype void Swap(job&, job&); // template prototype template <typename T> void Swap(T&, T&,); // explicit specialization for the job type template <> void Swap<job>(job&, job&,); 这里Swap<job>中的job是可选的,因为函数的参数类型表明,这是job的一个具体化,因此该原型也可以这样编写: template <> void Swap(job&, job&);
8.5.4 实例化和具体化
编译器使用模板为特定类型生成函数定义时,得到的是模板实例(即函数定义)。这种实例化方式被称为隐式实例化(inplicit instantiation)。编译器之所以知道定义,是因为我们提供了<int>参数。 C++现在也允许显示实例化(explicit instantiation)。这意味着可以直接命令编译器创建特定的实例,如Swap<int>()。其语法是,声明所需的种类——用<>符号指示类型,并在声明前加上关键字template: template void Swap<int>(int, int); 编译器看到上述说明后,将使用Swap()模板生成一个使用int类型的实例。也就是说,该声明的意思是"使用Swap()木耙生成int类型的函数定义"。 与显示实例化不同的是,显示具体化使用下面两个等价的声明之一: template <> void Swap<int>(int&, int&); template <> void Swap(int&, int&); 区别在于,这些声明的意思是"不要使用Swap()模板来生成函数定义,而应使用专门为int类型显示地定义的函数定义"。这些原型必须有自己的函数定义。显示具体化声明在关键字template后包含<>,而显示实例化没有。 下面给出显示具体化定义的代码: template <> void Swap<job>(job& a, job& b) { double t1; int t2; t1 = a.salary; a.salary = b.salary; b.salary = t1; t2 = a.floor; a.floor = b.floor; b.floor = t2; }
8.5.5 编译器选择使用哪个函数版本
匹配顺序(最佳->最差): · 完全匹配,但常规函数优先于模板 · 提升转换(char和short自动转换为int,float自动转换为double) · 标准转换(int转换为char,long转换为double) · 用户定义的转换,如类声明中定义的转换
8.5.6 模板函数的发展
1. 是什么类型?
请看下面的一个示例: template <typename T1, typename T2> void ft(T1 x, T2 y) { ... ?type? xpy = x + y; ... } xpy应为什么类型呢?我们无法确定
2. 关键字decltype(C++11)
int x; decltype(x) y; // make y the same type as x 给decltype提供的参数可以是表达式,因此在前面的模板函数ft()中,可以使用以下代码: decltype(x + y) xpy; // make xpy the same type as x + y xpy = x + y; 或者: decltype(x + y) xpy = x + y;
decltype比示例演示的要复杂些。为了确定类型,编译器必须遍历一个核对表,假设有如下声明: decltype(expression) var; 核对表大致简化如下: 第一步:如果expression是一个没有用括号括起的标识符,则var的类型与其一致 第二步:如果expression是一个函数调用,则var的类型与其返回类型相同(编译器不会调用函数,而是直接查找函数的返回类型) 第三步:如果expression是一个左值,则var为指向其类型的引用。但是,触发这种条件需要再加上一个括号(括号不改变表达式的值和左值性),因为不加括号的话早就执行第一步了: double xx = 4.4; decltype((xx)) r2 = xx; // r2 is double& decltype(xx) w = xx; // w is double 第四步:如果前面条件都不满足,则var的类型与expression相同
3. 另一种声明语法
有一个问题是decltype本身还不能解决的,例如: template <typename T1, typename T2> ?type? gt(T1 x, T2 y) { ... return x + y; } 虽然我们可以在函数内部定义decltype(x + y),但是当函数结束的时候这个变量已经消失了。为此,C++新增了一种函数声明语法。对于下面的原型: double h(int x, float y); 可以用下面的语句表达: auto h(int x, float y) -> double; 这将返回类型移到了参数声明的后面。->double被称为后置返回类型。auto是一个占位符,表示后置返回类型提供的类型。因此上述问题可用如下代码解决: template <typename T1, typename T2> auto gt(T1 x, T2 y) -> decltype(x + y) { ... return x + y; }
8.4 函数重载
8.4.1 重载示例
8.4.2 何时使用函数重载
介绍:名称修饰
C++给每个函数指定了一个秘密身份。C++编译器将执行一些神奇的操作——名称修饰(name decoration)或名称矫正(name mangling),它根据函数原型中指定的形参类型对每个函数名进行加密。请看下面未经修饰的函数原型: long MyFunctionFoo(int, float); 编译器会将名称转换为不太好看的内部表示,来描述该接口,如下所示: ?MyFunctionFoo@@YAXH 对原始名称进行的表面看起来无意义的修饰(矫正)将对参数数目和类型进行编码。添加的一组符号随函数特征标而异,而修饰时使用的约定随编译器而异
8.3 默认参数
设置方法: 将值赋给原型中的参数,,从右向左添加默认值。
实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数