C++ 函数探幽

2014/10/22 01:07 am posted in  C++

昨天刚制定好学习计划,今天的任务是C++第八章,╮(╯▽╰)╭C++拖得太久了,耽误了好多事情。

好不容易把引用变量之前的写完了,因为草稿保存故障,又要重写,真心伤不起。

第八章是第七章的继续,继讲了函数部分。如果说第七章讲的是C++与C语言函数部分的共同点和微妙差异的话那么第八章讲的就是C++函数的独特之处。C++较之C语言提供了许多新的函数特性,包括内联函数、按引用传递变量、默认的参数值、函数重载(多态)以及函数模板。

内联函数

内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的区别不在于编写方式,而在于C++编译器如何将它们组合到程序中的。As we all know,普通函数会在内存中占唯一的内存空间,每次调用函数的时候,CPU会终止当前运行的代码(并把当前运行地址保存到栈中),跳转到函数处继续执行,知道函数执行结束再跳转回来。然而跳转是需要时间的,尤其是跳转频繁的函数。因此,内联函数的目的就是把函数镶嵌到应当调用函数的代码块中,直接执行而不是跳转。虽然代价是消耗更多的内存空间来保存内联函数执行代码,但是却能节省下不少跳转时间。

也是因为内联函数节省的是跳转时间而且以内存作为代价,我们需要有选择的使用内联函数,如果执行函数代码时间比处理函数调用机制的时间长,则节省的时间将只占整个过程中很小的一部分,反之,如果代码执行时间很短,则时间优化很明显。我认为,对于调用频繁或代码短小的函数最好使用内联函数。

怎么使用内联函数

1.在函数声明前加上关键字inline

2.在函数定义前加上关键字inline

也就是说,如果想要使用内联函数,那么声明和定义前都需要加上关键字。当然,如果省略原型的函数,也就是声明加定义在一起的函数,写一次就够了。

总之,用不用内联函数是程序员的事,怎么实现内联函数是编译器的事。但是不是你认为使用了内联关键字就内联了,因为编译器不会总满足程序员的请求,如果编译器觉得函数代码过长或者内联函数调用了自己(内联函数不允许递归),因此不将其作为内联函数。当然这些标准的主动性在编译器。

因为内联函数和C语言的defin很像,但较之C的宏定义函数,还是内联函数更胜一筹,因为内联函数本来就是个函数,而define只不过是替换罢了,函数更容易查错和避免一些有歧义的语句。

引用变量

引用变量是C++新定义的一种复合类型。引用时已定义的变量的别名,我总感觉引用和指针差不多,只不过是对那些不先用*的安慰品。

其实引用最大的作用就是帮助函数传参,和指针类似,传递的实参没有被复制,而是直接访问原变量的内存空间,从而节省时间。(还有一个作用,像double在复制时也会有精度损失,引用传递double可以避免损失)。

创建引用变量,其实很简单,和指针类似

对于int a;

创建应用 int &b = a;

这时候b就可以和a一样使用了,他们两个控制的是同一个内存空间。

但是要注意,对新建一个引用,必须要在声明的时候把他定义,这一点有点像指针常量的声明。

也正是因为引用访问原变量的存储空间,在引用传参的时候,要和指针一样注意原数据的保护(不如,多多使用const)。

对于使用引用传参的函数,传递的必须是一个变量也不应是一个值(换句话说,就说传递的参数必须有个存储空间)。

比如 void fun(int &n)函数,传参fun(a + 3)是非法的。(有些老编译器只是警告,但是重新开辟了临时空间保存a+3的值)。

对于引用不匹配,编译器可能会开辟临时存储空间(和普通函数效果一样),但是开辟是有条件的。如果参数仅为const引用和满足一下条件时C++才允许这么做:

1.实参的类型不正确,但不是左值(不可以改变的常量)

2.实参的类型不正确但可以转换为正确的类型(比如int转double)。

但是,如果接受引用参数的函数的意图是修改作为参数传递的变量,那么创建临时变量将阻止这种意图的实现,因此此时要避免创建临时变量。

如果函数返回的是个引用,应该要注意,如果变量是在函数内定义的,如果返回这个变量的引用是危险的,因为这个变量在函数结束的时候便被释放了。而返回、访问一个被释放的变量的空间十分危险。

因为函数返回可以是引用,也就是可以修改的左值,那么 function(a,b) = c;这种语句是合法的。虽然高大上但有点费劲,避免这种语句的良策还是const返回类型。

C++有个有趣的现象,因为类可以继承,所以对基类的函数参数,既可以用基类,也可以用派生类。

如果需要显示小数模式则

setf(ios_base::showpoint); //将对象置于显示小数模式
precision(); //制定小数显示多少位

对于这些设置会一直保存,知道在修改回默认。

使用引用的原因有两个:

1.程序员能够修改调动函数中的数据对象

2.通过传递引用而不是整个数据对象可以提高程序运行速度

但要注意,对于本来就是传递地址的数据对象比如数组只能使用指针。

默认参数

默认参数是C++中十分炫酷的功能。默认参数指的是当函数调用中省略了实参是自动使用的一个值。

很吊大上的功能,使用起来也很方便。

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

当调用函数left的时候,如果只写了一个参数str,那么第二个参数默认n=1。

我认为很屌的功能但C++PP确认为并非编程方面的巨大突破,而只是提供了一种便捷的方式。在设计类时,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。

对于使用函数默认参数,只有原形制定了默认值,而函数定义与没有默认参数时完全相同

因为默认参数调用时可以省略,导致如果你为一个参数设定默认值,你必须为其右边所有参数设定默认值!

函数重载

函数多态是C++在C语言基础上新增的功能。而函数多态能够使用多个同名函数。术语“多态”指的是有多种形式,因此函数多态允许函数可以有多种形式。类似的,术语“函数重载”指的是可以有多个同名函数,因此对名称进行了重载。这两个术语指的是同一回事,但我们通常使用函数重载。可以通过函数重载来设计一系列函数——让他们完成相同的工作,但使用不同的参数列表

函数重载的关键是函数的参数列表,也称之为函数特征标,所谓参数列表就是,函数的参数的数目和数据类型(不包括形参的变量名以及返回类型)。

对于重载不是完全匹配,编译器不会武断的停止匹配而是尝试使用标准类型转换强制进行匹配,比如,把int实参转化为double类型。但是,如果在不完全匹配的情况下,通过转换存在多种较为合理的解释,编译器便会保持。其实,不论是否非完全匹配,只要在函数重载中存在歧义,编译器都会报错

不止是在调用的时候,如果在声明了两个特征标相同的函数,编译器也是不允许的比如下面(不要把数据类型的引用和数据的类型本身会为一谈):

double cube(double x);

double cube(double &x);

不仅如此,特征标不区分是否是const和非const变量。因为正如前面,非const函数只能接受非const实参,而const函数能接受非const实参和const实参,因此使用范围有交集,也就是说有歧义。编译器不允许这么做。

重载引用参数存在特殊性。

void stove(double & r1);
void stove(const double & r2);
void stove(double && r3);

double x = 1.0;
const double y = 2.0;

stove(x);
stove(y);
stove(x + y);

对于第一个函数,要求一个可以修改的左值,对于第二个函数,要求一个不可修改的左值,最后一个左值引用参数r3将于一个左值匹配。

因此,x调用第一个函数,y调用第二个函数,x+y调用第三个函数。

但是,这并不是绝对的,因为,对于x+y,第二个函数也是可以行得通的(不可修改的左值)。因此,如果

void stove(double && r3);

不存在的话,编译器会自动匹配到

void stove(const double & r2);

这就是重载引用参数时,编译器会自动调用最匹配的版本。

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

如果需要使用不同类型的参数,则默认参数便不管用了,在这种情况下,也可以考虑函数重载。

函数模板

函数模板也是C++新增特性,函数模板是通用的函数描述,也就是说它们使用泛型来定义函数,其中的泛型可用具体的类型如int,double替换。通过将类型转化为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型的方式编写程序,因此有时也被成为通用编程。由于累心是由参数表示的,因此模板特性有时也被成为参数化类型。

怎么使用函数模板

template
void Swap(AnyType &a,AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}

第一行指出,要建立一个模板,并将类型命名为AnyType,关键字template和typename是必须的。除非使用关键字class代替typename(在C++98添加typename之前,都是用class来创建模板的)。另外,必须使用尖括号,类型名可以任意选择(这里的AnyType)。

模板不会创建任何函数,只是告诉编译器如何定义函数,当调用实际参数的时候,编译器才会根据传递的参数类型自动生成函数。

如果声明和定义是分开的函数模板,声明部分和定义部分的前面都要加上template 以保证让编译器知道这是函数模板使用的数据类型。

注意:函数模板可能会缩短代码量但不会缩短可执行代码量,因为用不同的类型调用函数模板,会确确实实生成的不同函数,就像手工写了这两个函数一样。因此函数模板的本质就是给编译器一个自己写函数的模型。

函数模板的好处就是生成多个函数定义更简单更可靠。

更常见的形式是将模板放在头文件,并在需要时用的模板的文件中包含头文件。

需要对多种类型使用同一种算法时可以考虑函数模板,但是,并非所有的类型都使用相同的算法,因此,为了解决这个问题,可以像重载常规函数定义一样重载函数模板定义。但是同样要注意,要保证被重载的函数模板的特征标不同(在函数模板中,并非所有的参数为模板参数类型)。

函数模板的局限性

template
void f(T a,T b)
{
if(a > b)
a = b;
...
}

像这样的模板,如果a,b是普通的常规数据类型,也无大碍,但是如果a,b传入了数组,那么if比较的是两个地址没有意义,如果执行if里面的语句将是不合法的。

因此即使使用函数模板,但其中的小细节仍然需要注意语法。

对于上述也不是没有解决办法重载>和=运算符也可以实现。

显式具体化

如果想要交换两个值,传入的是两个结构,函数模板也能正常使用。但是我们传入两个结构,想交换其中两个结构的结构成员,那么我们就需要明确到底是什么结构的什么成员。这时候,单纯的函数模板就无法实现,我们需要显式具体化。

我们提供一个具体化函数定义,其中包含了所需代码,当编译器找到与函数匹配的具体化定义时,将使用该函数而不再寻找模板。

具体化方式有很多种,C++98采用了第三代具体化:

1.对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及他们的重载版本

2.显示具体化的原型和定义应以template<>打头,并通过名称来指出类型

3.具体化优先于常规模板,而非模板函数优先于具体化和常规模板

如果交换job结构的示例:

//非函数模板
void Swap(job & a,job & b);

//常规函数模板
template
void Swap(T & a,T & b);

//显式具体化函数模板
template<> void Swap(job & a,job & b);

其中是可选的,因为函数的参数类型表明,这是job的一个具体化,也就是说可写可不写。但是,如果在参数类型中没有job,那么,写这个还是很关键的!

在使用显示具体化的时候和使用函数模板一样要声明定义都要加template<>

实例化

就像前面说的,模板不会生成函数,但可以调用模板之后,使模板实例化,从而生成实例函数,这被称为隐式实例化,早期的C++也只允许这么做,但现在C++允许显示实例化。这就意味着可以直接命令编译器创建特定的实例,耆域法师声明所需的种类用<>符号指示类型,并在声明前加上关键字template,如:

template void Swap(int ,int );

实现了这种特性的编译器看到上述声明之后将使用Swap模板生成一个使用int类型的实例。

当比较一下显式具体化:

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

还是有差别的,一定要注意,差别不仅仅是有无<>,更大的区别在于是否使用Swap模板创建函数,实例化是用Swap创建函数,而具体化是不使用模板创建一个新函数

实例化有什么用呢?

比如我们有int a,double b;

有个函数模板:

template
T add(T a,T b)
{
return a
}

int a = 1;
double b = 2;

cout (a,b)

这样就会强制使用double的实例。

隐式实例化,显示实例化,显式具体化统称具体化,他们的相同之处在于表示的都是适用具体类型的函数定义而不是通用描述。

最后,一定要注意,要区分使用template和template<>来使用显示实例化和显示具体化。

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

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

1.创建候选函数列表,其中包括与被调用函数的名称相同的函数和函数模板

2.使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐形转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。

3.判断是否有最佳可行函数,否者报错。

那么,最后一步中的是否最佳的顺序如何确定呢?

1.完全匹配,但常规函数优先于模板

2.提升匹配,如(char,short提升为int后匹配)

3.标准转换,如(int转换为char,long转化为double)

4.用户定义的转换,如类声明中定义的转换。

C++允许某些无关紧要的匹配比如int匹配给const int,char[]和char *之类的。这意味着用作实参的函数名与用作形参的函数指针只要返回类型和参数列表相同,就是匹配了。

然而,有时候即使两个函数都完全匹配了,然可完成重载解析,首先,直线非const数据的指针和引用优先于非const指针和引用参数匹配。然而,const和非const之间的区别值适用于指针和引用指向的数据(const的影响,更强调与指针和引用)。

对于函数只有模板,那么,更为具体的模板优先。

对于只有需要转换的函数,那么,转换最少的函数优先(更具体)。

其实我们可以手动选择,比如在调用函数模板的时候

add<>(a,b);

这时选择模板优先有常规函数。

对于多参数的函数情况更复杂,编译器必须考虑所有参数匹配的情况,什么是一个合适的函数呢?

1.其所有参数的匹配程度都必须不比其他函数差

2.同时至少有一个参数的匹配程度比其他函数都高

C++11给了模板函数更大的发展。主要的进步在于添加了decltype关键字,用来确定数据类型。

那么,这种模板就成为了可能:

template
void Add(T1 a,T2 b)
{
decltype(a + b) ans = a + b;
}

这个是在过程中出现不知类型的中间变量就使用decltype关键字,如果返回类型呢?

C++11允许返回值后置

template
auto Add(T1 a,T2 b)
{
decltype(a + b) ans = a + b;
return ans;
}

强大的自动类型auto啊!

编译器如何确定使用哪个函数是个十分复杂的机制,还需后续了解。