Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1390575
  • 博文数量: 430
  • 博客积分: 9995
  • 博客等级: 中将
  • 技术积分: 4388
  • 用 户 组: 普通用户
  • 注册时间: 2006-05-24 18:04
文章存档

2013年(1)

2008年(2)

2007年(14)

2006年(413)

分类:

2006-06-05 12:44:41

一、类的定义与应用

C语言中,我们学过结构体,用于将描述某一对象的若干变量包装成一个整体使用,但没有但没有将与该对象相关的函数包含进来。C语言中的结构体只能描述一个对象的特征(属性),不能描述一个对象的动作(方法)。在C++中,我们是通过类的定义来解决这个问题的,在类的定义中,不仅可以包含变量,还可以包含函数。

我们通过一段程序来讲解类的使用。

#include "iostream.h"

class CPoint

{

public:

       int x1;

       int y1;

       void Output();

       CPoint();

       CPoint(int x2,int y2)

       ~CPoint();

private:

       int x2;

       int y2;

       int *pCount;

};

//注意类和结构定义完后,一定要用";"号结尾,忘记";"是许多人常犯的错误。

//c++中,//......用于注释一行,/*......*/可以注释多行。

void CPoint::Output()

{

       if(pCount)

              (*pCount)++;

       else

       {

              pCount=new int;

              *pCount=1;

       }

       cout<<"the first point is ("<

       cout<<"the second point is ("<

}

CPoint::CPoint()

{

       pCount=0;

       cout<<"the first constructor is calling"<

}

CPoint::CPoint(int x2,int y2)

{

       this->x2=x2;

       this->y2=y2;

       pCount=0;

       cout<<"the second constructor is calling"<

}

CPoint::~CPoint()

{

       if(pCount)

       {

              cout<<"你调用了Output成员函数共"<<*pCount<<""<

              delete pCount;

       }

       else

              cout<<"你还没有调用过Output成员函数"<

       cout<<"the deconstructor is calling"<

}

void Output(CPoint pt)

{

       cout<<"the first point is ("<

       //cout<<"the second point is ("<

       //上面被注释的语句会造成编译错误,因为不能从类的外部访问类中的私有成员。

}

void main()

{

       if(1==1)//限定pt变量的有效范围

       {

              CPiont pt;

              cout<<"请输入两个整数";

              cin>>pt.x1>>pt.y1;

              //pt.x2=10;

              //pt.y2=10;

       //上面被注释的语句会造成编译错误,因为不能从类的外部访问类中的私有成员。

              pt.Output();

              pt.Output();

              pt.Output();//故意演示Output被调用多次的情况。

              Output(pt);

       }

       CPoint pt(10,10);

       pt.Output();

}

 

上面的代码定义了一个类CPoint,其中包含有变量,称之为成员变量,也包含有函数的声明,称之为成员函数。

在类定义之外,我们必须对成员函数进行实现,成员函数的实现格式为:

返回类型 类名::函数名(参数列表)

{

       函数体代码

}

上面的代码也编写了一个名为Output的全局函数,注意与类CPoint中的Output成员函数区别。

上面的代码还编写了一个main主函数,其中的代码演示了如何使用CPoint类。

 

C++中提供了一套输入输出流方法的对象,它们是cintcout,cerr,对应c语言中的三个文件指针stdin,stdout,stderr,分别指向终端输入、终端输出和标准出错输出(也从终端输出)。cin>>一起完成输入操作,cout,cerr<<一起完成输出与标准错误输出。例如程序main函数中使用cinpt.x1,pt.y1输入两个整数,Output函数中使用cout连续输出字符串、整数、字符、换行。在输出中使用endlend of line)表示换行,相当与'\n'。利用cincoutscanfprintf要方便得多,cincout可以自动判别输入输出数据类型而自动调整输入输出格式,不必象scanfprintf那样一个个由用户指定。使用cincout不断方便,而且减少了出错的可能性。

 

从类CPointOutput成员函数的实现中,我们可以看到类中的成员函数可以直接访问同类中的成员变量,如:x1,y1,x2,y2。说明:如果成员函数中的局部变量与成员变量同名,则在局部变量的作用范围内,成员变量不起作用。如果有全局变量与成员变量同名,则在成员变量的作用范围内(所有同类成员函数中),全局变量不起作用。main函数中的if(1==1)语句部分,主要是为了说明局部变量的有效范围。局部变量的有效范围位于定义它的复合语句之中,一对{}中所定义的语句即一个复合语句。也就是说,局部变量的有效范围并不是在定义它的函数体当中,而是在外层最靠近它定义的那对{}中,main()函数中定义的第一个CPoint对象ptif语句的}处被系统释放。

 

在类中使用的privatepublic访问修饰符,它们限定成员被访问的范围。从一个修饰符的定义处,直到下一个修饰符定义之间的所有成员都属于第一个修饰符所定义的访问类型。

public定义的修饰符,能够被同类中的成员函数及类定义之外的所有其他函数访问,如CPoint类中的x1,y1,Output等成员变量与函数。但要注意在类之外的函数中访问类成员,必须是对象.成员的格式。

private定义的成员,只能被同类中的成员函数中访问,不能在其他函数中访问(即使是对象.成员的格式),如类CPoint中的成员变量x2,y2能被成员函数Output访问,但不能在main函数及全局Output函数中访问。说明:如果在类定义中的开始处没有使用任何修饰符,则在类定义的开始处使用private作为其默认修饰符。在C++中定义struct结构体也可以包含成员函数,除了开始处使用的默认修饰符为public外,其余之处与class类完全相同。

 

二、函数的重载:

C语言中,如果同一程序中有两个函数名一样,但参数类型或个数不一样的函数定义,编译时将会出错。如果程序中有两个名为Add的函数定义,如:

int Add(int x,int y);

int Add(int x,int y,int z);

C语言中编译,编译将提示函数名重复错误。在C++中上述定义是合法的,C++能够根据函数调用时所传递的参数个数及数据类型选择适当的函数。

 

三、构造函数与析构函数:

在类的定义中,有一种特殊的函数,函数名称与类的名称相同,我们称之为构造函数。构造函数不能有返回类型。因为C++支持函数的重载,所以一个类中可以有多个不同参数形式的构造函数。当用类去定义一个变量(后面可以附带参数),也就是在内存中产生一个类的实例(用类定义的实例变量通常也叫对象)时,程序将根据参数自动调用该类中对应的构造函数。如程序中CPoint pt;语句中调用函数CPoint()CPoint pt(10,10)语句调用函数CPoint(int x2,int y2)

如果类中有一个函数定义格式为~类名(),~CPoint(),这个函数就称为析构函数,同样析构函数也不允许有返回值,析构函数不允许带参数,并且一个类中只能有一个析构函数。析构函数的作用正好与构造函数相反,对象超出其作用范围,对应的内存空间被系统收回或被程序用delete删除时,析构函数被调用。

根据构造函数的这种特点,可以在构造函数中初始化对象的某些成员变量,也就是初始化实例对象。在析构函数中释放对象运行期间所申请的资源,如动态申请的内存空间。通俗地讲,构造函数的作用是,在对象产生时,自动为其赋初值;析构函数的作用是,在对象消失时,为对象处理后事。提示:在类中定义成员变量时,不能给这些变量赋初值。如:

class A

{

       int x=0;//错误,此处不能给变量x赋值。

};

 

在类中定义了一个指向整数的指针成员变量pCount,它所指向的内存地址中的数据(一个整数大小空间)用于统计成员函数Output被调用的次数。提示:在例子中的pCount的用法在实际应用中并不合理,我们这么使用主要是为了分析问题。在c++动态申请内存,是new操作符完成的,new操作符申请的内存是从堆中分配的。在程序中定义的变量所用内存是从栈中分配的,当变量超出其作用范围时,系统收回该变量所占用的内存,以后再分配给其他变量使用。new操作符申请的内存空间在程序运行期间是不会被系统收回的,除非程序调用delete操作符明确要求释放该内存空间。

我们对程序中关于pCount的语句进行分析。int *pCount定义了一个变量pCountpCount变量自身是系统从栈中分配的,占四个字节。这个过程同定义一个整数变量的过程(int x;)没有什么两样,其中的数据没有被初始化,是一个不确定的数,一般不等于零。与定义一个整数变量不一样的是,pCount中的数据是用来表示某一内存块的地址的。尽管系统已为pCount自身分配了内存空间,但pCount中的那个不确定的数据所对应的地址空间却是没有分配的,不可使用的,如图1所示。*pCount=1;表示将pCount所指向的内存块中数据置为1,如果pCount中的数据所指向的内存块是不存在的或未被分配的,程序将会出错。同样(*pCount)++;表示将pCount所指向的内存块中数据加1delete pCount;表示释放

pCount所指向的内存块,这些操作都要求pCount中的数据所指向的内存块是被分配过的,合法的。

pCount=new int;通过new操作符在堆中分配了一个整数变量空间大小的内存块,并将该内存块的首地址赋值给pCountpCount中的数据便指向了这一段合法的地址空间,如图2所示。如果程序超出了pCount的定义范围,pCount变量自身的内存空间将被系统收回,但不影响pCount中的数据所指向的内存块,如果该内存块是通过new分配的,必须保证该内存块不再被使用时用delete删除掉,否则将会造成内存泄漏。

我们如何确定pCount中的数据所指向的内存块是否被正常分配过的呢?我们一般通过检查pCount中的数据是否为零来判断的,这就需要我们在pCount被分配之后将其初始化为0。由于pCount是在类中定义的成员变量,不能在定义变量时为其赋值,所以必须在构造函数中将其初始化为0。这样,一旦用CPoint定义一个对象,该对象中的pCount成员将立即被初始化为0,这下读者应该明白构造函数的作用了吧!在Output成员函数中,检查pCount是否为0,如果为零,则用new int;为其分配一个整数变量大小空间,并将其地址赋值给pCount,否则,直接引用原来已分配的空间。当CPoint定义的对象超出其有效范围时,为该对象分配的空间将被释放,pCount变量也将随该对象一并被释放,如果pCount已指向一个用new操作符分配的内存空间,该内存空间不会被释放,所以,我们必须在析构函数中用delete操作符释放该内存空间,保证pCount被释放前也释放掉pCount中的数据所指向的内存空间,这下读者也应该明白析构造函数的作用了!

 

this指针:

如果成员函数Output被调用,一定是产生了一个对象实例,在这假设对象名称为a,并以a.Output形式调用的,Output的操作一定是针对对象a的。有时,成员函数需要访问它所依赖的那个对象,而不仅仅是这个对象中的其他成员。在类的成员函数中,可以用this关键字代表成员函数所依赖的那个对象的地址,所以,在成员函数中可以用this->成员的方式访问其它的成员,如CPoint(int x2,int y2)函数中用this->x2访问成员变量x2。在成员函数中,我们通常可以省略this->,直接访问类中的成员变量。在CPoint(int x2,int y2)函数中,由于函数参数变量x2,y2与成员CPoint中的成员变量x2,y2同名,要在该函数中访问成员变量x2,y2,可用this->x2this->y2与参数变量x2,y2区分。小技巧:在以后的MFC编程中,如果在成员函数中想调用同类中的某个成员,可以使用VC++提供的自动列出成员函数功能,使用this->,VC++将列出该类中的所有成员,我们可以从列表中选择我们想调用的成员。自动列出成员函数功能,可以提高编写速度,减少拼写错误。特别是我们不能完全记住某个函数的完整拼写,但却能够从列表中辨别出该函数时,自动列出成员函数功能更是有用。事实上,在各种IDE编程环境中,我们通常都没有完全记住某些函数的完整拼写,只是记住其大概写法和功能,要调用该函数时都是从自动列出成员函数中选取的。这样能够大大节省我们的学习时间,我们没有花大量的时间去死记硬背许多函数,利用自动列出成员函数功能和帮助系统,却也能够在编程使顺利使用这些函数,等用的次数多了,也就在不知不觉中完全掌握了这些函数。

 

注意比较Output全局函数与Output成员函数的差别。对Output全局函数的调用,可以理解成“输出某个pt点的坐标”,是一种谓宾关系,是面向过程(或函数)Output的。对Output成员函数的调用,可以理解成“pt这个点对象执行输出动作”,是面向对象pt的。希望通过这样的比较,能够有助于读者理解c++中关于面向对象的概念。

 

四、类的继承与protected访问修饰符:

类是可以继承的,如果类B继承了类A,我们称A为基类(也叫父类),B为派生类(也叫子类)。派生类不但拥有自己新的成员变量和成员函数,还可以拥有基类的成员变量和成员函数。派生类的定义方法是:

class 派生类名:访问权限 基类名称

{

       .....

};

要实现类B与类A的继承关系,我们在定义类B之前必须已定义了类A,并用如下的格式定义类B

class B:publicprivate A

{

       ....

};

讲到类的继承后,我们再讲解另一种成员访问权限修饰符,protectedpublic,protected,private三种访问权限的比较:

public定义的成员可以被在任何地方访问。

protected定义的成员只能在该类及其子类中访问。

private定义的成员只能在该类自身中访问。

派生类可以用publicprivate两种访问权限继承基类中的成员,如果在定义派生类时没有指定如何继承访问权限,则默认为private。如果派生类以private继承基类的访问权限,基类中的成员在派生类中都变成private类型的访问权限。如果派生类以public继承基类的访问权限,基类中的成员在派生类中仍以原来的访问权限在派生类中出现。注意:基类中的private成员不能被子类访问,所以private成员不能被子类所继承。

我们分析如下代码:

class CAnimal

{

       public:

              void eat();

              void breathe();

}

void CAnimal::eat()

{

       cout<<"eating"<

}

void CAnimal::breathe()

{

       cout<<"breathing"<

}

class CFish:public CAnimal

{

       public:

              void swim();

              void breathe();      

}

void CFish::swim()

{

       cout<<"swimming"<

}

void CFish::breathe()

{

       CAnimal::breathe();

       cout<<"breathing"<

}

void main()

{

       CFish f;

       f.eat();

       f.swim();

       f.breathe();

       //下面的代码演示虚拟函数的多态性

       CAnimal *pA;

       pA=&f;

       pA->breathe();

}

关于类的继承及类的访问特性可以参照如下表:

基类的访问特性

类的继承特性

子类的访问特性

Public
Protected
Private

Public

Public
Protected
No access1

Public
Protected
Private

Protected

Protected
Protected
No access1

Public
Protected
Private

Private

Private
Private
No access1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

由于CFish继承了CAnimal,所以在main函数中用CFish定义的对象f可以将CAnimal中定义的eat()成员函数当作自己的成员函数调用。f还调用了CFish中新定义的成员函数swim()

对象f还调用了breathe()函数,大家发现在基类CAnimal和派生类CFish中都定义了breathe函数,在这种情况下调用的到底是哪个类中定义的函数呢?在这里,调用的是子类CFish中定义的函数。如果在子类与父类中都定义了同样的函数,当用子类定义的对象调用这个函数时,调用的是子类定义的函数,这就是函数的覆盖。函数的覆盖,我们可以用生活中的例子来比喻,儿子继承了父亲的许多方法,包括“结婚”这一行为,但父亲“结婚”用的是花轿,而儿子“结婚”用的却是汽车,儿子不能使用父亲“结婚”的方式。如果儿子结婚时,即要花轿,也要汽车,也就是在子类的成员函数定义中,要调用父类中定义的那个被覆盖的成员函数,其语法为,父类名::函数名(参数)。如CFish定义的breathe函数中使用的CAnimal::breathe()语句,就是调用CAnimal中的breathe函数。

 

在程序中main函数的结尾处的代码:

CAnimal *pA;

pA=&f;

pA->breathe();

上述代码定义了一个CAnimal类型的指针pApA指向CFish定义的对象f的地址,用指针pA去调用breathe函数,在这种情况下调用的到底是哪个类中定义的函数呢?简单的死记硬背只能管一时,不能管一世。我们还是从类型转换的原理上寻找答案。将鱼CFish对象的首地址直接赋值给动物CAnimal类型的指针变量,是不用强制类型转换的,编译器能够自动完成这种转换,子类对象指针能够隐式转换成父类指针。这个过程好比现实生活中将一条鱼当作一个动物是没有什么问题的,但要将一个动物当作鱼来对待是存在问题的。如果某一动物确实是一条鱼,我们就可以将这个动物强制类型转换成鱼。也就是说,要将父类类型的对象转换成子类对象,在程序中必须强制类型转换,编译才能通过,但要保证内存中的对象确实是那种被转换成的类型,程序在运行时才不会有问题。我们可以这样想象类型转换,用目标类型的内存布局,去套取要类型转换的对象的首地址开始的那一段内存块(大小为目标类型的大小),套取的内容即为转换后的结果。见图x&f转换成pA后,转换完后的内容包含的breatheCAnimal中定义的那个。

 

五、虚函数与多态性。

如果我们在CAnimal中定义的void breathe()函数前增加virtual关键字,即改为如下定义:

class CAnimal

{

       public:

              void eat();

              virtual void breathe();

};

则上面的代码

CAnimal *pA;

pA=&f;

pA->breathe();breathe调用的是CFish中定义的那个,这就是编译器对虚函数调用的编译方式,这就是虚拟函数的多态性。如果在某个类的成员函数定义前加了virtual,这个函数就是虚函数,如果子类中有对该函数的覆盖定义,无论该覆盖定义是否有virtual关键字,都是虚拟函数。

阅读(642) | 评论(0) | 转发(0) |
0

上一篇:winuser.h

下一篇:vision 说 www.codeproject.com

给主人留下些什么吧!~~