工程下载:http://www.cppblog.com/Files/richardzeng/C++中实现串口操作类%20SerialPortLib.rar
最近封装了一个串口类,与大家分享,该类的主要特点是:能实现数据的异步接收;无须MFC的支持;只能在VS2003编译通过,但只要做少量修改就可以在VC6.0中使用.使用起来非常简单,主要代码如下:
1 #include "stdafx.h"
2 #include "comm_exception.h"
3 #include "SerialPort.h"
4 #include "serialportobservertest.h"
5
6 using namespace C2217::StdLib;
7 using namespace IBMS;
8
9 int _tmain(int argc, _TCHAR* argv[])
10 {
11 try
12 {
13 //声明一个串口观察者
14 CSerialPortObserverTest portObserver;
15 //声明串口1
16 CSerialPort port(1);
17 //注册串口的观察者
18 port.AtachPortObserver(&portObserver);
19 //打开串口
20 port.Open();
21 byte data[100] = {0};
22
23 port.Send(data,sizeof(data));
24 }
25 catch(comm_exception &e)
26 {
27 SET_CATCH_POS(e);
28 std::cout << e;
29 }
30
31 return 0;
32 }
33
串口数据的接收在
void CSerialPortObserverTest::OnSerialPortReceive(CSerialPort *pSerialPort, byte *pData, size_t nDataLen)
{
cout << pSerialPort->GetName().c_str() << "Received Data: "<<endl;
for(size_t i=0; i< nDataLen ;++i )
{
cout << pData[i] << " " ;
}
cout <<endl;
}
完成,你也可以不使用观察者,直接重写void CSerialPort::OnReceiveData(byte *pData, size_t nDataLen)可以获得更好的执行效率。去掉观察者对象list.
有什么问题邮件联系:dyj057@gmail.com
# re: C++中实现串口操作类 2005-12-22 18:34
我看你的程序使用了一个叫IbmsSerialPort.dll的dll来完成通讯
而这个IbmsSerialPort.dll首先使用CreateFile,然后使用GetCommState等等一系列communications resource function来完成端口通讯
ok,学到了一些东西
回复
# re: C++中实现串口操作类 2006-03-01 10:44
CreateFile对串口操作是独占的,其他的应用程序就不能打开,怎么实现观察者的角色呢?想请教楼主!msn:a.zlp@163.com
回复
# re: C++中实现串口操作类
2006-03-01 12:00
这个简单,当你发送数据的时候,也发送一份到观察者.接收到数据的时候,也转一份到观察者。
回复
摘要: 时间和日历类的设计(Java的Date和Calendar的C++实现)
C++通用框架的设计 作者:naven
1 介绍
时间和日历以及时间的格式化处理在软件的设计中起着非常重要的作用,但是目前C++的库却未有一个简单易用的时间类,大部分都需要开发者直接调...
阅读全文
摘要:开发模式的确立是软件开发过程中不可缺少的一部分,就目前来说,面向过程和面向对象是两种主要的设计方法,虽然面向对象OOP是比较流行的字眼,但不表示面向过程就一定好无作为,毕竟面向过程设计方法也有适合其应用的软件系统:以功能操作为主,扩展性要求不高,无需过多考虑复用以及软件的通用性能。那是不是面向过程的设计方法对于诸如系统框架扩展问题就丝毫没有办法了呢?
按照面向过程的基本原则,划分系统功能模块、模块细分到函数、生成系统整体的结构模型,似乎在整个过程中没有任何东西可以用来提供系统扩展,其实解决的方法还是有的,这根救命稻草就是回调机制。
一谈到回调机制,当然就少不了我们的主角:系统API(通常都是)和回调函数,这两者缺一不可。其实回调的基本思想就是由系统给我们提供一些接口,也就是常使用的API,这种函数可以将某个其他函数的地址作为其参数之一,而且可以利用该地址对这个函数进行调用,而被调用的函数就是我们通常所说的回调函数了。
下面给个回调函数使用的小例子:
------------------------------------------
//相当于我们提到的系统API
mainFunc( void* userFunc )//当然参数不会这么简单,只是模拟
{
while (...)
{
printf("ok!");
//调用回调函数了
if (userFunc!=NULL)
userFunc();
}
}
可以看出MainFunc可以根据函数userFunc的地址调用它。
------------------------------------------
这样使用者只需要定义一个函数:void myFunc(),然后按照mainFunc(&myFunc)(&只表示传递的是函数的地址,无具体含义),就可以让我们的mainFunc来调用myFunc从而实现相应的功能,这样当然可以完成我们预期的目的-扩展现有系统。
在windows系统中,支持这种回调机制的系统API不占少数,像实现ListControl排序的SortItem()函数,还有操作Font使用的函数EnumFontFamilies()都有提供这种回调机制,使得我们的用户有机会添加自己期望的功能实现。当然,使用回调函数并不是一个轻松的事情,如果我们的系统中存在了大量的回调函数是很难管理的,这个就与系统中存在大量全局变量一样,出现多个函数争相访问同一个变量我们就很难使用简单的逻辑来处理,容易陷入混乱,因此,尽管回调机制可以在某种程度上达到我们的目的,但切不可乱加使用,不然后果很难预料。
当然至于详细的回调函数实现,还需要大家潜心研究,这里我只是总结一下:
1 回调函数是由开发者按照一定的原型进行定义的函数(每个回调函数都必须遵循这个原型来设计)
例如:
------------------------------------------
BOOL CALLBACK DialogProc(
HWND hwndDlg, // handle of dialog box
UINT uMsg, // message
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
------------------------------------------
说明:
回调函数必须有关键词 CALLBACK
回调函数本身必须是全局函数或者静态函数,不可定义为某个特定的类的成员函数
2 回调函数并不由开发者直接调用执行(只是使用系统接口API函数作为起点)
3 回调函数通常作为参数传递给系统API,由该API来调用
4 回调函数可能被系统API调用一次,也可能被循环调用多次(SortItem就是自调用)
最后说句题外话,其实windows系统中还有另一种机制-消息机制,也是一个比较不错的工具,能够为很多实际的问题提供解决方法,这个以后再总结了。
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)
明确区分堆与栈
在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。
首先,我们举一个例子:
void f() { int* p=new int[5]; }
这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
00401028 push 14h
0040102A call operator new (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax
这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。
好了,我们回到我们的主题:堆和栈究竟有什么区别?
主要的区别由以下几点:
1、管理方式不同;
2、空间大小不同;
3、能否产生碎片不同;
4、生长方向不同;
5、分配方式不同;
6、分配效率不同;
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:
打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。
注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)
对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆,呵呵,清楚了?
static用来控制变量的存储方式和可见性
函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现? 最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。
需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。
static的内部机制:
静态数据成员要在程序一开始运行时就必须存在。因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化。
这样,它的空间分配有三个可能的地方,一是作为类的外部接口的头文件,那里有类声明;二是类定义的内部实现,那里有类的成员函数定义;三是应用程序的main()函数前的全局数据声明和定义处。
静态数据成员要实际地分配空间,故不能在类的声明中定义(只能声明数据成员)。类声明只声明一个类的“尺寸和规格”,并不进行实际的内存分配,所以在类声明中写成定义是错误的。它也不能在头文件中类声明的外部定义,因为那会造成在多个使用该类的源文件中,对其重复定义。
static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间,静态
数据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时,要保证所嵌套的成员已经初始化了。消除时的顺序是初始化的反顺序。
static的优势:
可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。
引用静态数据成员时,采用如下格式:
<类名>::<静态成员名>
如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式
来引用静态数据成员。
PS:
(1)类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致
了它仅能访问类的静态数据和静态成员函数。
(2)不能将静态成员函数定义为虚函数。
(3)由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊
,变量地址是指向其数据类型的指针 ,函数地址类型是一个“nonmember函数指针”。
(4)由于静态成员函数没有this指针,所以就差不多等同于nonmember函数,结果就
产生了一个意想不到的好处:成为一个callback函数,使得我们得以将C++和C-based X W
indow系统结合,同时也成功的应用于线程函数身上。
(5)static并没有增加程序的时空开销,相反她还缩短了子类对父类静态成员的访问
时间,节省了子类的内存空间。
(6)静态数据成员在<定义或说明>时前面加关键字static。
(7)静态数据成员是静态存储的,所以必须对它进行初始化。
(8)静态成员初始化与一般数据成员初始化不同:
初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;
初始化时不加该成员的访问权限控制符private,public等;
初始化时使用作用域运算符来标明它所属类;
所以我们得出静态数据成员初始化的格式:
<数据类型><类名>::<静态数据成员名>=<值>
(9)为了防止父类的影响,可以在子类定义一个与父类相同的静态变量,以屏蔽父类的影响。这里有一点需要注意:我们说静态成员为父类和子类共享,但我们有重复定义了静态成员,这会不会引起错误呢?不会,我们的编译器采用了一种绝妙的手法:name-mangling 用以生成唯一的标志。
在国内的C++图书中,关于构造函数的说明,要么是错误的,要么没有真正说清楚构造函数的作用,因此在我的视频中,对构造函数的讲解,也有一部分错误的叙述。对于C++构造函数一些错误认识的传播,我也相当于起了推波助澜的作用,在此反省一下,并给出正确的叙述。
(感谢西安软件园的王先生为我指出错误,感谢网友backer帮助我找出正确的答案。)
在光盘VC02中,在介绍构造函数时,我说:“构造函数最重要的作用是创建对象本身,对象内存的分配由构造函数来完成的”,这句话是错的,对象内存的分配和构造函数没有关系,对象内存的分配是由编译器来完成的,构造函数的作用是对对象本身做初始化工作,也就是给用户提供初始化类中成员变量的一种方式,在类对象有虚表的情况下,构造函数还对虚表进行初始化。
另外,我说:“C++又规定,如果一个类没有提供任何的构造函数,则C++提供一个默认的构造函数(由C++编译器提供)”,这句话也是错误的,正确的是:
如果一个类中没有定义任何的构造函数,那么编译器只有在以下三种情况,才会提供默认的构造函数:
1、如果类有虚拟成员函数或者虚拟继承父类(即有虚拟基类)时;
2、如果类的基类有构造函数(可以是用户定义的构造函数,或编译器提供的默认构造函数);
3、在类中的所有非静态的对象数据成员,它们对应的类中有构造函数(可以是用户定义的构造函数,或编译器提供的默认构造函数)。
――即VC++视频第三课this指针详细说明
作者:孙鑫 时间:2006年1月12日星期四
要更好地理解C++的多态性,我们需要弄清楚函数覆盖的调用机制,因此,首先我们介绍一下函数的覆盖。
1. 函数的覆盖
我们先看一个例子:
#include <iostream.h>
class animal
{
public:
void sleep()
{
cout<<"animal sleep"<<endl;
}
void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
};
void main()
{
fish fh;
animal *pAn=&fh;
pAn->breathe();
注意,在例1-1的程序中没有定义虚函数。考虑一下例1-1的程序执行的结果是什么?
答案是输出:animal breathe
在类fish中重写了breathe()函数,我们可以称为函数的覆盖。在main()函数中首先定义了一个fish对象fh,接着定义了一个指向animal的指针变量pAn,将fh的地址赋给了指针变量pAn,然后利用该变量调用pAn->breathe()。许多学员往往将这种情况和C++的多态性搞混淆,认为fh实际上是fish类的对象,应该是调用fish类的breathe(),输出“fish bubble”,然后结果却不是这样。下面我们从两个方面来讲述原因。
1、 编译的角度
C++编译器在编译的时候,要确定每个对象调用的函数的地址,这称为早期绑定(early binding),当我们将fish类的对象fh的地址赋给pAn时,C++编译器进行了类型转换,此时C++编译器认为变量pAn保存就是animal对象的地址。当在main()函数中执行pAn->breathe()时,调用的当然就是animal对象的breathe函数。
2、 内存模型的角度
我们给出了fish对象内存模型,如下图所示:
图1- 1 fish类对象的内存模型
我们构造fish类的对象时,首先要调用animal类的构造函数去构造animal类的对象,然后才调用fish类的构造函数完成自身部分的构造,从而拼接出一个完整的fish对象。当我们将fish类的对象转换为animal类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是图1-1中的“animal的对象所占内存”。那么当我们利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法。因此,出现图2.13所示的结果,也就顺理成章了。
2. 多态性和虚函数
正如很多学员所想,在例1-1的程序中,我们知道pAn实际指向的是fish类的对象,我们希望输出的结果是鱼的呼吸方法,即调用fish类的breathe方法。这个时候,就该轮到虚函数登场了。
前面输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用迟绑定(late binding)技术。当编译器使用迟绑定时,就会在运行时再去确定对象的类型以及正确的调用函数。而要让编译器采用迟绑定,就要在基类中声明函数时使用virtual关键字(注意,这是必须的,很多学员就是因为没有使用虚函数而写出很多错误的例子),这样的函数我们称为虚函数。一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显示的声明为virtual。
下面修改例1-1的代码,将animal类中的breathe()函数声明为virtual,如下:
#include <iostream.h>
class animal
{
public:
void sleep()
{
cout<<"animal sleep"<<endl;
}
virtual void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
};
void main()
{
fish fh;
animal *pAn=&fh;
pAn->breathe();
大家可以再次运行这个程序,你会发现结果是“fish bubble”,也就是根据对象的类型调用了正确的函数。
那么当我们将breathe()声明为virtual时,在背后发生了什么呢?
编译器在编译的时候,发现animal类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(即vtable),该表是一个一维数组,在这个数组中存放每个虚函数的地址。对于例1-2的程序,animal和fish类都包含了一个虚函数breathe(),因此编译器会为这两个类都建立一个虚表,如下图所示:
图1- 2 animal类和fish类的虚表
那么如何定位虚表呢?编译器另外还为每个类提供了一个虚表指针(即vptr),这个指针指向了对象的虚表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就能够找到正确的函数。对于例1-2的程序,由于pAn实际指向的对象类型是fish,因此vptr指向的fish类的vtable,当调用pAn->breathe()时,根据虚表中的函数地址找到的就是fish类的breathe()函数。
正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。那么虚表指针在什么时候,或者说在什么地方初始化呢?
答案是在构造函数中进行虚表的创建和虚表指针的初始化。还记得构造函数的调用顺序吗,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化父类的虚表指针,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类的虚表指针被初始化,指向自身的虚表。对于例2-2的程序来说,当fish类的fh对象构造完毕后,其内部的虚表指针也就被初始化为指向fish类的虚表。在类型转换后,调用pAn->breathe(),由于pAn实际指向的是fish类的对象,该对象内部的虚表指针指向的是fish类的虚表,因此最终调用的是fish类的breathe()函数。
要注意:对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。
总结(基类有虚函数):
1、 每一个类都有虚表。
2、 虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
3、 派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
3. VC视频第三课this指针说明
我在论坛的VC教学视频版面发了帖子,是模拟MFC类库的例子写的,主要是说明在基类的构造函数中保存的this指针是指向子类的,我们在看一下这个例子:
#include <iostream.h>
class base;
base * pbase;
class base
{
public:
base()
{
pbase=this;
}
virtual void fn()
{
cout<<"base"<<endl;
}
};
class derived:public base
{
void fn()
{
cout<<"derived"<<endl;
}
};
derived aa;
void main()
{
pbase->fn();
我在base类的构造函数中将this指针保存到pbase全局变量中。在定义全局对象aa,即调用derived aa;时,要调用基类的构造函数,先构造基类的部分,然后是子类的部分,由这两部分拼接出完整的对象aa。这个this指针指向的当然也就是aa对象,那么我们main()函数中利用pbase调用fn(),因为pbase实际指向的是aa对象,而aa对象内部的虚表指针指向的是自身的虚表,最终调用的当然是derived类中的fn()函数。
在这个例子中,由于我的疏忽,在derived类中声明fn()函数时,忘了加public关键字,导致声明为了private(默认为private),但通过前面我们所讲述的虚函数调用机制,我们也就明白了这个地方并不影响它输出正确的结果。不知道这算不算C++的一个Bug,因为虚函数的调用是在运行时确定调用哪一个函数,所以编译器在编译时,并不知道pbase指向的是aa对象,所以导致这个奇怪现象的发生。如果你直接用aa对象去调用,由于对象类型是确定的(注意aa是对象变量,不是指针变量),编译器往往会采用早期绑定,在编译时确定调用的函数,于是就会发现fn()是私有的,不能直接调用。:)
许多学员在写这个例子时,直接在基类的构造函数中调用虚函数,前面已经说了,在调用基类的构造函数时,编译器只“看到了”父类,并不知道后面是否后还有继承者,它只是初始化父类的虚表指针,让该虚表指针指向父类的虚表,所以你看到结果当然不正确。只有在子类的构造函数调用完毕后,整个虚表才构建完毕,此时才能真正应用C++的多态性。换句话说,我们不要在构造函数中去调用虚函数,当然如果你只是想调用本类的函数,也无所谓。
4. 参考资料:
1、文章《在VC6.0中虚函数的实现方法》,作者:backer ,网址:
http://www.mybole.com.cn/bbs/dispbbs.asp?boardid=4&id=1012&star=1
2、书《C++编程思想》 机械工业出版社
5. 后记
本想再写详细些,发现时间不够,总是有很多事情,在加上水平也有限,想想还是以后再说吧。不过我相信,这些内容也能够帮助大家很好的理解了。也欢迎网友能够继续补充,大家可以鼓动鼓动backer,让他从汇编的角度再给一个说明,哈哈,别说我说的。
我毕业于东华大学服装设计与工程,毕业后在一家服装CAD公司做售后服务工程师.
离开这家公司后感觉,终于感觉又长大拉,也又老拉.
之后来到一家外资公司,还不错.也没有什么可忙的.
想到自己一直想编个小软件(服装绘图方面)的,前一段时间
比较忙,现在可以静心下来学习一下基本理论.就建立了这个Blog
也学学时髦吧.
今年1月我女儿生拉,真挺高兴的.