C++ Advanced Training(二)
-------C++&OOP
今天侯老师花了2个小节的时间把昨天的“尾巴”讲完,然后就进入今天的正题OOP,注意是OOP,not OOD。
听了侯老师的两天课,感觉他的讲课风格是
² 关注细节
² 以讲”故事”的方式来讲解抽象的技术。
我将继续接上一节的内容谈C++。
1、Increment operator(++)
++ operator分为 ++A 和A++两种,实际在实现中A++调用了++A。我们举个例子
class Fraction
{
Fraction& operator++();
Fraction& operator++(int);
}
inline Fraction& operator++()
{
m_numerator += m_denominator;
return *this;
}
inline Fraction& operator++(int)
{
Fraction oldValue = *this;
++(*this); // call the prefix increment
return oldValue; //why?
}
从以上的代码段中我们可以得到两个结论:
1)从代码可以看出在使用++ operator时,特别是对自定义类型的++时,尽量选用++A型,因为A++在实现中实际上是调用A++,所以A++型要比++A型执行速度慢。
2)我们在设计数值型class时,最好以int为参照物。这也是为什么Fraction& operator++(int)返回oldValue的原因。我们举例说明在使用primitive type int时,++的用法:
int a = 5;
int b = a++;
cout << a << endl; // a = 6
cout << b << endl; // b = 5
可见A++型,是先返回A的值,再做++操作。所以我们在自定义数值型class的时候也要模拟这种方式,使++ operator的使用方式保持一致,无论对primitive type 还是user-defined type。
2、scope and lifetime
这里总结以下各种object的lifetime:
² global object program始 ,program终
² local(auto) object scope始 , scope终
² heap(dynamic allocated ) object new始 , delete终
² static local object scope始 ,program终
说明:
1) global object的建构是在main之前所以利用global object的ctor可以帮助你做一些有用的事,MFC就利用了这点完成了许多有用的操作。
2) 在program终止之前(即在main函数执行结束之前),有global object , static local object at somewhere 和local object in main等的dtor会被调用。但是次序不定(视编译器实作方式而定),下面代码列出VC++7.1的做法:
#include <iostream>
#include <string>
using namespace std;
class Test2
{
public:
Test2(const string& str) : m_name(str)
{
cout << "constructor called for " << m_name << endl;
}
~Test2()
{
cout << "destructor called for " << m_name << endl;
}
private:
string m_name;
};
void g_func()
{
static Test2 l_TestObj1("StaticLocalObjInGlobalFunc");
}
Test2 g_TestObj("GlobalObj");
int main(int argc, char *argv[])
{
Test2 l_TestObj2("LocalObjInMain");
g_func();
return 0;
}
Output:
constructor called for GlobalObj
constructor called for LocalObjInMain
constructor called for StaticLocalObjInGlobalFunc
destructor called for LocalObjInMain
destructor called for StaticLocalObjInGlobalFunc
destructor called for GlobalObj
3、static member
1)static data members
² 独立于objects之外,众多objects共享一份static data members,也就是说每个class只有一份;
² static data members可被继承(其access level)。
2)static member function 特点
² 没有this pointer ,因此就像non-member function一样;
² 必定不为virtual;
² 可以不通过object而直接访问(通过类的全名,如Accout::setRate())。
3)static member function 用途
² 用于处理static data member;
² 用于callback function。
static member function用于处理static data member无可厚非,我们也不必细讲,关键是为什么使用static member function来用于callback,为什么不直接是用non-static member function?
首先我们要知道什么是callback function?callback function是如何运行的?
callback中文译为“回调”,台湾译为“回呼”,我们拿一个实际的例子来解释什么是callback , callback function是如何工作的?
在Window平台上开发GUI应用程序时,我们会常常用到一个Win32 API,其原型如下:
BOOL LineDDA(
int nXStart, // x-coordinate of line's starting point
int nYStart, // y-coordinate of line's starting point
int nXEnd, // x-coordinate of line's ending point
int nYEnd, // y-coordinate of line's ending point
LINEDDAPROC lpLineFunc, // pointer to callback function
LPARAM lpData // pointer to application-defined data
);
这个函数的用途在msdn中被描述为 “The LineDDA function determines which pixels should be highlighted for a line defined by the specified starting and ending points. ”这个函数是做什么的我们不关心,我们关心的是它的第5个参数,这是一个LINEDDAPROC类型的函数指针,也就是说我们要使用LineDDA这个函数就必须传入一个函数地址,这是因为LineDDA在执行过程中有些动作不能确定,需要我们来告诉它怎么做,我们如何告诉它呢,就通过传入这个有着固定signature的函数的地址,而这个被LineDDA所使用的函数就叫做callback function。下面用一个图形表示这个过程:
通过上面的图示我们可以看出:
callback function的signature是事先定义好的,包括参数的类型和个数等。
下面我们我们就利用这个来解释为什么non-static member function不能作为callback function了。我们都知道一个class的non-static member function在被调用时,编译器会将this这一隐藏的指针加入到该funtion的参数列表中去,导致参数的个数增加而不符合callback预先定义好的signature。而static member function不含有this这一隐藏指针,所以完全胜任callback function这一角色。
4)static member function、non-static member function 、static data member和non-static data member之间的关系
告诉大家一个总的原则,理解上述几个member关系的关键在于this指针。
具体地说:
non-static member function既可以调用static member function,也可以处理static data member;
static member function则既不能调用non-static member function,也不能处理non-static data member。
4、new expression(new operator)&operator new
new operator和operator new这两个东西让一些初学者感到不能理解,甚至包括一些用过很长时间C++的老手都很可能被迷惑,这两个到底有什么区别?各自代表什么意思呢?
我们举个例子大家就清楚了。
Complex* pc = new Complex(1,2); //这句代码里的new就是new operator,它是C++ 的一个关键字,当这条语句执行时,编译器会执行一系列动作。依次为:
² 调用::operator new分配内存空间;
² casting(转型)
² invoke Complex的constuctor
其中第一步调用::operator new分配内存空间中的::operator new就是我们所说的后者,它是真正分配内存的执行者,相当于C中的malloc函数,与malloc不同的是::operator new可以被重新定义,你可以定义你自己class专用的operator new函数。为什么我们要这么做呢?因为使用默认的::operator new分配每一块内存的同时也会分配一块叫cookie的内存块用来存放一些帮助内存管理的信息,如分配的内存的大小,供delete使用。在一些embeded system中,memory是limited的。我们要尽量减少cookie的分配,所以我们要定义自己的operator new。比如我们可以事先分配一大块内存,以后再需要动态分配内存时,就在这个大块内存中再分配出来既可。
operator new 在对象产生之前被调用,所以必须是static的。(同理,operator delete在对象被销毁后被调用,也应该是static的),一般即使你不explicit的声明为static的,编译器也会自动默认为static的。
5、delete expression(delete operator)&operator delete
有了4中的new operator&operator new的基础,这节的东西就很好理解了。
delete pc;
编译器会执行一系列动作,依次是:
² invoke Complex的destructor;
² 调用::operator delete释放内存空间。
::operator delete 等价于C的free函数。
::operator delete和::operator new类似也可以被重新定义你自己的版本。
下面举个例子(包含operator new 和operator delete)
class Base
{
public:
static void* operator new(size_t size);
static void operator delete(void* rawMemory , size_t size);
};
void* Base::operator new(size_t size)
{
if(size != sizeof(Base)) //大小错误,可能是被子类调用
return ::operator new(size);//交给默认处理函数处理
else
//your code to alloc the memory
}
void Base::operator deletevoid* rawMemory , size_t size)
{
if(rawMemory == 0) return;
if(size != sizeof(Base)) //大小错误,可能是被子类调用
{
::operator delete(rawMemory);//交给默认处理函数处理
return;
}
else
{
//your code to free the memory
}
}
main()
{
Base *p = new Base(); //call the operator new which you defined
delete p; // call the operator delete which you defined
}
main代码中当编译器扫描到new时会看Base类中是否重新定义了operator new,如果是则调用Base专用的operator new。delete也是同理。
注:关于operator new &operator delete的一个原则就是:如果你写了一个operator new,就应该写一个对应的operator delete
开始OOP
侯老师认为学好OOP就要学好两方面:polymorphism和template method。
我的一个同事一直和侯老师争论下面的这两个概念的理解,这里我把我的理解写下来:
framework &application framework
framework---- it is always a library which is large ,complex and have many classes and many associations among these classes. such as c++ library , Microsoft .net class library,Win32 API
application framework---- it have helped you define the skeleton of the application ,what you should do is only to override some virtual functions or add some business logic code , that is all。such as MFC ,VCL等。
6、SubObject and virtual destructor
我们看一个例子来说明subobject的概念和virtual destructor的用途。
大家从上面的图中也会有所了解subobject的概念。在CSquare object中,既有CRect的suboject又有CShape的subobject。它们的构造顺序是:由内向外,而析构顺序为:由外向内。
如果有下面代码:
CRect* p = new CSquare();
delete p;
这时如果CRect的dtor为non-virtual的,上述的代码就相当于企图用一个拥有non-virtual dtor的base class的指针来删除一个derived class oject, 其结果是未定义的。最可能的是执行期未调用derived object的dtor, 因为compiler看到基类拥有的是non-virtual dtor,所以根据p的静态类型将dtor编死,而不经过虚拟机制的route。所以告诫如下:“总是让base class拥有virtual dtor”。这样通过虚拟机制route的编译会将derived类的dtor编进去,我们就能够通过基类指针销毁derived object了。
7、Template method
其实这是design pattern的内容,由于这个pattern比较好理解,所以侯老师把它拿到前面来了。我们还是画图理解比较容易,
侯老师说理解这个关键在于理解library code(你用money买的) 和application code(你自己写的),心中在这两个code之间划一条线(见图中那条虚线),库代码都是固定的,不会因为你的业务逻辑而改变的。在库代码中一般都存在这样的函数,它的动作流程很规律,比如Windows应用程序的打开文件操作,流程不过是“打开文件对话框”、“选择文件类型和文件名”、“读入文件内容”等,无论事打开什么文件这个流程都不会改变,这类函数被称为template method。还是以打开文件这一动作为例,在该template method中我们要有一个函数负责读取文件的内容,而文件的类型多种多样,内容的格式也不相同,那我们如何在代码执行到这个读取文件函数(primitiveFunc)时能根据不同的文件类型执行不同的动作呢?我们利用polyphorism机制,见上面的图形,当main中的代码执行到a.TemplateMethod中的primitiveFunc的时候,代码将调用不同的子类override的那个primitiveFunc而不是库代码中实现的那个primitiveFunc。
8、Polymorphism vs static type &dynamic type
我个人认为学好polymorphism的关键在于:
1) 看call through object 还是 call through pointer
2) static type or dynamic type
至于什么是多态,我这里就不多说了,任何一本C++教材都会有详细的讲解。
static type ---- 变量声明时的type;
dynamic type ---- 变量实际的type;
举例说明:
CShape* p ;
p = new CRect();
上述代码中指针p的static type为CShape* , 而dynamic type为CRect* 。
再看看下面代码:
class CShape
{
public:
virtual void draw()
{
cout << "Draw for CShape" << endl;
}
};
class CRect : public CShape
{
public:
virtual void draw()
{
cout << "Draw for CRect" << endl;
}
};
class CSquare : public CRect
{
public:
virtual void draw()
{
cout << "Draw for CSquare" << endl;
}
};
int main(int argc, char *argv[])
{
CShape* p;
CShape s;
s.draw(); //invoke CShape::draw()
CRect rc1;
rc1.draw(); //invoke CRect::draw()
p = new CRect();
p->draw(); //invoke CRect::draw()
delete p;
p = new CSquare();
p->draw(); //invoke CSquare::draw()
delete p;
return 0;
}
Output:
Draw for CShape
Draw for CRect
Draw for CRect
Draw for CSquare
通过pointer去call function时,编译器会去查看该pointer的动态类型来决定到底调用哪个函数。如上述代码中的指针p,第一次被赋予一个CRect* 类型,通过p call draw时,compiler得知p的dynamic type为CRect* ,而不是CShape*,所以调用CRect::draw;同理第二次调用的是动态类型CSquare的draw。
通过obj调用function时比较简单,obj是什么类型的就调用哪个类型的draw即可。
9、Inside the object model
这里涉及到virtual pointer、virtual table等而且要画大量的图才能理解的更好,我倒觉得不如看看inside the c++ object model这本书,所以这里就不详细描述了(^_^其实我比较懒)。
10、virtual func vs non-virtual func vs pure virtual func
pure virtual func --- 为了让derived class只继承其接口。
virtual virtual func --- 为了让derived class继承该函数的接口和预设行为。
non-virtual func --- 为了让derived class继承该函数的接口和实现(继承实现的前提是derived class没有hide该函数接口)。