C++ Primer Plus学习:第八章

C++入门第八章:函数探幽

本章将介绍C++语言区别于C语言的新特性。包括内联函数、按引用传递变量、默认的参数值、函数重载以及函数模板。

1 C++内联函数

内联函数是C++为提高程序运行速度所做的一项改进。常规函数与内联函数的区别在于编译器将其组合到程序中的方式而不是代码的编写方式。

编译过程的最终产品是可执行程序-由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址,计算机随后将逐步执行这些指令。常规函数的调用使计算机跳到函数的地址,在函数结束时返回。具体为:执行到函数调用指令时,程序在函数调用后立即存储指令的内存地址,并将函数参数复制到堆栈中,跳到标记函数起点的内存单元,执行函数代码,执行完毕后跳回到地址被保存的指令处。来回跳跃意味着使用函数时,需要一定的开销。

C++内联函数提供了另一种选择。内联函数的编译代码与其他程序代码"内联"起来。也就是说,编译器将使用相应的函数代码替换函数调用。内联函数运行速度比常规函数稍快,但是代价是需占用更多内存。

如果调用时间比函数执行时间长,则内联函数能够很好地提高效率。

要使用这种特性,必须采取以下措施;

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

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

有些编译器会拒绝创建内联函数的请求,但是有些编译器不会。

例:

声明定义:inline double square(double){return x*x;}

调用:b=square(a); c=square(c++);

内联函数和常规函数一样,是按值传递的。

宏不能按值传递参数,只是简单的替换。

  1. 引用变量

C++新增了一种复合类型:引用变量。引用是已定义的变量别名,例,将b作为变量a的别名,则可交替使用a和b表示该变量。引用变量的主要用途是作为函数的形参。通过引用变量作参数,函数将使用原始数据,而不是其副本。

创建引用变量

C++中&除了可以用来表示地址之外,还可以用来声明引用。

例:

int rats;

int &rodents=rats; //使rodents成为rats的别名

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

注意:引用变量必须在声明时就进行初始化,并且一旦与某个变量关联起来后(初始化后),就一直效忠于它。

int &raodents=rats;等效于 int *const pr=&rats;

将引用作为函数参数

引用经常被作为函数参数,使得函数中的变量名成为调用程序中的变量名。这种传递参数的方法称为按引用传递。按引用传递允许被调用函数能够访问调用函数中的变量。

例:

函数的声明:void swap(int &a,int &b);

函数定义:

void swap(int &a,int &b)

{

int temp;

temp=a;

a=b;

b=temp;

}

函数调用:

swap(wallet1,wallet2); //其中wallet1=100, wallet2=200

程序结果:

wallet1=200, wallet2=100

注意,引用变量作为参数传递时有很严格的限制。

比如:double z=refcube(x+3); 是不对的

double refcube(double &ra)

{

ra*=ra*ra;

return ra;

}

临时变量、引用参数和const

如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做。

如果引用参数是const,编译器将在下面两种情况下生成临时变量。

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

左值:左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。

非左值:字面常量(引号括起的字符串除外)和包含多项的表达式。

总之,常规变量和const变量都是左值,常规变量属于可修改的左值,const变量属于不可修改的左值。

例:

double refcube(const double &ra)

{

return ra*ra*ra;

}

long edge=5L; double side=5.0;

则下列函数调用会产生问题:

double c1=refcube(edge);

double c2=refcube(side+10.0);

在这种情况下,编译器会生成一个临时的匿名变量,并让ra指向它。这些临时变量只在函数调用期间存在,此后编译器就可将其随意删除。

对于上面举的常量引用的例子可行,但是以下例子却不可行:

void swap(int &a,int &b)

{

int temp;

temp=a;

a=b;

b=temp;

}

long a=3,b=5;

swap(a,b)

这是不可行的,在早起宽松的规则下,编译器会创建两个临时的int变量,并初始化为3和5,交换他们的位置,但是a和b却不变。

如果接受引用参数的函数意图是修改作为参数传递的变量,则创建临时变量将阻止此意图的实现,所以,此时禁止创建临时变量。

如果函数的目的仅为使用传递的值而不改变,此时临时变量不仅不会产生不利影响,反而会使函数在可处理参数种类方面更通用。

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

尽量使用const

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

将引用用于结构

a &accumulate(a &target,const a &source)// a为一结构

{

target.attempts+=source.attempts;

target.made+=source.made;

return target;

}

返回的为一个引用。

display(accumulate(one,two)); //display是接受引用变量的函数,one和two是两个已定义并初始化过的a结构。

将accumulate(one,two)返回的引用作为参数赋给display,显示结构。

等效于:

accumulate(one,two);

display(one);

还有

temp=accumulate(one,two); //将one的值赋给temp。

返回引用使函数处理的效率更高。

应尽量避免返回一个临时变量。

将引用用于类对象

string类:

string v1(const string &s1, const string &s2)

{

string temp;

temp=s1+s2+s1;

return temp;

}

string &v2(const string &s1, const string &s2)

{

string temp;

temp=s1+s2+s1;

return temp;

}

函数v1是可行的,函数v2是不可行的。因为函数试图指向已释放的内存。

对象、继承和引用

ofstream类能够使用ostream类的方法,这种使得能够将特性从一个类传递给另一个类的语言特性被称为继承。

ostream被称为基类,ofstream被称为派生类。派生类可以使用基类的格式化方法,如precision()和setf()。

何时使用引用参数

使用引用参数的原因:

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

当数据量巨大时,第二个原因极为重要。

对于使用值传递而不做修改的函数:

  • 如果数据对象很小,如内置数据类型和小型结构,按值传递。
  • 数据对象是数组,使用指针,最好使用指向const的指针
  • 数据对象是较大的结构,使用const指针和const引用,提高效率。
  • 数据对象是类对象,使用const对象。

对于修改调用函数中数据的函数

  • 如果数据对象是内置数据类型,使用指针。
  • 如果数据对象是数组,只能使用指针
  • 数据对象是结构,使用引用或指针
  • 数据对象是类对象,使用引用

以上仅为指导原则

注意事项

  • 引用不是一种独立的数据类型。对引用只有声明,没有定义。
  • 声明一个引用时,必须同时初始化。
  • 声明一个引用后,不能再使之作为另一变量的引用。
  • 不能建立引用数组
  • 不能建立引用的引用
  1. 默认参数

默认参数是指当函数调用中省略了实参时自动使用一个值。

设置默认值通过函数的原型实现:

char *left(const char *str,int n=1);

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

int chico(int m,int n=7,int k=3);

实参按从左到右的顺序依次赋给相应的形参,不能跳过任何参数。

  1. 函数重载

函数多态是C++在C语言基础上新增的功能。函数多态(函数重载)让您能够使用多个同名的函数。

函数重载的关键是函数的参数列表——也成为函数特征标(function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则其特征标相同,与变量名无关。C++允许定义名称相同的函数,条件是它们的特征标不同。参数数目和参数类型不同,特征标也不同。

使用被重载的函数时,需要在函数调用中使用正确的参数类型。如果函数调用不与任何类型匹配,C++报错。

一些看起来彼此不同的特征标是不能共存的,例:

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

编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。

匹配函数时,不区分const和非const变量。编译器将根据实参是否为const来决定使用哪个原型。非const赋给const变量是合法的,反之,不合法。

注意:是特征标而不是函数类型使得可以对函数进行重载。返回类型可以不同,但是特征标必须不同。

编译器将根据调用时的实参选择调用最匹配的重载函数版本。这让您能够根据参数是左值、const还是右值来定制函数的行为。

C++通过名称修饰来跟踪重载函数。

  1. 函数模板

函数模板是通用的函数描述,它使用泛型来定义函数,其中的泛型可用具体的类型(int、double)替换。

函数模板示例:交换模板

template <typename AnyType>

void Swap(Anytype &a,AnyType &b)

{

AnyType temp;

temp=a;

a=b;

b=temp;

}

AnyType为类型名,template 和typename为不可缺的关键字。AnyType为任选的,许多程序员使用简单的名称。

在C++98标准添加关键字typename之前,C++使用关键字class来创建模板。例:

template <class AnyType>

void Swap(Anytype &a,AnyType &b)

{

AnyType temp;

temp=a;

a=b;

b=temp;

}

class和typename等价。

例子:仅核心代码

函数声明:

template <typename T>

void Swap(T &a,T &b);

函数定义:

template <typename AnyType>

void Swap(Anytype &a,AnyType &b)

{

AnyType temp;

temp=a;

a=b;

b=temp;

}

函数调用:

int x=1,y=2;

Swap(x,y); //结果:x=2,y=1;

double x=1.2,y=2.4;

Swap(x,y); //结果:x=2.4,y=1.2

第一种情况,编译器将T替换为int,第二种情况,编译器将T替换为double。程序员看不到替换后的代码,但是编译器的确是这样做的。

重载的模板

可定义重载函数的模板,与常规重载一样,被重载函数的特征标必须不同。同时,并非所有模板参数都必须是模板参数类型。

例子:

函数声明:

template <typename T>

void Swap(T &a,T &b);

template <typename T>

void Swap(T *a,T *b,int n);

函数定义:

template <typename T>

void Swap(T &a,T &b)

{

T temp;

temp=a;

a=b;

b=temp;

}

template <typename T>

void Swap(T *a,T *b,int n)

{

T temp;

for (int i=0;i<n;i++)

{

temp=a[i];

a[i]=b[i];

b[i]=temp;

}

}

函数调用:

int x=10,y=20;

Swap(x,y); //结果:x=20,y=10

int x[3]={1,2,3},y[3]={3,4,5};

Swap(x,y,3); //结果:y[3]={1,2,3},x[3]={3,4,5}

模板的局限性

编写的模板函数可能无法处理某些类型。

如:a=b不适用于数组,

a<b不适用于结构。

显式具体化

定义如下结构:

struct job

{

char name[20];

double salary;

int floor;

};

如果只想交换结构中部分成员,可提供一个具体化函数定义——成为显式具体化。

第三代具体化(ISO/ANSI C++ 标准)

C++98标准:

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

显式具体化函数原型:

template <> void Swap<job>(job &,job &);

其中,Swap<job>中的job是可选的,因为函数的参数类型表明,这是job的一个具体化。该原型也可这样编写:

template <> void Swap(job &,job &);

例子:

函数定义:

template <> void Swap<job>(job &j1,job &j2)

{

double t1;

int t2;

t1=j1.salary;

j1.salary=j2.salary;

j2.salary=t1;

t2=j1.floor;

j1.floor=j2.floor;

j2.floor=t2;

}

调用:

Swap(a,b); //a和b为已定义的两个结构

实例化和具体化

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

例如函数调用Swap(i,j)导致编译器生成Swap的一个实例,该实例使用int类型。模板本身并非函数定义,但使用int的模板实例是函数定义。这种实例化方式被称为隐式实例化。

C++允许显式实例化,这意味着可以直接命令编译器创建特定的实例。语法:生命所需的种类,用<>表示类型,并在声明前加上关键字template。以下为例子:

template void Swap<int>(int,int);

编译器看到上述声明后,将使用Swap()模板生成一个使用int类型的实例。

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

template <> void Swap<int>(int &,int &);

template <> void Swap(int &,int &);

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

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

在程序中使用显式实例化:

template <class T>

T Add(T a,T b)

{

return a+b;

}

int a=10;double b=1.2;

Add<double>(a,b);

使用Add<double>(a,b)将函数转化为double类型实例化,并将a强制转化为double型。

注:以上对引用变量无效。

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

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

对于函数重载、函数模板和函数模板重载,C++必须决定使用哪一个,尤其是多个参数时。这个过程称为重载解析。

  • 创建候选函数列表。其中包含与被调函数名称相同的函数和模板函数
  • 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数。如float参数调用可转化为double,
  • 确定是否有最佳的匹配函数,如有则使用,否则调用出错。

特征标相同时,从最佳到最差的方案:

  • 完全匹配,但常规函数优先于模板
  • 提升转换(char、short自动转换为int,float转换为double)
  • 标准转换(int转换为char,long转换为double)
  • 用户自定义的转换

完全匹配和最佳匹配

完全匹配允许的无关紧要的匹配

从实参

到形参

type 

type & 

type & 

type 

type [] 

*type 

type (argument-list) 

type (*)(argument-list) 

type 

const type 

type 

volatile type

type * 

const type 

type * 

volatile type *

两个函数都完全匹配,仍可完成重载解析。

首先,指向非const数据的指针和引用优先于非const指针和引用参数匹配。

一个完全匹配优于另一个的另一种情况是,其中一个是非模板函数,而另一个不是。在这种情况下,非模板函数优于模板函数(包括显示具体化)。

如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。

部分排序规则

简而言之,重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果多个这样的函数,且它们都为模板函数,但是其中有一个比其它函数更具体,则函数调用将是不正确的,因此是错误的;当然,如果不存在匹配的函数,则也是错误的。

自己选择

function<>(a,b);表示应选择模板函数。

函数模板的发展

C++11中的关键字decltype

decltype(x+y) xpy=x+y; //xpy为x+y的类型

注:

decltype(expression) var;

程序按下列核对表一步步核对。

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

2,如果expression是一个函数,var与函数返回值类型相同。

3,如果expression是一个左值,var为指向其类型的引用(带括号的变量)。(第一步处理过的除外)

4,如果expression不满足前面的任何条件,var的类型与expression类型相同。

C++11后置返回值类型

double h(int x,int y);

新增语法:

auto h(int x,int y)->double;

用于不知道返回值类型时,先定义形参,再定义返回值类型。

原文地址:https://www.cnblogs.com/xyb930826/p/5267019.html