首页 > 代码库 > C++primer函数进阶

C++primer函数进阶

一、内联函数

内联函数是为了提高效率而出现的,它和普通函数的的不同不表现在编写形式上,而是组合到整个程序当中的方式不同。函数的每一次调用都需要开辟栈空间,跳转等一系列的操作。对于一些函数代码比较少,并且多次使用的函数,可以把他们声明为内联函数,内联函数在 程序中直接把函数调用的地方替换成函数的实际代码,这样一来就会提高效率,但是需要占用更多的内存。一般来说,内联函数将会丢弃声明部分,直接在声明的部分进行函数的声明和定义。在 定义内联函数的时候,只需要在函数返回类型的前面加上inline这个关键字即可,但是,即使你声明这是一个内联函数,但是程序在组合这个函数的时候不一定就真的会按照内联的特点来处理这个函数,如果这个函数有递归调用,或者代码很长,那么这些函数即使你声明了为内联函数,他也不会把你当作内联函数处理的,只会当作普通函数。


对于内联函数来说,他和C语言中的宏定义是由相似之处的。但是两者的不同主要体现在函数参数的传递上。

内联函数,仍然是严格意义上的一个函数,按照函数的值传递规则进行传递函数参数。

但是宏定义却不是这样,在利用宏定义去定义个类似函数的功能时,例如

#define square(x) x * x 

利用宏定义定义一个计算一个数字平方根的函数,加入我有以下三种的调用方式

square(1+5)

square(c++)

这两者在调用 的时候就会出现问题,第一个会出现运算顺序不当造成的结果不正确,第二个会造成c变量在一次的函数调用中+1两次破坏变量的结果。

为什么会出现这种情况,因为宏定义是通过文本替换来实现的,并不是实际意义上的函数调用。

按理来说,第一个例子传递的应该是6,在程序的编译阶段这些常量表达式的值都应该会被算出来并且把相应的位置都替换成结果相同的常量。但是宏定义只是做了简单的替换。总的来说就是,不够智能。在定义一个类函数功能的时候,不能按照函数的一些规则去进行实施。


这个时候你用内联函数就是最好不过了,两者的行为几乎差不多,但是内联函数毕竟是一个真正的函数,他会按照函数传递参数的规则算出传递函参数的表达式的值然后再进行传递。

所以在对一些函数功能的运用时,还是用内联函数比较好。


二、引用变量

引用变量也是C++新特性中的一个新加入的复合类型。它有一个准确的定义就是已经定义的变量的别名。当把一个引用变量和一个变量进行绑定的时候,那么使用本身的变量和使用引用变量是一样的。

那么引用变量最大的用处在哪里呢,就是在函数参数的传递上面。

通过上面的简单论述你可能会知道,引用变量可以使用和他绑定的那个实际的变量,并不是拷贝了一个变量的副本,而是使用这个变量的原本的数据,这就在函数参数的传递一些比较大的数组和结构上面有个更大的用处。引用变量的使用方式相对较为简单,至少比使用指针的出错几率要小很多。

而且还能达到和指针一样的效果。


1、引用的定义

先给一个例子

int a;

int &p = a;

p就是a这个变量的引用。这里的&运算符在这里定义引用的时候也算是一种运算符重载的情况。它不再是 取地址符,或者是按位与操作,而是像char *这样的定义,int &是一个整体,表明了p是一个指向int型数据的一个引用变量。

他是这个变量的一个别名,两者都指向同一块内存空间。


引用和指向一个变量的指针还是有区别的。比如

int *ptr = &a;

那么 ptr和&p是一回事,这里的&是取地址的作用

并且在引用定义的时候,一个最重要的和指针的区别就是,引用在定义的时候必须进行初始化操作,但是指针不用。

引用更像是一个const的指针,必须在定义的是就初始化为一个变量,这样,这个指针或者引用将一直与初始化时候的变量进行绑定,不能够再重新指向其他的变量。


引用的值的改变,只能在它定义的时候给他初始化。如果你在之后的程序中,通过一个变量的赋值企图让引用变量改变之前绑定的变量,那么 效果只会适得其反。你会因为这种行为改变初始化给引用变量的那个变量的值。总而言之,引用变量,只能在初始化的时候指定它所要绑定的变量,不能通过之后的赋值改变其值。

再次重申一次,引用变量一旦被初始化,那么就再也不能被改变了。


2、引用用作函数参数

很简单的一个例子就是交换两个变量的函数

int swap(int &a, int &b)

{

int  t;

t = a;

a = b;

b = t;

}

相信大家都已经知道,这个函数的内部虽然和按值传递a b 这个参数的swap函数没什么区别,但是按值 传递的话,这个函数体内的代码是不能实现变量转换的。int swap(int a, int b);如果函数是这样的话,那么a b是实参的副本,也就是说拷贝了一份实参,a b是两个新的变量和调用时传递的实参没有区别。但是前面的例子把变量初始化给引用变量,a b就相当于实参的别名,对ab的操作就是对调用者传递的实参进行操作,类似于指针但是和指针的运用方式不一样。



3、关于传递引用参数以及临时变量的创建

在当前的标准下,仅仅是被调用函数的参数是常量引用变量的时候,在函数传递的过程中才会创建临时变量。

那么当引用参数是const,又需要什么进一步的条件才能真正确定的要创建临时变量呢,有两种情况:

(1)实参的类型正确,但是不是左值。

这里面有必要给大家讲一个概念就是左值:左值就是可以被引用的数据对象,比如变量,数组元素,结构成员等等,也就是有名的数据对象。现在,常规变量和const变量都可以作为左值,但是一个是可修改的左值,另一个是不可修改的左值。左值就是可以通过地址来访问的数据

(2)实参类型不正确,比如要传递给一个double的引用,但是实参确是一个int型变量。


上述的两种情况有如下两个例子

int temp(const int & t);一个函数原型是这样的。

double x;

temp(x);

temp(2.5);

第一种调用是实参类型不正确,这个时候编译器将创建一个临时的无名变量,然后把x转化为int型变量,然后传递给引用参数。

第二种是是实参的类型正确,但是不是左值,也就是说传递的这个实参是没有名字的。所以也将创建一个临时变量。 临时变量只在函数调用期间存在,一旦用完,即可随意删除。

为什么对于常量引用就是可行的,但是对于其他的情况就是不可行的呢。首先我们要明确,一个常量引用,它绑定了一个变量,但是我们不能通过这个引用去更改他所绑定的变量,如果这个引用是常规的引用,那就不会是这样情况。

由于他不能够用引用去更改变量,那么 其实就和值传递是一样的,

long a = 5;

long  b = 3;

void swap(int &x, int &y);

swap(a,b)

如果是像这样执行的话,那么a b会因为实参类型不对而创建两个临时变量,然后把x y 两个引用参数绑定到两个临时变量上,那么之后进行变量转化,转换的也是临时变量,和实参的a b是没有任何关系的。所以在你想修改作为参数传递的实参变量的时候,创建临时变量会阻止这种事情发生的。唯一的办法就是禁止创建临时变量。现在C++的标准规定,只有函数参数有常量引用参数的时候才允许创建临时变量,不然都会报错或者警告。


综上所述,在形参为const引用的函数中,如果出现上述的两个条件之一,他们的行为都类似于值的传递,因为临时变量的关系他们不会更改原来的实参。


4、将引用用于结构

引用关于结构上的,一个是传递结构参数的时候传递给引用参数,另外一个就是函数的返回值。

在传递结构参数的过程时,有三种方式,传递指针,传递值,传递引用。传递值不但效率低而且不能够改动原来的实参,传递指针则较为复杂 ,传递引用是结合了两者的优点。


函数的返回值,大多数都是和按值传递的函数一样,return算出后面的表达式的值,然后将值赋值给一个临时的变量,之后调用者去使用这个值。但是传递引用就省去了中间要把值传递给临时变量的这个一个环节,直接赋值给接受这个函数的返回值的目的变量即可。

 

而且在返回引用的时候要注意,不要返回函数内部的局部变量的引用,因为在函数运行结束后,相应的分配给这个函数的栈空间都会一次性的回收,很有可能会返回一个并不存在的变量的引用。

一种办法是返回参数中的一个引用,因为参数中的引用是和调用者传递的实参绑定的。


在函数的引用类型是string对象的时候,传递的实参可以是string对象,或者是char *的指针,字符串的字面量,以空字符结尾的char数组名。

一方面来说,string中定义了可以把C风格的数组转化为string的功能。另一方面,如果这时的引用参数是const string,那么他会创建一个临时的变量,把实参放进去,然后在绑定一个类型正确的引用。


5、关于类的继承和引用的联系

类的继承,自然有基类和派生类。在定义一个基类的函数的时候,派生类的对象也是可以调用它的。关于引用的话,假设基类的一个方法的参数是一个基类的对象的引用变量,但是我们在使用派生类对象调用这个方法并且传递的实参也是派生类的对象的话一样可以调用。



三、默认参数

默认参数的定义很简单,就是在调用一个函数的时候,没有传递相应的实参而自动的使用的一个值。

在程序中 我们可以通过函数的原型来设置默认函数的值。

再给函数设置默认参数的时候唯一的一点就是要注意顺序,它是从右向左的顺序。

也就说,如果你设置可一个默认参数,那么你的右边将都会是默认参数,左边是常规参数。这是因为,在调用函数传递参数的过程中,必须依次把实参传递给对应的形参,如果你的默认参数在中间,程序是没办法识别你的意图的,他只会把要传递给后面形参的实参提前传递。

定义默认参数一定要在原型中定义,函数的定义部分和之前是没有区别的。


四、函数重载

默认函数能够让你使用不同的参数调用同一个函数,但是函数的重载可以使用不同的参数调用同一个函数。

函数重载的关键是函数的参数列表也成为函数的特称标。

C++允许定义同名函数,但是他们的参数列表必须是有区别的,这里的区别是指类型,顺序,不是参数的名字。

可以这么理解,C++是靠函数的特征标来识别不同的函数。

在重载了一个函数多次得到不同版本的时候,在调用函数的过程中会自动找到与其对应的特征标的函数进行调用。

但是如果定义的这个函数并且重载了几个版本之后,函数的某一次调用仍然找不到正确的特征标进行调用时,C++将会使用强制类型转换,转换成一个已有函数的版本进行调用。

但是,在强制类型转换的过程中也会出现问题。

例如我定义了一个int swap(int do)的函数,并且重载他int swap(char * p)

我在调用的时候使用

float x = 9.0

swap(x)

因为传递的x的值和函数的形参的类型是不对应的,所以要进行强制类型转换,因为double不可能转化为指针,所以就会尝试转化为int进行继续调用。

但是如果再重载一次函数,int swap(double dd);

这样的同时出现了两种可能的转化,那么此时计算机就不知道该怎么做了。

还有一个要注意的是,在函数重载中,编译器把变量和其引用看作是同一个特征标。

也就是说

int swap(int yt)

int swap(int &yt)

int x = 9;

这两个在函数重载的过程中会出错。swap(x)将会和上面两个版本都匹配,所以编译器不知道要调用哪个函数,这样就会报错。

并且在重载函数的函数列表中,不要把加不加const来作为特征标的决定性因素,函数的重载是不区分const和常规变量的。

但是在传递参数的时候要注意,不能把const的参数赋值给非const的参数,但是反过来是可以的。


函数的重载,区分不同的版本主要是靠参数列表。和函数的类型没什么关系,函数的类型可以相同也可以不同,但是如果你的函数名相同,特征标相同,那么编译器就会认定这样你重复定义了两个相同的函数。所以要保证特征标的不同才能做到函数重载。


五、函数模板

函数模板,简单来说就是要定义一种通用的函数。这个函数中的所有的数据类型,都可以通过之后调用的时候传递你想要的数据类型进行计算,那么这就很方便那些一个算法用于不同种数据类型的函数。

模板只是定义函数但是不创建函数,只是给了一个整体的框架,需要我们传递 给他以相应的类型参数才能够真正的使用它。


如何定义函数模板呢:

temlate <typename anytype>

void swap(anytype &a , anytype &b)

{

anytype x;

x = a;

a = b;

b = x;

}

上述的代码就定义了一个函数的模板。

需要交换两个什么样的数据类型的数据的时候,anytype就会变成相应的那个数据类型,编译器就会根据指定的类型根据模板创建这个函数。早期的标准使用class代替 typename.


在定义了函数的模板之后,我们不用管其他的事情,还是按照正常声明定义使用这三个步骤进行调用函数。当你传递的数据类型明确的时候的,编译器会自动为你生成和你的数据类型相对应的版本的函数供你调用。在最后的可执行程序中是没有模板存在的。我们也可以看到了,由模板生成一个确定的函数都是在函数的编译阶段下完成的。所以最后的结果还和我们手工定义的函数一样,没什么区别,但是应用模板最好的好处就是代码简洁,可以减少我们的犯错率。


1、重载模板

需要对多个不同类型使用同一个算法的时候,可以使用函数重载也可以使用模板。但是,如果不同的类型需要不同的算法呢。那么我们就需要重载模板。重载模板和重载函数其实很相似,重载的重要标志就是特征标要不同。当然temlate <typename anytype>应该都是一样的,所有的模板都是用这个开头的。


2、模板的局限性

由于模板函数的通用型,所以很可能一个模板处理不了一些类型。比如函数代码是a = b * a。如果说是整数或者说是浮点数那么还好,如果传进来的ab是两个字符串的指针呢,这就无法处理了。

一种比较直接的方法就是为特定的类型指定特定的模板。


3、显示具体化

 在使用模板代码的时候,如果我们需要一个不同版本的代码,但是同时我们又不能够改变模板中的特征标,那么我们就无法通过重载模板来达到目的。所以就要提供一个具体函数的的定义,这种操作叫做显示具体化。

其中包括我们所需要的代码,当编译器找到与函数调用相匹配的具体化定义的时候,就不再调用模板,直接使用该具体化定义。


对于一个给定的函数名来说,可以有常规函数,具体化函数,模板函数。如果上述三种函数同时存在的话,那么在选择的时候,优先级依次降低。也就是说,如果能找到对应的常规函数就不用管后两个,如果在调用函数找到了和函数对应的数据类型也对应的具体化函数,那么就调用这个,最好在决定调用模板函数。


具体化的函数定义如下

template<> void swap<job>(job&, job&)其中<job>可以省略,这个具体化函数是专门为job数据类型准备的,如果具体化函数和模板函数同时存在的话,那么job类型的数据只会调用具体化函数。


4、实例化和具体化

实例化:

函数模板不是函数的定义,只是给编译器一个创建函数的方案。当我们在以平常使用函数的方式传递给函数数据类型的时候,得到的是一个模板的实例化的实体,也就是特定类型的一个函数,这个函数的是通过模板建立的。这种实例化的方式叫做隐式实例化。

那么什么是显示实例化呢,就是template void swap<int>(int&, int&)

与显示实例化不同的是,具体化使用这样的声明方式

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

主要的区别就是,具体化的声明是不要用函数的模板来生成函数的定义,而是单独的为他生成一个函数。而显示实例化呢,是通过模板创建一个特定类型的函数,以及template后面的<>。


另外一种创建显示实例化的形式就是直接在程序中调用相应的函数,这样即使与模板的规范不对并且不在之前声明,编译器也会正常的生成一个实例化的函数。比如

int x = 3;

double y = 0.9

swap模板显然和swap(x,y)这样的 调用是不对应的。所以直接在程序中使用swap<double>(x, y),编译器会强制的生成一个double的实例化。并且 将x参数的类型转化为double型,以便于的个参数匹配。

这里传递的仅仅是数字是没有问题的,如果函数模板中的参数是引用呢。

还是swap<double>(x,y)的调用。虽然会生成一个double的显示实例化swap<double>(double &a, double &b),但是之后再传入参数的时候,整数类型的数据是不能够绑定到double数据类型的引用上的。


显示 具体化,显示实例化,隐式实例化都是具体化。对于隐式实例化和显示具体化,都是根据相应的调用来决定去使用哪一个,显示具体化是我们手动要在程序运行之前定义好的。但是显示实例化不是的,如果是显示实例化,只需要一个声明,那么就会使用模板来定义生成这个相应的版本。而隐式实例化是在函数调用的时候根据传递的参数的数据类型由编译器生成的一个函数。


5、关于decltype

decltype在声明模板函数时,如果碰到不确定使用什么数据类型将有很大的帮助

比如一个两数相加赋值给第三个变量的模板函数,三个变量分别是a b c

c = a + b

那么C的数据类型将是不确定的,因为由于ab类型的不同,可能会有数据类型上的提升。所以这时候,利用decltype(a+b)c = a + b就可以成功解决问题。将c变为a+b结果的数据类型。


但是如果碰见这样的模板函数呢

?type? gt(t x2, t x1)

{

return x2 + x1;

}

如果你想把type处用decltype的话,那么就错了,因为在函数返回类型处,还没有定义x2 和 x1这两个参数,所以不能用。

C++为了处理这种情况,用了一种叫做后置返回类型的办法。

auto gt(t x2, t x1)->decltype(x+y)

这样的话先用auto占位,之后用后面的decltype明确了类型之后来代替前面的auto.

这种办法也可以用于函数定义

auto swap(T x, T y)->double

则函数的返回类型最后为double。

C++primer函数进阶