写点什么

C++primer- 函数探幽

用户头像
Dreamer
关注
发布于: 2020 年 11 月 02 日
C++primer-函数探幽



以下的内容为二次阅读《c++ primer》第六版的一些笔记,只为自己加深理解,也为记录下自己的学习过程,如果有问题,欢迎交流学习。




本章的主要内容是对inline, 引用,默认参数, 函数重载,函数模版的一些认识




1. C++ inline函数

  1. 出现的原因:

inline函数出现的原因是什么? 我们先来看看正常的函数调用过程中,程序在操作系统上是如何运行的。在执行函数调用指令时,程序将在函数调用后,立即存储该指令的内存地址,并将函数参数复制到堆栈,跳到标记函数的起点的内存单元,执行函数代码,然后跳回到地址被保存的至指令处,来回的跳跃,并且记录跳跃位置需要一定的开销。



  1. 本质:

inline函数提供了另外一种方案,编译器直接在函数调用的时候用相应的函数代码替换,无需跳转和保存的开销。本质上就是使用空间换时间。



  1. 使用的时机: 但是应该有选择的使用inline函数,如果执行函数代码的时间比处理函数调用的时间长,则节省的时间只占整个过程的很小一部分。如果代码执行时间短,则inline调用就可以节省大部分时间。



  1. 使用方法采用以下之一:

* 函数声明前加上关键字inline

* 函数定义前加上关键字

常用的做法是,省略原型,将定义放在本该使用原型的地方。

  1. 对比inline 和 define :



define : define SQUARE(x) x*x 宏定义,这不是通过传递参数实现的,而是通过文本替换实现的,

应尽量使用inline来替换,避免出现不必要的错误

2. 引用变量

引用就是变量的别名,主要用途就是作为函数的形参,通过将引用作参数,函数使用原始数据,而不是数据的副本,使得除了指针之外,也为函数处理大型结构提供了一种方便的途径



2.1 创建引用变量

int rates;

int & rodents = rates;

rodents 就是声明为int & 的变量,& 在这里不是去地址的意思,特别说明的是,和指针对比起来,除了表示方法不同外,必须在声明引用时初始化,而不能像声明指着那样,先声明,再复制。



引用使用起来更加接近const,必须创建时初始化,一旦与某个变量关联起来,就无法改变关联关系



2.2 引用作为函数参数

c语言只能按值传递参数和按指针传递,按照引用传递是C++的新特性,这里举个swap 函数的例子,看看其用法的区别



void swapr(int & a, int & b) // use reference

{

int temp;

temp = a ;

a = b;

b = temp;

}

void swapp(int a, int b) // use pointers

{

int temp;

temp = *a;

a = b;

*b = temp;

}

void swapv(int a, int b) // using values but not working

{

int temp;

temp = a;

a = b;

b= temp;

}

2.3 引用的属性和特别之处

看下面的例子



double cube(double a)

{

a = a a;

return a;

}

double refcube(double& ra)

{

ra = ra ra;

return ra;

}

使用 refcube() 后,传入的ra 会修改传入的实参, 如果程序员的意图是使用实参传递的消息,而不对这些信息进行修改,应该在函数原型和函数投中使用const :double refcube(const double & ra);

按照这个用法,如果编译器发现代码修改了ra, 将生成错误信息。

当然针对以上情况, 其实使用按值传递是更好的用法。



对于引用传递的情况,如果按照下面的代码调用,会发生错误

double z = refcube(x + 3.0)

现代的C++编译器不允许将表达式传递给引用类型, 因为引用对于传递来的参数有更加严格的要求。然而在早期的编译器中,任然可以:由于 x + 3.0 不是double类型的值,程序会创建一个临时变量,并将它初始化为 x + 3.0 的值,然后ra就会做为这个临时变量的引用。



关于临时变量,引用和const



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

具体的使用情况时,当引用参数是const,编译器在如下的情况会生成临时变量



  • 实参的类型正确,但不是左值

  • 实参的类型不正确,但可以转换成正确的类型



这里先要说明, 什么是左值。左值参数是可被引用的数据对象,例如,变量,数组元素,结构成员,引用和解除引用的指针。非左值包括字面常量(除引用括起来的字符串,它们由地址表示)和包含多项的表达式。 常规变量和const都可以视为左值,因为都可以通过地址访问,差别是常规变量可修改,const变量不能修改

回到之前的例子:

double refcube(const double & ra)

{

return ra ra ra;

}

考虑下面的调用:



double side = 3.0;

double * pd = &side;

double & rd = side;

long edge = 5L;

double lens[4] = {2.0, 5.0, 10.0, 12.0};

double c1 = refcube(side); // ra is side

double c2 = refcube(lens[2]); //ra is lens[2]

double c3 = refcube(rd); //ra is ra is side

double c4 = refcube(pd); //ra is rd is side

double c5 = refcube(edge); //ra is temporary variable

double c6 = refcube(7.0); //ra is temporary variable

doubel c7 = refcube(side + 10.0); //ra is temporary variable



创建临时变量只能对常量引用然而如果传入参数的函数的意图是修改参数传递的变量,则创建临时变量会阻止这种意图。



如果使用引用参数的意图是传递值,而不是修改它们,临时变量不会造成不利的影响,反而会使函数在可处理参数种类方面更加通用



《c++primer》中给的建议是,尽可能的使用const,原因如下:



  • 避免无意中修改数据

  • 使用const 可以处理const 和非const实参,否则只能接受非const数据

  • 可以使用临时变量



关于C++11 中新加入的右值引用问题,后面会总结到。



2.4 将引用用于结构



引用主要是用于struct和类



为何返回引用



double m = sqrt(25.0); // a

cout << sqrt(36.0) ; // b



a 中值5, 被复制到一个临时位置,然后复制给m, b 中同理。



如果返回值是引用,可以直接复制。返回引用时,需要注意,避免返回函数终止时的局部内存变量,下面是一个错误的示范:



const freethrows & clone2(freethrows & ft)

{

free_throws newguy;

newguy = ft;

return newguy;

}

为避免这种错误,最简单的处理方法是,返回传入函数的参数。

方法二是适应new来分配新的存储空间



引用用于类对象



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

{

string temp;

temp = s2 + s1 + s2;

return temp;

}

// call version1

result = version1(input, "***");

上面的例子中会用到临时变量



对象,继承和引用



继承的一个特性是,基类引用可以指向派生类对象,而无需进行强制类型转换。实际使用时,定义一个接受基类引用作为参数的函数,调用该函数,可以将基类对象作为参数,也可以将派生类对象作为参数。



使用引用的原因:



  • 可以修改函数中数据的对象

  • 传递引用,而不是整个数据对象,可以提高程序的运行速度



3. 默认参数



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



int harpo(int n, int m = 4, int j = 5);//VALID

int chico(int n, int m = 6, int j); //INVAILD

int groucho(int k = 1, int m = 2, int n = 3); //VALID



默认参数并非编程方面的重大突破,而只是提供了一种便捷的方式,在设计类的时候,通过使用默认参数,可以减少定义的析构函数,方法以及方法重载的数量。



4.函数重载



默认参数可以使用不同数目的参数调用同一个函数,函数多态(函数重载)可以使用多个同名的函数。函数重载像多义词。函数重载的关键是函数的参数列表--也称为 函数特征标(function signature).

如果两个函数的参数数目,类型相同,同时参数的排列也相同,则特征标相同,而变量名无关紧要。C++ 允许定义名称相同,条件是它们的特征标不同。



例如,请看下面的两个原型:



double cube(double x);

double cube(double & x);

从编译器的角度来思考这个问题,假如有这样的代码 cout << cube(x);



参数x 与double x 原型和double& x 都匹配,编译器无法决定使用哪个原型,为避免这种歌调用,编译器在检查函数特征标时,将类型和引用本身视为同一个特征标。

匹配函数的时候,并不区分const和非const变量

海牙注意的一点是,是特征标, 而不是函数类型使得函数可以进行重载,以下的声明是互斥的:



long gronk(int n, float m); //same signature

double gronk(int n, float m); // not allowed

C++ 不允许特征标相同,而返回类型不同的重载



c++实现函数重载的本质是名称修饰(name decoration),它根据函数原型中指定的形参类型对每个函数加密。



5 函数模版

c++新增的特性-函数模版, 实例如下:

template <typename Anytype>

void swap(Anytype & a, Anytype & b)

{

Anytype temp;

temp = a;

a = b;

b = temp;

}

C++ 98中也使用class来创建关键字,上面的例子写成如下的形式:



template <class Anytype>

void swap(Anytype & a, Anytype & b)

{

Anytype temp;

temp = a;

a = b;

b = temp;

}

需要注意的是,函数模版不能缩短可执行程序,还是会实例化成独立的函数。



重载的模版



为满足不同的需求,可以对模版进行重载



模版的局限性



当传入的参数是数组,指针或者结构的时候,很可能是无法处理内部的运算。这里的解决方法是



  • 重载运算符

  • 为特定的类型提供具体化的模版定义(显式具体化[explicit specialization])



C++98中提出了下面的具体化要求:



  • 对于给定的函数名,可以有非模版函数, 模版函数, 显式具体化函数

  • 显示具体化的原型和定义都以template开头,并且通过名称指出类型

  • 优先级 ,具体化 > 常规模版, 非模版函数 > 具体化和常规模版



实例如下:



struct job

{

char name[40];

double salary;

int floor;

};

//non template function

void swap(job&, job &);

//template prototype

template<typenmae T>

vpid swap(T &, T & )

//explicit specialization for the job type

template <> void swap<job>(job &, job &)

实例化和具体化



记住,代码模版本身并不会生成函数定义,只是一个用于生成函数定义的方案。编译器使用模版为特定类型生成函数定义时,得到的是模版实例化(instantiation).传入int的到的实例化为 隐式实例化(implicit instantiation),最初还只能使用隐式实例化,现在C++允许使用显式实例化。这表明可以直接命令编译器创建特定的实例,声明语法如下:



template void swap<int> (int , int )//explicit instantiation

与显式实例化不同的是,显式具体化使用以下的两种声明之一:



//both of them are explicit specialization

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

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

语法区别是:显式具体化声明在关键字template 后包含<>, 而显式实例化没有

特别注意:在同一个文件中,使用同一类型的显式实例和显式具体化将出错



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



对于函数重载,函数模版, 函数模版重载的调用选择问题,被称为函数解析

函数解析的步骤如下:



  • 1. 创建候选函数列表,其中包含与调用函数名称效用的函数和模版函数

  • 2. 使用候选函数列表创建可行函数列表

  • 3. 确定是否有最佳的可行函数。如果有,就使用它,否则,改函数调用出错。



判断最佳的可行函数,从最佳最差的顺序如下:



  • 1.完全匹配,但常规函数优于模版

  • 2.提升转换(例如,char和short转换成int, float 转换成double)

  • 3.标准转换(例如,int转换成char, long 转换成double)

  • 4.用户定义的转换



完全匹配与最佳匹配



Type表示任意类型, 完全匹配允许的无关紧要转换



| 从实类 | 到形参 |

| -----------| -----------|

|Type| Type&|

|Type & | Type|

|Type []|*Type|

|Type(argument-list)|Type(*)(argument-list)|

|Type | const Type|

|Type | volatile Type|

|Type * | const Type|

|Type | volatile Type |



如果没有最佳的匹配,编译器无法完成重载解析过程,将生成错误信息"ambiguous".

然而即使2个函数完全匹配,仍可以完成重载解析,首先,指向非const的指针和引用优先与非const指针和引用匹配,其次非模版优于模版



关键字 decltype



C++ 11新增关键字decltype



int x;

decltype(x) y; //make y the same type as x

也可以给decltype提供表达式参数



decltype(x + y) xpy; //make xpy the same type as x + y

使用decltype时,编译器需要一个核对表

decltype(expression) var;



核对表简化如下



  • a.如果expression 是一个没有括号起来的标识符,则var的类型与标识符的类型相同,包括const



double x = 5.5;

double y = 7.9;

double &rx = x;

const double * pd;

decltype(x) w; //w is type double

decltype(rx) u = y;//u is type double&

decltype(pd) v; // v is type const double *

  • b.如果expression 是一个函数调用,则var的类型与函数返回的类型相同

long indeed(int);

decltype (indeed(3)) m; // m is type long

这时,并不会实际调用函数,编译器会通过查看函数原型来获悉返回类型,而无需实际调用函数

  • c. 如果expression 是一个左值,则var为指向其类型的引用

  • d. 如果前面的都不满足,则var与expression类型相同



C++新增的声明和定义函数的用法



double h(int x, float y);

现在新增的用法是:



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



用户头像

Dreamer

关注

一个不想做搜索的NLPer不是一个好的CVer 2019.12.18 加入

还未添加个人简介

评论

发布
暂无评论
C++primer-函数探幽