15.1 概述
15.2 类和对象
15.3 构造函数和析构函数
15.4 继承与派生第 15章 C++的面向对象基础
15.1 概述
C++不仅扩充了 C面向过程的功能,而且增加了面向对象的功能,这就使 C++成为能适应面向对象程序设计的现代大型语言。 C++不是简单地对 C做了某些改进,而是在 C成功的基础上进行了一场革命。
赋予 C以新的生命力。
客观世界中任何一个事物都可以看成一个对象。或者说,客观世界是由千千万万个对象组成的,它们之间通过一定的渠道相互联系,如图 15.1示意。
在实际生活中,人们往往在一个对象中进行活动,
或者说对象是进行活动的基本单位。作为对象,
它应该至少有两个要素:一是从事活动的主体;
二是活动的内容。要进行活动,或者是事先安排好一个活动计划,或者由外界临时通知。
图 15.1 图 15.2
从计算机的角度看,一个对象应该包括两个要素:
一是数据;二是需要进行的操作。对象就是一个包含数据以及与这些数据有关的操作的集合。图
15.2 表示一个对象是由数据和操作代码组成的。
传统的面向过程程序设计是围绕功能进行的,用一个函数实现一个功能。所有的数据都是公用的,
一个函数可以使用任意一组数据,而一组数据又能被多个函数所使用(见图 15.3)。程序设计者必须考虑每一个细节,什么时候对什么数据进行操作。当程序规模较大、数据很多、操作种类繁多时,程序设计者往往感到难以应付。
图 15.3
面向对象程序设计采用新的思路。它面对的是一个个对象。所有的数据分别属于不同的对象。实际上,每一组数据都是有特定的用途的,是某种操作的对象。把相关的数据和操作放在一起,形成一个整体,与外界相对分隔。面向对象程序设计方法的一个重要特点就是“封装性”,把数据和操作代码封装在一个对象中。程序设计者的任务包括两个方面:一是设计对象,即决定把哪些数据和操作封装在一起;二是在此基础上怎样通知有关对象完成所需的任务。这时他如同一个总调度,不断地向各个对象发出命令,让这些对象活动起来,完成自己范围内的操作。各个对象的操作完成了,整体任务也就完成了。显然,对一个大型任务来说,面向对象程序设计方法将是十分有效的,它能大大降低程序设计人员的工作难度,
减少出错机会。
15.2 类 和 对 象每一个实体都是对象。有一些对象是具有相同的结构和特性的。在 C++中对象的类型称为
“类” (class) 。类代表了某一批对象的共性和特征。
可以说:类是对象的抽象,而对象是类的具体实例。正如同结构体类型和结构体变量的关系一样,
人们先声明一个结构体类型,然后用它去定义结构体变量。同一个结构体类型可以定义出多个不同的结构体变量。在 C++中也是先声明一个“类”
类型,然后用它去定义若干个同类型的对象。对象就是一个“类”类型的变量。类是用来定义对象的一种抽象数据类型,或者说它是产生对象的模板。它的性质和其他数据类型(如整型、实型、
枚举类型、结构体类型)相同。在一开始时弄清对象和类的关系是十分重要的。
C++对 C的改进,最重要的就是增加了“类” 这样一种数据类型。所以 C++开始时被称为“带类的
C”。所有面向对象的语言都提供了这种数据类型。
在 C++中怎样声明一个“类”类型呢?其方法和声明一个结构体类型是相似的。下面是我们已熟悉的声明一个结构体类型:
struct student
{int num;
char name[ 10] ;
char sex ;
};
struct student student1,student2;
上面声明了一个名为 student的结构体类型并定义了两个结构体变量 student1和 student2。可以看到它只包括数据(变量),没有包括操作。如果希望对结构体变量中各成员赋值或输出它们的值,需要另外编写有关的操作语句。结构体变量中各成员在本作用域中都是“敞开”的,大家都可以用,
而且不受任何限制。这就造成程序的不安全。现在我们声明一个类:
class stud//以 class开头
{int num;
char name[ 10] ;
char sex ; //以上 3行是数据成员
void display( ) //这是成员函数
{cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl; } //以上 4行是操作代码
};
stud stud1,stud2; //定义了两个 stud 类的对象这就声明了一个名为 stud的类。可以看到声明“类”
的方法是由声明结构体类型的方法发展而来的。
它除了包含数据部分以外,还包括了对这些数据的操作部分,也就是把数据和操作封装在一起。
display是一个函数,用来输出本对象中学生的学号、姓名和性别。类除了具有封装性外,还采用了信息隐蔽原则,使类中的成员与外界的联系减少到最低限度。现在封装在 stud中的成员都对外界隐蔽,外界不能调用它们。只有本类中的函数
display可以调用同一类中的数据。
在本类中没有指定的操作一律不能执行。这当然安全了,但是谁来通知执行 display函数呢?它无法启动,缺少对外界的接口。因此,不能把全部成员与外界隔离,一般是把数据隐蔽起来,而把成员函数作为对外界的接口,譬如可以从外界发出一个命令,通知该对象执行 display函数,输出某一学生的有关数据。
类的成员包括两大类,一类是“私有的” (private),
即外界不能调用;另一类是“公用的” (public),
有的书译为“公有的”,即公开的,外界可以调用(稍后还要介绍一类即“受保护的” (protected)。
可以将上面类的声明改为
class stud
{private,//声明以下部分为私有的
int num;
char name[ 10] ;
char sex ;
public,//声明以下部分为公用的
void display( )
{cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl; } //以上 4行是操作代码
};
stud stud1,stud2; //定义了两个 stud 类的对象现在声明了 display函数是公用的,外界就可以调用该函数了。
如果在类的声明中既不指定 private,也不指定 public,
则系统就认为是私有的(第一次的类声明就属此情况)。
顺便介绍在面向对象程序设计中的几个名词:类中的成员函数称为,方法,,“方法”是对数据的操作 。一个“方法”对应一种操作。显然,只有被声明为 public的方法(成员函数)才能被对象外界所激活。外界是通过发,消息,来激活有关方法的。所谓“消息”,其实就是一个 命令,由程序语句来实现。例如想输出对象 stud1中的学生学号、姓名、性别等信息,可以在程序中写
stud1.display( );
这就是向对象 stud1发出的一个“消息”,通知它执行 display“方法”(即 display函数)。在这里一个语句中涉及到 3个术语:对象、方法和消息。
归纳以上对类型的声明,可以得到其一般形式:
class 类名
{private,
私有的数据和成员函数 ;
public:
公用的数据和成员函数 ;
};
C++增加了 class类型后,仍保留了结构体类型 (struct)
和共用体类型 (union),而且把它们的功能也扩展了,允许在声明的结构体和共用体类型中包括成员函数,也就是可以用 struct和 union来声明一个类型。但它们和 class声明的类有所区别。
用 struct声明的类,如果不作 private或 public声明,
系统将其成员默认定为 public(公用的),在需要时可以自己用显式声明重新指定为 private或 public。
用 union声明的类,如果不作 private或 public声明,
系统将其成员默认定为 public(公用的),且不能改变。
用 class声明的类,如果不作 private或 public声明,系统将其成员默认定为 private(私有的),在需要时可以自己用显式声明改变。
15.3 构造函数和析构函数
15.3.1 构造函数在建立一个对象时,常常需要作某些初始化的工作
(例如对数据赋予初值),C++提供了一种特殊的成员函数 ——构造函数 (constructor) 。这种函数与其他成员不同,不需要用户发“消息” 来激活它,
而是在建立对象时自动执行。构造函数是由用户定义的,它必须与类名同名,以便系统能识别它并把它作为构造函数。现在我们在前面声明的类中加入构造函数。
class stud
{private,//声明以下部分为私有的
int num;
char name[ 10] ;
char sex ;
public,
stud( ) //定义构造函数,函数名与类名相同
{num=10010;
strcpy(name,"Wang-li");
sex=′F′; } //以上 3行为给数据赋初值 void
display( ) //定义成员函数
{cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl; }
};
stud stud1; //在定义对象 stud1时自动执行构造函数注意,构造函数不需用户调用,而是在定义一个对象时由系统自动执行,而且只能执行一次。构造函数一般声明为 public,无返回值,也不需加 void类型声明。
现在写成一个完整的程序。
例 15.1 建立一个对象,输出学生的学号、姓名、性别。
#include<string.h>
#include<iostream.h>
void main( )
{ class stud //声明一个类
{private,//私有部分
int num;
char name[ 10] ;
char sex ;
public,//公用部分
stud( ) //定义构造函数,函数名与类名相同
{num=10010; //给数据赋初值
strcpy(name,"Wang-li");
sex=′F′; } void display( ) //定义成员函数,输出对象的数据
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl; }
};
stud stud1; //在定义对象 stud1时自动执行构造函数
stud1.display( ); //从对象外面调用 display函数
}
可以看到整个程序很简单,它包括三部分:① 声明一个类;② 定义一个对象;③ 向对象发出消息,
执行对象中的成员函数 display。在定义 stud1对象时自动执行了构造函数 stud( ),因此对象中的数据成员均被赋了值。执行 display函数输出以下信息:
num,10010
name,Wang-li
sex,F
在程序中可以看到只有对象中的函数才能引用本对象中的数据。如果在对象外面直接用
cout<<stud1.num ;
企图输出学生的学号是不行的。由此可体会到类的特点。
如果要建立两个对象,分别对数据赋予初值,就不能这样定义构造函数 stud了,因为它会使两个学生的初值相同,例如姓名都是 Wang-li。应该分别赋予不同的初值。可将构造函数修改如下:
stud(int n,char nam[],char s ) //定义构造函数,有形参
{num=n;
strcpy(name,nam);
sex=s;
}
此时数据的值不由构造函数 stud确定,而是在调用此函数时由实参传来。但应注意构造函数不同于一般的成员函数,不能这样调用:
stud1.stud(10010,,Wang-li”,′f′); //企图用调用一般成员函数的方法来调用构造函数前已说明构造函数是在建立对象时调用的,因此实参应该在建立对象时给出。如:
stud stud1(10010,"Wang-li",′f′),stud2(10011,
"Zhang-fun",′m′);
现在定义了两个对象 stud1和 stud2,它们的数据初值是不同的。如果想分别输出两个学生的数据,可以用以下的语句:
stud1.display( );
stud2.display( );此时的输出如下,num,10010
name,Wang-li
sex,f
num,10011
name,Zhang-fun
sex,m
用户也可以不定义构造函数,编译系统会自动生成一个构造函数,该函数没有参数,不进行任何操作。
构造函数也可以重载,下面是两个重载函数:
stud( ) //在构造函数中对数据赋初值
{num=10010;
strcpy(name,"Wang-li");
sex=′F′;
} stud(int n,char nam[],char s ) //有形参,由实参将值传给形参
{num=n;
strcpy(name,nam);
sex=s;
}
在定义对象时允许有实参和无实参。如:
stud stud1; //不带实参,数据初值在构造函数中指定
stud stud2(10011,"Zhang-fun",′m′); //带实参,数据初值由实参给出
15.3.2 析构函数析构函数 (destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),
系统自动执行析构函数。析构函数往往用来做
“清理善后” 的工作(例如在建立对象时用 new
开辟了一片内存空间,应在退出前在析构函数中用 delete释放)。
析构函数名也应与类名相同,只是在函数名前面加一个波浪符 ~,例如 ~stud( ),以区别于构造函数。
它不能带任何参数,也没有返回值(包括 void类型)。只能有一个析构函数,不能重载。如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数,它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
例 15.2 包含构造函数和析构函数的 C++程序。
#include<string.h>
#include<iostream.h>
class stud //声明一个类
{private,// 私有部分
int num;
char name[ 10] ;
char sex ;
public,//公用部分
stud(int n,char nam[],char s ) //构造函数
{num=n;
strcpy(name,nam);
sex=s; }
~stud( ) //析构函数
{ }
void display( ) //成员函数,输出对象的数据
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl; }
};
void main( )
{
stud stud1(10010,"Wang-li",′f′),stud2(10011,
"Zhang-fun",′m′); //建立两个对象
stud1.display( ); //输出学生 1的数据
stud2.display( ); //输出学生 2的数据
}
现在把类的声明放在 main函数之前,它的作用域是全局的。这样做可以使 main函数更简练一些。在
main函数中定义了两个对象并且给出了初值。然后输出两个学生的数据。运行结果和例 15.1中给出的相同。在本例中,析构函数并无任何实质上的作用,我们把它写出来,只是为了说明析构函数的使用方法。
在本程序中,成员函数是在类中定义的,如果成员函数的数目很多以及函数的长度很长,类的声明就会占很大的篇幅,不利于阅读程序。可以在类的外面定义成员函数,而在类中只用函数的原型作声明。
例 15.3 在类的外面定义成员函数。
#include<string.h>
#include<iostream.h>
class stud//声明一个类
{private,
int num;
char name[ 10] ;
char sex ;
public,
stud(int n,char nam[],char s ) ; //对构造函数的原型声明
~stud( ); //对析构函数的原型声明
void display( ) ; //对成员函数 display的原型声明
}; stud∷ stud(int n,char nam[],char s ) //对构造函数的定义
{num=n;
strcpy(name,nam);
sex=s; }
stud∷ ~stud( ) //对析构函数的定义
{ }
void stud∷ display( ) //对成员函数 display的定义
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl;
}
void main( )
{
stud stud1(10010,"Wang-li",′f′),stud2(10011,
"Zhang-fun",′m′);
stud1.display( ); //输出学生 1的数据
stud2.display( ); //输出学生 2的数据
}
请读者注意在类声明的外部定义函数,必须指定类名。函数首行的形式为函数类型 类名 ∷ 函数名(形参表列)
不能写成
void display( ) //未指定类名如果这样写,编译系统会把它作为普通的函数处理,
而不作为类中的成员函数。也不要写成:
stud∷ void display( ) //函数类型位置不对
stud∷ display( ) 是一个整体,说明是 stud类中的
display函数。不能把 stud∷ 和 display( ) 分隔开来。
虽然函数在类的外部定义,但在调用成员函数时会根据类中函数的原型声明找到函数的定义(函数代码),从而执行该函数。
也可以将在类外部定义的函数声明为“内置函数”,
这样在编译时就将函数代码代入到类中的函数调用处,以提高程序执行效率。只需在函数定义的首行最左端加上 inline即可。如:
inline void stud∷ display( )
15.4 继 承 与 派 生
15.4.1 继承与派生的概念面向对象技术强调软件的可重用性。在 C++中可重用性是通过“继承” 这一机制来实现的。因此,
继承是 C++的一重要组成部分。
前面介绍了类,一个类中包含了若干数据成员和成员函数。每一个类的数据成员和成员函数是不相同的。但有时两个类的内容基本相同或有一部分相同。例如前面我们声明了学生基本数据的类 stud:
class stud
{private,
int num;
char name[ 10] ;
char sex;
public,
void display( ) //对成员函数 display的定义
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl; }
};
如果学校的某部门除了需要用到学号、姓名、性别以外,还需要用到年龄、地址等信息。当然可以重新声明另一个类:
class stud 1
{private,
int num; //此行原来己有
char name[ 10] ; //此行原来己有
char sex; //此行原来己有
int age;
char addr[ 20] ;
public,
void display( ) ; //此行原来己有
{cout<<"num,"<<num<<endl; //此行原来己有
cout<<"name,"<<name<<endl; //此行原来己有
cout<<"sex,"<<sex<<endl; //此行原来己有
cout<<"age,"<<age<<endl;
cout<<"address,"<<addr<<endl;}
};
可以看到有相当一部分是原来已有的。很多人自然会想到能否利用原有声明的类作为基础,再加上新的内容即可,以减少重复的工作量。 C++提供的“继承” 机制就是为了解决这个问题。
举一个日常生活中的例子。如果已经定义了“马”
的特征,现在需要说明什么是“公马”,只需在
“马”的特征的基础上增加“雄性”这一特征即可,不必从头说明什么是马。如果想进一步说明什么是“白色的公马”,只需在“公马”的基础上再增加说明“颜色是白的”即可。也就是说:
“公马”继承了“马”的全部特征,加上“雄性”
的新特征。“白公马” 继承了“公马”的全部特征,再增加“白色”的特征。而“公马”是“马”
派生出来的一个分支,“白公马” 是“公马” 派生出来的一个分支。见图 15.4示意。
在 C++中所谓“继承” 就是在一个已存在的类的基础上建立一个新的类。已存在的类(好比“马”)
称为“基类” 或“父类” 。新建立的类称为“派生类” 或“子类”。 见图 15.5示意。 派生类继承了基类的所有数据成员和成员函数,并增加新的成员。
图 15.4 图 15.5
15.4.2 建立派生类的方法先通过一个例子说明怎样通过继承来建立派生类。
例 15.4通过继承来建立派生类。
假设已经声明了一个基类 stud(见前面的介绍),
在此基础上声明一个派生类 student:
class student,public stud //声明基类是 stud
{
private:
int age; //新增加的数据成员
char addr[ 30] ; //新增加的数据成员
public:
void display-1( ) //新增加的成员函数
{cout<<"age,"<<age<<endl;
cout<<"address,"<<addr<<endl;}
};
仔细观察第一行,
class student,public stud
在 class后面的 student是新建的类名。冒号后面的
stud表示是已存在的基类。在 stud之前有一关键字
public,用来表示基类 stud中的成员在派生类
student中的使用权限。基类名前面有 public的称为
“公用派生类”。其含义将在稍后讨论。
请仔细阅读以上声明的派生类 student和上一节中给出的基类 stud,并将它们放在一起进行分析。
定义派生类的一般形式为
class 派生类名,[引用权限] 基类名
{
派生类新增加的数据成员派生类新增加的成员函数
} ;
“引用权限”可以是 private和 public。默认为 private。
派生类包括基类成员和自己增加的成员,派生类的成员函数在引用派生类自己的数据成员时,按前面介绍过的规则处理(即私有数据成员只能被同一类中的成员函数引用,公用成员可以被外界引用)。而对从基类继承来的成员的引用并不是简单地把基类的私有成员和公用成员直接作为派生类的私有成员和公用成员,而要根据基类成员的引用权限和派生类声明的引用权限共同决定。下面将分别介绍。
15.4.3 公用派生类在声明一个派生类时将基类的引用权限指定为 public
的,该类称为基类的公用派生类。
在公用派生类中,基类的公用成员和保护 (protected)
成员仍然成为派生类中的公用成员和保护成员 (关于保护成员将在 15.4.5节介绍 ),而基类的私有成员不能被派生类引用,即成为派生类“不可访问的成员”,只有基类的成员函数可以引用它。基类的成员在公用派生类中的引用权限见表 15.1。
表 15.1 公用派生类的继承关系基类 私有成员 公用成员公用派生类 不可访问的成员 公用成员例 15.5 访问基类成员。
class stud //声明基类
{private,//基类私有成员
int num;
char name[ 10] ;
char sex;
public,//基类公用成员
void display( )
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl; }
}; class student,public stud //声明一个公用派生类
{
private:
int age;
char addr[ 30] ;
public:
void show( )
{ cout<<"num,"<<num<<endl; //企图引用基类的私有成员,
错误。
cout<<"name,"<<name<<endl; //企图引用基类的私有成员,错误。
cout<<"sex,"<<sex<<endl; //企图引用基类的私有成员,错误。
cout<<"age,"<<age<<endl; //引用派生类的私有成员,
正确。
cout<<"address,"<<addr<<endl;} //引用派生类的私有成员,正确。
};
由于基类的私有成员对派生类来说是不可访问的,因此在派生类中的 show函数中直接引用基类的私有数据成员 num,name和 sex是不允许的。可以通过基类的公用成员函数来引用基类的私有数据成员。上面对派生类 student的声明可改为
class student,public stud //声明一个公用派生类
{
private:
int age;
char addr[ 20] ;
public:
void show( )
{ display( ); //引用基类的公用成员函数,允许。
cout<<"age,"<<age<<endl; //引用派生类的私有成员,
正确。
cout<<"address,"<<addr<<endl;} //引用派生类的私有成员,
正确。
};
在派生类成员函数 show中引用基类的公用成员函数
display,通过 display引用基类 stud中的私有数据
num,name 和 sex。可以这样写 main函数(假设对象 a中已有数据):
void main( )
{student a;//定义一个 student派生类的对象 a
…
a.show( ); //输出 a对象的 5个数据
};请分析在主函数中能否出现以下语句,a.display( ); //正确。从基类继承的公用成员函数
a.age=18; //错误。外界不能引用派生类的私有成员
a.num=10020; //错误。外界不能引用基类的私有成员
15.4.4 私有派生类在声明一个派生类时,将基类的引用权限指定为
private的,该类称为基类的私有派生类。
在私有派生类中,基类的公用成员和保护成员成为派生类中的私有成员,基类的私有成员成为派生类“不可访问的成员”,只有基类的成员函数可以引用它。基类的成员在私有派生类中的引用权限见表 15.2。
表 15.2 私有派生类的继承关系基类 私有成员 公用成员私有派生类 不可访问的成员 私有成员如果派生类声明首行改为
class student,private stud//声明一个私有派生类
{ private:
int age;
char addr[ 30] ;
public:
void show( )
{ display( ); //基类的公用成员函数变成派生类的私有函数
cout<<"age,"<<age<<endl; //引用派生类的私有成员,正确。
cout<<"address,"<<addr<<endl;} //引用派生类的私有成员,正确。
}; void main( )
{student a;//定义一个派生类的对象 a
a.display( ); //错误。基类的公用成员函数变成派生类私有函数
a.age=18; //错误。外界不能引用派生类的私有成员
}
可以看到:
(1) 不能通过私有派生类对象 (如 a)引用从基类继承过来的任何成员。
(2) 在派生类的成员函数中不能访问基类的私有成员,
但可以访问基类的公用成员。
由于私有派生类限制太多,一般不常使用。
15.4.5 保护成员前面已接触过“保护”( protected)这一名词。它和 private,public一样是用来声明成员的引用权限的。由 protected声明的成员称为保护成员。保护成员不能被外界引用(这点和私有成员类似),
但它可以被派生类的成员函数引用。
将表 15.1和表 15.2综合表示并增加保护成员的内容,
见表 15.3。
表 15.3 派生类的继承关系基类 私有成员 公用成员 保护成员公用派生类 不可访问的成员 公用成员 保护成员私有派生类 不可访问的成员 私有成员 私有成员从前面的介绍已知基类的私有成员被派生类(不论是私有派生类还是公用派生类)继承后变为“不可访问的成员”。 如果想在派生类引用基类的成员,可以将基类的成员声明为 protected。
例 15.6 派生类引用保护成员。
class stud//声明基类
{protected,//基类保护成员
int num;
char name[ 10] ;
char sex;
public,//基类公用成员
void display( )
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl; }
}; class student,public stud //声明一个公用派生类
{
private:
int age;
char addr[ 30] ;
public:
void show( )
{ cout<<"num,"<<num<<endl;//引用基类的保护成员,合法。
cout<<"sex,"<<sex<<endl; //引用基类的保护成员,合法。
cout<<"age,"<<age<<endl; //引用派生类的私有成员,合法。
cout<<"address,"<<addr<<endl;}//引用派生类的私有成员,合法。
};
void main( )
{student a; //a是派生类 student类的对象。
a.show( ); //合法。 show是派生类中的公用成员函数。
a.num=10023; //错误。外界不能访问保护成员。
}
请与例 15.5对比分析。可以看到基类的保护成员对类的外界来说仍然是不可引用的(由于 stud中的
num是保护成员,外界不能用 a.num来引用它),
但对派生类来说它相当于私有成员,可以通过成员函数引用。也就是把保护成员的引用范围扩展到派生类中。
15.4.6 派生类的构造函数派生类从基类继承了非私有成员函数和数据成员,
但是在建立派生类的对象时,系统只执行派生类的构造函数,而不会自动执行基类的构造函数。
也就是说,基类的构造函数是不能继承的。如果基类的构造函数包含对变量的初始化,那么在建立派生类的对象时,由于没有执行基类的构造函数,因而就会使基类的变量未初始化。所以在设计派生类的构造函数时,不仅要考虑派生类所增加的变量初始化,还应当考虑基类的变量初始化。
在执行派生类的构造函数时,应当调用基类的构造函数。
例 15.7 派生类的构造函数。
#include<string.h>
#include <iostream.h>
class stud//声明基类
{protected,//保护部分
int num;
char name[ 10] ;
char sex ;
public,//公用部分
stud(int n,char nam[],char s ) //基类构造函数
{num=n;
strcpy(name,nam);
sex=s; }
~stud( ) { } //基类析构函数
};
class student,public stud //声明公用派生类 student
{ private,//派生类的私有数据
int age;
char addr[ 30] ;
public:
student(int n,char nam[],char s,int a,char ad[] ),
stud(n,nam,s)
//派生类构造函数
{age=a; //在此处只对派生类新增的变量初始化
strcpy(addr,ad);
}
void show( )
{ cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl;
cout<<"age,"<<age<<endl;
cout<<"address,"<<addr<<endl;}
~student ( ) { } //派生类析构函数
};
void main( )
{student a(10010,"Wang-li",′f′,19,"115 Beijing Road,
Shanghai");
student b(10011,"Zhang-fun",′m′,21,"213
Shanghai Road,Beijing");
a.show( ); //输出第一个学生的数据
b.show( ); //输出第二个学生的数据
}
请注意派生类构造函数首行的写法:
student (int n,char nam[],char s,int a,char ad
[] ),stud(n,nam,s)
其一般形式为派生类构造函数名(参数表列):基类构造函数名
(参数表列)
派生类构造函数名后面括号内的参数表列包括参数的类型和参数名。基类构造函数名后面括号内的参数表列只有参数名而不包括参数类型。从基类的声明中可以看到基类构造函数 stud有 3个参数( n,
nam,s),派生类构造函数 student有 5个参数,
前 3个是用来传递给基类构造函数的,后面 2个( a
和 ad)是用来对派生类所增加的变量初始化的。
在 main函数中建立对象 a时指定了 5个实参。它们按顺序传递给派生类构造函数的形参。然后,派生类构造函数将前面 3个传递给基类构造函数的参数。
见图 15.6。
图 15.6
通过 stud (n,nam,s) 把 3个值再传给基类构造函数,见图 15.7。
stud (n,nam,s)
↓ ↓ ↓
stud(int n,char nam[],char s )//这是基类构造函数的首部图 15.7
在上例中也可以将派生类构造函数在类外面定义,而在类的声明中只写该函数的声明:
student(int n,char nam[],char s,int a,char ad[]) ;
在类的外面定义派生类构造函数:
student ∷ student (int n,char nam[],char s,int a,char
ad[] ),stud(n,nam,s)
{age=a;
strcpy(addr,ad);
}
注意:在类中对派生类构造函数作声明时,不包括基类构造函数名和参数表列(即 stud(n,nam,
s))。只在定义函数时才将它列出。
在建立一个对象时,由派生类构造函数先调用基类构造函数,然后再执行派生类构造函数本身。对上例来说,先初始化 num,name,sex,然后再初始化 age和 addr。
在派生类对象消失时,先执行派生类析构函数
~student( ),再执行其基类析构函数 ~stud( )。
15.4.7 继承在软件开发中的重要意义继承是面向对象技术的一个重要内容。有了继承,
使软件的重用成为可能。过去,软件人员开发新的软件,能从已有的软件中直接选用完全符合要求的部件不多,一般都要进行许多修改才能使用,
实际上有相当部分要重新编写,工作量很大。继承机制解决了这个问题。将已有的一个类为基础,
生成一些派生类(子类),在子类中保存父类中有用的数据和操作,去掉(屏蔽掉)不需要的部分。新生成的子类还可以再生成孙类 …… 而且一个子类可以从多个父类中获得继承(即多继承机制)。编写面向对象的程序时要把注意力放在实现有用的类上面,对已有的类加以整理和分类就有可能使这些类能够被程序设计的许多领域使用。
软件设计者可以最大限度地重用已有软件,对已有的类根据需要进行剪裁和修改,在此基础上集中精力编写子类新增加的部分即可。因此有人认为继承是 C++和 C的最重要的区别之一。
在本章中我们只是很简单地介绍了 C++面向对象的一些初步知识。面向对象技术和 C++的内容很丰富,
有许多深入的概念,语法规定也比较复杂,需要花较多的精力和时间去学习和消化。 C++最重要的概念是类,其他许多概念都是与类有关的。为了便于理解,我们在本章中没有介绍复杂的程序,
所举的例题都是比较简单的,只是为了说明一些概念。正如在本章开始时提到的,只有编写过大程序的人才能真正体会 C++的优越性。我们只是希望通过本章的介绍,使读者对 C++的特点有初步的了解,为以后系统地学习 C++建立一些基础。
15.2 类和对象
15.3 构造函数和析构函数
15.4 继承与派生第 15章 C++的面向对象基础
15.1 概述
C++不仅扩充了 C面向过程的功能,而且增加了面向对象的功能,这就使 C++成为能适应面向对象程序设计的现代大型语言。 C++不是简单地对 C做了某些改进,而是在 C成功的基础上进行了一场革命。
赋予 C以新的生命力。
客观世界中任何一个事物都可以看成一个对象。或者说,客观世界是由千千万万个对象组成的,它们之间通过一定的渠道相互联系,如图 15.1示意。
在实际生活中,人们往往在一个对象中进行活动,
或者说对象是进行活动的基本单位。作为对象,
它应该至少有两个要素:一是从事活动的主体;
二是活动的内容。要进行活动,或者是事先安排好一个活动计划,或者由外界临时通知。
图 15.1 图 15.2
从计算机的角度看,一个对象应该包括两个要素:
一是数据;二是需要进行的操作。对象就是一个包含数据以及与这些数据有关的操作的集合。图
15.2 表示一个对象是由数据和操作代码组成的。
传统的面向过程程序设计是围绕功能进行的,用一个函数实现一个功能。所有的数据都是公用的,
一个函数可以使用任意一组数据,而一组数据又能被多个函数所使用(见图 15.3)。程序设计者必须考虑每一个细节,什么时候对什么数据进行操作。当程序规模较大、数据很多、操作种类繁多时,程序设计者往往感到难以应付。
图 15.3
面向对象程序设计采用新的思路。它面对的是一个个对象。所有的数据分别属于不同的对象。实际上,每一组数据都是有特定的用途的,是某种操作的对象。把相关的数据和操作放在一起,形成一个整体,与外界相对分隔。面向对象程序设计方法的一个重要特点就是“封装性”,把数据和操作代码封装在一个对象中。程序设计者的任务包括两个方面:一是设计对象,即决定把哪些数据和操作封装在一起;二是在此基础上怎样通知有关对象完成所需的任务。这时他如同一个总调度,不断地向各个对象发出命令,让这些对象活动起来,完成自己范围内的操作。各个对象的操作完成了,整体任务也就完成了。显然,对一个大型任务来说,面向对象程序设计方法将是十分有效的,它能大大降低程序设计人员的工作难度,
减少出错机会。
15.2 类 和 对 象每一个实体都是对象。有一些对象是具有相同的结构和特性的。在 C++中对象的类型称为
“类” (class) 。类代表了某一批对象的共性和特征。
可以说:类是对象的抽象,而对象是类的具体实例。正如同结构体类型和结构体变量的关系一样,
人们先声明一个结构体类型,然后用它去定义结构体变量。同一个结构体类型可以定义出多个不同的结构体变量。在 C++中也是先声明一个“类”
类型,然后用它去定义若干个同类型的对象。对象就是一个“类”类型的变量。类是用来定义对象的一种抽象数据类型,或者说它是产生对象的模板。它的性质和其他数据类型(如整型、实型、
枚举类型、结构体类型)相同。在一开始时弄清对象和类的关系是十分重要的。
C++对 C的改进,最重要的就是增加了“类” 这样一种数据类型。所以 C++开始时被称为“带类的
C”。所有面向对象的语言都提供了这种数据类型。
在 C++中怎样声明一个“类”类型呢?其方法和声明一个结构体类型是相似的。下面是我们已熟悉的声明一个结构体类型:
struct student
{int num;
char name[ 10] ;
char sex ;
};
struct student student1,student2;
上面声明了一个名为 student的结构体类型并定义了两个结构体变量 student1和 student2。可以看到它只包括数据(变量),没有包括操作。如果希望对结构体变量中各成员赋值或输出它们的值,需要另外编写有关的操作语句。结构体变量中各成员在本作用域中都是“敞开”的,大家都可以用,
而且不受任何限制。这就造成程序的不安全。现在我们声明一个类:
class stud//以 class开头
{int num;
char name[ 10] ;
char sex ; //以上 3行是数据成员
void display( ) //这是成员函数
{cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl; } //以上 4行是操作代码
};
stud stud1,stud2; //定义了两个 stud 类的对象这就声明了一个名为 stud的类。可以看到声明“类”
的方法是由声明结构体类型的方法发展而来的。
它除了包含数据部分以外,还包括了对这些数据的操作部分,也就是把数据和操作封装在一起。
display是一个函数,用来输出本对象中学生的学号、姓名和性别。类除了具有封装性外,还采用了信息隐蔽原则,使类中的成员与外界的联系减少到最低限度。现在封装在 stud中的成员都对外界隐蔽,外界不能调用它们。只有本类中的函数
display可以调用同一类中的数据。
在本类中没有指定的操作一律不能执行。这当然安全了,但是谁来通知执行 display函数呢?它无法启动,缺少对外界的接口。因此,不能把全部成员与外界隔离,一般是把数据隐蔽起来,而把成员函数作为对外界的接口,譬如可以从外界发出一个命令,通知该对象执行 display函数,输出某一学生的有关数据。
类的成员包括两大类,一类是“私有的” (private),
即外界不能调用;另一类是“公用的” (public),
有的书译为“公有的”,即公开的,外界可以调用(稍后还要介绍一类即“受保护的” (protected)。
可以将上面类的声明改为
class stud
{private,//声明以下部分为私有的
int num;
char name[ 10] ;
char sex ;
public,//声明以下部分为公用的
void display( )
{cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl; } //以上 4行是操作代码
};
stud stud1,stud2; //定义了两个 stud 类的对象现在声明了 display函数是公用的,外界就可以调用该函数了。
如果在类的声明中既不指定 private,也不指定 public,
则系统就认为是私有的(第一次的类声明就属此情况)。
顺便介绍在面向对象程序设计中的几个名词:类中的成员函数称为,方法,,“方法”是对数据的操作 。一个“方法”对应一种操作。显然,只有被声明为 public的方法(成员函数)才能被对象外界所激活。外界是通过发,消息,来激活有关方法的。所谓“消息”,其实就是一个 命令,由程序语句来实现。例如想输出对象 stud1中的学生学号、姓名、性别等信息,可以在程序中写
stud1.display( );
这就是向对象 stud1发出的一个“消息”,通知它执行 display“方法”(即 display函数)。在这里一个语句中涉及到 3个术语:对象、方法和消息。
归纳以上对类型的声明,可以得到其一般形式:
class 类名
{private,
私有的数据和成员函数 ;
public:
公用的数据和成员函数 ;
};
C++增加了 class类型后,仍保留了结构体类型 (struct)
和共用体类型 (union),而且把它们的功能也扩展了,允许在声明的结构体和共用体类型中包括成员函数,也就是可以用 struct和 union来声明一个类型。但它们和 class声明的类有所区别。
用 struct声明的类,如果不作 private或 public声明,
系统将其成员默认定为 public(公用的),在需要时可以自己用显式声明重新指定为 private或 public。
用 union声明的类,如果不作 private或 public声明,
系统将其成员默认定为 public(公用的),且不能改变。
用 class声明的类,如果不作 private或 public声明,系统将其成员默认定为 private(私有的),在需要时可以自己用显式声明改变。
15.3 构造函数和析构函数
15.3.1 构造函数在建立一个对象时,常常需要作某些初始化的工作
(例如对数据赋予初值),C++提供了一种特殊的成员函数 ——构造函数 (constructor) 。这种函数与其他成员不同,不需要用户发“消息” 来激活它,
而是在建立对象时自动执行。构造函数是由用户定义的,它必须与类名同名,以便系统能识别它并把它作为构造函数。现在我们在前面声明的类中加入构造函数。
class stud
{private,//声明以下部分为私有的
int num;
char name[ 10] ;
char sex ;
public,
stud( ) //定义构造函数,函数名与类名相同
{num=10010;
strcpy(name,"Wang-li");
sex=′F′; } //以上 3行为给数据赋初值 void
display( ) //定义成员函数
{cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl; }
};
stud stud1; //在定义对象 stud1时自动执行构造函数注意,构造函数不需用户调用,而是在定义一个对象时由系统自动执行,而且只能执行一次。构造函数一般声明为 public,无返回值,也不需加 void类型声明。
现在写成一个完整的程序。
例 15.1 建立一个对象,输出学生的学号、姓名、性别。
#include<string.h>
#include<iostream.h>
void main( )
{ class stud //声明一个类
{private,//私有部分
int num;
char name[ 10] ;
char sex ;
public,//公用部分
stud( ) //定义构造函数,函数名与类名相同
{num=10010; //给数据赋初值
strcpy(name,"Wang-li");
sex=′F′; } void display( ) //定义成员函数,输出对象的数据
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl; }
};
stud stud1; //在定义对象 stud1时自动执行构造函数
stud1.display( ); //从对象外面调用 display函数
}
可以看到整个程序很简单,它包括三部分:① 声明一个类;② 定义一个对象;③ 向对象发出消息,
执行对象中的成员函数 display。在定义 stud1对象时自动执行了构造函数 stud( ),因此对象中的数据成员均被赋了值。执行 display函数输出以下信息:
num,10010
name,Wang-li
sex,F
在程序中可以看到只有对象中的函数才能引用本对象中的数据。如果在对象外面直接用
cout<<stud1.num ;
企图输出学生的学号是不行的。由此可体会到类的特点。
如果要建立两个对象,分别对数据赋予初值,就不能这样定义构造函数 stud了,因为它会使两个学生的初值相同,例如姓名都是 Wang-li。应该分别赋予不同的初值。可将构造函数修改如下:
stud(int n,char nam[],char s ) //定义构造函数,有形参
{num=n;
strcpy(name,nam);
sex=s;
}
此时数据的值不由构造函数 stud确定,而是在调用此函数时由实参传来。但应注意构造函数不同于一般的成员函数,不能这样调用:
stud1.stud(10010,,Wang-li”,′f′); //企图用调用一般成员函数的方法来调用构造函数前已说明构造函数是在建立对象时调用的,因此实参应该在建立对象时给出。如:
stud stud1(10010,"Wang-li",′f′),stud2(10011,
"Zhang-fun",′m′);
现在定义了两个对象 stud1和 stud2,它们的数据初值是不同的。如果想分别输出两个学生的数据,可以用以下的语句:
stud1.display( );
stud2.display( );此时的输出如下,num,10010
name,Wang-li
sex,f
num,10011
name,Zhang-fun
sex,m
用户也可以不定义构造函数,编译系统会自动生成一个构造函数,该函数没有参数,不进行任何操作。
构造函数也可以重载,下面是两个重载函数:
stud( ) //在构造函数中对数据赋初值
{num=10010;
strcpy(name,"Wang-li");
sex=′F′;
} stud(int n,char nam[],char s ) //有形参,由实参将值传给形参
{num=n;
strcpy(name,nam);
sex=s;
}
在定义对象时允许有实参和无实参。如:
stud stud1; //不带实参,数据初值在构造函数中指定
stud stud2(10011,"Zhang-fun",′m′); //带实参,数据初值由实参给出
15.3.2 析构函数析构函数 (destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),
系统自动执行析构函数。析构函数往往用来做
“清理善后” 的工作(例如在建立对象时用 new
开辟了一片内存空间,应在退出前在析构函数中用 delete释放)。
析构函数名也应与类名相同,只是在函数名前面加一个波浪符 ~,例如 ~stud( ),以区别于构造函数。
它不能带任何参数,也没有返回值(包括 void类型)。只能有一个析构函数,不能重载。如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数,它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
例 15.2 包含构造函数和析构函数的 C++程序。
#include<string.h>
#include<iostream.h>
class stud //声明一个类
{private,// 私有部分
int num;
char name[ 10] ;
char sex ;
public,//公用部分
stud(int n,char nam[],char s ) //构造函数
{num=n;
strcpy(name,nam);
sex=s; }
~stud( ) //析构函数
{ }
void display( ) //成员函数,输出对象的数据
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl; }
};
void main( )
{
stud stud1(10010,"Wang-li",′f′),stud2(10011,
"Zhang-fun",′m′); //建立两个对象
stud1.display( ); //输出学生 1的数据
stud2.display( ); //输出学生 2的数据
}
现在把类的声明放在 main函数之前,它的作用域是全局的。这样做可以使 main函数更简练一些。在
main函数中定义了两个对象并且给出了初值。然后输出两个学生的数据。运行结果和例 15.1中给出的相同。在本例中,析构函数并无任何实质上的作用,我们把它写出来,只是为了说明析构函数的使用方法。
在本程序中,成员函数是在类中定义的,如果成员函数的数目很多以及函数的长度很长,类的声明就会占很大的篇幅,不利于阅读程序。可以在类的外面定义成员函数,而在类中只用函数的原型作声明。
例 15.3 在类的外面定义成员函数。
#include<string.h>
#include<iostream.h>
class stud//声明一个类
{private,
int num;
char name[ 10] ;
char sex ;
public,
stud(int n,char nam[],char s ) ; //对构造函数的原型声明
~stud( ); //对析构函数的原型声明
void display( ) ; //对成员函数 display的原型声明
}; stud∷ stud(int n,char nam[],char s ) //对构造函数的定义
{num=n;
strcpy(name,nam);
sex=s; }
stud∷ ~stud( ) //对析构函数的定义
{ }
void stud∷ display( ) //对成员函数 display的定义
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl;
}
void main( )
{
stud stud1(10010,"Wang-li",′f′),stud2(10011,
"Zhang-fun",′m′);
stud1.display( ); //输出学生 1的数据
stud2.display( ); //输出学生 2的数据
}
请读者注意在类声明的外部定义函数,必须指定类名。函数首行的形式为函数类型 类名 ∷ 函数名(形参表列)
不能写成
void display( ) //未指定类名如果这样写,编译系统会把它作为普通的函数处理,
而不作为类中的成员函数。也不要写成:
stud∷ void display( ) //函数类型位置不对
stud∷ display( ) 是一个整体,说明是 stud类中的
display函数。不能把 stud∷ 和 display( ) 分隔开来。
虽然函数在类的外部定义,但在调用成员函数时会根据类中函数的原型声明找到函数的定义(函数代码),从而执行该函数。
也可以将在类外部定义的函数声明为“内置函数”,
这样在编译时就将函数代码代入到类中的函数调用处,以提高程序执行效率。只需在函数定义的首行最左端加上 inline即可。如:
inline void stud∷ display( )
15.4 继 承 与 派 生
15.4.1 继承与派生的概念面向对象技术强调软件的可重用性。在 C++中可重用性是通过“继承” 这一机制来实现的。因此,
继承是 C++的一重要组成部分。
前面介绍了类,一个类中包含了若干数据成员和成员函数。每一个类的数据成员和成员函数是不相同的。但有时两个类的内容基本相同或有一部分相同。例如前面我们声明了学生基本数据的类 stud:
class stud
{private,
int num;
char name[ 10] ;
char sex;
public,
void display( ) //对成员函数 display的定义
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl; }
};
如果学校的某部门除了需要用到学号、姓名、性别以外,还需要用到年龄、地址等信息。当然可以重新声明另一个类:
class stud 1
{private,
int num; //此行原来己有
char name[ 10] ; //此行原来己有
char sex; //此行原来己有
int age;
char addr[ 20] ;
public,
void display( ) ; //此行原来己有
{cout<<"num,"<<num<<endl; //此行原来己有
cout<<"name,"<<name<<endl; //此行原来己有
cout<<"sex,"<<sex<<endl; //此行原来己有
cout<<"age,"<<age<<endl;
cout<<"address,"<<addr<<endl;}
};
可以看到有相当一部分是原来已有的。很多人自然会想到能否利用原有声明的类作为基础,再加上新的内容即可,以减少重复的工作量。 C++提供的“继承” 机制就是为了解决这个问题。
举一个日常生活中的例子。如果已经定义了“马”
的特征,现在需要说明什么是“公马”,只需在
“马”的特征的基础上增加“雄性”这一特征即可,不必从头说明什么是马。如果想进一步说明什么是“白色的公马”,只需在“公马”的基础上再增加说明“颜色是白的”即可。也就是说:
“公马”继承了“马”的全部特征,加上“雄性”
的新特征。“白公马” 继承了“公马”的全部特征,再增加“白色”的特征。而“公马”是“马”
派生出来的一个分支,“白公马” 是“公马” 派生出来的一个分支。见图 15.4示意。
在 C++中所谓“继承” 就是在一个已存在的类的基础上建立一个新的类。已存在的类(好比“马”)
称为“基类” 或“父类” 。新建立的类称为“派生类” 或“子类”。 见图 15.5示意。 派生类继承了基类的所有数据成员和成员函数,并增加新的成员。
图 15.4 图 15.5
15.4.2 建立派生类的方法先通过一个例子说明怎样通过继承来建立派生类。
例 15.4通过继承来建立派生类。
假设已经声明了一个基类 stud(见前面的介绍),
在此基础上声明一个派生类 student:
class student,public stud //声明基类是 stud
{
private:
int age; //新增加的数据成员
char addr[ 30] ; //新增加的数据成员
public:
void display-1( ) //新增加的成员函数
{cout<<"age,"<<age<<endl;
cout<<"address,"<<addr<<endl;}
};
仔细观察第一行,
class student,public stud
在 class后面的 student是新建的类名。冒号后面的
stud表示是已存在的基类。在 stud之前有一关键字
public,用来表示基类 stud中的成员在派生类
student中的使用权限。基类名前面有 public的称为
“公用派生类”。其含义将在稍后讨论。
请仔细阅读以上声明的派生类 student和上一节中给出的基类 stud,并将它们放在一起进行分析。
定义派生类的一般形式为
class 派生类名,[引用权限] 基类名
{
派生类新增加的数据成员派生类新增加的成员函数
} ;
“引用权限”可以是 private和 public。默认为 private。
派生类包括基类成员和自己增加的成员,派生类的成员函数在引用派生类自己的数据成员时,按前面介绍过的规则处理(即私有数据成员只能被同一类中的成员函数引用,公用成员可以被外界引用)。而对从基类继承来的成员的引用并不是简单地把基类的私有成员和公用成员直接作为派生类的私有成员和公用成员,而要根据基类成员的引用权限和派生类声明的引用权限共同决定。下面将分别介绍。
15.4.3 公用派生类在声明一个派生类时将基类的引用权限指定为 public
的,该类称为基类的公用派生类。
在公用派生类中,基类的公用成员和保护 (protected)
成员仍然成为派生类中的公用成员和保护成员 (关于保护成员将在 15.4.5节介绍 ),而基类的私有成员不能被派生类引用,即成为派生类“不可访问的成员”,只有基类的成员函数可以引用它。基类的成员在公用派生类中的引用权限见表 15.1。
表 15.1 公用派生类的继承关系基类 私有成员 公用成员公用派生类 不可访问的成员 公用成员例 15.5 访问基类成员。
class stud //声明基类
{private,//基类私有成员
int num;
char name[ 10] ;
char sex;
public,//基类公用成员
void display( )
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl; }
}; class student,public stud //声明一个公用派生类
{
private:
int age;
char addr[ 30] ;
public:
void show( )
{ cout<<"num,"<<num<<endl; //企图引用基类的私有成员,
错误。
cout<<"name,"<<name<<endl; //企图引用基类的私有成员,错误。
cout<<"sex,"<<sex<<endl; //企图引用基类的私有成员,错误。
cout<<"age,"<<age<<endl; //引用派生类的私有成员,
正确。
cout<<"address,"<<addr<<endl;} //引用派生类的私有成员,正确。
};
由于基类的私有成员对派生类来说是不可访问的,因此在派生类中的 show函数中直接引用基类的私有数据成员 num,name和 sex是不允许的。可以通过基类的公用成员函数来引用基类的私有数据成员。上面对派生类 student的声明可改为
class student,public stud //声明一个公用派生类
{
private:
int age;
char addr[ 20] ;
public:
void show( )
{ display( ); //引用基类的公用成员函数,允许。
cout<<"age,"<<age<<endl; //引用派生类的私有成员,
正确。
cout<<"address,"<<addr<<endl;} //引用派生类的私有成员,
正确。
};
在派生类成员函数 show中引用基类的公用成员函数
display,通过 display引用基类 stud中的私有数据
num,name 和 sex。可以这样写 main函数(假设对象 a中已有数据):
void main( )
{student a;//定义一个 student派生类的对象 a
…
a.show( ); //输出 a对象的 5个数据
};请分析在主函数中能否出现以下语句,a.display( ); //正确。从基类继承的公用成员函数
a.age=18; //错误。外界不能引用派生类的私有成员
a.num=10020; //错误。外界不能引用基类的私有成员
15.4.4 私有派生类在声明一个派生类时,将基类的引用权限指定为
private的,该类称为基类的私有派生类。
在私有派生类中,基类的公用成员和保护成员成为派生类中的私有成员,基类的私有成员成为派生类“不可访问的成员”,只有基类的成员函数可以引用它。基类的成员在私有派生类中的引用权限见表 15.2。
表 15.2 私有派生类的继承关系基类 私有成员 公用成员私有派生类 不可访问的成员 私有成员如果派生类声明首行改为
class student,private stud//声明一个私有派生类
{ private:
int age;
char addr[ 30] ;
public:
void show( )
{ display( ); //基类的公用成员函数变成派生类的私有函数
cout<<"age,"<<age<<endl; //引用派生类的私有成员,正确。
cout<<"address,"<<addr<<endl;} //引用派生类的私有成员,正确。
}; void main( )
{student a;//定义一个派生类的对象 a
a.display( ); //错误。基类的公用成员函数变成派生类私有函数
a.age=18; //错误。外界不能引用派生类的私有成员
}
可以看到:
(1) 不能通过私有派生类对象 (如 a)引用从基类继承过来的任何成员。
(2) 在派生类的成员函数中不能访问基类的私有成员,
但可以访问基类的公用成员。
由于私有派生类限制太多,一般不常使用。
15.4.5 保护成员前面已接触过“保护”( protected)这一名词。它和 private,public一样是用来声明成员的引用权限的。由 protected声明的成员称为保护成员。保护成员不能被外界引用(这点和私有成员类似),
但它可以被派生类的成员函数引用。
将表 15.1和表 15.2综合表示并增加保护成员的内容,
见表 15.3。
表 15.3 派生类的继承关系基类 私有成员 公用成员 保护成员公用派生类 不可访问的成员 公用成员 保护成员私有派生类 不可访问的成员 私有成员 私有成员从前面的介绍已知基类的私有成员被派生类(不论是私有派生类还是公用派生类)继承后变为“不可访问的成员”。 如果想在派生类引用基类的成员,可以将基类的成员声明为 protected。
例 15.6 派生类引用保护成员。
class stud//声明基类
{protected,//基类保护成员
int num;
char name[ 10] ;
char sex;
public,//基类公用成员
void display( )
{cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl; }
}; class student,public stud //声明一个公用派生类
{
private:
int age;
char addr[ 30] ;
public:
void show( )
{ cout<<"num,"<<num<<endl;//引用基类的保护成员,合法。
cout<<"sex,"<<sex<<endl; //引用基类的保护成员,合法。
cout<<"age,"<<age<<endl; //引用派生类的私有成员,合法。
cout<<"address,"<<addr<<endl;}//引用派生类的私有成员,合法。
};
void main( )
{student a; //a是派生类 student类的对象。
a.show( ); //合法。 show是派生类中的公用成员函数。
a.num=10023; //错误。外界不能访问保护成员。
}
请与例 15.5对比分析。可以看到基类的保护成员对类的外界来说仍然是不可引用的(由于 stud中的
num是保护成员,外界不能用 a.num来引用它),
但对派生类来说它相当于私有成员,可以通过成员函数引用。也就是把保护成员的引用范围扩展到派生类中。
15.4.6 派生类的构造函数派生类从基类继承了非私有成员函数和数据成员,
但是在建立派生类的对象时,系统只执行派生类的构造函数,而不会自动执行基类的构造函数。
也就是说,基类的构造函数是不能继承的。如果基类的构造函数包含对变量的初始化,那么在建立派生类的对象时,由于没有执行基类的构造函数,因而就会使基类的变量未初始化。所以在设计派生类的构造函数时,不仅要考虑派生类所增加的变量初始化,还应当考虑基类的变量初始化。
在执行派生类的构造函数时,应当调用基类的构造函数。
例 15.7 派生类的构造函数。
#include<string.h>
#include <iostream.h>
class stud//声明基类
{protected,//保护部分
int num;
char name[ 10] ;
char sex ;
public,//公用部分
stud(int n,char nam[],char s ) //基类构造函数
{num=n;
strcpy(name,nam);
sex=s; }
~stud( ) { } //基类析构函数
};
class student,public stud //声明公用派生类 student
{ private,//派生类的私有数据
int age;
char addr[ 30] ;
public:
student(int n,char nam[],char s,int a,char ad[] ),
stud(n,nam,s)
//派生类构造函数
{age=a; //在此处只对派生类新增的变量初始化
strcpy(addr,ad);
}
void show( )
{ cout<<"num,"<<num<<endl;
cout<<"name,"<<name<<endl;
cout<<"sex,"<<sex<<endl;
cout<<"age,"<<age<<endl;
cout<<"address,"<<addr<<endl;}
~student ( ) { } //派生类析构函数
};
void main( )
{student a(10010,"Wang-li",′f′,19,"115 Beijing Road,
Shanghai");
student b(10011,"Zhang-fun",′m′,21,"213
Shanghai Road,Beijing");
a.show( ); //输出第一个学生的数据
b.show( ); //输出第二个学生的数据
}
请注意派生类构造函数首行的写法:
student (int n,char nam[],char s,int a,char ad
[] ),stud(n,nam,s)
其一般形式为派生类构造函数名(参数表列):基类构造函数名
(参数表列)
派生类构造函数名后面括号内的参数表列包括参数的类型和参数名。基类构造函数名后面括号内的参数表列只有参数名而不包括参数类型。从基类的声明中可以看到基类构造函数 stud有 3个参数( n,
nam,s),派生类构造函数 student有 5个参数,
前 3个是用来传递给基类构造函数的,后面 2个( a
和 ad)是用来对派生类所增加的变量初始化的。
在 main函数中建立对象 a时指定了 5个实参。它们按顺序传递给派生类构造函数的形参。然后,派生类构造函数将前面 3个传递给基类构造函数的参数。
见图 15.6。
图 15.6
通过 stud (n,nam,s) 把 3个值再传给基类构造函数,见图 15.7。
stud (n,nam,s)
↓ ↓ ↓
stud(int n,char nam[],char s )//这是基类构造函数的首部图 15.7
在上例中也可以将派生类构造函数在类外面定义,而在类的声明中只写该函数的声明:
student(int n,char nam[],char s,int a,char ad[]) ;
在类的外面定义派生类构造函数:
student ∷ student (int n,char nam[],char s,int a,char
ad[] ),stud(n,nam,s)
{age=a;
strcpy(addr,ad);
}
注意:在类中对派生类构造函数作声明时,不包括基类构造函数名和参数表列(即 stud(n,nam,
s))。只在定义函数时才将它列出。
在建立一个对象时,由派生类构造函数先调用基类构造函数,然后再执行派生类构造函数本身。对上例来说,先初始化 num,name,sex,然后再初始化 age和 addr。
在派生类对象消失时,先执行派生类析构函数
~student( ),再执行其基类析构函数 ~stud( )。
15.4.7 继承在软件开发中的重要意义继承是面向对象技术的一个重要内容。有了继承,
使软件的重用成为可能。过去,软件人员开发新的软件,能从已有的软件中直接选用完全符合要求的部件不多,一般都要进行许多修改才能使用,
实际上有相当部分要重新编写,工作量很大。继承机制解决了这个问题。将已有的一个类为基础,
生成一些派生类(子类),在子类中保存父类中有用的数据和操作,去掉(屏蔽掉)不需要的部分。新生成的子类还可以再生成孙类 …… 而且一个子类可以从多个父类中获得继承(即多继承机制)。编写面向对象的程序时要把注意力放在实现有用的类上面,对已有的类加以整理和分类就有可能使这些类能够被程序设计的许多领域使用。
软件设计者可以最大限度地重用已有软件,对已有的类根据需要进行剪裁和修改,在此基础上集中精力编写子类新增加的部分即可。因此有人认为继承是 C++和 C的最重要的区别之一。
在本章中我们只是很简单地介绍了 C++面向对象的一些初步知识。面向对象技术和 C++的内容很丰富,
有许多深入的概念,语法规定也比较复杂,需要花较多的精力和时间去学习和消化。 C++最重要的概念是类,其他许多概念都是与类有关的。为了便于理解,我们在本章中没有介绍复杂的程序,
所举的例题都是比较简单的,只是为了说明一些概念。正如在本章开始时提到的,只有编写过大程序的人才能真正体会 C++的优越性。我们只是希望通过本章的介绍,使读者对 C++的特点有初步的了解,为以后系统地学习 C++建立一些基础。