[C++ 从入门到精通] 17.基类与派生类关系的详细再探讨

2023-12-21 20:54:52
  • 📢博客主页:https://loewen.blog.csdn.net
  • 📢欢迎点赞 👍 收藏 ?留言 📝 如有错误敬请指正!
  • 📢本文由 丶布布原创,首发于 CSDN,转载注明出处🙉
  • 📢现在的付出,都会是一种沉淀,只为让你成为更好的人?


一. 派生类对象模型简述

若一个类,继承自一个父类(基类),那么该类称之为子类(派生类)。

并且该子类的对象包含两种成分:

  1. 该子对象含有子类自己的对象成分(包括子类自己的成员函数以及成员变量);
  2. 该子对象也含有基类的对象成分(包括基类自己的成员函数以及成员变量);

回顾:基类的指针为什么可以new派生类的对象?

Human* phuman = new Men;

基类指针可以用来new一个子类对象本质上是因为子类对象中含有基类的成分,因此,子类对象也可以当做是一个特殊的父类对象了。实际上,编译器在我们用多态时,帮我们做了隐式的,从派生类到基类的类型转化。而这种转换的好处就是,当需要用到基类引用的地方,你可以用这个派生类对象的引用来代替or当需要用到派生类引用的地方,你可以用这个基类引用来代替。因此我们就可以用多态这种知识来实现更加复杂的代码。


二. 派生类构造函数

派生类实际上是使用基类的构造函数来初始化其基类部分的。即,基类控制基类部分的成员初始化,派生类控制派生类部分的成员初始化。

new myMen;

所以,当我们创建一个派生类的对象时,既会调用派生类的构造函数,也会调用基类的构造函数(调用顺序:先调用基类的构造函数,再调用派生类的构造函数;释放顺序:先调用派生类的析构函数,再调用基类的析构函数)。

那么,当定义派生类对象的时候,如果基类构造函数需要传递参数,该如何完成呢?

class Human
{
public:
	Human();
	Human(int);
};

可通过派生类的构造函数初始化列表中为基类构造函数传递参数

如:

class Human {
public:
	Human (int age):m_Age(age){ 
		cout << "this is Human 的构造函数!" << endl;
	}
	
	vitrual ~Human() {}//为基类析构声明为virtual的!
private:
	int m_Age;
};
class Men: public Human {
public:
    //在子类的初始化列表中,直接调用父类的构造函数并传参进进去!
	Men(int age,int a) :Human(age), nums(a) {
		cout << "this is Son 的构造函数!" << endl;
	}
		
	virtual ~Men() {}//此时子类的析构其实本质上也是virtual的,因为你继承自Human 
private:
	int nums;
};

这时,定义子类对象时可用:

Men men(10,10);

三. 既当父类又当子类(多继承)

一个类可以既可以作为某一个类的子类,也可以作为另一个类的父类。

class GrandDad{/.../};            
class Dad: public GrandDad{/.../};//GrandDad类为Dad类的直接基类
class Son: public Dad{/.../};     //GrandDad类为Son类的间接基类

继承关系一直继承,构成了一种继承链,最终结果就是派生类Son会包含它的直接基类的成员以及每个间接基类的成员。但是,在实际开发中,尽量少用这种多继承来写代码,不然很容易造成你写的代码难维护,也不易读。


四. 不想当基类的类final

对于不想用于基类的类,C++中给出了 final 关键字,放在不想做基类的类后面(最终类),可以防止我们写代码时误用了不想当基类的类作为基类。

如图:这时Human类不会再被当做基类使用

在这里插入图片描述

注意:若在一个类的成员函数声明后加final关键字,则该类的子类在继承该类时,不可重写该成员函数。

总结C++11中引入的final关键字的用法:

  • 对于不想被子类重写的成员函数,需要用final对基类成员函数进行声明,那么子类就不再有权限对该成员函数进行重写了。
  • 对于不想当做基类的类,用final对类进行声明后,该类就不可以给其他类用作继承时的基类了

五. 静态类型与动态类型

静态类型:变量声明时的类型,编译的时候是已知的。

动态类型: 指针或引用所代表的内存中的对象的类型,在运行的时候才能知道。

只有在基类指针/引用,才存在这种静态类型和动态类型不一致的情况。

Human* pHuman1 = new Men();    //静态类型是Human *,动态类型是Men *
Human& p1 = *pHuman1;          //静态类型是Human &,动态类型是Men &
Human* pHuman2 = new Woman();  //静态类型是Human *,动态类型是Woman *
Human& p2 = *pHuman2;          //静态类型是Human &,动态类型是Woman &

如果不是基类的指针/引用,那么动态类型和静态类型永远都是应该一致的:

Human* pHuman = new Human();   //静态类型是Human *,动态类型也是Human *
Human human;                   //静态类型是Human,  动态类型也是Human 
Man* pman = new Man();         //静态类型是Man*,   动态类型也是Man* 
Man man;                       //静态类型是Man,    动态类型也是Man

六. 派生类向基类的隐式类型转换

Human *phuman = new Men();  //基类指针指向一个派生类对象,编译器隐式地帮我们将Men类对象转换为了pHuman对象
Human &q = *phuman;         //基类引用绑定到派生类对象上

当我们使用多态时,编译器是隐式地帮我们执行了派生类到基类的转化工作的。这种隐式转换只所以能成功,是因为每一个派生类对象中都包含着基类的成分,所以基类的指针或者引用是可以绑定到子类对象的基类部分上的。也就是说,基类对象可以独立存在,也可以作为派生类对象的一部分存在。

但注意:并不存在从基类到派生类的自动类型转换。(因为子类是从基类中继承过来的,因此子类中含有的成分基类中不一定含有)

Men *pmen = new Human ();  //非法!不能将基类转为派生类
Human human;
Men& men = human;          //非法!不能将基类转为派生类(派生类的引用不能绑定到基类对象上去)
Men* pmen = &human;        //非法!不能将基类转为派生类(派生类指针不能指向基类地址)
Men men;
Human* phuman = &men;     //可以,编译器是通过静态类型来推断转换的合法性(派生类Men*可以转换到基类Human*上)
Men* pmen = phuman;       //非法!不能将基类转为派生类(基类Human*不可以转换到派生类Men*上)

//但是,如果基类中含有至少一个虚函数的话,就可以通过dynamic_cast<Type*>进行类型转换!
Men* pmen = dynamic_cast<Men* >(phuman);//合法!

七. 父类子类之间的拷贝与赋值

方式一

Men men;
Human human(men);// 用子类对象初始化(拷贝给)基类对象,这个会导致基类的拷贝构造函数的执行

此时调用的是基类的拷贝构造函数,将其形参const Human& thuman中的thuman动态绑定到了子类对象men上。

Human(const Human& thuman) {
	cout << "拷贝构造函数!" << endl;
}

方式二:用子类对象赋值给基类对象也是合法的

Men men;
Human human;
human = men;  //用子类对象赋值给基类对象,men对象里基类的那部分就被human拿去了

此时调用的是基类的拷贝赋值运算符的重载函数,将其形参const Human& thuman中的thuman动态绑定到了子类对象men上。

Human& operator=(const Human& thuman) {
	cout << "拷贝赋值运算符函数!" << endl;
	return *this;
}

结论:用派生类对象为一个基类对象初始化或赋值时,派生类对象只会将自己基类那部分对其进行拷贝或者赋值,派生类部分将被忽略掉

也就是:基类只干基类自己的事,多余的部分不会去操心。


下雨天,最惬意的事莫过于躺在床上静静听雨,雨中入眠,连梦里也长出青苔。

文章来源:https://blog.csdn.net/weixin_43197380/article/details/135099496
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。