第 15章 C++面向对象基础
15.1面向对象程序设计的基本概念面向对象技术( Object-Oriented echnology)是在 80
年代末出现的,它是为了适应开发和维护复杂应用软件的需要,为解决软件危机而诞生的。面向对象的程序设计方法是继结构化程序设计方法之后的一种新的程序方法。在面向对象的程序设计中,通过对象来表示事物,
用对象( Object)与对象间消息的传递来表现事物间的联系;用对象的方法实现对对象的操作。什么是对象呢?
从概念上讲,对象是代表着正在创建的系统中的一个实体。从实现形式上讲,对象是一个状态和操作(或方法)
的封装体。状态由对象的数据结构的内容和值定义,方法是一系列的实现步骤,它由若干操作构成。对对象进行抽象形成类 。
抽象是一种提炼对象特征的方法,它可以将具有公共行为的对象组织成类。类是抽象数据类型的实现,
一个类的所有对象都有相同的数据结构,并且共享相同的实现操作的代码,而各个对象有着各自不同的状态,即私有的存储。因此,类是所有对象的共同的行为和不同状态的集合体。面向对象技术的基本特征主要有:封装性、继承性、多态性。
封装性:是将数据结构和对数据进行的操作结合在一起,形式一个整体,对外隐蔽其内部实现细节,
同时避免了数据紊乱带来的调试与维护的困难。
继承性:是一个对象可以获得另一个对象的特性的机制。对象的特性包括对象的属性(数据)和方法(函数)。继承增强了软件的可扩充性,并为代码重用提供了强有力的手段。
多态性:指相同的函数调用被不同的对象接收时,可以导致不同的行为。它使程序员在设计程序时可以对问题进行更好的抽象,以设计出重用性和维护性俱佳的程序。
15.2类和对象
C++作为 C语言的超集,涵盖了 C语言的主要概念和功能,
但它同时又引入了一些新的概念,其中最主要的是类和对象的概念。类的设计和使用体现了面向对象的设计思想。
面向对象的程序设计是从分析对象开始的。对象分析方法的有力工具是分类 —— 找出一类具有相同属性的对象,并将它们的共同属性用类表示。在实际的程序设计中,是先定义问题域中的相关对象类 (class),然后由类生成对象。因此,类是由用户定义的特殊数据类型。
15.2.1类的定义与实现
类中定义的数据和函数分别称为数据成员和成员函数。类的定义格式一般地分为说明部分和实现部分。
说明部分是用来说明该类中的成员,包含数据成员的说明和成员函数的说明。成员函数是用来对数据成员进行操作的,又称为“方法”。实现部分是成员函数的定义。概括起来,说明部分将告诉使用者
“干什么”,而实现部分是告诉使用者“怎么干”。 。 类的定义与 C语言中的结构体类似,但结构体中只能定义属性不能定义对这些属性进行操作的方法(函数)。
类的定义使用关键字 class,其后面的标识符定义了一个新的类型,可以使用这个标识符说明类的变量和指向类的指针。
例:定义一个名为 TPerson的类,包括这个人的姓名、年龄、
性别、家庭住址、电话等不同属性,以及对这些属性操作的两个函数。
class TPerson //通常用 T字母开始的字符串作为类名,以示与 //对象、函数名区别
{ private:
char name[20];
int age;
char sex;
char address[20];
long tel;
public:
void setdata( );
void print( );
}; //分号不可缺少
面向对象的程序设计强调信息隐藏,将实现细节和不允许外部访问的部分隐藏起来,为此它把类成员分为公开的( public)与私有的
( private)两类。外界不能直接访问一个对象的私有部分,它们与对象间的信息传送只能通过公开成员进行。上面的例子中一共定义了 7个成员,5个成员数据,2个成员函数。成员数据一般不能让外界直接访问,只能通过本类的成员函数访问。所以把 5个成员数据定义成私有成员
(用 private定义),把成员函数定义为公开成员
(用 public定义)。
关键字 private,public被称为访问权限修饰符或访问控制修饰符。在一个类的定义中,关键字 private,public出现的顺序与次数可以是任意的。 C++规定,类成员隐含的访问权限是私有的,不加声明的成员都默认为私有的。因此,
最前面的关键字 private可以缺省。而结构体类型的成员的隐含访问权限是公开的。
类的实现,就是进一步定义它的成员函数。
成员函数是类定义中用以描述对象行为的成员。
在成员函数中,可以直接访问类的所有成员。
成员函数的定义方式与普通函数大体相同,以下几点需加以说明:
① 成员函数可以在类中定义。例如:
class TPerson
{ private:
char name[20];
int age;
char sex;
char address[20];
long tel;
public:
void setdata( )
{ strcpy(name,"liling");
age=18;
sex='f';
strcpy(address,"249 shanghailu");
tel=3041725; }
void print( )
{ cout<<name<<","<<age<<","<<sex<<","<<address<<","<<tel<<endl; }
};
② 在类定义外部定义成员函数时,应使用作用域限定符
“::”指明该函数是哪个类中的成员函数。例如:
class TPerson
{ private:
char name[20];
int age;
char sex;
char address[20];
long tel;
public:
void setdata( ); //函数原型
void print( );
};
void TPerson:,setdata( ) //函数实现
{ strcpy(name,"liling");
age=18;
sex='f';
strcpy(address,"249 shanghailu");
tel=3041725; }
void TPerson:,print( ) //函数实现
{ cout<<name<<","<<age<<","<<sex<<","<<address<<","<<tel<<endl; }
类的成员函数也可以重载。
例 15.1:
#include <iostream.h>
class point
{ int x,y;
public:
void set(int xp,int yp) //成员函数 set
{ x=xp;y=yp;
cout<<"x="<<x<<endl;
cout<<"y="<<y<<endl;
};
void set(point p) //成员函数 set重载
{ x=p.x;y=p.y;
cout <<"point:"<<x;
cout<<";"<<y<<endl; }
};
void main()
{ point pp,qq; //定义 point类的对象 pp和 qq
pp.set(10,20);
qq.set(pp); }
运行结果:
x=10
y=20
point:10,20
注意:在类定义中,不允许对所定义的数据成员进行初始化。
例 15.2分析下面程序的输出结果。
#include <iostream.h>
class R
{public:
R(int r1,int r2){R1=r1;R2=r2;}
void print() const;
private:
int R1,R2;
};
void R::print()const
{cout<<R1<<";"<<R2<<endl; }
void main()
{ const R b(20,52);
b.print(); }
程序运行结果如下:
20;52
说明:
在类 R中,说明了一个常成员函数 print()。
常成员函数说明格式如下:
<类型说明符 > <函数名 > (<参数表 >) const;
其中,const是加在函数说明后面的类型修饰符,它是函数类型的一个组成部分,因此,
在函数实现部分也要带 const关键字。
在 main()函数中,定义了一个常对象 b,只有常成员函数才能操作常对象,没有使用
const关键字说明的成员函数不能用来操作常对象。
15.2.2对象的定义
定义了一个类之后,便可以像用 int,char等类型符声明简单变量一样,用它们定义对象,也称为类的实例化。有时也可以将对象称为类变量,因为它同变量一样是程序实体,并具有像变量一样的属性,
如生存期等存储属性。
类的实例化通过声明语句进行。如已经定义了类
TPerson,便可以用声明语句生成对象:
TPerson zhang,li;
应当注意,一个类只是定义了一种类型,只有它被实例化,
生成对象后,才能接收和存储具体的值。 Zhang,li便是两个不同的对象,它们占有不同的内存区域,保存有不同的数据,但它们形式相同,操作代码也相同。对象的定义格式为:
类名 对象名表;
每个对象都是由数据成员和成员函数组成的。既可以访问对象的数据成员,也可以访问对象的成员函数。访问成员函数时,将执行实现该成员函数的代码。 C++在实现时,成员函数为该类的所有对象共享。
访问对象的成员使用,.”运算符,其格式为:
对象名,对象成员名
例 15.3:使用 TPerson类的一个简单程序。
#include <string.h>
#include <iostream.h>
class TPerson
{ …… };
void main( )
{ TPerson my; //声明 TPerson类的对象 my
my.setdata(); //调用 TPerson类的成员函数
setdata()
my.print(); //调用 TPerson类的成员函数
print()
}
运行结果如下:
liling18f249 shanghailu3041725
用指向对象的指针访问对象的成员,其格式为:
指向对象的指针名 ->对象成员名
例如,TPerson zhang; //生成 TPerson类的对象 zhang
TPerson *pc; //声明指向 TPerson类的对象的指针
pc=&zhang; //将对象 zhang的地址赋给指针 pc
当指针得到了对象的地址后,就可以用其访问对象的成员。如:
pc->print();
它等价于:
( *pc),print();
前面讲述了通过定义成员函数的方法给对象的数据成员赋值。下面讲述如何对对象进行初始化。
在 C++中,声明一个类的变量时,可以自动的调用一个用户定义的初始化函数。这个函数是类的特殊的成员函数,称为构造函数。
构造函数可以由用户定义,也可以由系统给出。系统给出的的构造函数称为缺省构造函数。缺省构造函数不能对对象中的成员数据进行有效的初始化。当声明一个对象时,程序将自动调用类的构造函数。类的构造函数和类有相同的名字。构造函数不返回任何值,
也不能返回 void。
例:对类 TPerson可以定义如下构造函数取代上面定义的 setdata()函数。
TPerson::TPerson(void)
{ strcpy(name," liling");
age=18;
sex=' f ';
strcpy(address,"249 shanghailu");
tel=3041725; }
析构函数也是一种特殊的类成员函数,它的功能正好与构造函数相反。析构函数用于释放对象被分配的内存空间,在对象删除前,用它来做一些清理工作。析构函数的名字是类名前加一个,~”符号。析构函数不返回任何值,也不能返回 void。如果一个类中没有定义析构函数时,编译系统也生成一个缺省析构函数,缺省析构函数是一个空函数。
例 15,43:
#include <iostream.h>
class point
{ int x,y;
public:
void show();
point() //定义构造函数
{ x=0;y=0;cout<<"Constructor\n";}
~point() //定义析构函数
{ cout<<"Destructor\n";}
};
#include <iostream.h>
void point::show()
{cout<<x<<","<<y<<endl; }
void main()
{ point p; //声明变量时自动调用构造函数。
p.show();
} //变量作用域的结尾自动调用析构函数
程序运行结果:
Constructor
0,0
Destructor
说明:
1、构造函数和析构函数不能使用 return语句返回值。
3、构造函数可以重载,可以有形参;析构函数不能重载。一个类中只可能定义一个析构函数。
何时调用构造函数和析构函数:
1、自动变量的作用域是某个模块,当此模块被激活时,自动变量调用构造函数,当退出此模块时,会调用析构函数。
2、全局变量在进入 main()函数之前会调用构造函数,在程序终止时会调用析构函数。
3、动态分配的对象当使用 new 时为对象分配内存时会调用构造函数;使用 delete删除对象时会调用析构函数。
4、临时变量是为支持计算,由编译器自动产生的。临时变量的生存期的开始和结尾会调用构造函数和析构函数。
15.3派生类与继承
继承性是面向对象程序设计中最重要的机制,也是传统的结构化程序语言所不具有的。它提供了组织程序和复用代码的强有力的手段。
通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。例如,
要定义一个类 TEmployee(职工 ),它与类
TPerson(人 )之间有这样的关系:
① 类 TEmployee(职工 )是类 TPerson(人 )的子集。
②类 TEmployee(职工 )的属性要比类
TPerson(人 )多。一个 TPerson类对象增加成员 department(部门)和 salary(工资 )才能得到 TEmployee类。
继承可以清晰自然的表达实际问题中分类结构或层次结构,它通过对一个已存在的类进行特殊化来建立新的类。已存在的类称为基类(也称为父类或超类);新建立的类称为派生类(也称为子类)。派生类不但继承了基类所有的数据成员和成员函数,而且可以添加新的数据成员和成员函数,改变所继承的成员函数的语义。
派生类的定义格式如下:
class <派生类名 >,<继承方式 > <基类名 >
{ <派生类新定义成员 > };
如:定义 TEmployee(职工 )类。
class TEmployee,public TPerson
{ char department[20];
float salary;
};
TEmployee(职工 )类除了继承 TPerson类的所有成员外,又增加了 2个新的成员。
例 15.5:
#include <string.h>
#include <iostream.h>
class TPerson
{
protected:
char name[20];
int age;
char sex;
char address[20];
long tel;
public:
TPerson(void)
{ strcpy(name," liling");
age=18;
sex='f';
strcpy(address,"249 shanghailu");
tel=3041725; }
void print( )
{ cout<<name<<","<<age<<","<<sex<<","<<address<<","<<tel<<endl; }
};
class TEmployee,protected TPerson
//定义新的类 TEmployee,它是 TPerson类的派生类
{
protected:
char department[20];
float salary;
public:
TEmployee():TPerson()
{ strcpy(department,"Computer");
salary=2010.34f;}
void print()
{cout<<name<<","<<age<<","<<sex<<","<<address<<","<<tel<<",";
cout<<department<<","<<salary<<endl;}
};
void main()
{ TEmployee te1; //定义 TEmployee类的对象 te1
te1.print(); } //调用 TEmployee的成员函数 print()
程序运行结果:
liling,18,f,249 shanghailu,3041725,Computer,2010.34
<继承方式 >常使用如下三种关键字给予表示:
public,表示公有继承。其特点是在公有继承的情况下,在派生类中可以访问基类中的公有成员和保护成员。但不能访问基类的私有成员。
protected,表示保护继承。其特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
private,表示私有继承。其特点是基类的公有成员和私有成员在私有派生类中作为私有成员,并且不能被这个派生类的子类所访问,而基类中的私在成员在私有派生类中是不能直接访问的。
将上述三种不同的继承方式的基类特性与派生类特性列出表格,见表 15.1
在一般情况下,都使用公有继承,很少使用私有继承。因为私有继承当再派生出下一级时,基类的所有成员都将被私有化,其他类成员也不可再直接访问。如果基类成员只由有血缘关系的成员访问,而不被无血缘关系的对象成员访问时,则要使用保护继承。
private,protected,public作为类成员的可见性修饰符,将产生如下影响:
①在一个类中定义的成员函数,可以访问本类中的任何成员,但只能直接访问基类中的 protected
成员和 public成员。
②一个类对象,只能直接访问本类或其基类中的
public成员。
通过 C++语言中的继承机制,可以扩充和完善旧的程序设计以适应新的需求,这样不仅可以节省程序开发的时间和资源,并且为未来程序设计增添了新的资源。
像例 15.3.1,从一个基类 (TPerson)派生的继承称为单继承。在C ++语言中,一个派生类可以从一个基类派生,也可以从多个基类派生。从多个基类派生的继承称为多继承。多继承的定义格式如下:
class <派生类名 >,<继承方式 1> <基类名 1>,<
继承方式 2> <基类名 2>,…
{ <派生类新定义成员 >
};
可以看出,多继承的派生类有多个基类。
例 15.6定义一个圆柱体类,该类可实现求圆柱体体积,它继承了圆类和高类。圆类可以求圆面积,能描述圆心和半径。
类层次结构如图 15.1所示。
class Circle
{float x,y,r;
public:
Circle(float a,float b,float c){x=a;y=b;r=c;}
float getx(){return x;}
float gety(){return y;}
float getr(){return r;}
float area(){float a; a=r*r*3.14159f; return a;}
};
class High
{float high;
public:
High(float h){ high=h;}
float geth(){return high;}
};
class Cylinder:public Circle,private High
{ public:
float volumn;
Cylinder(float a,float b,float c,float
d):Circle(a,b,c),High(d)
{volumn=area()*geth(); }
float getvolumn(){return volumn;}
};
#include <iostream.h>
void main()
{ Cylinder a(10,20,15,20);
cout<<"圆柱体的体积为,"<<a.getvolumn(); }
多继承会产生二义性问题。例如:
class A
{public:
void f();
};
class B
{public:
void f();
void g();
};
class C:public A,public B
{public:
void g();
void h();
};
如果定义一个类C的对象 c1:
C c1;
则对函数 f()的访问
c1.f();
便具有二义性:是访问类A中的 f(),还是访问类B中的 f()呢?
解决的方法可以用成员名限定法来消除二义性,例如:
c1.A::f();
或者
c1.B::f();
也可以在类C中定义一个同名成员 f(),类C中的 f()再根据需要来决定调用 A::f(),还是 B::f(),还是两者皆有,这样,c1.f()将调用 C::f()。
下面再讨论另一种情况下的二义性问题。当一个派生类从多个基类派生,而这些基类又有一个共同的基类,则对该基类中说明的成员进行访问时,可能会出现二义性,例如:
class A
{public,int a;};
class B1:public A
{private,int b1;};
class B2:public A
{private,int b2;};
class C:public B1,public B2
{public,int f();
private,int c;};
已知,C c1;
下面的两个访问都有二义性:
c1.a;
c1.A::a;
而下面的两个访问是正确的:
c1.B1::a;
c1.B2::a;
C++中通过引入虚基类来解决二义性问题。例如:
class A
{public,int a;};
class B1,virtual public A //声明A为虚基类
{private,int b1;};
class B2,virtual public A //声明A为虚基类
{private,int b2;};
class C:public B1,public B2
{public,int f();
private,int c;};
由于使用了虚基类,使得类 A、类 B1、
类 B2、类 C之间的关系如图 15.4所示,
消除了合并之前可能出现的二义性。
下面的访问是正确的:
C c1;
c1.a;
c1.A::a;
15.4多态性
当不同的对象接收到相同的消息名(或者说当不同的对象调用相同名称的成员函数)时,可能引起不同的行为(执行不同的代码)。这种现象称为多态性。多态性通过联编实现。联编是指一个计算机程序自身彼此关联的过程。按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。在 C++中,
根据联编的时刻不同,存在两种类型多态性:
函数重载和虚函数。
静态联编是指联编工作出现在编译连接阶段,在编译时就解决了程序中的操作调用与执行该操作代码间的关系。静态联编所支持的多态性称为编译时多态性。函数重载就属于编译时多态性。
在程序运行时才能确定调用哪一个函数,
这种在运行时的函数联编称为动态联编。
动态联编所支持的多态性称为运行时多态性。在 C++中,只有虚函数才可能是动态联编的。可以通过定义类的虚函数和创建派生类,然后在派生类中重新实现虚函数,
实现具有运行时的多态性对象。
虚函数是用关键字 virtual修饰的基类中的
protected或 public成员函数。当成员函数被说明是虚函数时,那么它在所有的派生类及派生类的子类中都是虚函数,即使在派生类中没有明确的使用关键字 virtual;然而,这不能颠倒过来:在派生类中说明的虚函数在基类中不能自动成为虚函数。基类和派生类中同一个虚函数不仅要有相同的函数名字,而且函数的参数个数和类型必须相同。否则,派生类中将出现函数的重载(存在两个同名但参数不同的函数)。另外,派生类中的虚函数的返回值类型也必须与基类中的虚函数定义一致。通过使用虚函数实现动态联编使得扩充程序变得容易。
例 15.7:
class base
{ public:virtual int f(dluble); //定义一个虚函数。
虚函数一般和派生类一起使用
};
class derived1:public base
{public,int f(double); //派生类中的虚函数
int f(int); //函数的参数类型不同,这不是虚函数
};
class derived2:public base
{ public:double f(double);
//f()返回值类型与基类中的虚函数定义不同,所以不是虚函数
};
许多情况下,在基类中不能为虚函数给出一个有意义的定义。这时可以在基类中将它说明为纯虚函数。它的实现留给派生类去做。纯虚函数不能被直接调用,仅起提供一个与派生类相一致的接口作用。声明纯虚函数的形式为:
virtual 类型 函数名(参数表列) =0;
包含有纯虚函数的类称为抽象类。一个抽象类只能作为基类派生出新的子类,而不能在程序中被实例化(即不能说明抽象类的对象),但是可以使用指向抽象类的指针。
例 15.8计算由几个不同形状的图形组成的总面积。
设要计算的总面积中包括有三角形
( triangle)、圆( circle)、矩形 (rectangle)
的面积。求总面积的一种方法是分别定义不同的类,triangle,circle与 rectangle,然后生成各自的一些对象,再一一对它们的面积求和。另一种方法是,先定义一个抽象类 TFigure,再定义派生类,通过定义一个由指向 TFigure的指针组成的向量,使这些指针指向求不同形状的面积的虚函数,
由实际生成的不同形状类的对象调用,并用重复结构求和。下面介绍这种方法。
class TFigure
{public:
virtual double area()=0;
//纯虚函数
};
const double PI=3.141593;
class TCircle,public TFigure
{private:
double radius;
public:
TCircle (double r) //构造函数
{ radius=r; }
double area( ) //重定义
{ return radius*radius*PI; }
};
class TTriangle:public TFigure
{ protected,
double high,wide;
public:
TTriangle(double h,double w)
{ high=h;
wide=w; }
double area() //重定义
{ return high*wide*0.5; }
};
class TRectangle,public TTriangle
{ public:
TRectangle (double h,double
w ):TTriangle(h,w){ }
//TRectangle类是 TTriangle类的派生类
double area( )
{ return high*wide; }
};
double total(TFigure *pf[ ],int n)
// 求面积和
{
double sum=0.0;
for(int i=0;i<n;i++)
sum+=pf[i]->area();
//按实际对象调用 area()
return sum;
}
#include <iostream.h>
void main( )
{ TFigure *pf[5]; int j;
pf[0]=new TTriangle(3.0,4.0);
pf[1]=new TRectangle(2.0,3.5);
pf[2]=new TRectangle(5.0,1.0);
pf[3]=new TRectangle(3.0,6.0);
pf[4]=new TCircle(10.0);
cout<<total(pf,5)<<endl;
for(j=0;j<5;j++)
delete pf[j];
}
纯虚函数不可以被继承。当基类是抽象类时,在派生类中必须给出基类中纯虚函数的定义,或在该类中再声明其为纯虚函数。只有在派生类中给出了基类中所有纯虚函数的实现时,该派生类就不再成为抽象类。因此,抽象类的主要作用是为类的家族建立一个统一的接口。