【C++ Primer Plus】第八章:函数探幽

第八章

8.1 内联函数

编译过程的最终产品是可执行程序——由一组机器语言指令组成。

编译过程的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时(如有循环或分支语句时),将跳过一些指令,向前或向后跳到特定地址。常规函数调用也使程序跳到另一个地址(函数的地址),并在函数结束时返回。下面更详细地介绍这一过程的典型实现。

执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。

C++内联函数提供了另一种选择。内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本(参见图8.1)。

请添加图片描述

应有选择地使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。

如果要使用这项特性,则下述措施必选其一:

  • 在函数声明前加上关键字inline;
  • 在函数定义前加上关键字inline。

通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。

但内联函数只是建议性的一种操作。程序员向编译器请求将函数作为内联函数时,编译器并不一定会满足这种要求。它可能认为该函数过大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数;而有些编译器没有启用或实现这种特性。

输出表明,内联函数和常规函数一样,也是按值来传递参数的。如果参数为表达式,如4.5 + 7.5,则函数将传递表达式的值(这里为12)。这使得C++的内联功能远远胜过C语言的宏定义,请参见旁注“内联与宏”。

inline工具是C++新增的特性。C语言使用预处理器语句#define来提供宏——内联代码的原始实现。

#define SQUARE(X) X*X;

但这并不是通过传递参数实现的,而是通过文本替换来实现的。这种文本替换很容易出现错误。

显然,内联函数可以视作宏的高级版本,如果使用C语言的宏执行了类似函数的功能,应考虑将它们转换为C++内联函数。

8.2 引用变量

C++新增了一种复合类型——引用变量。引用是已定义的变量的别名(另一个名称)。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除指针之外,引用也为函数处理大型结构提供了一种非常方便的途径,同时对于设计类来说,引用也是必不可少的。

8.2.1 创建引用变量

C和C++使用&符号来指示变量的地址。C++给&符号赋予了另一个含义,将其用来声明引用。

int rats;
int & rodents = rats;

其中,&不是地址运算符,而是类型标识符的一部分。就像声明中的char*指的是指向char的指针一样,int &指的是指向int的引用。上述引用声明允许将rats和rodents互换——它们指向相同的值和内存单元。

引用还是不同于指针的。除了表示法不同外,还有其他的差别。例如,差别之一是,必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值:

int rat;
int &rodent;
rodent = rat;//NO,you can not do this;可以通过初始化声明来设置引用,但不能通过赋值来设置。

引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。也就是说:

int & rodents  = rats;
//实际上是下述代码的伪装表示:
int * const pr = &rats;

其中,引用rodents扮演的角色与表达式*pr相同。

8.2.2 将引用用作函数参数

C++新增的这项特性是对C语言的超越,C语言只能按值传递。按值传递导致被调用函数使用调用程
序的值的拷贝(参见图8.2)。当然,C语言也允许避开按值传递的限制,采用按指针传递的方式。

请添加图片描述

常规情况下,推荐将函数的形参写成下述的方式:

int fun(const int & ra);

但是如果这么写就会发现传递引用的限制更严格。毕竟,如果ra是一个变量的别名,则实参应是该变量,于是下面的调用就会显得不合理:因为表达式X+3并不是变量:

fun(X+3);

现代C++中不允许这样写。

但是一些较老的编译器会有一些温和的反应,它并不认为这是个错误,而是创建一个临时的无名变量,并将其初始化为表达式X+3.0的值。

如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做,但以前不是这样。下面来看看何种情况下,C++将生成临时变量,以及为何对const引用的限制是合理的。

首先,什么时候将创建临时变量呢?如果引用参数是const,则编译器将在下面两种情况下生成临时变量:

  • 实参的类型正确,但不是左值;
  • 实参的类型不正确,但可以转换为正确的类型。

左值是什么呢?左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况。现在,常规变量和const变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。

如果参数正确传递,就不会创建临时变量。相反,如果遇到下述情况:

long a = 1;
int b = 4;
fun(a);
fun(b);

a虽然是变量,类型却不正确,int引用不能指向long。另一方面,参数7.0和side + 10.0的类型都正确,但没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让ra指向它。这些临时变量只在函数调用期间存在,此后编译器便可以随意将其删除。

还有一些比较奇怪的情况出现:

void swap(int & a,int & b);//交换ab

如果在早期C++较宽松的规则下,执行下述代码:

long a = 3, b = 5;
swap(a,b);

这里的类型不匹配,因此编译器将创建两个临时int变量,将它们初始化为3和5,然后交换临时变量的内容,而a和b保持不变。

简而言之,如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方法是,禁止创建临时变量,现在的C++标准正是这样做的。

如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。

所以应使用函数时,如果可以的话,应尽量将函数的参数写成这种形式:

int fun(const int & ra);

**应尽可能的使用引用。**其好处有以下三点:

  • 使用const可以避免无意中修改数据的编程错误;
  • 使用const使函数能够处理const和非const实参,否则将只能接受非const数据;
  • 使用const引用使函数能够正确生成并使用临时变量。

右值引用

C++11新增了另一种引用——右值引用(rvalue reference)。这种引用可指向右值,是使用&&声明的。新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现。以前的引用(使用&声明的引用)现在称为左值引用。

为什么返回也推荐使用引用?

下面更深入地讨论返回引用与传统返回机制的不同之处。传统返回机制与按值传递函数参数类似:计算关键字return后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。请看下面的代码:

double m = sqrt(16.0);
cout<<sqrt(25.0);

在第一条语句中,值4.0被复制到一个临时位置,然后被复制给m,在第二条语句中,值5.0被复制到一个临时位置,然后被传递给cout(这是理论上的描述,实际上,编译器可能合并某些步骤)。

dup = acc(temp,five);

如果acc返回的是一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将其拷贝到dup。但在返回引用时,就会直接把temp赋值给dup,其效率更高。

返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元(局部变量)引用。

  1. 为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。
  2. 另一种方法是用new来分配新的存储空间。前面见过这样的函数,它使用new为字符串分配内存空间,并返回指向该内存空间的指针。
const free_throws & clone (free_throws & ft){
    free_throws * pt;
    *pt = ft;
    return *pt;
}

free throws & jolly = clone(three);

这使得jolly成为新结构的引用。这种方法存在一个问题:在不再需要new分配的内存时,应使用delete来释放它们。调用clone( )隐藏了对new的调用,这使得以后很容易忘记使用delete来释放内存。

为何将const用于引用返回类型:

来看一个问题:

accumulate(dup,five) = four;

其效果如下:首先将five的数据添加到dup中,再使用four的内容覆盖dup的内容。这条语句为何能够通过编译呢?在赋值语句中,左边必须是可修改的左值。也就是说,在赋值表达式中,左边的子表达式必须标识一个可修改的内存块。在这里,函数返回指向dup的引用,它确实标识的是一个这样的内存块,因此这条语句是合法的。

另一方面,常规(非引用)返回类型是右值——不能通过地址访问的值。这种表达式可出现在赋值语句的右边,但不能出现在左边。但为何常规函数返回值是右值呢?这是因为这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在。

假设您要使用引用返回值,但又不允许执行像给accumulate()赋值这样的操作,只需将返回类型声明为const引用:

const free_throws & accumulate(free_throws & target , const free throws & source);

现在返回类型为const,是不可修改的左值,因此上面的赋值语句不再合法。

8.2.5 将引用用于类对象

s3 = s1 + s2 + s1;//将两个字符串加到s2的前面和后面;

8.2.6 对象,继承和引用

使得能够将特性从一个类传递给另一个类的语言特性被称为继承。继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。

方法setf( )让您能够设置各种格式化状态。例如,方法调用setf(ios_base::fixed)将对象置于使用定点表示法的模式;setf(ios_base::showpoint)将对象置于显示小数点的模式,即使小数部分为零。方法precision( )指定显示多少位小数(假定对象处于定点模式下)。所有这些设置都将一直保持不变,直到再次调用相应的方法重新设置它们。方法width( )设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值时有效,然后将恢复到默认设置。默认的字段宽度为零,这意味着刚好能容纳下要显示的内容。

ios_base::fmtflags initial;
initial = os.setf(ios_base::fixed);
...
os.setf(initial);

ios_base::fmtflags是存储这种信息所需的数据类型名称。因此,先将返回值赋给initial,将存储调用file_it( )之前的格式化设置。然后便可以使用变量initial作为参数来调用setf( ),将所有的格式化设置恢复到原来的值。因此,该函数将对象回到传递给file_it( )之前的状态。

8.2.7 何时使用引用参数

使用引用参数的主要原因主要有两个:

  • 程序员能够修改调用函数中的数据对象。
  • 通过传递引用而不是整个数据对象,可以提高程序的运行速度。

8.3 默认参数

对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。

现有一程序,它将获得用户输入的字符串和一个int值,然后输出改字符串的前n个值,默认情况n=1;

char * left(const char * str,int n){
    if(n<0){
        n=0;
    }
    char *p = new char[n+1];
    int i;
    for(i = 0;i<n && str[i];i++){
        p[i] = str[i];
    }
    while(i <= n){
        p[i++] = '\0';
    }
    return p;
}

该程序使用new创建一个新的字符串,以存储被选择的字符。一种可能出现的尴尬情况是,不合作的用户要求的字符数目可能为负。在这种情况下,函数将字符计数设置为0,并返回一个空字符串。另一种可能出现的尴尬情况是,不负责任的用户要求的字符数目可能多于字符串包含的字符数,为预防这种情况,函数使用了一个组合测试:

i<n && str[i];

i < n测试让循环复制了n个字符后终止。测试的第二部分——表达式str[i],是要复制的字符的编码。遇到空值字符(其编码为0)后,循环将结束。这样,while循环将使字符串以空值字符结束,并将余下的空间(如果有的话)设置为空值字符。另一种设置新字符串长度的方法是,将n设置为传递的值和字符串长度中较小的一个:

int len = strlen(str);
n = (n<len) ? n : len;
char * p = new char[n+1];

这将确保new分配的空间不会多于存储字符串所需的空间。如果用户执行像left(“Hi!”, 32767)这样的调用,则这种方法很有用。

8.4 函数重载

8.4.1 重载示例

函数多态是C++在C语言的基础上新增的功能。默认参数让您能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让您能够使用多个同名的函数。可以通过函数重载来设计一系列函数——它们完成相同的工作,但使用不同的参数列表。

函数重载的关键是函数的参数列表——也称为函数特征标(function signature)

不能存在这种:

double cube(double x);
double cube(double& x);

但允许存在这种:

double dribble(char * bits);
double dribble(const char *cbits);

dribble()函数有两个原型,一个用于const指针,另一个用于常规指针,编译器将根据实参是否为const来决定使用哪个原型。dribble( )函数只与带非const参数的调用匹配,而drivel( )函数可以与带const或非const参数的调用匹配。drivel( )和dabble( )之所以在行为上有这种差别,主要是由于将非const值赋给const变量是合法的,但反之则是非法的。

但是请记住,是特征标,而不是函数类型使得可以对函数进行重载。例如,下面的两个声明是互斥的:

long gronk(int n,float m);
double gronk(int n,float m);

C++不允许以这种方式重载gronk()。返回类型可以不同,但是特征标也必须不同。

类设计和STL经常使用引用参数,因此知道不同引用类型的重载很有用。请看下面三个原型:

void sink(double & r1);
void sank(const double & r2);
void sunk(double && r3);
  • 左值引用参数r1与可修改的左值参数(如double变量)匹配;
  • const左值引用参数r2与可修改的左值参数、const左值参数和右值参数(如两个double值的和)匹配;
  • 最后,右值引用参数r3与右值匹配。

注意到与r1或r3匹配的参数都与r2匹配。这就带来了一个问题:如果重载使用这三种参数的函数,结果将如何?答案是将调用最匹配的版本。

8.4.2 何时使用函数重载

虽然函数重载很吸引人,但也不要滥用。仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。

8.5 函数模板

现在的C++编译器实现了C++新增的一项特性——函数模板。函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。

template <typename Type>
//这里和 template <class Type>一样
Type ADD(Type a, Type b) {
	return a + b;
}

注意,函数模板不能缩短可执行程序。就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,而只包含了为程序生成的实际函数,使用模板的好处是,它使生成多个函数定义更简单、更可靠。

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 模板的局限性

假设有如下模板函数:

template<class T>
void f(T a, T b){...}

如果T为数组,这种假设就不成立。编写的模板函数很可能无法处理某些类型。

8.5.3 显式具体化

将两个包含位置坐标的结构相加是有意义的,虽然没有为结构定义运算符+。

一种解决方案是,C++允许您重载运算符+,以便能够将其用于特定的结构或类。这样使用运算符+的模板便可处理重载了运算符+的结构。

另一种解决方案是,为特定类型提供具体化的模板定义,下面就来介绍这种解决方案。

struct job{
    char name[40];
    double salary;
    int floor;
}

假设想要交换这种结构的内容,原来的模板使用下面的代码来完成:

temp = a;
a = b;
b = temp;

由于C++允许将一个结构赋给另一个结构,因此即使T是一个job结构,上述代码也适用。然而,假设只想交换salary和floor成员,而不交换name成员,则需要使用不同的代码,但Swap( )的参数将保持不变(两个job结构的引用),因此无法使用模板重载来提供其他的代码。

可以提供一个具体化函数定义——称为显式具体化,其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

具体化方法:

  • 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
  • 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型。
  • 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。

下面是用于交换job结构的非模板函数、模板函数和具体化的原型:

void Swap<job&,job&>;//普通函数
template<typename T>
void Swap(T &,T &);//模板函数
template <> void Swap<job>(job&,job&);//<job>是可选的,也可以这么写:显式具体化
template <> void Swap(job&,job&);

调用顺序:普通函数>显示具体化>模板函数

8.5.4 实例化和具体化

记住,在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。

模板并非函数定义,但使用int的模板实例是函数定义。这种实例化方式被称为隐式实例化,因为编译器之所以知道需要进行定义,是由于程序调用Swap( )函数时提供了int参数。

最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但现在C++还允许显式实例化。这意味着可以直接命令编译器创建特定的实例,如Swap( )。其语法是,声明所需的种类——用<>符号指示类型,并在声明前加上关键字template:

template void Swap<int>(int,int);//显式实例化
template<> void Swap(int &,int &);//显式具体化

实现了这种特性的编译器看到上述声明后,将使用Swap( )模板生成一个使用int类型的实例。也就是说,该声明的意思是“使用Swap( )模板生成int类型的函数定义。”

隐式实例化实际上就是模板函数根据传入的实参类型实例化一个函数的过程。

与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:

template<> void Swap<int> (int&,int&);
template<> void Swap(int &,int &);//显式具体化

区别在于,这些声明的意思是“不要使用Swap( )模板来生成函数定义,而应使用专门为int类型显式地定义的函数定义”。这些原型必须有自己的函数定义。显式具体化声明在关键字template后包含<>,而显式实例化没有。

试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。

隐式实例化、显式实例化和显式具体化统称为具体化。

它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。

引入显式实例化后,必须使用新的语法——在声明中使用前缀template和template <>,以区分显式实例化和显式具体化。通常,功能越多,语法规则也越多。

    template <class T>
    void Swap(T&,T&);//函数模板

	template <> void Swap<job>(job&,job&);//显式具体化
int main(){
    template void Swap<char>(char&,char&);//为char生成显式实例
    short a,b;
    Swap(a,b);//为short生成隐式实例化
    job n,m;
    Swap(n,m);//job::显式具体化
    char g,h;
    Swap(g,h);//显示实例
    Swap<>(g,h);//如果这时候定义了普通函数Swap,这里会指定使用模板函数
}

编译器看到char的显式实例化后,将使用模板定义来生成Swap( )的char版本。对于其他Swap( )调用,编译器根据函数调用中实际使用的参数,生成相应的版本。例如,当编译器看到函数调用Swap(a, b)后,将生成Swap( )的short版本,因为两个参数的类型都是short。当编译器看到Swap(n, m)后,将使用为job类型提供的独立定义(显式具体化)。当编译器看到Swap(g, h)后,将使用处理显式实例化时生成的模板具体化。

8.5.5 编译器选择使用哪个函数版本

对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析。我们来了解一下这个过程:

  1. 第1步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
  2. 第2步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
  3. 第3步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。

举个例子:

may('B');

void  may(int);                         //#1
float may(float,float = 3);             //#2
void  may(char);                        //#3
char *may(const char*);                 //#4
char  may(const char&);                 //#5
template<class T> void may(const T&);   //#6
template<class T> void may(T *);        //#7

注意,只考虑特征标,而不考虑返回类型。其中的两个候选函数(#4和#7)不可行,因为整数类型不能被隐式地转换(即没有显式强制类型转换)为指针类型。剩余的一个模板可用来生成具体化,其中T被替换为char类型。这样剩下5个可行的函数,其中的每一个函数,如果它是声明的唯一一个函数,都可以被使用。

而接下来,编译器必须确定哪个可行函数是最佳的,最佳到最差的顺序如下:

  1. 完全匹配,但常规函数优先于模板。
  2. 提升转换(例如,char和shorts自动转换为int,float自动转换为double)。
  3. 标准转换(例如,int转换为char,long转换为double)。
  4. 用户定义的转换,如类声明中定义的转换。

例如,函数#1优于函数#2,因为char到int的转换是提升转换,而char到float的转换是标准转换(参见第3章)。函数#3、函数#5和函数#6都优于函数#1和#2,因为它们都是完全匹配的。#3和#5优于#6,因为#6函数是模板。这种分析引出了两个问题。什么是完全匹配?如果两个函数(如#3和#5)都完全匹配,将怎么办呢?通常,有两个函数完全匹配是一种错误,但这一规则有两个例外。显然,我们需要对这一点做更深入的探讨。

完全匹配与最佳匹配

进行完全匹配时,C++允许某些“无关紧要的转换”:

从实参 到形参
Type Type&
Type& Type
Type[] *Type
Type(argument-list) Type(*)(argument-list)
Type const Type
Type volatile Type
Type* const Type
Type* volatile Type *
struct blot{
    int a;
    char b[10];
};
blot ink = {25,"spots"};
...
recycle(ink);

//在这种情况下,下面的原型都是完全匹配的:
void recycle(blot);
void recycle(const blot);
void recycle(blot &);
void recycle(const blot&);

正如您预期的,如果有多个匹配的原型,则编译器将无法完成重载解析过程;如果没有最佳的可行函数,则编译器将生成一条错误消息,该消息可能会使用诸如“ambiguous(二义性)”这样的词语。

**然而,有时候,即使两个函数都完全匹配,仍可完成重载解析。**这一规则有两个例外:

第一种情况:首先,指向非const数据的指针和引用优先与非const指针和引用参数匹配。也就是说,在recycle( )示例中,如果只定义了函数#3和#4是完全匹配的,则将选择#3,因为ink没有被声明为const。然而,const和非const之间的区别只适用于指针和引用指向的数据。也就是说,如果只定义了#1和#2,则将出现二义性错误。

第二种情况:其中一个是非模板函数,而另一个不是。在这种情况下,非模板函数将优先于模板函数(包括显式具体化)

如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。这也意味着显式具体化将优于使用模板隐式生成的具体化。术语“最具体(most specialized)”并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。例如,请看下面两个模板:

template<class Type> void recycle(Type t);
template<class Type> void recycle(Type *t);

struct blot{
    int a;
    char b[10];
}
blot ink = {25,"spots"};
recycle(&ink);

8.5.6 函数模板的发展

recycle(&ink)调用与#1模板匹配,匹配时将Type解释为blot *。

recycle(&ink)函数调用也与#2模板匹配,这次Type被解释为ink。因此将两个隐式实例——recycle<blot *>(blot *)和recycle (blot *)发送到可行函数池中。

在这两个模板函数中,recycle<blot *>(blot *)被认为是更具体的,因为在生成过程中,它需要进行的转换更少。也就是说,#2模板已经显式指出,函数参数是指向Type的指针,因此可以直接用blot标识Type;而#1模板将Type作为函数参数,因此Type必须被解释为指向blot的指针。也就是说,在#2模板中,Type已经被具体化为指针,因此说它“更具体”。

用于找出最具体的模板的规则被称为函数模板的部分排序规则。和显式实例一样,这也是C++98新增的特性。

部分排序规则示例:

#include<iostream>
using namespace std;
 
template <class T>
void showArray(T arr[],int n);

template <class T>
void showArray(T arr[],int n);

template <typename T>
void showArray(T arr[],int n){
	cout<<"template A:"<<endl;
	for(int i = 0; i < n;i++){
		cout<<arr[i]<<" ";
	}
	cout<<endl;
}

template<typename T>
void showArray(T * arr[],int n){
	cout<<"template B:"<<endl;
	for(int i = 0;i<n;i++){
		cout<<*arr[i]<<" ";
	}
	cout<<endl;
}

struct debts{
	char name[50];
	double amount;
};

int main()
{
    int thing[6] = {13,31,103,301,130};
    
    struct debts mr[3] = 
	{
    	{"Dog",2400.0},
    	{"Cat",1300.0},
    	{"Fox",1800.0}
	};
	double * pd[3];
	
	for(int i = 0; i < 3; i++){
		pd[i] = &mr[i].amount;
	}
	
	cout<<"show thing:"<<endl;
	showArray(thing,6);
	cout<<"show mr:"<<endl;
	showArray(pd,3);
 
    return 0;
}

其中pd是一个double *数组的名称。这与模板A匹配:T被替换成double*。在这种情况下,模板函数将显示pd数组的内容,即3个地址,同时,该函数调用也与模板B匹配:在这里,T被替换为类型double,而函数将显示被解除引用的元素*arr[i],即数组内容指向的double值。在这两个模板中,模板B更具体,因为它做了特定的假设——数组内容是指针,因此被使用。下面是运行结果:

show thing:
template A:
13 31 103 301 130 0
show pd:
template B:
2400 1300 1800

如果将模板B从程序中删除,则编译器将使用模板A来显示pd的内容,因此显示的将是地址,而不是值。

简而言之,重载解析将寻找最匹配的函数。

  • 如果只存在一个这样的函数,则选择它;
  • 如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;
  • 如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数。
  • 如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的;
  • 当然,如果不存在匹配的函数,则也是错误。

关键字decltype

  • decltype(expression)

其参数可以是表达式,也可以是普通的变量。它可以声明定义一个跟表达式内一模一样的值,包括const。

如果expression是一个没有用括号括起的标识符,则var的类型与该标识符的类型相同,包括const等限定符。

如果expression是一个函数,则var的类型与函数的返回类型相同。但并不会调用函数,只是获取其返回类型。

如果expression是一个左值,则var为指向其类型的引用。要进入第三步,expression不能是未用括号括起的标识符。一种显而易见的情况是,expression是用括号括起的标识符:

double xx = 4.4;
decltype((xx)) r2 = xx;  //r2 is double &;
decltype(xx) r2 = xx;    //w is double ;

如果前面的条件都不满足,则var的类型与expression的类型相同:

int j = 3;
int &k = j;
int &n = j;
decltype(j+6)  i1;//int
decltype(100L) i2;//long
decltype(k+n)  i3;//int

请注意,虽然k和n都是引用,但表达式k+n不是引用;它是两个int的和,因此类型为int。

如果需要多次声明,可结合使用typedef和decltype:

template<class T1, class T2>
    void ft(T1 x, T2 y){
    typedef decltype(x+y) xytype;
    xytype xpy = x+y;
    xytype arr[10];
}

C++后置返回类型

说了那么多,其实有一个问题是decltype无法解决的。

template<class T1, class T2>
?type? gt(T1 x, T2 y){
    return x+y;
}

同样,无法预先知道将x和y相加得到的类型。好像可以将返回类型设置为decltype ( x + y),但不幸的是,此时还未声明参数x和y,它们不在作用域内(编译器看不到它们,也无法使用它们)。必须在声明参数后使用decltype。为此,C++新增了一种声明和定义函数的语法。下面使用内置类型来说明这种语法的工作原理。对于下面的原型:

double h(int x , float y);//使用新增的语法可以编写成下面这样:
auto h(int x,float y)->double;

这将返回类型移到了参数声明后面。->double被称为后置返回类型。其中auto是一个占位符,表示后置返回类型提供的类型,这是C++11给auto新增的一种角色。

所以可以通过这种方法去解决:

template<class T1, class T2>
auto gt(T1 x, T2 y)->decltype(x+y){
    ...
    return x+y;
}

8.6 总结

  • C++扩展了C语言的函数功能。通过将inline关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以使得C++编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用。只有在函数很短时才能采用内联方式。
  • 引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量主要被用作处理结构和类对象的函数的参数。通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类(ofstream)是从另一个类(如ostream)派生出来的,则基类引用可以指向派生类对象。
  • C++原型让您能够定义参数的默认值。如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值)。只能在参数列表中从右到左提供默认参数。因此,如果为某个参数提供了默认值,则必须为该参数右边所有的参数提供默认值。
  • 函数的特征标是其参数列表。程序员可以定义两个同名函数,只要其特征标不同。这被称为函数多态或函数重载。通常,通过重载函数来为不同的数据类型提供相同的服务。
  • 函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。

猜你喜欢

转载自blog.csdn.net/weixin_43717839/article/details/129866126
今日推荐