现在的位置: 首页 > 综合 > 正文

学习笔记之函数探幽(一)

2013年07月18日 ⁄ 综合 ⁄ 共 4062字 ⁄ 字号 评论关闭

以下内容为Hughen个人学习C++ Primer Plus(第6版)后所写,欢迎一起探讨学习。

内联函数

给我印象最深的莫过于内联函数(inline),因为平时在编程中对程序效率考虑关心的较少,所以看到这个关键词时感觉像新的一样,从满了好奇,从满了求知欲。

解释内联函数需要用到下面一段代码:

#include <iostream>

using namespace std;

inline void inlineFunc(int x);          // 注意内联函数的声明和定义前需要加上关键字 inline
void normalFunc();

int main()
{
inlineFunc(0);
normalFunc();
inlineFunc(5);
}

inline void inlineFunc(int x)
{
cout<<"\n这是内联函数,其中x为 "<<x;
}
void normalFunc()
{
cout<<"\n这是普通函数";
}

所谓的内联函数就是在编译器将函数代码替换成函数调用,导致的效果就是程序无需像函数调用那样跳到另一个位置处开始执行代码,然后再跳回来。上述代码被编译之后的直接效果如下图:(演示)

我们可以很明显的看出,原来调用inlineFunc(int)的地方都已经直接替换为了函数的代码,计算机在执行的时候可以直接执行到底,无需跳转,加快了程序执行速度。

注意:内联函数是不容许递归调用的。

当然,C++中的宏(#define)也实现方式也和内联函数外在上基本相同,不过宏不能按值传递。

既然讲到了按值传递,自然就会想到按引用传递……

C++中的引用

使用引用类型数据,需要一个特征符号—— &

例如:

int x;
int & tx = x;         // tx就相当于x的一个别名,它们指向同一个内存单元

这里的“&”不是地址运算符,而是类型标识符的一部分,int &指的是指向int的引用。其中必须在声明引用的同时初始化,而不能像指针那样,先声明,再赋值。也就是说引用更接近于const指针,必须在创建时就给予初始化工作,一旦与某个变量关联起来,就将一直小众于它。

我们使用引用的大多数目的都不像上面那样,使用引用类型主要是用于做函数参数的时候——按引用传递。(C语言中只能按值传递,不过使用指针就避免了按值传递的限制)

所谓按引用传递,就是说函数体在执行的时候,是直接操作的传进来的形参,不像按值传递那样,要先复制一遍传进来的形参,产生一个临时变量,再执行函数体内容。所以按引用传递就会产生一个直接效果——无需复制形参,节省了时间,也节省了空间(对数据比较大,如结构和类)。

这里说到临时变量,又是可以说很多,大致的理解是“如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做。”(见C++ Primer Plus (第6版)的262页)

C++11新增了一种引用——右值引用(rvalue reference),如下:

int x = 34;
int && y = 9 * x + 100 - x * x;         // 不允许使用int &
cout<<y;

对于函数的返回值倘若要使用引用类型,规则也类似。不过无法返回非引用类型变量,因为程序无法修改已经释放了内存。基本格式如下:

const string & func(string &s1, const string &s2)
{
s1 = s2 + s1 +s2;
return s1;
}

有了按引用返回的值后,我们就可以用下面的代码,虽然它可能没有任何使用意义:

string & _funcT(string &s1, string &s2)
{
s1 = s2 + s1 + s2;
return s1;
}
.......
_funcT(s1,s2) = s3;

// 效果等同于下面的代码
_funcT(s1,s2);
s1 = s3;

当然,引用的还有一个优点就是:基类引用可以指向派生类对象,而无需进行强制转换,也就是说,可以用一个基类引用类型做函数的参数,例如:

void myFunc(ostream & os)
{
......
}
......
ostream t1;
ofstream t2;
myFunc(t1);
myFunc(t2);         // 两个函数都合法,因为ofstream继承至ostream

函数重载

在函数重载方面,大概需要注意就是如下代码是合法的重载:
void dribble(char *bits);
void dribble(const char *bits);         // 合法的dribble函数重载

为什么会这样呢??

说原因嘛,就要追溯到重载解析中的匹配优先级的问题,其中当遇到像上述两个函数时,也就是完全匹配的时候,指向非const数据的指针和引用优先与非const指针和引用参数匹配,也就是说,假如一个变量char *b,那么在调用dribble的时候,首先会与非const参数的函数结合,也就是

void dribble(char *bits);

然后const和非const之间的区别只适用于指针和引用指向的数据,假如是下面的这种代码将出现二义性

void recycle(char ch);
void recycle(const char ch);       // 这种重载是不允许的,会出现二义性

函数模板

首先,一看到这个就觉得很头大,因为平时用的少,觉得也很难设计出一个复用性高的模板,所以在平时候的编程中函数模板使用的太少,对于有些知识点都遗忘了。

先说一下基本的格式规则:

template <typename AnyType>
void Swap(AnyType &a, AnyType &b)
{
AnyType tmp;
tmp = a;
a = b;
b = tmp;
}

这里的typename可以用class替换。(C++98之前是使用的class来创建的模板,且此class类class
在模板函数声明的时候也需要用 template <typename AnyType> 来标注出使用的模板类型
注意:使用函数模板并不能缩短可执行程序,经过编译器编译链接之后,程序包含的全部是实际的代码。

在函数模板上,有一个比较特殊的模板,具体化函数定义——显式具体化(explicit specialization),使用它的目的是为了跳过模板函数,直接使用这个特殊定义的模板,格式如下:

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

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

关于模板具体化这一块,涉及的内容很多,也比较复杂,其中关键点就是在编译器选择使用哪个函数版本的时候,讲究的优先级比较多,从最佳匹配到最差匹配,大致规则如下:

注意:只考虑模板参数特征标,而不考虑返回类型。

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

2. 提升转换(例如,charshort自动转换为int,float自动转换为double);

3. 标准转换(例如,int转换为charlong转换为double);

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

在模板函数的发展上,C++11有了一个很大的改变,以前最C++98上不可能完成的任务,比如说:

template <class T1, class T2>
void ft(T1 x, T2 y)
{
......
?? xandy = x + y;           // 这里的xandy该用什么类型呢?
......
}

假如上述中的T1为short,T2为char,x+y后,自动整型提升,结果xandy就是int。在C++98中,这个是没办法做到的,无法表示xandy的类型。

C++11新增了一个关键字 decltype,有了它就能解决上述问题,使用方法如下:

decltype(x+y) xandy;
xandy = x + y;

// 上述代码就可以修改为
template <class T1, class T2>
void ft(T1 x, T2 y)
{
......
decltype(x+y) xandy = x + y;
......
}

看起来是不是非常的完美,瞬间觉得decltype太伟大了,解决了模板的致命缺陷。

decltype的声明格式如下:

decltype(expression) var;

假如expression是一个没有括号括起来的标识符,则var的类型就是该标识符的类型相同,包括const等限定符,比如:

int x = 5, y = 32;
int & tx = x;
const int *px;
decltype(x) w;            // w类型为int
decltype(tx) u = y;       // tx类型为int &
decltype(px) v;           // v的类型为const int *

假如expression是一个函数调用,则var的类型由函数的返回值来决定,比如:

long func(int);
decltype(func(5)) m;           // 这里的m就是long类型的

当然上述代码的func(5)实际上并不执行,编译器只是通过查看函数的原型来获得函数的返回类型,无需调用实际调用函数。
假如expression是一个左值(左值就是可以被引用的数据对象,例如,变量,数组元素,结构成员,引用和接触引用的指针都是左值;常规变量和const变量都可视为左值),则var为指向其类型的引用。要达到这个假设条件,需要将expression用括号给括起来,如下:

double xx = 5.5;
decltype((xx)) yy = xx;           // yy类型为double &
decleype(xx) w = xx;              // w类型为double

如果这些假设都不满足,则var的类型与expression的类型相同,比如:

int j = 3;
int & k = j;
int & m = j;
decltype(j+5) i1;             // i1类型为int
decltype(500L) i2;            // i2类型为long
decltype(k+m) i3;             // i3类型为int

decltype应用于函数声明上,格式如下:

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

其中auto是一个占位符,表示后置返回类型提供的类型,这当然是C++11赋予auto新增的一种角色。

抱歉!评论已关闭.