[C++ 从入门到精通] 17.基类与派生类关系的详细再探讨
- 📢博客主页:https://loewen.blog.csdn.net
- 📢欢迎点赞 👍 收藏 ?留言 📝 如有错误敬请指正!
- 📢本文由 丶布布原创,首发于 CSDN,转载注明出处🙉
- 📢现在的付出,都会是一种沉淀,只为让你成为更好的人?
文章预览:
一. 派生类对象模型简述
若一个类,继承自一个父类(基类),那么该类称之为子类(派生类)。
并且该子类的对象包含两种成分:
- 该子对象含有子类自己的对象成分(包括子类自己的成员函数以及成员变量);
- 该子对象也含有基类的对象成分(包括基类自己的成员函数以及成员变量);
回顾:基类的指针为什么可以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;
}
结论:用派生类对象为一个基类对象初始化或赋值时,派生类对象只会将自己基类那部分对其进行拷贝或者赋值,派生类部分将被忽略掉。
也就是:基类只干基类自己的事,多余的部分不会去操心。
下雨天,最惬意的事莫过于躺在床上静静听雨,雨中入眠,连梦里也长出青苔。 |
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!