《深入浅出MFC》六大关键技术简单剖析:入门篇

来源:百度文库 编辑:神马文学网 时间:2024/04/29 19:38:10
《一》前言
很多人看《深入浅出MFC》时,看到前两章,觉得很简单,看到第三章时,头开始大了。觉得很难。而最要命的是第三章又偏偏是全书的关键,精华都在第三章了。为什么会这样呢?
其原因在于你的基础知识需要补强一下:)
下面我将会为大家将这本书的精华部分用深入浅出的方法分析出来,帮助大家理解。我争取都用最浅显的例子来说明道理,让大家用最轻松的姿态来获得知识。如果大家看完后还有疑问,欢迎留言,我将修改我的教程,以达到相对完美的境界。
在进入第三章之前,我把将会用到的基础知识为大家介绍一下。大家看完基础知识之后,然后再学习第三章就会比较轻松了。
简单的说,进入第三章之前你需要有如下的知识:第一是虚函数,第二是对宏的格式的认识,第三是结构体的基本知识。我将一个一个为大家介绍。
第一, 虚函数的介绍
函数的覆盖与虚函数
我们看下面的这段代码:
//程序1.1
#include
class CAnimal
{
public:
void eat();
void breathe();
CAnimal(){};
private:
int height;
int weight;
};
void CAnimal::eat()
{
cout<<"eating"<}
void CAnimal::breathe()
{
cout<<"breathing"<}
class CFish:public CAnimal
{
public:
void swim();
void breathe();
CFish(){};
};
void CFish::swim()
{
cout<<"swimming"<}
void CFish::breathe()
{
cout<<"bubble"<}
void main()
{
CFish f;
f.breathe();
}
我们看到Cfish类是从Canimal类派生出来的,Canimal类有个成员函数叫breathe,Cfish类也有个成员函数叫breathe,在这里由于鱼的呼吸方法是用吐泡法来进行的。
所以我们在这儿重新定义了breathe方法,于是产生了一个问题,当我们构造鱼的对象时,调用这个breathe成员函数,那么是Cfish类的成员函数被调用还是Canimal类的成员函数被调用呢?
我们看一下输出结果先:
bubble
于是我们知道是Cfish的成员函数被调用了。在此我们引申出一个概念,函数的覆盖。
函数的覆盖发生在父类与子类之间,当子类构造的对象调用被覆盖的成员函数时,调用的是子类的成员函数。可能这里有人会问,如果我们想要调用父类的成员函数,应该怎么办呢?
解决之道是在子类的成员函数中调用时加上作用域标示符。示例程序见下。
//程序1.2
#include
class CAnimal
{
public:
void eat();
void breathe();
CAnimal(){};
private:
int height;
int weight;
};
void CAnimal::eat()
{
cout<<"eating"<}
void CAnimal::breathe()
{
cout<<"breathing"<}
class CFish:public CAnimal
{
public:
void swim();
void breathe();
CFish(){};
};
void CFish::swim()
{
cout<<"swimming"<}
void CFish::breathe()
{
CAnimal::breathe();   //加上这样的一句话即可调用父类的成员函数
cout<<"bubble"<}
void main()
{
CFish f;
f.breathe();
}
下面我们看一下函数的多态性。
//程序1.3
#include
class CAnimal
{
public:
void eat();
void breathe();
CAnimal(){};
private:
int height;
int weight;
};
void CAnimal::eat()
{
cout<<"eating"<}
void CAnimal::breathe()
{
cout<<"breathing"<}
class CFish:public CAnimal
{
public:
void swim();
void breathe();
CFish(){};
};
void CFish::swim()
{
cout<<"swimming"<}
void CFish::breathe()
{
cout<<"bubble"<}
void fh(CAnimal *pAn)
{
pAn->breathe();
}
void main()
{
CFish f;
CAnimal *pAn;
pAn=&f;
fh(pAn);
}
输出结果:
breathing
我们看到粗体字的地方发生了类型转换,前面我们知道,此处我们将子类的对象强制转换成了父类的对象,在理论上这种转换完全可行,没有任何问题。并且我们知道,此时fh(PAn)这个函数的输出结果一定是父类的breathe成员函数,输出结果为breathing,这正是我们所预期的。
我们再看一下如果下面的程序,它和程序1.3相比只发生了一处的改变,见下面的粗体部分
//程序1.4
#include
class CAnimal
{
public:
void eat();
virtual void breathe();  //跟上例相比我们只是在此处加了virtual这个关键词
CAnimal(){};
private:
int height;
int weight;
};
void CAnimal::eat()
{
cout<<"eating"<}
void CAnimal::breathe()
{
cout<<"breathing"<}
class CFish:public CAnimal
{
public:
void swim();
void breathe();
CFish(){};
};
void CFish::swim()
{
cout<<"swimming"<}
void CFish::breathe()
{
cout<<"bubble"<}
void fh(CAnimal *pAn)
{
pAn->breathe();
}
void main()
{
CFish f;
CAnimal *pAn;
pAn=&f;
fh(pAn);
}
输出为:
bubble
为什么会发生这样的结果呢?是因为函数的多态性。
多态性:
当C++编译器在编译的时候,发现Animal类的breathe()函数是虚函数,这个时候C++就会采用迟绑定(late binding)的技术,在运行时,依据对象的类型(在程序中,我们传递的Fish类对象的地址)来确认调用的哪一个函数,这种能力就做C++的多态性。
在我们这个例子中,由于采用迟绑定技术,在运行时,编译器发现breathe是虚函数,就会等对象生成时,看到传递过来的内存是Cfish类的对象,所以就会调用后者的成员函数。
下面我们再介绍一下纯虚函数,如果我们在父类的虚函数写为:virtual void breathe()=0;
这样就将它标志为纯虚函数。具有纯虚函数的类称为抽象类,它是不能直接实例化对象的。从抽象类派生的类,如果它实现了纯虚函数,那么这个派生类可以实例化对象,否则它也不能实例化对象,于是它也仍然是抽象类。闲话少话,我们还是先看一下示例程序。
//程序1.5
#include
class CAnimal
{
public:
void eat();
virtual void breathe()=0;  //在此处我们将其赋值为0表明它是纯虚函数
CAnimal(){};
private:
int height;
int weight;
};
void CAnimal::eat()
{
cout<<"eating"<}
/*void CAnimal::breathe()
{
cout<<"breathing"<}*/
class CFish:public CAnimal
{
public:
void swim();
void breathe();
CFish(){};
};
void CFish::swim()
{
cout<<"swimming"<}
/*void CFish::breathe()
{
cout<<"bubble"<}*/       //纯虚函数,所以程序编译时会报错。
void fh(CAnimal *pAn)
{
pAn->breathe();
}
void main()
{
CFish f;
CAnimal *pAn;
pAn=&f;
fh(pAn);
}
编译时出错的提示如下:
Compiling...
animal.cpp
Linking...
animal.obj : error LNK2001: unresolved external symbol "public: virtual void __thiscall CFish::breathe(void)" (?breathe@CFish@@UAEXXZ)
Debug/animal.exe : fatal error LNK1120: 1 unresolved externals
Error executing link.exe.
animal.exe - 2 error(s), 0 warning(s)
改正此错误的方法很简单,由于我们在派生类中没有实现这个纯虚函数,所以编译不能通过,所以在派生类中加上这个纯虚函数即可。简单的说,就是将上例中派生类的纯虚函数的注释取消即可,见下例。
//程序1.6
#include
class CAnimal
{
public:
void eat();
virtual void breathe()=0;  //在此处我们将其赋值为0表明它是纯虚函数
CAnimal(){};
private:
int height;
int weight;
};
void CAnimal::eat()
{
cout<<"eating"<}
/*void CAnimal::breathe()
{
cout<<"breathing"<}*/
class CFish:public CAnimal
{
public:
void swim();
void breathe();
CFish(){};
};
void CFish::swim()
{
cout<<"swimming"<}
void CFish::breathe()
{
cout<<"bubble"<}       //所以程序可以编译通过
void fh(CAnimal *pAn)
{
pAn->breathe();
}
void main()
{
CFish f;
CAnimal *pAn;
pAn=&f;
fh(pAn);
}
输出结果为:
bubble
为什么要定义纯虚函数呢?可能有人会提出这样的问题。我们用一个简单的例子来说明问题,比如我们定义了动物这个类,这个类中有个睡觉的成员函数,但是我们要将动物派生出很多种类,比如马,蝙蝠,鱼,人。等,它们的睡觉方法都不相同,马是站着睡觉,蝙蝠是倒挂睡觉的,而鱼不需要睡觉,人呢,是躺着睡觉。所以这时候我们将动物这个类的睡觉这个成员函数定义为纯虚函数,由它派生类来实现它,这样我们既保证了动物这个类有睡觉的方法,又为派生类不同的睡觉方法提供了改写方法的入口,并且派生类必须要改写这个成员函数,否则实现不了对象。
我为什么要在此处很啰嗦的介绍虚函数呢,因为我们在后面中需要经常性的用到这个东东,如果对它理解不深刻,会给我们造成障碍。
对于虚函数我们得记住一点,如果在子类中修改了这个虚函数,那么指向子类的指针调用这个虚函数时,调用的是子类中改写过的虚函数,否则才调用父类的虚函数!切记!
第二, 宏的介绍
大家都用过宏,比如大家看到#define Pi 3.1415926就知道当在程序代码中如果出现Pi这个字的时候,用3.1415926来替换Pi。例:
#define PRICE 30  //这是宏的声明
main()
{
int num,total;
num=10;
total=num* PRICE;  //当编译器看到PRICE时,就会用30来代替它。
printf(“total=%d”,total);
}
如果我们在程序中多次用到PRICE这个宏,修改它的值时只要在#define PRICE 30修改就可以了,可以减小我们的工作量。比如我们把#define PRICE 30改为#define PRICE 40,这样只需要改动一处即可。
下面我们看个稍为复杂点的宏。
#define DECLARE_DYNAMIC(class_name) public: static CRuntimeClass class##class_name; virtual CRuntimeClass* GetRuntimeClass() const;
上面同样是宏的声明。
这个宏是什么意思呢?中间的“\”又是干什么用的?“##”又是什么东东?宏怎么会有多行?呵呵,这可能是很多读者想问的问题,且听我慢慢介绍。
首先”\”是连接符号,表示下面的那行代码和上面的是同一行,只是上面显示不下,为了大家看起来方便,才放到下一行的。所以当你看到”\”时,它下面的那行代码其实也和它是一行。所以我们看到这个复杂的宏,其实只是一行代码:)
而##号是连接作用的标识符,当看到##号时,编译器会将##前面和后面的两个字符连成一个字符。比如class##class_name,编译器会将它连接成classclass_name。
好了,现在所有的技术细节已经解决了,下面我们用个实例将它展开。
比如说我们在程序中看到这样的代码
DECLARE_DYNAMIC(CView)
编译器前置处理器为你做出的码是:
public:
static CRuntimeClass classCView;
virtual CRuntimeClass* GetRuntimeClass() const;
明白了吗?呵呵,其实这个宏在此处帮我们声明了一个静态的结构体classCView对象和一个静态的函数指针GetRunTimeclass(),当然这个函数返回的是指向CRuntimeClass的对象。
第三, 关于结构体的基本知识。其实结构体也是一个类,我个人认为你把结构体当成类来理解就对了。当然结构体和类有一点不同之处在于,它们的继承方法有点不同,不过此处我们不需要理解那么多,因为没有必要。下面看一个结构体的例子
struct CRuntimeClass{
LPCSTR m_lpszClassName;                   //指向类名的指针
int m_nObjectSize;                        //该类的大小
UINT m_wSchema;
CObject* (PASCAL *m_pfnCreateObject)();   //指向用于创建对象的函数的指针
CRuntimeClass* m_pBaseClass;              //基类的CRuntimeClass结构
CObject* CreateObject();                  //根据类是否支持动态创建而进行
//对象的创建工作,如果支持,则调用
//安插类中的CreateObject函数,否则
//返回NULL
static CRuntimeClass* PASCAL Load();       //遍历整个类型链表,
//找到要进行动态创建的类的
//CRuntimeClass对象
static CRuntimeClass* pFirstClass;        //类型链表的头指针
CRuntimeClass* m_pNextClass;              //指向链表的下一个元素
};
大家可以看到,结构体中有成员变量,成员函数。其实它也是一种类。在上面我们用DECLARE_DYNAMIC(CView),这个宏帮我们声明了一个结构体的对象。因为这个宏展开后,就成了下面的代码
public:
static CRuntimeClass classCView;  //此处我们看到了这个结构体声明的对象了:)
virtual CRuntimeClass* GetRuntimeClass() const;//这个虚函数返回了一个指向结构体的
//指针
OK!基本知识我们就介绍到这儿,大家对上面的有没有不理解的呢?如果没有我们进入下一步,
下一步将会为大家分析MFC是如何实现这些功能的。如果还有疑问就请再看一次上面的内容,还是不懂,就留言给我,我来帮你解答疑问。