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

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函数,一旦它有任何改动,客户端只需要重新连接就好,远比重新编译的负担少很多。如果程序库采用动态连接,升级版函数甚至可以不知不觉地被应用程序吸纳。

时间: 2024-08-30 23:15:19

【进阶】关于宏定义和内联函数的相关文章

C++中宏定义和内联函数区别

一.宏定义和内联函数的区别 1. 宏定义不是函数,但是使用起来像函数.预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率.     内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while.switch,并且内联函数本身不能直接调用自身.如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数. 2. 宏定义是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换     内联函数则是在编译的时候进行代码

C语言中的内联函数(inline)与宏定义(#define)详细解析_C 语言

先简明扼要,说下关键:1.内联函数在可读性方面与函数是相同的,而在编译时是将函数直接嵌入调用程序的主体,省去了调用/返回指令,这样在运行时速度更快. 2.内联函数可以调试,而宏定义是不可以调试的.内联函数与宏本质上是两个不同的概念如果程序编写者对于既要求快速,又要求可读的情况下,则应该将函数冠以inline.下面详细介绍一下探讨一下内联函数与宏定义. 一.内联函数是什么?内联函数是代码被插入到调用者代码处的函数.如同 #define 宏(但并不等同,原因见下文),内联函数通过避免被调用的开销来提

宏与内联函数

第一部分:宏     为什么要使用宏呢?     因为函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方.这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现场,并按原来保存地址继续执行.因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率.而宏只是在预处理的地方把代码展开,不需要额外的空间和时间方面的开销,所以调用一个宏比调用一个函数更有效率.     但是宏也有很多的不尽人意的地方.    

深入探讨:宏、内联函数与普通函数的区别_C 语言

内联函数的执行过程与带参数宏定义很相似,但参数的处理不同.带参数的宏定义并不对参数进行运算,而是直接替换:内联函数首先是函数,这就意味着函数的很多性质都适用于内联函数,即内联函数先把参数表达式进行运算求值,然后把表达式的值传递给形式参数.    内联函数与带参数宏定义的另一个区别是,内联函数的参数类型和返回值类型在声明中都有明确的指定:而带参数宏定义的参数没有类型的概念,只有在宏展开以后,才由编译器检查语法,这就存在很多的安全隐患.    使用内联函数时,应注意以下问题:    1)内联函数的定

c++内联函数(inline)使用详解_C 语言

介绍内联函数之前,有必要介绍一下预处理宏.内联函数的功能和预处理宏的功能相似.相信大家都用过预处理宏,我们会经常定义一些宏,如 复制代码 代码如下: #define TABLE_COMP(x) ((x)>0?(x):0) 就定义了一个宏. 为什么要使用宏呢?因为函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方.这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现场,并按原来保存地址继续执行.因此,函数调

特殊用途语言特性——默认参数、内联函数和constexptr函数

1 默认实参 某些函数有这样一些参数,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参.调用含有默认实参的函数时,可以包含该实参,也可以省略该实参. 我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值.   使用默认实参调用函数 如果我们想使用默认实参,只要在调用函数的时候省略该实参就可以了. 函数调用调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置). 当设计含有

内联函数inline与宏定义深入解析_C 语言

内联函数的优越性:一:inline定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换(像宏定义一样展开),没有了调用的开销,效率很高.二:类的内敛函数是一个真正的函数.三:使用内联函数inline可以完全取代表达式形式的宏定义. 例子: 复制代码 代码如下: Class A{public:int readTest(){return nTest:}void setTest(int i);};inline void A::setTest(int i){nTest=i;}; 说明:类A

C++中内联函数的定义和使用

引入内联函数的目的是为了解决程序中函数调用的效率问题. 函数是一种更高级的抽象.它的引入使得编程者只关心函数的功能和使用方 法,而不必关心函数功能的具体实现:函数的引入可以减少程序的目标代码,实 现程序代码和数据的共享.但是,函数调用也会带来降低效率的问题,因为调用 函数实际上将程序执行顺序转移到函数所存放在内存中某个地址,将函数的程序 内容执行完后,再返回到转去执行该函数前的地方.这种转移操作要求在转去前 要保护现场并记忆执行的地址,转回后先要恢复现场,并按原来保存地址继续执 行.因此,函数调

c++-C++类定义体内定义内联函数时public部分问题

问题描述 C++类定义体内定义内联函数时public部分问题 类定义体内定义内联函数时public部分的每个成员函数间不需要用分号隔开吗?如: class Tdate { public: void set(int m, int d, int y) { month = m; day = d; year = y; }%%%%%%%% int isLeapYear() { }%%%%%%% private: int month; int day; int year; }; 在这个例子中的%%%部分是否