今天侯老师花了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的用途。
CShape
/|\
|
CRect
/|\
|
CSquare
大家从上面的图中也会有所了解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该函数接口)。
评论