阅读676 返回首页    go 阿里云 go 技术社区[云栖]


【进阶】关于宏定义和内联函数

Tips:

1. 对于单纯常量,尽量用const对象或者enums替换 #define

2. 对于形似函数的宏(marcos),最好改用inline函数替换#define

我们先来看一般的宏定义

 #define ASPECT_RATIO 1.653;

记号名称为ASPECT_RATIO也许从未被编译器看见: 也许在编译器开始处理源码之前它就被预处理器取走了。于是记号ASPECT_RATIO有可能没有进入到几号表(symbol table)内,于是当你运用此常量获得一个编译错误信息时,可能会带来困惑,因为这个错误信息也许会提到1.653而不是ASPECT_RATIO。如果ASPECT_RATIO被定义在一个非你所写的头文件内,你肯定会对1.653是从何而来的感到费解。

解决之道是以一个常量替换上述宏:

const double AspectRatio = 1.653;
作为一个语言常量,AspectRadio肯定会被编译器看到,当然会进入记号表内。

当我们以常量替换#define,有两种特殊情况需要注意:

第一是定义常量指针(constant pointers)。 由于常量定义通常被放在头文件内(以便被不同的源码含入),因此有必要将指针(而不只是指针指向之物)声明为const。即指向常量的常量指针。

第二是class专属常量。为了将常量的作用域(scope)限制于class内,必须将它成为class的一个成员(member);而为了确保此常量之多只有一份实体,必须让它成为一个static成员:

class GamePlayer{
    private:
    static const int NumTurns = 6;//常量声明式
    int scores[NumTurns];//使用该常量
    ...
    };  

然而你所看到的NumTurns的声明式而非定义式通常C++要求你对你所使用的任何一个东西提供一个定义式,但如果它是一个class的专属常量又是static并且为整数类型(integral type,例如ints, chars, bools)则需要特殊处理只要不取它们的地址,你可以声明并且使用他们而无需提供定义式。但如果你取某个class专属常量的地址,或者纵使你不取其地址而编译器却(不正确地)坚持要看到一个定义式,你就必须另外提供定义如下:

const int GamePlayer:: NumTurns;// NumTurns的定义式,下面告诉大家为什么没有给予数值

请把这个式子放进一个实现文件而非头文件,由于class常量已在声明时获得了初值(例如先前声明时将其赋值为6),因此定义时 不可以 在定义初值。

顺带一提:我们无法利用#define创建一个class的专属常量,因为#define并不重视scope。一旦宏被定义了,它就在其后的编译过程中有效(除非在某处被#undef)。同时意味着#define不仅不能用来定义class专属常量,也不能提供任何封装性,也就是说没有private #define之类的东西。

如果你的编译器(错误地)不允许“static整数型class常量完成in class 初值设定”可改用所谓的“the enum hack”补偿做法。理论基础是“一个属于枚举类型(enumerated type)的数值可权充ints被使用”:

class GamePlayer{
private: 
	enum{NumTurns = 5} ;
	int scores[NumTurns];
	...
}


基于数个理由enum hack值得我们认识:

第一: enum hack的行为某方面说比较像#define而不像const,有时候这正是你所想要的。例如取一个const的地址是合法的,而取一个enum的地址就不合法,而取一个define的地址通常也不合法。如果你不想要一个pointer或者reference指向你的某个整数常量,enum可以帮助你实现这个约束。

第二:实用主义。许多代码用了它,所以看到它的时候你必须认识它- -! ,好吧我承认我刚认识。。。事实上,enum hack是template metaprogramming(模板元编程)的基础


另一种常见的#define的误用情况是以它实现宏(marcos)。宏看起来像函数,但不会招致函数调用(function call)带来的额外开销。例如:

#define CALL_WITH_MAX(a,b) f((a)>(b) ? (a):(b))

这般长相的宏有太多缺点,光是想到它们就让人苦不堪言。

无论何时你写出这种宏,必须记住为宏中的所有实参加上小括号。但纵使你为所有实参都加上了小括号,看看下面意想不到的事

int a = 5,  b = 0;
CALL_WITH_MAX(++a,b);		//a 被累加二次
CALL_WITH_MAX(++a, b+10);  //a 被累加一次

调用f之前, a的递增次数竟然取决于 它被拿来和谁比较!!

到了template inline上场的时候了!

tips:

1. inline函数的代码被放在符号表中,像宏一样展开,效率高

2. 类的inline函数是一个真正的函数,检查参数类型,确保调用正确

3. inline函数可作为类的成员函数,可在其中使用private和protect成员。

4. 将大多数inline限制在小型,被频繁调用的函数身上,这可使得日后的调试过程和二进制升级(binary upgradability)变得更容易。也可使得潜在的代码膨胀问题最小化,使程序的速度提升机会更大。

inline函数,看起来像函数,动作像函数,比宏好的多,可以调用它们又不需要蒙受函数调用所招致的额外开销。实际上,能够获取的比想到的更多,因为“免除函数调用成本”只是故事的一部分而已。编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码,所以当你inline某个函数,或许编译器就因此有能力为它执行语境相关最优化,大部分编译器绝不会对着一个“outline函数调用”动作执行如此之最优化。

inline函数当然也有缺点。inline函数背后的整体观念是将“对此函数的每一次调用”都以函数本体替换之。这样做的最直接结果就是增加目标码(object code)大小。在一台内存有限的机器上,过渡热衷inlining会造成程序体积太大(对可用空间而言)。即时拥有虚拟内存,inline造成的代码膨胀也会导致额外的换页行为(paging),降低指令高速缓存装置的击中率(instruction cache hit rate),以及伴随这些而来的效率损失。

REMEMBER: inline只是对编译器的一个申请,不是强制命令,这项申请可以隐喻提出,也可以明确提出。

隐喻方式是将函数定义于class定义式内:

class Person{
public:
	...
	int age() const {return theAge;} // 一个隐喻的inline申请,age被定义域class定义式内;
	...
private:
	int theAge;
};
 这样的函数通常是成员函数,friend函数也可被定义于class内,如果真是这样, friend函数也是被隐喻声明为inline

明确声明inline函数的方式是在函数定义式前面加上关键字inline。标准的max temple是这样实现的:

template<typename T>
inline const T& std::max(const T&a, const T&b){ return a<b?a:b;}

max是个template  带出了一个观察结果:inline函数和template两者通常都被定义于头文件内,这使得某些程序员以为function template一定必须是inline。这个结论不但无效而且有害。

inline函数通常一定被置于头文件内,因为大多数建置环境(build enviroment)在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。虽然有个例,但大多数c++程序中,inline是编译期行为。

大部分编译器拒绝将太过复杂(例如带有循环和递归)的函数inline。而对所有vitual函数的调用(除非是最平淡无奇)的也都会使inlining落空。这应该不感到惊讶,因为virtual意味着“等待,知道运行期才确定调用哪个函数”,而inline意味着“执行前,现将调用动作置换为被调用函数的本体”。

构造函数和析构函数往往是inlining的糟糕候选人--虽然在漫不经心的情况下我们可能不会这样认为,看下面的代码:

class Base{
public:
	...
private:
	std::string bm1, bm2;		//base 成员1和2
};
class Derived: public: Base{
public:
	Derived(){}			//Derived构造函数是空的,really?
private:
	std::string dm1, dm2, dm3;// derived 成员1,2和3
};
derived类的构造函数是空的,看起来像是个inlining的绝佳候选人,因为它根本不含任何代码,但是。。。

C++对于“对象被构建和销毁时发生什么事情”做了各式各样的保证。当你使用new,动态创建的对象被其构造函数自动初始化;当你使用delete,对应的析构函数会被调用。当你创建一个对象,其每一个base class及每一个成员变量都会被自动构造;当你销毁一个对象,反向程序的析构行为也会自动发生。如果有个异常在对象构造期间被抛出,该对象已经构建好的那一部分会自动销毁。在这些情况下C++描述了什么一定会发生,但没有描述是如何发生的。“事情如何发生”是编译器的权责,不过有一点很清楚,就是它不可能凭空发生。程序内的一定有某些代码让这些事情发生,而那些代码---由编译器于编译期间代为产生并安插到你的程序中的代码--肯定存在某个地方,有时候就放在构造函数和析构函数中。下面看一下编译器为上面代码中空的Derived函数做了哪些工作:

Derived::Derived(){
	Base::Base();					//初始化Base成分
	try{dm1.std::string::string();}		//试图构造dm1
	catch(...){						//如果异常,销毁base class成分,并抛出该异常
		Base::~Base();
		throw;
	}
	
	try{dm2.std::string::string();}		//试图构造dm2
	catch(...){						//如果异常,销毁base class成分,并抛出该异常
		dm1.std::string::~string();		//还要销毁已经创建的dm1
		Base::~Base();
		throw;
	}
	
	try{dm3.std::string::string();}		//试图构造dm3
	catch(...){						//如果异常,销毁base class成分,并抛出该异常
		dm2.std::string::~string();		//先销毁创建的dm2
		dm1.std::string::~string();		//还要销毁已经创建的dm1
		Base::~Base();
		throw;
	}
}

这段代码不能代表编译器真正制造出来的代码,因为真正的编译器会更精确复杂地做法来处理异常,尽管如此,这已经能准确反映Derived的空白构造函数必须提供的行为。相同的理由也适用于Base构造函数,所以如果它被inline,所以替换“base构造函数调用”而插入的代码都会被插入到“Derived 构造函数调用”内。程序库设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。换句话说如果f是程序库内的一个inline函数,客户将“f函数本体”编进其程序中,一旦程序设计者决定改变f,所有用到f的客户端程序都需要重新编译。而如果f是non-inline函数,一旦它有任何改动,客户端只需要重新连接就好,远比重新编译的负担少很多。如果程序库采用动态连接,升级版函数甚至可以不知不觉地被应用程序吸纳。


最后更新:2017-04-03 14:54:11

  上一篇:go [Qt教程] 第48篇 进阶(八) 3D绘图简介
  下一篇:go [Qt教程] 第47篇 进阶(七) 定制Qt帮助系统