WEB开发网
开发学院软件开发VC 接触VC之二:MFC类基础,C++程序编写规范介绍 阅读

接触VC之二:MFC类基础,C++程序编写规范介绍

 2006-07-19 11:36:53 来源:WEB开发网   
核心提示:由于本文是面对C语言基础的(因为我就是从C学起来的),而MFC是利用C++类技术构建起来的,接触VC之二:MFC类基础,C++程序编写规范介绍,因此有必要在此为只了解C的朋友们,普及一下C++语言中类的概念,要用27%的时间去书写文档,只能用1%的时间去编写代码,熟悉C++的朋友可以跳过本部分, 从总体来说C++是向下

由于本文是面对C语言基础的(因为我就是从C学起来的),而MFC是利用C++类技术构建起来的。因此有必要在此为只了解C的朋友们,普及一下C++语言中类的概念。熟悉C++的朋友可以跳过本部分。 从总体来说C++是向下兼容C的,你可以很不费力气地将用C编好了的程序拿到C++环境下编译执行。其C++只不过是在C的基础上添加面向对象技术(OOP),也就是类的概念,且值得一提的是C与C++都是由美国的贝尔实验室(在之前我只知道发明过电话)发明的。

一、什么是类?

按一些书本上的定义来说“就是一种复杂的数据类型,它是将不同类型的数据和与这些数据相关的操作封装在一起的集合体。因此,类中的数据具有隐藏性,类还具有封装性。”嗯,类还像上面的那句话一样,具有很强的抽象性。让我来用一个例子来解释类吧。 嗯,我们世界上有一个生物种类叫做鸟,在C++上世界我们也可以制作一个类叫做鸟类。它应该有头,有躯干,有腿,有内脏,还有一个非常重要的翅膀。于是,其类描述如下:

class Aves
{
  char m_strHead[10];
  char m_strTrunk[10];
  char m_strCrura[10];
  char m_strWing[10];
  char m_strBowels[10];
};

哈,这样一个鸟类建立好了,怎么样与C中结构体没什么两样吧。(在C++中struct与class基本上是同义词,过一会儿会说到它们有什么不同的。)如果你想建立一个小鸟的话,不用像C中那样麻烦地打struct Aves XXX,而是直接使用Aves XXX就可以了,不打前面的struct或class。在人类对鸟类形成概念之前,鸟的翅膀、躯体等等就真的存在了(没有人有疑议吧?),但在人们根本不知道鸟的那对长满羽毛的扑扇扑扇就可以飞的东西叫什么名字,也不会知道翅膀这个词指的是什么意思。现在我们的这个C++鸟类也正处于这个状态,在那些成员变量中没有被赋与任何值。而现实生活中,一个种类中的具体名字是在一个类对象形成初期被命名的,这是一个名词初始化的过程。在C++类中,当建立一个类对象时总也要有一个初始化各成员变量的过程,于是构造函数被引入了。它在一个实例被声明和被建立(这两个有一些区别)时调用。我们的C++鸟类各个成员变量的赋值命名就可以利用它来实现:

class Aves
{
  Aves ()
  {
     strcpy(m_strHead, "Head");
     strcpy(m_strTrunk, "Trunk");
     strcpy(m_strCrura, "Crura");
     strcpy(m_strWing, "Wing");
     strcpy(m_strBowels,”Bowels”);
  }
  char m_strHead[10];
  char m_strTrunk[10];
  char m_strCrura[10];
  char m_strWing[10];
};

瞧,我是怎么在类里面建立一个构造函数的。一个类的构造函数的名字要与其类名同名,且不能有返回值,void也不行。我们在构造函数中,对各个成员函数命名。当Aves bird;时(声明一个bird对象),Aves类的构造函数将会被调用,把bird.m_strHead,bird.m_strTrunk等等成员变量分别赋值为”Head”,”Trunk”。这样一讲,希望大家对构造函数有了一定的了解了。我们既然有构造函数可以对类成员进行初始化,那么用什么来对类成员销注呢?说白了就是有在建立类对象时调用的函数,什么函数是在类对象被删除时调用的函数呢?那就是析构函数,其命名规则与构造函数是一样的,只不过需要在其函数名前紧加一个~(波浪号),且不能有参数。如我们的类就是~Aves(); 至于析构函数具体作用嘛…举个例子来说,当在构造函数中申请了一段内存,我们就必须在析构函数中释放这段内存,否则会内存泄漏。那么什么时候会引发声明的类对象被删除呢?要解决这个问题,我还需要借用一个叫名域(name space)的概念。当系统执行指针离开声明的类对象所在名域时,就会引发类对象的删除(类型的有效型也可以如此解释)。而名域这个概念最实称的理解就是一对大括号,在这对括号内的空间就是一个名域。(当然名域其实不是这么简单的。如类本身就是一个名域,还可以自己设定一个名域,用于类型声明设定,可以用已有的类型冲突。名域真实用途是这个。具体含义参见《C++标准库》,图书大厦有侯捷先生的译本)比如:

{//名域1
  char * strValue;
  {//名域2
     Aves bird;
     {//名域3
       strValue=bird.m_strWing;
     }
}//<<就在后大括号这里引发了bird对象的析构函数
strValue=”Blue Atlantis”;
}

这里有3个名域,我在名域2中声明了一只小鸟。因为名域3也被包括在名域2里,所以名域3中的空间也属于名域2,于是在名域3中引用小鸟对象是正确的。当执行指针离开名域2那一瞬,C++系统将会把在名域2中声明的所有变量及对象删除掉。当对象被删除时,首先会执行析构函数,然后系统再去释放对象所占用的内存空间。所以当执行到strValue="Blue Atlantis";这一句时,这只小鸟就已经不存在,再去引用它就会编译错误。另外,要讲一讲对象的建立。一种是像变量一样声明建立起来对象,像上面的Aves bird;另一种就是用new语句来建立起来对象。如:

Aves *bird;
bird= new Aves();

new语句跟着一个类的构造函数,它会在内存建立起来一个对象,并把这个对象的指针返回出来。这样建立起来的对象没有名域空间的限制。如果要将这个对象删除掉必须手动的使用delete语句。如:

delete bird;

delete后面跟着指向要删除对象的指针变量。注意,这个指针变量的类型直接影响到对象的删除时所使用的析构函数。所以,什么类型的对象,就要用什么类型的指针来指向删除。真实的鸟类应该是可以飞翔(绝大部分),可以发出叫声,可以在陆上跑(至少可以跳)。所以我们也应该让我们的鸟类也可以跳,可以飞吧。于是,我要向类中添加成员函数。声明成员函数可以有两种方法,一种是在类的声明体里面,像上面例子中构造函数的声明方法,另一种是在类的声明体外面。外部的表现写法为 返回值 类名::函数名(参数列表)。注意::是由于两冒号组成。下面的代码就是使用第二种声明方法(当然,两种方法可以混用):

#include <iostream.h>
#include <string.h>
class Aves
{ 
public:
  Aves ();
  ~Aves ();
  void tweet();
  void run();
  void fly();
  char m_strHead[10];
  char m_strTrunk[10];
  char m_strCrura[10];
  char m_strWing[10];
private:
  char m_strBowels[10];
};
Aves::Aves()
{
  strcpy(m_strHead, "Head");
  strcpy(m_strTrunk, "Trunk");
  strcpy(m_strCrura, "Crura");
  strcpy(m_strWing, "Wing");
  strcpy(m_strBowels, "Bowels");
  cout<<"a bird born!"<<endl;
}
Aves::~Aves()
{
  cout<<"a bird die!"<<endl;
}
void Aves::tweet()
{
  cout<<"jijijijijijiji"<<endl;
}
void Aves::run()
{
  cout<<"I can run by "<<m_strCrura<<endl;
}
void Aves::fly()
{
  cout<<"I can fly by "<<m_strWing<<endl;
}
void main()
{
  Aves bird;
  bird.fly();
  bird.run();
}

我们可以在主函数的运行下看到整个类的运作。在声明时,一只小鸟诞生,构造函数被运行,输出”a bird born”。当对象被删除时,析构函数被执行,输出”a bird die”。请注意,在类声明体中,我添加了public:和private:这样的语句。这是设定访问权限的语句,它是将从它这一行起直到下一个访问权限语句前的所有成员变量和成员函数设置成它指定的权限。如public则设置成公有权限,设置这种权限的成员可以被外部所使用,private则设置私有权限,设置成这种权限的成员是不可以被外部所使用,只能够在其成员函数内被使用,如:使用bird. m_strBowels是非法的,因为其是私有成员变量。因为鸟的内脏在外部是看不见的,它只能被鸟本身所使用。前面说过,在C++中struct和class基本上是同义词。在class中如果一开始没有指定权限关键字,那么默认权限为private,而在struct中默认权限是public。当成员函数需要使用其它成员时,可以直接写其名称,如在run()函数中直接使用其成员变量m_strCrura。在实际上,完全的写法应该是this->XXX成员。this是一个本类的指针,在这个类中就是Aves*。它代表当前实例对象的指针,在bird对象中应用run()时,this就是&bird。可以将run函数改写为

void Aves::run()
{
  cout<<"I can run by "<<this->m_strCrura<<endl;
}

讲了这么多,相信大家应该可以写一个自己的类了吧?快用你的VC,建立一个Win32 Console Application工程,用新建Files向工程添加一个C++ Source file文件,来试试写一个自己的C++程序吧。

二、类的继承

接着,要来说说类的继承。 再拿鸟类来举例子吧。鸟类还可以细分成很多类,鸡啊,鸵鸟啦,什么的。它们都是鸟类,在整体上有着共同的特征和行为,但也有其它不同点。C++类中也会存在相似的地方。怎么办?重新再写一个类,重新再写那些相同的成员吗?这时就需要继承了。我们可以写这样一个类:

class Ostrich : public Aves
{
}

继承的写法为:

class 派生类名 : 权限关键字(在VC中一般为public) 基类名<,基类名2<…,基类名n>>

这个Ostrich类继承于Aves类,Ostrich类现在就拥有Aves类中所有的成员。如果从Ostrich类声明一对象aOstrich,就可以直接调用其aOstrich.run()。实际上就是在调用Aves类中run成员函数。现在要是在Ostrich类上添加成员的话,就是在Aves类的基础上添加成员。需要说的,在Aves类中有一个私有成员m_strBowels。因为其是私有成员,所以对于其派生类Ostrich也是不可见的。为了解决这个问题,需要将Aves类中的private关键字改为protected关键字。将m_strBowels成员描述为保护型。保护型,对其派生类是可见,对于外部和私有一样是不可见的。在现实中,鸵鸟是不会飞的,叫声也不一样,那么我们就需要更改其行为。代码如下:

class Ostrich : public Aves
{
public:
  void tweet();
  void fly();
}
void Ostrich::tweet()
{
  cout<<"gugugugugugugu"<<endl;
}
void Ostrich::fly()
{
  cout<<"I can''t fly by "<<m_strWing<<endl;
}

我们在Ostrich类的基础写了fly(),tweet()成员函数,这是与基类的成员函数名字相同。那么它们将覆盖基类的函数,如果再调用Ostrich类的fly(),tweet()函数的话则会调用我们新写的这两个函数了。基本代码如下:

#include <iostream.h>
#include <string.h>
class Aves
{ 
public:
  Aves ();
  ~Aves ();
  void tweet();
  void run();
  void fly();
  char m_strHead[10];
  char m_strTrunk[10];
  char m_strCrura[10];
  char m_strWing[10];
protected:
  char m_strBowels[10];
};
Aves::Aves()
{
  strcpy(m_strHead, "Head");
  strcpy(m_strTrunk, "Trunk");
  strcpy(m_strCrura, "Crura");
  strcpy(m_strWing, "Wing");
  strcpy(m_strBowels, "Bowels");
  cout<<"a bird born!"<<endl;
}
Aves::~Aves()
{
  cout<<"a bird die!"<<endl;
}
void Aves::tweet()
{
  cout<<"jijijijijijiji"<<endl;
}
void Aves::run()
{
  cout<<"I can run by "<<m_strCrura<<endl;
}
void Aves::fly()
{
  cout<<"I can fly by "<<m_strWing<<endl;
}
class Ostrich : public Aves
{
public:
  void tweet();
  void fly();
};
void Ostrich::tweet()
{
  cout<<"gugugugugugugu"<<endl;
}
void Ostrich::fly()
{
  cout<<"I can''t fly by "<<m_strWing<<endl;
}
void main()
{
  {
     Aves bird;
     bird.fly();
     bird.run();
     bird.tweet();
  }
  cout <<"====================="<<endl;
  {
     Ostrich aOstrich;
     aOstrich.fly();
     aOstrich.run();
     aOstrich.tweet();
  }
}

在主函数中我加多加了两对大括号,请大家分析一下bird,aOstrich生存区域。 以上是一个单继承的例子,至于多继承解释理论是一样。大家可以自己尝试。在后的第七部分中的COM编写中将出现多继承的现象。 在继承派生还记得,派生类对象也是其基类的对象,基类的指针是可以指向派生类的对象的。如我们有是一个Aves *lpBird;指针,那么我们写lpBird=&aOstrich是合法的,因为鸵鸟也是一种鸟。 现在,要提到类最后的一个重要概念就是虚成员函数。

三、虚函数

上一段文字里说到一个基类指针可以指向一个派生类的对象。如果当lpBird指向了aOstrich,那么调用lpBird->fly();的结果会是什么呢?哇,是”I can fly by Wing”,快来看呐,我们指的那只鸟居然会飞了!显然这是我们不希望看到的结果。为了解决这个问题,我在Aves类声明体中将所有成员函数定义为virtual虚函数。

class Aves
{ 
public:
  Aves ();
  ~Aves ();
  virtual void tweet();
  virtual void run();
  virtual void fly();
  char m_strHead[10];
  char m_strTrunk[10];
  char m_strCrura[10];
  char m_strWing[10];
protected:
  char m_strBowels[10];
};

再试试看,结果成为我们要的” I can’t fly by Wing”了。为什么呢?是这样的。当一个类中有虚函数(包括基类含有的)的时候,会给这个类的所有虚函数建立起一个表,函数名与函数地址的映射(包括基类的虚函数)。当对象执行一个虚函数时,则系统先会查这个虚函数表(vtable),找到这个函数名对应的函数地址,调用它。当在派生类添加了与基类虚函数同名的函数,系统会自动将其设定为虚函数。并将这个函数地址改写到虚函数表中。如果再调用这个虚函数时,就会调用新添加的虚函数。像上面的例子,当调用lpBird->fly()时,系统会先查lpBird指向对象的虚函数表,而不会不管三七二十一地直接调用其本类的函数。示例代码如下:

#include <iostream.h>
#include <string.h>
class Aves
{ 
public:
  Aves ();
  ~Aves ();
  virtual void tweet();
  virtual void run();
  virtual void fly();
  char m_strHead[10];
  char m_strTrunk[10];
  char m_strCrura[10];
  char m_strWing[10];
protected:
  char m_strBowels[10];
};
Aves::Aves()
{
  strcpy(m_strHead, "Head");
  strcpy(m_strTrunk, "Trunk");
  strcpy(m_strCrura, "Crura");
  strcpy(m_strWing, "Wing");
  strcpy(m_strBowels, "Bowels");
  cout<<"a bird born!"<<endl;
}
Aves::~Aves()
{
  cout<<"a bird die!"<<endl;
}
void Aves::tweet()
{
  cout<<"jijijijijijiji"<<endl;
}
void Aves::run()
{
  cout<<"I can run by "<<m_strCrura<<endl;
}
void Aves::fly()
{
  cout<<"I can fly by "<<m_strWing<<endl;
}
class Ostrich : public Aves
{
public:
  void tweet();
  void fly();
};
void Ostrich::tweet()
{
  cout<<"gugugugugugugu"<<endl;
}
void Ostrich::fly()
{
  cout<<"I can''t fly by "<<m_strWing<<endl;
}
void main()
{
  {
     Aves bird;
     bird.fly();
     bird.run();
     bird.tweet();
  }
  cout <<"====================="<<endl;
  {
     Ostrich aOstrich;
     aOstrich.fly();
     aOstrich.run();
     aOstrich.tweet();
     Aves *lpBird;
     lpBird = &aOstrich;  
     lpBird->fly();
  }
}

关于虚函数更具体的情况,请参看vckbase第12期的《解析动态联编》。关于其它C++语法,请自行查看C++教材。 那么MFC类呢?简单地来说,MFC类只是将许多有关联的API函数将其封装在一起。在WinSDK中WinAPI都是一些零零散散的函数,它们大部分中都会有一个参数是它的服务对象的句柄。比如,CreateWindow函数会需要一个句柄输出来返回一个窗口句柄来表达其建立的窗口对象,ShowWindow函数需要一个窗口句柄来指定哪一个窗口要改变显示状态,CloseWindow函数需要一个窗口句柄来指定哪一个窗口要被关闭。可以理解为是句柄在围绕着函数转,句柄在以函数为中心。而MFC是将几个若干有服务关联的函数封装在一起成为成员函数,每一个类会有一个保护的句柄成员变量来保存当前类对象所代表的服务对象,在对外调用上看就可以将其类对象看成其服务对象,这些成员函数就可以看成其服务对象本来固有的方法。在使用上比使用WinAPI更为形象和理解。下面做一个比照的例子.

SDK写法

HWND hCurrentWnd;
hCurrentWnd = ::CreateWindow (...);
::ShowWindow(hCurrent,SW_SHOW);
::CloseWindow(hCurrent);

MFC写法

CWnd CurrentWnd;
CurrentWnd.CreateWindow (…);
CurrentWnd.ShowWindow (SW_SHOW);
CurrentWnd.CloseWindow();

怎么样,MFC没有枯燥零碎的句柄的概念。我们足可以想象成一个类对象就是一个服务对象,它本身有许多对其控制的方法。这就是制作MFC的主要目的。 所有的MFC类的基类是CObject的。你可以用CObject的指针指代所有MFC类。CWnd类是所有关于窗口的API函数进行了封装。所有的控件都是派生于这个类的如CEdit,CButton,CDialog, CFrameWnd, CMDIFrameWnd, CMDIChildWnd, CView, CDialog等 MFC更深的理论比如消息映射,CRuntimeClass等在《深入浅出MFC》,《C++技术内幕》等书有详细探讨与讲解,强力推荐。我就是看这几本书入的门。MFC类的各个功能也参见MSDN。 下一部分将详细解释一个MFC对话框程序。 下面,我要介绍一下一些C++程序建议编写规范。

一、变、常、参量的建议:

1.常、变量应当定义函数体最前面或一对大括号的最前面,全局常、变量就当放在整个文件的最前面。这样便于管理与维护。 2.声明变、参量的应当使用匈牙利命名法。为变、参量添加适当的前缀,并以有意义的可拼读的名词性英文单词来命名,每个英文语素首字母都应当大写。如: m_nCount;则表示这是一个类的成员,为整型,是用做计数器用的。 常用的前缀有:

前缀表示内容
_或Afx表示为全局
m_ 表示为某个类的成员
b表示为布尔型
h表示为句柄
c或ch表示为字符型
l表示为长型
clr表示为32位颜色值
n表示为整型
cx或cy表示为坐标的水平或垂直值
p或lp表示为指针
w表示为字(WORD)型
sz表示为以0结尾的字符串
str表示为CString字符型
dw表示为双字(DWORD)型
3.常量应当用const来定义,而不是用预处理指令#define。并且常量名应当大写。

4.如果是全局的常、变量应当加前缀_。

二、函数的建议:

1.参数的定义位置要附和人性化,输出参数在前,输入参数在后。

2.如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。

3.函数名如果是全局的应当加前缀Afx,函数名以有意义的可拼读的动词性英文单词或短语来命名,且每个英文语素首字母都就应当大写。如:AfxGetMessage(),Close()。

4.一般函数的返回值最好用来返回错误标志,而真正的返回值应当用输出参数来返回。

5.在函数的前几行,应当对入口参数进行有效性检查。

6.函数体的规模要小,尽量控制在50 行代码之内。否则,应当进行拆分。

7.函数体中不要声明使用静态变量,那样会使用函数难以控制。

三、 类的建议:

1.所有的成员变量应有m_前缀。

2.在类的声明体中应遵循公有,保护,最后私有,前函数,后成员的顺序进行声明。

3.不能在类的声明体中进行操作,这是不正确的,也是编译器所不允许的。即是初始化(例如:int m_nCount=0;)也不可以。

4.应当在一个以类名为名字,后缀为.h的文件中写入类的声明体,应当在一个以类名为名字,后缀为.cpp的文件中写入类的成员函数的实现语句。并且在.cpp文件的第一行写上#include “类名.h”。

四、其它的建议:

1.for,if,do,while等语句中,无论后面的语句是否为一行,必须用大括号括起来。

2.不可以在一行上写入多条语句,那样会使程序的可读性降低。

3.应当在不易理解的程序段或行上(比如内联汇编语句),加上注释。不要在显而易懂的语句上加上注释,(如:i++;)

4.在编程中,应当想到今后的可重性,给今后便于修改留下余地。

5.在要求技术性的程序上,尽量以最简捷的代码来完成功能。除非必要,否则不要去追求视觉界面效果。因为,界面代码要比功能代码混乱得多。会使代码不易维护。

6.应当将一些常用的功能整理成可以直接使用的类,这样不仅省功且使代码看起来简洁。如界面代码等。

7.在正式编程的时候,切勿直接编写代码。应当先规划好程序结构及其算法等程序实现,再去动手。因为在编程中重要的不是如何去代码,而是程序的实现方法。而且直接写码,肯定会导致程序代码的修修改改,使代码看起来很杂乱。

8.要习惯于在大部分时间去书写文档,因为程序关键是在于是否会被人能够接受使用。

9.要用70%的时间去设想程序的算法,要用27%的时间去书写文档,只能用1%的时间去编写代码,最后用2%的时间去调试代码。

(第二部分完)

Tags:接触 VC MFC

编辑录入:爽爽 [复制链接] [打 印]
赞助商链接