======================================================
大家请把我的文章当参考,详细内容 还请参照 权威书籍如果文中
有错误和遗漏, 请指出,Aear会尽力更正, 谢谢!
Aear Blog: http://blog.sina.com.cn/u/1261532101
======================================================
不知不觉已经写了6章。今天的内容是Leaf Class Design, 是讲的非继承类的设计。基本上是前面6章内容的综合运用。
所谓的Leaf Class, 就是在类层次中最低下一层,不能被其他任何的类所继承的。在Java中可以用final来表示。实际上这种类型的class又可以叫做singleton class. 为了简化这一章的内容,我们不考虑template的情况,以及不考虑从其他的类继承而得到的leaf class的情况,这样就不用考虑overloading和 virtual的问题。另外,inline的使用在不在今天的文章范围内。关于template,inline和overloading,将在以后的内容中介绍。
设计一个class,有一个最主要的宗旨,那就是:容易使用,而不容易犯错误。
相信大部分的的programmer都是从独自编码开始的,这样就造成了一个问题:因为对于所有的实现细节都非常熟悉,所以在设计上也不太关心是不是有防止其他使用者犯错误的机制。因此,根据Aear的个人经验,在设计class的时候,要假设这个class的使用者不是你自己。实际上在真正的团队开发中,你永远无法估计别人如何使用你的类。你只有尽量的使用良好的设计来防止别人错误的使用你的代码。
==================== Access Control ====================
对于类的访问控制,由3个关键字来管理: public, protected, and private。下面是public, protected, and private 的作用范围:
public: 可以被任何人访问
protected: 可以被继承类,继承类的friends,以及类成员和friends访问
private: 只可以被类成员和friends访问
这里值得注意的是protected和private区别仅仅在继承类上,由于我们讨论的是Leaf Class, Leaf class 不会被其他类继承。因此,Leaf Class 不需要protected. 所以我们讨论的范围缩小到 public 和 private.
关于public其实很简单,就是谁都可以访问,相当于structure,这里就不详细说明了。关于private有必要说下。首先,private起到的是访问保护, 而不是可见行保护. 也就是说,一个private member对程序来说是可见的,但是是不可访问的。举个简单的例子:
class Test {
public:
private:
}
Test k(1.0f); // Error, Test(float i) is private
对于Test有2个constructor,其中一个public,一个private,当我们运行k(1.0f)的时候,compiler并不直接去进行类型转换把float装成double,而是尝试调用Test(float i),为什么呢? 因此compiler在编译的时候,先会进行overload resolution,根据k(1.0f)的结构,使用的function是Test(float i)。在此之后,compiler才会进行access check. 这样很容易造成让人迷惑的情况,所以:不要把相同的overloaded methods 放在不同的访问控制域中。
其次, private只是在编译的过程中进行访问控制检查,并无法在程序运行中进行控制。因此,即使是private member,也能够通过对内存操作进行修改。所以在写程序的时候,尽量不要做类似的操作,这样很容易破坏数据的一致性。
下面让我们来看看什么需要放在public里,什么需要放在private里。首先要说明的是,所有的data member,也就是attribute,需要放在private中。举个例子来说明这么做的重要性:
class String {
public:
private:
}
String TempStr;
TempStr. pStrBuf = "123123";
这里我们可以访问pStrBuf并改变其内容,但是数据的一致性就破坏了。为了有效的保护private中的数据,还需要注意以下2条:1. 不要在类的method中返回private member的非const reference
2. 不要在类的method中返回private member的非const pointer
这样就基本上能保证client无法通过正常的途径破坏class内部数据的一致性了。
==================== 防止被继承 ====================
在Java中,我们可以通过Final来使一个class无法被其他class继承,但是C++中不提供final的关键字,我们可以通过其他途径来防止类被继承。
首先,在写类的时候,使用non-virtual destructor,就是明确的告诉使用者,这个类是不应该被继承的。当然,别人也可以继承你的类,但是如果你只有public和private两种access control,继承你的类和不继承你的类没有大的区别。这就是为什么leaf class不需要protected member的原因。如果你有protected member,那么别人可以通过继承你的类,来进行一些你不希望的操作,从而破坏内部数据的一致性。
但是,我们也有另外一种类方法,来硬性的使得继承类是非法的。其代码如下:
class Test {
public:
private:
};
我们看到Test() 是private,因此,如果我们编译下列代码,会得到错误:
同样,如果我们从Test Derive一个class出来,也会发生错误。在这个类声明里,需要注意的是Destructor ~Test()是public,因为我们通过CreateInstance new 一个Test(),需要通过client来delete掉它,要不就会发生memory leakage.所以下面的代码是合法的:
使用CreateInstance有很多好处,比如防止被继承,而且可以有效的控制instance的数目,在优化的时候还可以使用object pool从而提高速度,等等。在Test里,copy constructor和operator= 一般是要放在private里边的,这样可以防止很多不通过CreateInstance创建Test的途径,比如:
==================== Explicit Constructor ====================
关于Explicit Type Conversion和Implicit Type Conversion已经在前几章讲过了,对于Class的constructor来说,只要有可能引起implicit type conversion的constructor,在大部分情况下,都应该使用explicit关键字,从而避免implicit type conversion引起的麻烦。比如:
class Test {
public:
};
上面是几种比较常见的情况,其他的情况需要大家自己判断决定。
==================== Operator Overloading ====================
对于大多数的leaf class来说,只有operator overloading能成为Class的friends。 通常为了效率考虑,下列的operator都是可以通过friend来实现:
+ - * / 等2元operator
几乎所有的一元operator都应该是member function.
如果data member中有pointer,那么就必须提供 copy constructor和 operator=.
==================== const & reference ====================
正如第一章所讲到的,尽量使用const 来保证你的类的使用者不犯错误,尽量使用reference来提高程序的效率。
==================== static data member ====================
如果使用了static data member, 那么需要注意的是,尽量附初值,并且尽量不要在constructor和初始化列表(initialize list)中使用,因为C++ standard并没有规定static data member应该在什么时候被初始化。
最后祝愿大家过的愉快,有空来我的Blog坐坐: http://blog.sina.com.cn/u/1261532101, 下次见!
======================================================
大家请把我的文章当参考,详细内容 还请参照 权威书
籍如果文中有错误和遗漏, 请指出,Aear会尽力更正,
谢谢!
Aear Blog: http://blog.sina.com.cn/u/1261532101
======================================================
今天讲的是inline. 其实大家都知道,inline并不是c++独有的特性。其实Aear的个人观点,inline根本就不应该是语言标准的一部分。因为inline的具体实现,跟compiler有很大的关系,把inline说成compiler的一个开关更合适一些。今天我们就说下在什么情况下可以使用inline, 使用inline的好处和坏处。
================= Inline能做什么 =================
关于inline能做什么,什么原理,相信很多人都知道。Aear再在这里重复一下:
1. inline能够去掉function call的overhead
2. inline能够帮助函数调用时候常量参数进行优化
举个例子来说明一:
void function(void)
{
}
function();
在调用function()前,一般会进行下面的操作(根据操作系统和compiler来决定,并不一定按照这个顺序):
在完成function()调用后,也会进行一系列的操作:
整个过程大约需要花费20-150个CPU clock cycle.具体要根据参数的多少,还有函数的具体实现来决定。使用inline function,可以去掉这些函数调用前和调用后的工作,从而节省CPU clock cycle,提高程序的运行效率。
关于2的情况,例子如下:
如果cos不是inline,compiler有可能会生成cos()的调用代码,计算cos(0),然后返回给test.如果cos被inline了,由于cos(0) == 1,所以经过优化的代码可是直接生成test = 1.
================= Inline 坏处 =================
Aear个人认为,inline只在特定的情况下起到正面的作用,在大部分情况下,起到负面的作用,也就是说,坏处多过好处。使用inline的缺点包括:
1. 不容易debug,并不是所有的debugger都支持在inline function中设置断点,不过幸运的是,VS .net支持。
2. 很容易增加代码的长度,在很多情况下,代码长度的增加直接导致了代码执行速度的减慢,特别是在循环中,因为整个循环的长度超过了cache的容量,从而使得cache miss的几率增大
3. 使得编译的时间过长。如果一个inline function被更改,所有调用这个程序的代码都要重新编译。对于一般的游戏代码,整个编译时间可能会有1,2个小时。
4. 暴露了代码实现细节。由于inline function在大多数情况下必须在header file中定义(不过目前由于compiler越来越好,也可以在.cpp中定义inline了),所以导致使用者能够看到你的代码实现细节。
Inline并不是一个好东西,也不是一个万能药,在很多时候,它会给你带来无穷无尽的麻烦。所以在inline的时候,最好慎重慎重。
================= 如何使用 Inline =================
下面给出几种inline的用法。由于个人的代码结构,平台,实现细节的不同,请参考相关资料自行决定.
========= Platform Inline =========
首先要指出的是,inline并不是一条命令,而是对compiler的建议。compiler会具体分析你的代码,如果合适inline,才会进行inline的工作。在VS .net下,一共有3个关于inline的定义:
inline
__inline (__inline__ for GCC)
__forceinline
inline和__inline相同, 只有compiler认为inline合适,才会进行。__forceinline是强制compiler进行inline. 比如:
__forceinline void f(void);
如果你使用了inline或者__inline, 并且打开了编译器的 /Ob开关,那么compiler就会告诉你一个function是不是被真正的inline了。
========= Macro Inline =========
使用一个macro来控制inline,我们可以做如下定义:
#ifdef _DEBUG
#define COND_INLINE
#else
#define COND_INLINE inline
#endif
// inline function declaration
COND_INLINE function();
这样我们在debug模式下,就不会有inline带来的烦恼了。只有在release version,编译器才会进行真正的inline.
========= Inline Condition =========
一般来讲,根据程序员主观感觉inline得到的很有可能就是负面的效果。下面的一些情况,能够帮大家决定是否inline一个函数:
1. 使用profiler分析的结果告诉你需要inline一个function。 这个是最有把握的。windows下比较流行的profiler是vTune
2. class的属性访问函数(Accessor Methods)。一般来讲,这些是可以inline的,而且很多compiler都会帮你自动inline这些函数。
3. 小函数。这个需要举个例子说明,代码如下:
int Cal(int x, int y, int z)
{
}
我们可以看到 Cal的代码很少,就是计算 x+y-Z,最多也就3,4条汇编代码。但是如果不inline, Cal函数的调用和返回过程所产生的代码,都比cal实际运行的代码多。因此Cal最好inline,通过消除Cal的调用和返回的代码,可以减少可执行程序的大小,同时加快运行的速度。
因此,一般来讲,5行内逻辑结构简单,没有if else的代码,可以比较放心的inline,大过5行的代码,就需要仔细的斟酌了。
================= 不能Inline的情况 =================
1. 包含Static Member的function. 很多compiler不能inline有static member的function.或者即使inline了,也会生成错误的代码(把static当成local处理)。所以最好先测试下compiler支持不支持static inline.测试代码可以这么写:
inline void f(void)
{
}
f();
f();
2. virtual member function.
================= 结论 =================
总的来说,inline的坏处多过好处,请大家inline的时候,慎重慎重再慎重,并不是所有的函数都适合inline的。
======================================================
大家请把我的文章当参考,详细内容 还请参照 权威书
籍如果文中有错误和遗漏, 请指出,Aear会尽力更正,
谢谢!
Aear Blog: http://blog.sina.com.cn/u/1261532101
======================================================
关于virtual 一般有3种用法: virtual function, pure virtual function, 和 virtual inheritance. 今天就分别来讲讲这几种virtual.
================ virtual function ================
virtual函数是在类里边的一种特殊的函数,至于具体的含义,相信大家都知道,我就不多说了。而virtual最基本的思想,就是OO语言中多态的体现。把函数从编译时的棒定,转移到运行时的动态绑定。
virtual的好处很多,比如能使程序结构更加清晰,代码的重复利用率更高, 但是也是有代价的。让我们来看下代码:
class Test {
};
class Test2 : public Test {
};
Test * pTest = new Test2();
pTest->VirtualFunc();
上面代码的输出结果是"Test2"。也就是说,虽然你是通过Test * 进行的调用,但真正执行的代码是你创建的Test2的VirtualFunc. 这样的效果是通过在编译的时候创建virtual pointer: _vptr 和 virtual table: _vtbl 来实现的(大部分编译器的实现是通过_vptr & _vtbl)。对于每个有virtual function的class,compiler创建一个_vtbl,用来记录所有的virtual function的地址。同时在所有这个class的instance(实例)里,都有一个_vptr,指向_vtbl。因此, pTest->VirtualFunc() 这段代码等同于:
其中X是VirtualFunc在_vtbl中对应的位置。(其实对于不同的compiler,都有不同的实现,不同的类层次结构,_vptr和_vtbl的数目也不一定相同。)
从上面的代码考虑,virtual 和 non-virtual的class对比,在性能上有3个方面受到影响:
对于第一条,如果是使用 virtual function,那是无可避免的,对于第2条,有通过显式的调用virtual function来提高速度,代码如下:
class Test2 : public Test {
};
由于Test2::VirtualFunc指定了调用函数的版本,所以是在编译时候就绑定了,免去了指针的间接调用过程,而且VirtualFuncFast本身也是可以inline的。当然了,这样做也是有坏处的,就是VirtualFuncFast本身是Test2的函数,而不是Test的,所以不能通过Test的指针掉用。因此只有在确定是Test2的情况下,通过static_cast才能掉用VirtualFuncFast.
同理,在一个virtual function里调用另外一个virtual function的时候,使用显试的调用也能提高一定的速度,比如:
class Test2 : public Test {
};
对于第3种情况,我们可以通过用Template代替Virtual Function来获得性能上的提高,举个简单的例子:
========== virtual 实现 ==========
class Base{
public:
};
class Derive1 : public Base{
public:
};
class Derive2 : public Base{
public:
};
class SomeClass {
public:
};
// 用法
Test * pTest1 = new Derive1();
Test * pTest2 = new Derive2();
SomeClass Temp1.test(pTest1);
SomeClass Temp2.test(pTest2);
========== 对应的 Template 实现 ==========
class D1{
public:
};
class D2{
public:
};
template <class DCLASS>
class SomeClass {
public:
private:
};
// 用法
SomeClass <D1> Temp1;
SomeClass <D2> Temp2;
Temp1.test();
Temp2.test();
========== 如何选择 ==========
对于到底是使用 Virtual ,还是使用Template或者Virtual的优化形式, 在大多数情况下不难做出决定。具体选择的过程,只需要问问自己到底是程序的速度重要,还是其他重要,也就是,要在: 速度,程序结构,灵活性,易用易维护性,代码的复杂度,代码大小 这些中间做出选择。
如果速度排第一位,那么就使用template或者优化,如果其他排在速度的前面,应该尽量的使用virtual。
================ pure virtual function ================
pure virtual function主要是用在abstract class里。有pure virtual function的class,就是abstract class,是不能被实例化的。因此,pure virtual function的代码只有在指定的情况下才能被调用。 例如:
class Test {
};
class Test2 : public Test {
};
================ virtual inheritance ================
其实Virtual Inheritance只有可能在Multiple Inheritance中使用。对于Muliple Inheritance, Aear是坚决反对反对再反对的。Aear个人认为,single inheritance是足够用的,Java就是最好的例子。 MI实在是太让人头疼了,所以Aear不会在这个系列中讲MI,这里只说说 Virtual Inheritance和普通的Inheritance的区别。
class Base {
};
class Derived1 : public Base {
};
class Derived2 : virtual public Base {
};
这里:
sizeof(Derived1) == 4
sizeof(Derived2) == 8
sizeof(Derived1) == 4 是因为 Derived1继承 Base以后,使用Derived1的 _vptr
sizeof(Derived1) == 8 是因为由于在所有的vritual inheritance里,Base作为基类在内存中只能出现一次。所以必须把Base和Derived2的附加部分单独对待。因此有2个_vptr,一个是Base的,一个是Derived2的。
virtual inheritance的直接结果就是大大增加了指令缓存失效的可能性,同时降低了类内部数据的访问速度。因为Base 和 Derived2的内部数据不再是放在连续的内存中了。如果virtual inheritance的层次越多,对运行速度的影响就越大。
所以Aear在这里极度不推荐MI和Virtual Inheritance.
================ virtual 的一些用法 ================
下面是virtual的一些用法。
========== virtual destructor ==========
这个比较简单,就是所有的Base Class,都应该是 public virtual destructor 或者是protected non-virtual destructor,例如:
class Base {
public:
};
或者
class Base {
protected:
};
========== virtual constructor ==========
其实本没有virtual constructor,不过C++中有特殊的实现,实现类似virtual constructor 和 virtual copy constructor的。代码如下:
class Base {
};
class Derived : public Base {
};
========== pure virtual destructor ==========
如果我们想设置base class 为abstract class,而又只有一个destructor作为唯一的成员函数,可以把它设成为pure virtual
class Base {
public:
};
========== protected & private virtual ==========
对于virtual function是不是应该放在public里边,学术结论和现实中的代码往往不能统一。虽然很多人认为virtual function 不应该放在public里边,但是这样的设计经常会造成编码中的不方便,
实际上对与Java来说,protected 或 private 所有 virtual function,几乎是不太可能的。因此,Aear个人观点,是不是要protected & private virtual function,要根据情况而定。下面是个private virtual的例子:
class Base {
public:
private:
};
实际上这样的处理,要比public virtual速度上慢点。
========== 一些要点 ==========
1. 不建议使用MI (Mission Impossible?), 在大多数情况下,它引入的问题,要比它解决的问题多的多的多。
2. 类层次结构越多,类成员和函数的访问效率越低。
3. Virtual Inheritance的类成员访问效率要低于 Non-virtual Inheritance.
今天说了好多东西,下次见!祝大家过的愉快。