第十三章.类继承(P481-533)

本文详细介绍了C++中的类继承,包括公有派生、构造与析构函数的使用、虚函数的作用以及抽象基类的概念。通过实例展示了如何定义派生类、使用构造函数初始化列表、以及如何实现动态联编。此外,还讨论了析构函数为何应为虚函数、保护成员的访问权限以及静态与动态联编的区别。文章最后提到了抽象基类和动态内存分配在继承中的应用。
摘要由CSDN通过智能技术生成

简单实例及概念

c++提供了比修改代码更好的方法来扩展和修改类,这种方法叫做----类继承,它能从已有的类派生出新的类,而派生类继承了原有类 (称为基类) 的特征,包括方法,并且

  • 可以在已有类的基础上增加功能。
  • 可以给类增加数据。
  • 可以修改类方法的行为
#include<iostream>
using namespace std;
class Student{
   private:
   	string name;
   	int age;
   public:
   	Student(const string &s = "none", int n = 0){
   		name = s;
   		age = n;
   	}
   	~Student();
   	void show();
};
Student::~Student(){
   
}
void Student::show(){
   cout<<"hello "<<name<<" !,here is your inforations:"<<endl;
   cout<<"your age: "<<age<<endl;
}
int main(void){
   Student s1("kyle", 19);
   s1.show();
   return 0;
}

派生类:

class MaleStudent : public Student{
   
};

上述代码新定义一个类,使用public表示Student是一个公有基类,这被称为共有派生。派生类对象包含基类对象。使用共有派生,基类的公有成员将成为派生类的公有成员;基类的私有成员也成为派生类的一部分,但只能通过基类的共有和保护方法访问。即实现了

  • 派生类对象存储了基类的数据成员
  • 派生类对象可以使用基类的方法
  • 派生类需要自己的构造函数
  • 派生类可以根据需要增加额外的数据成员和成员函数。

补充一个内容,在函数执行前给变量初始化,可以使用初始化列表:

Student::Student(const string &s = "none", int n = 0) : name(s),age(n){
}

冒号后,加变量名,然后括号内为形参。

派生类的具体声明:

class MaleStudent : public Student{
	private:
		double height;
	public:
		MaleStudent(double , const string &, int );
		MaleStudent(double , const Student &);
		void show();
		
};
MaleStudent::MaleStudent(double h = 170.0, const string &s = "jack", int n = 18) : Student(s,n){
	height = h;
}
MaleStudent::MaleStudent(double h , const Student &s):Student(s),height(h){
}
void MaleStudent::show(){
	cout<<"hello boy!"<<endl;
	cout<<"your height is "<<height<<endl;
}

针对该派生类,说明一些问题:

  1. 派生类的构造函数必须给新成员和继承的成员提供数据,该派生类的构造函数有两种,第一种时传入三个变量,第二种是传入了一个新成员参数和一个基类对象。
  2. 派生类不能直接访问基类私有成员,所以派生类的构造函数不能直接设置继承的成员,而必须使用基类公有方法来访问私有的基类成员,准确来说,派生类构造函数必须使用基类的构造函数
  3. 在第一种构造函数中,如果派生类没有使用基类的构造函数,也就是忽略掉成员初始化列表,则会使用默认的基类构造函数。
  4. 第二种派生类构造函数,会调用基类的复制构造函数。

派生类与基类的关系

首先,派生类对象可以使用基类的非私有方法。
最重要的是:基类指针可以在不进行显式类型转换的其工况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。

	Student *s1 = new Student("exceedingly",200);
	
	Student * s2 = new MaleStudent(180.0,"heightly",150);

	delete s1;
	delete s2;

上述代码中,s1为基类指针,指向动态基类对象,属于正常情况,但s2是基类指针,指向的却是派生类对象,这是允许的,但是只能调用基类的方法
并且这种关系是单向的,也就是说,一个派生类指针不能指向基类对象,引用也是。

多态公有继承

在大部分情况,我们派生类中会修改基类的方法,即同一个方法,但在基类与派生类的功能不相同。方法的行为应取决于调用该方法的对象。这种复杂的行为称为多态-----具有多种形态。有两种方法可用于实现多态公有继承:

  • 在派生类中重新定义基类方法
  • 使用虚方法

通过对象调用,可以直接区分是基类还是派生类的方法。如果方法是通过引用或者指针调用的,如果没有使用关键字—virtual,程序将根据引用类型或者指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法

int main(void){
	Student *s1 = new Student("exceedingly",200);
	s1->show();
	Student * s2 = new MaleStudent(180.0,"heightly",150);
	s2->show();
	delete s1;
	delete s2;
	return 0;
}

上述代码中(派生类与基类都有show方法,且功能不同),如果show方法没有使用关键字virtual,那么,虽然s1和s2指向的对象类型不同,但因为,两个指针的类型都是基类,因此,调用的都是基类的方法。

如果show函数使用了关键字virtual,那么,虽然两个指针的类型都是基类,但因为该方法被定义为虚方法,因此,会根据指针指向的对象类型选择方法,所以s1使用的基类的方法,s2则使用派生类的方法。

因此,如果派生类会将基类方法重新定义,应提前将基类的该方法定义为虚函数,基类方法为虚方法,其派生类中也为虚方法,但为了显示明显,通常也在派生类中使用关键字来表示哪些函数是虚方法。

最后一点,我们使用new运算符定义了类对象,也就需要delete清除内存,同时调用析构函数。如果析构函数不是虚函数,则只调用基类的析构函数。如果析构函数是虚函数,将调用相应对象类型的析构函数,即指针指向派生类对象,将调用派生类的析构函数,然后自动调用基类的析构函数。所以,使用虚析构函数可以确保正确的析构函数序列被调用
如果派生类中析构函数执行某些操作,那么基类必须有一个虚析构函数,即使该析构函数无任何操作,所以,无论派生类怎样操作,在基类定义一个虚析构函数是一个好的习惯。

静态联编与动态联编

程序调用函数时,将使用哪个可执行代码块,这是由编译器决定的。将源码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在c++中,因为函数重载的缘故,编译器必须查看函数参数以及函数名才能确定使用哪个函数。在编译过程中进行联编被称为静态联编,又称为早期联编。

然而,因为虚函数的存在,使用哪一个函数不是在编译时能决定的,要根据用户反馈的信息决定使用哪种函数,所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编,或者晚期联编。

在大部分情况下,动态联编好,可以让程序选择特定的方法,但我们默认的却是静态联编?两个的区别?动态联编如何工作?

最重要的就是----效率和概念模型。动态联编必须采取一些方法跟踪程序中的对象类型,这会增加额外的处理开销。如果类不做基类,或者派生类不重新定义基类的方法,也就不需要动态联编。这样使用静态联编是的效率更高,所以c++默认为静态联编,只有在特殊需求(虚函数)才会使用动态联编。

虚函数的工作原理:会在每个对象中增加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针。这种数组被称为虚函数表,虚函数表中存储了为类对象进行声明的虚函数的地址,总之使用虚函数时,在内存与执行速度方面都有成本:

  • 每个对象都要增加,增大量为存储地址的空间。
  • 对于每个类,编译器都要创建一个虚函数地址表
  • 对于每个虚函数的调用,都需要执行额外的操作,即到表中查找地址。

下面强调一些点:

  1. 构造函数不能是虚函数,派生类不继承基类的构造函数,只会在调用派生类的构造函数时使用基类的构造函数,所以将构造函数声明为虚函数没有意义
  2. 析构函数应当是虚函数,除非类不做基类。假设不是虚函数,在派生类对象过期时,会调用基类的构造函数,但只能释放掉基类数据的内存,而派生类新增的数据不会被消除,但如果析构函数是虚函数,会先调用派生类的析构函数,清除新增数据的内存,然后调用基类的构造函数,清除原始基类数据的内存。所以应声明基类析构函数为虚函数,即使其中无任何操作。
  3. 友元函数不能是虚函数,因为友元不是类成员,只有类成员才能使虚函数
  4. 重新定义将隐藏方法: 重新定义不是重载。即如果同一个函数名,在基类中需要传入一个参数,但在派生类中重新定义,不需要传递参数。此时派生类的该函数将会覆盖基类的该函数,即如果一个派生类对象传递一个参数调用该函数,这是错误的!所以,重新定义继承的方法时,应确保与原来的原型完全相同。如果返回类型是基类引用或指针,则可修改为指向派生类的引用或指针。这种特性被称为返回类型协变,允许返回类型随类类型的变化而变化
  5. 接着第四点,如果在基类中该方法有多个重载版本,在派生类中必须将所有重载版本重新定义,否则未被定义的重载函数将被隐藏,派生类对象无法使用它们。

控制访问:protected

前面使用关键字private和public来声明类的私有成员与公有成员,但还有另一种访问类别-----protected(受保护成员)。
保护成员数据与私有成员数据相似,在类外只能使用公有类成员来访问其中的数据。两者的区别只在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。所以对于类外来说,保护与私有一样。但对于派生来说,保护与公有一样。

这样存在一个缺陷,原本基类只能通过方法去访问保护成员数据,但派生类中却可以直接访问,即在派生类中变成了共有变量,这一点不符合类数据的封装性。
所以,通常将数据成员采用私有访问控制,不使用保护访问控制。而对于成员函数来说,保护访问控制很有用,它使派生类能够访问公众不能使用的内部函数。

抽象基类

继承可以使一个类具有另一个类的方法数据,但有时,在继承时不需要基类的变量,只继承某些数据。具体点说,原本类A是继承类B的,但现在,经过修改,虽然A和B有相似之处,但在数据方面有很大不同,因此A继承B就有一些问题。
解决方式是,从类A和类B中抽象出它们的共性,将这些共性定义为一个类AB,然后从AB类中派生出A类和B类。如果某个方法是派生之后的A类有,而基类AB没有的,**c++可以通过使用纯虚函数提供为实现的函数。**纯虚函数在声明结尾处为=0;:

class AB{
	...
	public:
		void show()=0;//表示该函数为纯虚函数
}

当类声明中包括纯虚函数时,则不能创建类的对象。因为,包括纯虚函数的类只能用作基类,称为------抽象基类,并且,纯虚函数可以不做定义,仅声明即可。抽象基类不能有实例化对象

自己的理解就是,以人成长作为例子,抽象基类就像是一个胚胎,它可以演变为男孩或者女孩这两种性别的个体,但“胚胎”本身却不属于某种个体(相当于抽象基类不能实例化),它更表现为一种潜力,使它能够变为某种个体。

继承与动态内存分配

之前说过,如果类的构造函数涉及new动态分配内存,其析构函数,重载赋值运算符以及复制构造函数都要定义,其中要有delete与构造函数的new相匹配。
那么,继承与动态内存分配相结合呢?
有两种情况:
1.基类有new,派生类无new
此时,派生类无需定心新的析构函数,重载赋值运算符以及复制构造函数,因为基类部分delete在基类的析构函数中,而派生类新的数据无new,有默认的复制构造函数即可,无需delete,所以无需新定义。
2.基类有new,派生类也有new
此时,派生类需要定义新的析构函数,重载赋值运算符以及复制构造函数,因为其基类的方法只能delete基类数据部分,而派生类新的数据也有new,所以需要新的delete将其消除。因此需要重新定义三种方法。

回顾总结

  • 默认构造函数,要么所有参数都有默认值,要么没有定义构造函数,给每个类定义了个默认构造函数是一个好习惯
  • 复制构造函数,其参数是类对象:a.将新对象初始化为同一类对象。b.按值将对象传递给函数。c.函数按值返回对象。d.编译器临时生成对象
  • 赋值运算符(与复制构造函数区分),要将字符串赋值给类对象,就要重载赋值运算符。或者利用转换函数,先将字符串转换为类对象,在复制给另一个类对象。
  • 构造函数:用来创建新对象,不能被继承
  • 析构函数:一定要定义显式析构函数来释放构造函数中new分配的所有内存,对于基类,即使不需要析构函数,也得提供一个虚析构函数。
  • 转换:使用一个参数就可以调用相应构造函数;将可转换的类型传递给以类为参数的函数时,将调用转换构造函数。即:S1 = “kyle”;利用一个参数为字符数组的构造函数生成一个对象,该对象将被用作上述赋值运算符函数的参数,如果在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显式转换:S1 = Student(“kyle”);如果要将类转换为其他类型,需要定义转换函数:Student::Student double ()…将类变为double类型。也可以使用explicit
  • 值传递与引用:按值传递需要调用复制构造函数,然后调用析构函数,效率低。并且,使用对象引用作为参数的函数时,可以传递其派生类,方便。
  • 返回对象和返回引用:返回对象会建立一个对象的临时副本,效率低,因此,返回引用最好。但有限制,不能返回在函数中创建的临时对象的引用。
  • 使用const:参数前加const,表示不修改参数。函数定义参数括号后加const,表示不修改调用它的对象。如果参数即this都不能修改,即都为const,那么函数返回的引用也必须声明为const。
  • 析构函数不能继承,构造函数不能继承,赋值原算符不能继承。
  • 私有成员与保护成员,对于类本身,是一样的,不能被类外访问。但作为基类,私有成员以及不能被派生类访问,但保护成员在派生类中相当于公有成员。
  • 虚方法:如果在派生类中需要重新定义该方法,应在基类中将该方法声明为虚方法,这样可以启动动态联编。
  • 析构函数;基类的析构函数应当是虚的。这样程序在通过指针或引用删除派生类对象时,先调用派生类的析构函数,然后调用基类的构造函数。
  • 友元函数:并非成员函数,不能被继承。如果派生类对象需要调用基类友元函数,要先将派生类的指针或者引用强行转换为基类的指针或引用,然后调用基类的友元函数。
  • 派生类自动使用继承来的基类方法,如果没有重新定义的话;派生类的构造函数自动调用基类的构造函数;派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他的构造函数;派生类方法可以使用作用域解析运算符来调用公有的和受保护的基类方法;派生类可以通过强制类型转换调用基类的友元函数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值