读书笔记-Thinking in C++-第9章 内联inline函数

来源:互联网 发布:mac双系统安装 编辑:程序博客网 时间:2024/06/05 06:06

9、内联inline函数

 

C中保持效率的一种方法是使用宏,他的行为类似于函数调用但却没有调用的开销(like a function call without the normal function call overhead.)。

宏是由由预处理器preprocessor而非编译器compiler处理的,其直接替换宏代码,没有参数入栈、函数调用及返回等开销。

 

但是在C++中使用宏有两个问题:

宏类似于函数调用但并非总是如此,其有副作用;

预处理器不能访问类的成员变量(preprocessor has no permission to access class member data),因此不能作为成员函数。

为了保持预处理器宏的效率同时增添真正的函数的安全及类范围的特性,C++中采用了内联函数。

 

预处理器的陷阱

如果你认为编译器和预处理器的行为相似,那么你则进入了陷阱,如:

 

#define FLOOR(x,b) x>=b?0:1

当调用形式如下时,

if(FLOOR(a&0x0f,0x07)) // ...

宏将扩展为

if(a&0x0f>=0x07?0:1)

由于&的优先级最低,因此其行为和预想的不一样了

 

因此在使用宏时,要将参数加上(),以防止改变了表达式的优先顺序

#define FLOOR(x,b) ((x)>=(b)?0:1)

 

但即使如此,宏仍然有其副作用,如

#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)

//: C09:MacroSideEffects.cpp

#include "../require.h"

#include <fstream>

using namespace std;

#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)

int main() {

ofstream out("macro.out");

assure(out, "macro.out");

for(int i = 4; i < 11; i++) {

int a = i;

out << "a = " << a << endl << '/t';

out << "BAND(++a)=" << BAND(++a) << endl;

out << "/t a = " << a << endl;

}

} ///:~

a = 4

BAND(++a)=0

a = 5

a = 5

BAND(++a)=8

a = 8

a = 6

BAND(++a)=9

a = 9

a = 7

BAND(++a)=10

a = 10

a = 8

BAND(++a)=0

a = 10

a = 9

BAND(++a)=0

a = 11

a = 10

BAND(++a)=0

a = 12

 

根据a的大小,++a的执行次数不一样,因此系统的副作用始终在变,即函数的功能特性不稳定,这是程序员所不希望的,其本质原因在于宏是简单的文本替换。

 

宏和访问权限

谨慎的使用宏可以避免上述问题,但是仍然有一个不可逾越的障碍是,宏没有对于成员访问范围的概念。

class X {

int i;

public:

。。。

}

#define VAL(X::i) // Error

宏不能访问类的私有成员,另外不能确定你访问的是哪个对象。因此很多程序员为了性能将某些成员变量属性更改为public的,但这样就失去了private的安全性能。

 

内联函数

为了解决宏对于类的私有成员变量的访问权限问题,将宏的概念纳入编译器的控制范围即可,这就是内联函数,他具备函数的一切特性,但是没有函数调用的开销,因为内联函数象预处理宏一样在调用处被扩展。

 

类内部定义的函数自动扩展为内联函数,但是你也可以通过inline关键字声明某函数为内联函数。但是声明时必须和函数定义放在一起,否则编译器将其视为普通函数。对于内联函数,编译器会进行参数和返回值的类型检查。

inline int plusOne(int x);这样是不起任何作用的,必须如下方式声明

inline int plusOne(int x)

{ return ++x; }

 

必须将内联函数的定义放在头文件中,编译器将函数类型和函数体放在其符号表中,当遇到相应的调用时,就将其替换;头文件中的内联函数有个特殊状态,每个文件中都有内联函数的实现,但并不会出现重复定义的错误,因为内联函数是在编译阶段替换的,并没有链接过程,不涉及导函数分配的内存地址等问题。

 

内联函数和编译器

为了理解内联函数何时有效,需要了解编译器如何处理内联。

编译器将函数类型包括函数名、参数个数及其类型还有返回值类型保存在符号表中,当函数体的语法无误时将其实现也保存在符号表中,代码的形式取决于编译器。当遇到调用内联函数时,编译器会分析参数和返回值类型并可能做适当的强制转换,都没有问题时就会进行代码替换,并可能还有进一步的优化。

 

什么时候不能使用内联

内联函数有两种限制,当不能使用内联时,编译器将之视为普通函数,为其分配内存空间,通常会出现多重定义的错误,但是链接器被告知忽略这种问题。

 

当函数的功能过于复杂时,编译器不会实施内联,这取决于编译器,但通常情况下,循环或者过多的代码不会被内联,因为此时代码执行的时间可能比函数调用的时间多很多,内联失去了意义。

 

另外一种情况是需要显式或隐式的得到某函数的地址时,编译器要产生地址则必须为其分配内存空间;而进行内联替换时只是将其保存在符号表中,并不为其分配空间。

 

总之,inline关键词只是对编译器的一种建议,并非强制,是否内联取决于编译器的分析。

 

内联中的前向引用

当在内联函数中调用了类中还未声明的函数怎么办呢?

这种情况,编译器仍然可以将其内联,因为语法规则表明只有到类声明的“}”处才进行内联函数的替换。

//: C09:EvaluationOrder.cpp

// Inline evaluation order

class Forward {

int i;

public:

Forward() : i(0) {}

// Call to undeclared function:

int f() const { return g() + 1; }

int g() const { return i; }

};

int main() {

Forward frwd;

frwd.f();

} ///:~

 

构造和析构函数中的隐藏活动

在构造和析构函数中你可能误认为内联比实际的效率高,因为构造和析构过程中可能含有隐含活动,如当类中含有子对象时必须调用子对象的构造和析构函数。这种子对象可能是成员函数也可能是继承而来的。成员对象的例子如下:

 

// Hidden activities in inlines

#include <iostream>

using namespace std;

class Member {

int i, j, k;

public:

Member(int x = 0) : i(x), j(x), k(x) {}

~Member() { cout << "~Member" << endl; }

};

class WithMembers {

Member q, r, s; // Have constructors

int i;

public:

WithMembers(int ii) : i(ii) {} // Trivial?

~WithMembers() {

cout << "~WithMembers" << endl;

}

};

int main() {

WithMembers wm(1);

} ///:~

 

减少clutter

在实际的工程项目中,若在类中定义函数,则会弄乱类的接口并使得类很难使用,因此有些人认为任何成员函数的定义应该放在类的外部实现,以保持类的整洁如果需要优化的话,则用inline关键字。

 

//: C09:Noinsitu.cpp

// Removing in situ functions

class Rectangle {

int width, height;

public:

Rectangle(int w = 0, int h = 0);

int getWidth() const;

void setWidth(int w);

int getHeight() const;

void setHeight(int h);

};

inline Rectangle::Rectangle(int w, int h)

: width(w), height(h) {}

inline int Rectangle::getWidth() const {

return width;

}

inline void Rectangle::setWidth(int w) {

width = w;

}

inline int Rectangle::getHeight() const {

return height;

}

inline void Rectangle::setHeight(int h) {

height = h;

}

int main() {

Rectangle r(19, 47);

// Transpose width & height:

int iHeight = r.getHeight();

r.setHeight(r.getWidth());

r.setWidth(iHeight);

} ///:~

 

Inline成员函数应该放在头文件中,而非inline函数应该放在定义文件中。

使用inline关键字的形式声明还有一个好处是使得各个成员函数的定义具备统一的风格。

 

更多的预处理器特性

当需要用到预处理器中的三种特性时,采用宏而非内联函数。

分别为字符串化(stringizing,字符串连接(string concatenation,及符合粘贴(token pasting)。字符串化即强制将x转化为字符数组,通过#实现。当两个字符串之间没有任何符号时,字符串连接将使其合并为一个字符串。这两个特性在编写调试代码时特别有用,如:

#define DEBUG(x) cout << #x " = " << x << endl

 

可以用此技术来跟踪代码的执行,在执行代码的同时打印相应信息

#define TRACE(s) cerr << #s << endl; s

这种方法在只有单一语句的for循环中可能会出现问题,如

for(int i = 0; i < 100; i++)

TRACE(f(i));

此时可以将更改为成为逗号表达式。

#define TRACE(s) cerr << #s << endl s

或者更改为do while结构,如:

#define TRACE(s) do{ cerr << #s << endl; s ;} while(0)

 

符号粘贴

符号粘贴即将两个符号粘贴在一起生成一个新的符号,通过“##”实现。

#define FIELD(a) char* a##_string; int a##_size

class Record {

FIELD(one);

FIELD(two);

FIELD(three);

// ...

};

上述方式生成一个标识符作为字符串,另一个作为串的长度,不仅便于阅读,同时消除了编码出错的可能性,并且便于维护。

Linux的内核代码中存在大量这样数据的定义,尤其是些init初始化阶段的数据,或者是某些保存在特殊段的数据结构

 

原创粉丝点击