继承与派生
- 面向对象程序设计有4个主要特点:抽象、封装、继承和多态性
- 面向对象计算强调软件的可重用性。C++语言提供了类的继承机制,解决了软件重用问题。
继承与派生的概念
- 在C++中可重用性是通过"继承"这一机制来实现的。因此,继承是C++的一个重要组成部分。
- 在C++中所谓"继承"就是在一个已存在的类的基础上建立一个新的类。已存在的类称为"基类(base class)“或"父类(father class)”。新建立的类称为"派生类(derived class)“或"子类(son class)”。
- 一个新类从已有的类那里获得其已有特性,这种现象称为类的继承。通过继承,一个新建子类从已有的父类那里获得父类的特性。从另一角度来说,从已有的类(父类)产生一个新的子类,称为类的派生。
- 派生类继承了基类的所有数据成员和成员函数(不包括基类的构造函数和析构函数),并可以增加自己的新成员,同时也可以调整来自基类的数据成员和成员函数。因此基类和派生类是相对而言的。一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类。一代一代地派生下去,就形成了类的继承层次结构。
- 一个派生类只从一个基类派生,这称为单继承(single inheritance),这种继承关系所形成的层次是一个树形结构。一个派生类不仅可以从一个基类派生,也可以从多个基类派生,也就是说,一个派生类可以有两个或多个基类(或者说,一个子类可以有两个或多个父类)。这种情况称为多重继承(multiple inheritance)。
- 关于基类和派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。
派生类的声明方式
- 声明派生类的一般形式为:
class 派生类名:[继承方式] 基类名
{
派生类新增加的成员
};
- 继承方式包括:public(公用的),private(私有的),protected(受保护的),继承方式是可选项,如果不写此项,则默认为private(私有的)
例:假设已经声明了一个基类Student,在此基础上通过单继承建立一个派生类Student1
class Student1 :public Student //声明基类是Student
{
public:
void display_1() //新增加的成员函数
{
cout << "age:" << age << endl;
cout << "address:" << addr << endl;
}
private:
int age; //新增加的数据成员
string addr; //新增加的数据成员
};
程序分析:
- 在class后面的Student1是新建的类名。冒号后面的Student表示是已声明的基类。在Student之前有一个关键字public,用来表示基类Student中的成员在派生类Student1中的继承方式。基类名前面有public的称为"公用继承"
派生类的构成
- 派生类的成员包括从基类继承过来的成员和自己增加的成员两大部分。从基类继承的成员体现了派生类从基类继承而获得的共性,而新增加的成员体现了派生类的个性。正是这些新增加的成员体现了派生类与基类的不同,也体现了不同派生类的区别 。
- 实际上,并不是把基类的成员和派生类自己增加的成员简单的加在一起就成为派生类。构造一个派生类包括以下3个部分:
- 从基类接受成员
- 派生类把基类全部的成员(不包括构造函数和析构函数)接收过来,也就是说是没有选择的,不能选择接收其中一部分成员,而舍弃另一部分成员。 从定义派生类的一般形式中可以看出是不可选择的。
- 这样就可能出现一种情况:有些基类的成员,在派生类中是用不到的,但是也必须继承过来。这就会造成数据的冗余,尤其是在多次派生之后,会在许多派生类对象中存在大量无用的数据,不仅浪费了大量的空间,而且在对象的建立、赋值、复制和参数的传递中,花费了许多无谓的时间,从而降低了效率。
- 调整从基类接受的成员
- 接收基类成员是程序员不能选择的,但是程序员可以对这些成员作某些调整。可以在派生类中声明一个与基类成员同名的成员,则派生类中的新成员会覆盖基类的同名成员。但应注意:如果是成员函数,不仅应使函数名相同,而且函数的参数表(参数的个数和类型)也应相同,如果不相同,就成为函数的重载而不是覆盖了。用这样的方法可以用新成员取代基类的成员。
- 在声明派生类时增加的成员
- 例如在前面例子中,基类的display函数的作用是输出学号、姓名和性别,在派生类中要求输出学号、姓名、性别、年龄和地址,不必单独另写一个输出这5个数据的函数,而要利用基类的display函数输出学号、姓名和性别,另外再定义一个display_1函数输出年龄和地址,先后执行这两个函数。也可以在display_1函数中调用基类的display函数,再输出另外两个数据,在主函数中只需调用一个display_1函数即可,这样可能更清晰一些,易读性更好。
- 此外,在声明派生类时,一般还应当自己定义派生类的构造函数和析构函数,因为构造函数和析构函数是不能从基类继承的。
- 派生类是基类定义的延续。可以先声明一个基类,在此基类中只提供某些最基本的功能,而另外有些功能并未实现,然后在声明派生类时加入某些具体的功能,形成适用于某一特定应用的派生类。通过对基类声明的延续,将一个抽象的基类转化成具体的派生类。因此,派生类是抽象基类的具体实现。
派生类成员的访问属性
- 既然派生类中包含基类成员和派生类自己增加的成员,就产生了这两部分成员的关系和访问属性的问题。在建立派生类的时候,并不是简单地把基类的私有成员直接作为派生类的私有成员,把基类的公用成员直接作为派生类的公用成员。实际上,对基类成员和派生类自己增加的成员是按不同的原则处理的。
- 具体说,在讨论访问属性时,要考虑以下几种情况:
- 基类的成员函数访问基类成员。
- 派生类的成员函数访问派生类自己增加的成员。
- 基类的成员函数访问派生类的成员。
- 派生类的成员函数访问基类的成员。
- 在派生类外访问派生类的成员。
- 在派生类外访问基类的成员。
- 对于第(1)和第(2)种情况,比较简单,基类的成员函数可以访问基类成员,派生类的成员函数可以访问派生类成员。私有数据成员只能被同一类中的成员函数访问,公用成员可以被外界访问。
- 第(3)种情况也比较明确,基类的成员函数只能访问基类的成员,而不能访问派生类的成员。
- 第(5)种情况也比较明确,在派生类外可以访问派生类的公用成员,而不能访问派生类的私有成员。
- 对于第(4)和第(6)种情况,就稍微复杂一些,也容易混淆。这牵涉到如何确定基类的成员在派生类中的访问属性的问题,不仅要考虑对基类成员所声明的访问属性,还要考虑派生类所声明的对基类的继承方式,根据这两个因素共同决定基类成员在派生类中的访问属性。
前面已提到,在派生类中,对基类的继承方式可以有public(公用的)、private (私有的)和protected(保护的)3种。不同的继承方式决定了基类成员在派生类中的访问属性。
简单地说:
- 公用继承(public inheritance)
基类的公用成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类私有 - 私有继承(private inheritance)
基类的公用成员和保护成员在派生类中成了私有成员,其私有成员仍为基类私有 - 受保护的继承(protected inheritance)
基类的公用成员和保护成员在派生类中成了保护成员,其私有成员仍为基类私有。保护成员的意思是,不能被外界引用,但可以被派生类的成员引用。
公用继承
- 在定义一个派生类时将基类的继承方式指定为public的,称为公用继承,用公用继承方式建立的派生类称为公用派生类(public derived class),其基类称为公用基类(public base class)。
- 采用公用继承方式时,基类的公用成员和保护成员在派生类中仍然保持其公用成员和保护成员的属性,而基类的私有成员在派生类中并没有成为派生类的私有成员,它仍然是基类的私有成员,只有基类的成员函数可以引用它,而不能被派生类的成员函数引用,因此就成为派生类中的不可访问的成员。
- 既然是公用继承,为什么不能访问基类的私有成员呢?——因为私有成员体现了数据的封装性,隐藏私有成员有利于测试、调试和修改系统。如果把基类所有成员的访问权限都原封不动地继承到派生类,使基类的私有成员在派生类中仍保持其私有性质,派生类成员能访问基类的私有成员,那么岂非基类和派生类没有界限了?这就破坏了基类的封装性。如果派生类再继续派生一个新的派生类,也能访问基类的私有成员,那么在这个基类的所有派生类的层次上都能访问基类的私有成员,这就完全丢弃了封装性带来的好处。
公用基类的成员在派生类中的访问属性:
在基类的访问属性 | 继承方式 | 在派生类中的访问属性 |
---|---|---|
private(私有) | public(公用) | 不可访问 |
public(公用) | public(公用) | public(公用) |
protected(保护) | public(公用) | protected(保护) |
例:访问公有基类的成员
#include<iostream>
using namespace std;
class Student //声明基类
{
public: //基类公用成员
void get_value() //输入基类数据的成员函数
{
cin >> num >> name >> sex;
}
void display() //输出基类数据的成员函数
{
cout << "num: " << num << endl;
cout << "name: " << name << endl;
cout << "sex: " << sex << endl;
}
private: //基类私有成员
int num;
string name;
char sex;
};
class Student1 : public Student //以public方式声明派生类Student1
{
public:
void get_value_1() //输入派生类数据
{
cin >> age >> addr;
}
void display_1()
{
cout << "num: " << num << endl; //试图引用基类的私有成员,错误
cout << "name: " << name << endl; //试图引用基类的私有成员,错误
cout << "sex: " << sex << endl; //试图引用基类的私有成员,错误
cout << "age: " << age << endl; //引用派生类的私有成员,正确
cout << "address: " << addr << endl; //引用派生类的私有成员,正确
}
private:
int age;
string addr;
};
程序分析:
- 由于基类的私有成员对派生类来说是不可访问的,因此在派生类中的display_1函数中直接引用基类的私有数据成员num,name和sex是不允许的。只能通过基类的公用成员函数来引用基类的私有数据成员。
可以将派生类Student1的声明改为:
class Student1 : public Student //以public方式声明派生类Student1
{
public:
void get_value_1() //输入派生类数据
{
get_value(); //调用基类的公有成员函数,输出基类中3个数据成员的值
cin >> age >> addr;
}
void display_1()
{
display(); //调用基类的公用成员函数,输出基类中3个数据成员的值
cout << "age: " << age << endl; //引用派生类的私有成员,正确
cout << "address: " << addr << endl; //引用派生类的私有成员,正确
}
private:
int age;
string addr;
};
然后在main函数中分别调用基类的display函数和派生类中的display_1函数,先后输出5个数据。
可以这样写main函数(假设对象stud中已有数据):
int main()
{
Student1 stud; //定义派生类Student1的对象stud
stud.get_value_1(); //调用派生类公有成员函数,输出派生类中2个数据成员的值
stud.display_1(); //调用派生类公有成员函数,输出派生类中2个数据成员的值
return 0;
}
运行结果:
注意:在主函数中不能出现以下语句:
stud.age = 18; //错误,在类外不能引用派生类的私有成员
stud.num = 10020; //错误,在类外不能引用基类的私有成员
私有继承
- 在声明一个派生类时将基类的继承方式指定为private的,称为私有继承,用私有继承方式建立的派生类称为私有派生类(private derived class), 其基类称为私有基类(private base class)。
- 私有基类的公用成员和保护成员在派生类中的访问属性相当于派生类中的私有成员,即派生类的成员函数能访问它们,而在派生类外不能访问它们。私有基类的私有成员在派生类中成为不可访问的成员,只有基类的成员函数可以引用它们。一个基类成员在基类中的访问属性和在派生类中的访问属性可能是不同的。私有基类的成员可以被基类的成员函数访问,但不能被派生类的成员函数访问。
私有基类在派生类中的访问属性:
在基类的访问属性 | 继承方式 | 在派生类中的访问属性 |
---|---|---|
private(私有) | private(私有) | 不可访问 |
public(公用) | private(私有) | private(私有) |
protected(保护) | private(私有) | private(私有) |
若基类A有公用数据成员i和j,私有数据成员k,采用私有继承方式声明了派生类B,新增加了公用数据成员m和n,私有数据成员p。在派生类B作用域内,基类A的公用数据成员i和j呈现私有成员的特征,在派生类B内可以访问它们,而在派生类B外不可访问它们。在派生类内不可访问基类A的私有数据成员k。此时,从派生类的角度来看,相当于有公用数据成员m和n,私有成员i,j,p。基类A的私有数据员k在派生类B中成为"不可见"的。
- 只须理解:既然声明为私有继承,就表示将原来能被外界引用的成员隐藏起来,不让外界引用,因此私有基类的公用成员和保护成员理所当然地成为派生类中的私有成员。私有基类的私有成员按规定只能被基类的成员函数引用,在基类外当然不能访问它们,因此它们在派生类中是隐蔽的不可访问的。
- 对于不需要再往下继承的类的功能可以用私有继承方式把它隐蔽起来,这样,下一层的派生类无法访问它的任何成员。
- 可以知道:一个成员在不同的派生层次中的访问属性可能是不同的。它与继承式有关。
例:将上例中地公用继承方式改为用私有继承方式(基类Student不改),只需将派生类中继承方式public改为private即可
注意:在main函数中的错误写法:
stud.display(); //错误,私有基类的公用成员函数在派生类中是私有的
stud.age = 18; //错误,外界不能引用派生类的私有成员
结论:
- 不能通过派生类对象引用从私有基类继承过来的任何成员
- 派生类的成员函数不能访问私有基类的私有成员,但可以访问私有基类的公用成员
- 虽然在派生类外不能通过派生类对象调用私有基类的公用成员函数,但可以通过派生类的成员函数调用私有基类的公用成员函数(此时它是派生类中的私有成员函数,可以被派生类的任何成员函数调用)。
保护成员和保护继承
- 由protected声明的成员称为"受保护的成员",或简称"保护成员"。受保护成员不能被类外访问,这点和私有成员类似,可以认为保护成员对类的用户来说是私有的。从类的用户角度来看,保护成员等价于私有成员。但有一点与私有成员不同,保护成员可以被派生类的成员函数引用。
- 如果基类声明了私有成员,那么任何派生类都是不能访问它们的,若希望在派生类中能访问它们,应当把它们声明为保护成员。如果在一个类中声明了保护成员,就意味着该类可能要用作基类,在它的派生类中会访问这些成员。
- 保护继承的特点是:保护基类的公有成员和保护成员在派生类中都成了保护成员,其私有成员仍为基类私有。 也就是把基类原有的公用成员也保护起来,不让类外任意访问。
基类成员在派生类中的访问属性
在基类的访问属性 | 继承方式 | 在派生类中的访问属性 |
---|---|---|
private(私有) | public(公用) | 不可访问 |
private(私有) | private(私有) | 不可访问 |
private(私有) | protected(保护) | 不可访问 |
public(公用) | public(公用) | public(公用) |
public(公用) | private(私有) | private(私有) |
public(公用) | protected(保护) | protected(保护) |
protected(保护) | public(公用) | protected(保护) |
protected(保护) | private(私有) | private(私有) |
protected(保护) | protected(保护) | protected(保护) |
分析:
- 保护基类的所有成员在派生类中都被保护起来,类外不能访问,其公用成员和保护成员可以被其派生类的成员函数访问
- 比较私有继承和保护继承(也就是比较在私有派生类和在保护派生类中的访问属性),在直接派生类中,以上两种继承方式的作用实际上是相同的,即在类外不能访问任何成员,而在派生类中可以通过成员函数访问基类中的公用成员和保护成员。但是如果继续派生,在新的派生类中,两种继承方式的作用就不同了。
- 基类的私有成员被派生类继承(不论是私有继承、公用继承还是保护继承)后变为不可访问的成员,派生类中的一切成员均无法访问它们。
- 在派生类中,成员有4种不同的访问属性:
①公用的,派生类内和派生类外都可以访问
②受保护的,派生类内可以访问,派生类外不能访问,其下一层的派生类可以访问
③私有的,派生类内可以防问,派生类外不能访问
④不可访问的,派生类内和派生类外都不能访问
派生类中的成员的访问属性
派生类中访问属性 | 在派生类中 | 在派生类外部 | 在下一层公用派生类中 |
---|---|---|---|
公用 | 可以 | 可以 | 可以 |
保护 | 可以 | 不可以 | 可以 |
私有 | 可以 | 不可以 | 不可以 |
不可访问 | 不可以 | 不可以 | 不可以 |
- 类的成员在不同作用域中有不同的访问属性,一个成员的访问属性是有前提的,就是在哪一个作用域中。
例:在派生类中引用保护成员
#include<iostream>
#include<string>
using namespace std;
class Student //声明基类
{
public: //基类无公用成员
protected: //基类保护成员
int num;
string name;
char sex;
};
class Student1:protected Student //用protected方式声明派生类Student1
{
public:
void get_value1(); //派生类公用成员函数
void display1(); //派生类公用成员函数
private:
int age; //派生类私有数据成员
string addr; //派生类私有数据成员
};
void Student1::get_value1() //定义派生类公用成员函数
{
cin >> num >> name >> sex; //输入保护基类数据成员
cin >> age >> addr; //输入派生类数据成员
}
void Student1::display1() //定义派生类公用数据成员
{
cout << "num: " << num << endl; //引用基类的保护成员
cout << "name: " << name << endl; //引用基类的保护成员
cout << "sex: " << sex << endl; //引用基类的保护成员
cout << "age: " << age << endl; //引用派生类的私有成员
cout << "address: " << addr << endl; //引用派生类的私有成员
}
int main()
{
Student1 stud1; //定义派生类Student1的对象stud
stud1.get_value1(); //get_value_1是派生类Student1类的公用函数
stud1.display1(); //display_1是派生类Student1类的公用函数
return 0;
}
程序分析:
- 在派生类的成员函数中引用基类的保护成员,这是合法的。基类的保护成员对派生类的外界来说是不可访问的,但在派生类内,它相当于私有成员,可以通过派生类的成员函数访问。
运行结果:
多级派生时的访问属性
- 多级派生如: A ← B ← C A \leftarrow B \leftarrow C A←B←C,类A为基类,类B是类A的派生类,类C是类B的派生类,则类C也是类A的派生类。类B称为类A的直接派生类,类C称为类A的间接派生类。类A是类B的直接基类,是类C的间接基类。
例:多级派生的访问属性
class A //基类
{
public:
int i;
protected:
void f1();
int j;
private:
int k;
};
class B : public A //public派生类
{
public:
void f2();
protected:
void f3();
private:
int m;
};
class C :protected B //protected派生类
{
public:
void f4();
private:
int n;
};
类A是类B的公用基类,类B是类C的保护基类。
i | f1() | j | k | f2() | f3() | m | f4() | n | |
---|---|---|---|---|---|---|---|---|---|
基类A | 公用 | 保护 | 保护 | 私有 | |||||
公用派生类B | 公用 | 保护 | 保护 | 不可访问 | 公用 | 保护 | 私有 | ||
保护派生类C | 保护 | 保护 | 保护 | 不可访问 | 保护 | 保护 | 不可访问 | 公用 | 私有 |
- 可以看到:无论哪一种继承方式,在派生类中是不能访问基类的私有成员的,私有成员只能被本类的成员函数所访问,毕竟派生类与基类不是同一个类。
- 如果在多级派生时都采用公有继承方式,那么直到最后一级派生类都能访问基类的公有成员和保护成员。如果采用私有继承方式,经过若干次派生之后,基类的所有的成员已经变成不可访问的了。如果采用保护继承方式,在派生类外是无法访问派生类中的任何成员的。