继承与派生、虚函数、多态


重载:函数名相同,参数列表不同、处在同一个作用域。
隐藏:基类和派生类当中 函数名相同 派生类会将基类的同名方法隐藏。如果想通过派生类对象访问基类的成员方法需要加作用域。
覆盖:基类和派生类中 函数的返回值 函数名 参数列表,而且基类的函数是virtual虚函数

继承

继承的作用:面向对象技术强调软件的可重用性。C++使用类的继承机制,解决了软件重用问题。

所谓继承就是在一个已存在的类的基础上建立一个新的类。已存在的类称为基类或父类。新建立的类称为派生类或子类。
一个新类从已有的类哪里获得其已有特性,这种现象称为类的继承

单继承

一个派生类只从一个基类派生,称为单继承,这种继承关系 所形成的曾次是一个树形结构。

多重继承

一个派生类有两个或多个基类的称为多重继承。

派生

从已存在类(父类)产生一个新的子类,称为类的派生

派生类继承了基类的所有数据成员和成员函数.

一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类,因此基类和派生类是相对而言的。类的每一次派生,都继承类其基类的基本特征,同时又根据需要调整和扩充原有特征。
在这里插入图片描述

基类和派生类的关系:派生类是基类的具体化,而基类是派生类的抽象。
基类综合了派生类的公共特征,派生类则在基类的基础上增加了某些特性,把抽象编成具体的、实用的类型。

派生类的声明方式

class A
{
public:
	int ma;
protected:
	int mb;
private:
	int mc;
};
class B : private A // 类B私有继承了类A
{
public:
	void test() { cout << mb << endl; }
	int md;
protected:
	int me;
private:
	int mf;
};

在这里插入图片描述
在class后面的B是新建的类名,冒号后面的A是已经存在的基类。在A前有一个关键字private,用来表示基类A中的成员在派生类B中的继承方式。
继承方式包括public(公用的),private(私有的)和protected(受保护的),如果不写,默认是private(私有的),而用struct 定义的类,如果不写,默认是public(公用的)。

派生类的构成

派生类中的成员

  • 从基类继承过来的成员
  • 自己增加的成员

从基类 继承的成员体现了派生类从基类继承而获得的共性,而新增加的成员体现了派生类的个性。正是这些新增加的成员体现了派生类与基类的不同,体现了不同派生类之间的区别。

构造一个派生类

(1)从基类接收成员

派生类把基类的全部的成员(不包括构造函数和析构函数)接收过来,不能选择接收其中一部分成员,而舍弃另一部分成员。

这就有可能出现一种情况:有些基类的成员在派生类是用不到的,但是也必须继承过来。这样就或造成数据的冗余,尤其是在多次派生之后,会在许多派生类对象中存在大量无用的数据,不仅浪费了大量的空间,而且在对象的建立、复制、赋值和参数的传递中,花费更多无谓的时间,降低了效率。所以,我们要根据根据派生类的需要合理的选择基类,使冗余量尽可能的小。

(2)调整从基类接收的成员

可以改变基类成员在派生类中的访问属性,这是通过指定继承方式实现的。

如可以通过继承把基类的公有成员指定位在派生类的访问属性为私有(派生类外不能访问)。
可以在派生类中声明一个与基类成员同名的成员,则派生类中的新成员户覆盖基类的同名成员。

(3)增加自己的成员

在声明派生类时,一般还应当自己定义派生类的构造函数和析构函数,因为构造函数和析构是不能从基类继承的。

派生类成员的访问属性

确定基类的成员在派生类的访问属性,不仅要考虑对基类成员所声明的访问属性,还要考虑派生类所声明的对基类的继承方式,这两个因素共同决定基类成员在派生类的访问属性。

公用继承

可以看出公有继承唯一改变了不能访问基类的私有成员。

因为私有成员体现了数据的封装性,隐藏私有成员有利于测试、调试和修改系统。如果把基类所有成员的访问权限都原封不动地继承到派生类,使基类的私有成员在派生类中仍保持其私有性质,派生类成员能访问基类的私有成员,那么基类和派生类就没有界限了,这就破坏了基类的封装性。
要记住:保护私有成员是一条重要的原则!!!

私有继承

在这里插入图片描述

可以看出:既然声明为私有继承,就表示将原来能被外界引用的成员隐藏起来,不让外界引用,因此基类的公有成员和保护成员理所当然地成为派生类的私有成员。

对于不需要再往下继承的类的功能可以用私有继承方式把它隐藏起来,这样下一层的派生类就无法访问它的任何成员。

保护继承

由"protected"声明的成员称为“受保护的成员”。受保护的成员不能被类外访问。

保护成员私有成员类似,但有一点和私有成员不同,保护成员可以被派生类的成员函数引用。
在这里插入图片描述

普通派生类的构造函数

前面已经说过,基类的构造函数是不能继承的,再声明派生类时,派生类并没有把基类的构造函数继承过来,因此对继承过来的基类成员的初始化工作也要由派生类的构造函数承担。所以在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员的初始化,还应当考虑基类的数据成员初始化。也就是说,在执行派生类的构造函数时,使派生类的数据成员和基类的数据成员同时都被初始化。

解决办法:在执行派生类的构造函数时,调用基类的构造函数。

class Base
{
public:
	Base(int a) :ma(a) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
	void show() { cout << "Base::show()" << endl; }
	void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	//“Base”: 没有合适的默认构造函数可用
	//Derive(int data) :ma(data), mb(data) { cout << "Derive()" << endl; }//error
	Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }
/*
可以看到:派生类构造函数(Derive)后面括号内中的参数表中包括参数的类型和参数名,而基类构造函数后面括号内的参数列表只有参数名而不包括参数类型,因为这里不是定义基类构造函数,而是调用基类的构造函数,因此这些参数是实参而不是形参。
它们可以是常量、全局变量和派生类构造函数总参数列表中的参数。

也可以将派生类的构造函数在类外面定义,只在类体中写该函数的声明:
Derive(int data);
请注意:在类中对派生类构造函数作声明时,不包括一般形式中的“基类构造函数名(参数表)”部分,即Base(data).只在定义函数时才将它列出
在类的外面定义派生类构造函数:
Derive::Derive(int data):Base(data),mb(data){}

*/
	~Derive() { cout << "~Derive()" << endl; }
	void show() 
	{ 
		cout << "Derive::show()" << endl;
	}
private:
	int ma;
	int mb;
};

创建普通对象的构造函数

  • 派生类构造函数先调用基类构造函数
  • 再执行派生类构造函数本身

有子对象的派生类的构造函数

对子对象的初始化是在建立派生类的构造函数来实现的。
此时派生类构造函数的任务应该包括3部分:

  • 对基类数据成员仅从初始化
  • 对子对象数据成员初始化
  • 对派生类对象成员初始化

创建子对象派生类构造函数

  • 调用基类构造函数,对基类数据成员初始化
  • 调用子对象构造函数数,对派生类数据成员初始化
  • 执行派生类构造函数本身,对派生类数据成员进行初始化。
#include<iostream>
#include<string>
using namespace std;

class Student  //基类
{
public:
	Student(int n, string nam) //基类的构造函数
	{
		num = n;
		name = nam;
	}

	void display() //基类的成员函数,用来输出基类数据成员
	{
		cout << "num:" << num << endl << "name:" << name << endl;
	}
protected:
	int num;
	string name;
};

class Student1 :public Student  //声明公用派生类 Student1
{
public:
	Student1(int n, string nam, int n1, string nam1, int a, string ad) :Student(n,nam), monitor(n1, nam1)
	{
		age = a;
		addr = ad;
	}

	void Show()
	{
		cout << "This Student is:" << endl;
		display();          //输出num和name
		cout << "age:" << age << endl;//输出age
		cout << "address:" << addr << endl;//输出addr
	}

	void show_monitor()  //用来输出子对象的成员函数
	{
		cout << endl << "Class monitor is :" << endl;
		monitor.display();  //调用基类的成员函数
	}

private://派生类的私有数据
	Student monitor; //定义子对象(班长)
	int age;
	string addr;
};

int main()
{
	Student1 stud1(161001,"张三", 10001, "李四", 20, "weiyang Rode,xi'an");
	stud1.Show();
	stud1.show_monitor();
	return 0;
}


对上例来说,先初始化基类的中的数据成员num,name,然后再初始化子对象的数据成员num,name,最后初始化派生类中的数据成员age,addr.

派生类构造函数的总参数表中的参数,应当包括基类构造函数和子对象的参数表中的参数。基类构造函数和子对象的次序可以是任意的,如上面的派生类构造函数首部可以写成

Student1(int n,string nam,int n1,string nam1,int a,string ad):monitor(n1,nam1),Student(n,nam)

编译系统是根据相同的参数名(而不是根据参数的顺序)来确立它们之间的传递关系的。如总参数表中的n传给基类构造函数Student的参数n,总参数表中的n1传给子对象的参数n1.但是习惯上一般先写基类构造函数,以与调用的顺序一致,看起来清晰一些。

普通派生类的析构函数

执行析构函数的顺序是:

  • 先执行派生类的析构函数
  • 再执行其基类的析构函数

有子对象的派生类的析构函数

执行析构函数的顺序是:

  • 先执行派生类自己的析构函数,对派生类新增加成员进行清理
  • 然后调用子对象的析构函数,调用子对象的析构函数,对子对象进行清理
  • 最后调用基类的析构函数

虚函数

虚函数的作用

允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

#include<iostream>
#include<string>
using namespace std;

class Base
{
public:
	Base(int a) :ma(a) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
   void show() { cout << "Base::show()" << endl; }
   void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }
	~Derive() { cout << "~Derive()" << endl; }
     void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main()
{
	Derive d(20);

	Base *pb = &d;
	pb->show(); //静态绑定 Base::show   call  Base::show (0E912DAh)
	
	cout << sizeof(Base) << endl;  // 4  
	cout << sizeof(Derive) << endl; // 8  

	// 识别的都是静态(编译时期)类型
	cout << typeid(pb).name() << endl;  // Base*  
	cout << typeid(*pb).name() << endl; // Base  
	return 0;
}

虚函数的使用方法

  • 在基类中用virtual声明成员函数为虚函数。在类外定义虚函数时,不必再加virtual.
  • 在派生类中重新定义此函数,函数名,函数的参数列表和返回值必须与基类的虚函数相同,根据派生类的需要重新定义函数体。
  • 当一个成员函数被声明为虚函数时,其派生类中的同名函数都自动成为虚函数。因此,在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明函数时都加virtual,使程序更清晰。
    如果在派生类中没有对基类的虚函数重新定义,则派生类简单的继承其直接基类的虚函数
  • 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
  • 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
#include<iostream>
#include<string>
using namespace std;

class Base
{
public:
	Base(int a) :ma(a) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
	virtual void show() { cout << "Base::show()" << endl; }//虚函数
	virtual void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }
	~Derive() { cout << "~Derive()" << endl; }
	virtual void show() { cout << "Derive::show()" << endl; }//同名虚函数
private:
	int mb;
};
int main()
{
	Derive d(20);

	Base *pb = &d;
	pb->show(); //动态绑定
	
	cout << sizeof(Base) << endl;  //  8
	cout << sizeof(Derive) << endl; //  12

	
	cout << typeid(pb).name() << endl;  //  Base*
	cout << typeid(*pb).name() << endl; //  Derive

	return 0;
}

虚函数表

每个含有虚函数的类有一张虚函数表(vtbl),表中每一项是一个虚函数的地址。也就是说,虚函数表的每一项是一个虚函数的指针。(在编译阶段产生)
我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

在这里插入图片描述
(1)虚函数按照其声明顺序放于表中。

(2)父类的虚函数在子类的虚函数前面。

在什么情况下应当声明为虚函数

使用虚函数要注意的:

  • 只能用virtual声明类的成员函数,把它作为虚函数,而不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中队基类的虚函数重新定义。显然,它只能用于类的继承层次中。
  • 一个成员函数被声明为虚函数之后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同参数和返回值类型的同名函数。

在哪些情况下可以考虑将一个成员函数声明为虚函数

  • 看成员函数所在的类是否会作为基类,然后看成员函数在类的继承后有无可能被更改功能,如果希望更改功能,一般应该将它声明为虚函数。
  • 看对成员函数的调用时通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数

非虚函数

有时在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数,如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为。

静态关联

函数重载、通过对象名调用的虚函数,在编译时期就可以确定其调用的虚函数属于哪一个类,其过程称为静态关联。

动态关联

在调用虚函数使没有指定对象名,那么系统是如何确定关联的呢?是通过基类指针和虚函数的结合来实现多态性的,先定义一个指向基类的指针变量,并使他指向相应的类对象,然后通过该基类指针去调用虚函数,显然对于这样的调用方式,编译系统在编译该行时是无法确定调用哪一个类对象的虚函数的。因为编译只做静态的语法检查,光从语句形式是无法确定调用对象的。
在这种情况下,编译系统把它放到运行阶段处理,在运行阶段确定关联关系。在运行阶段,基类指针变量先指向了某一个类对象,然后通过该指针变量调用该对象中的函数。此时调用哪一个对象的函数就是确定的了。由于是在运行阶段把虚函数和类对象绑定在一起,此过程称为动态关联。这种多态性时运行时的多态性。

多态性

通过虚函数与指向基类对象的指针变量的配合使用,就能实现动态的多态性。

如果想调用同一类族中不同类的同名函数,只要先用基类指针指向该类的对象即可。如果指针先后指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数

纯虚函数

定义:纯虚函数是在声明虚函数时被“初始化”为0的函数。
声明纯虚函数的一般形式是

virtual  函数类型  函数名  (参数列表) =0;

注意:

  • 纯虚函数没有函数体
  • 最后面的“=0”并不是说函数的返回值为0,它只起到形式上的作用,告诉编译系统“这是纯虚函数”
  • 这是个声明语句,最后要记得加分号
    作用:在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。如果在基类中没有保留函数名字,则无法实现多态性。

抽象类

定义:不用来定义对象而只作为一种基本类型用作继承的类,称为抽象类
凡是包含纯虚函数的类都是抽象类
注意:

  • 抽象类不能定义对象,但是可以定义指向抽象类的指针变量。当派生类成为具体类之后,就可以用这种指针指向派生类对象,然后 通过该指针调用虚函数,实现多态性。
  • 如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该函数在派生类中仍然为纯虚函数

猜你喜欢

转载自blog.csdn.net/qq_43313035/article/details/89421143