C++因继承引发的隐藏与重写
在区分隐藏和重写之前,先来理一理关于继承的东西。。。
【继承】
继承是面向对象复用的重要手段,是类型之间的关系建模。通过继承一个类,共享公有的东西,实现各自本质不同的东西。简单的说,继承就是指一个对象直接使用另一对象的属性和方法。C++中的继承关系就好比现实生活中的父子关系,继承一套房子通常比白手起家自己挣要容易得多。所以原始类被称为父类或基类,继承类称为子类或派生类,而子类又可以当成父类,可再被其它类继承。这种关系和java是一样道理,不过C++多了一个麻烦的地方就是它还支持多继承,于是就引发出很多坑人的地方。
1.继承的方式:
(1) 公有继承(public)
基类的公有和保护成员被子类继承时,它们都保持原有的状态,而基类的私有成员同样被继承下来,只是在子类表现为私有,子类不能访问。
(2)私有继承(private)
它的特点是基类的公有和保护成员被子类继承时,都会成为子类的私有成员;基类的私有成员也被继承下来,但不能被该子类访问。
(3)保护继承(protected)
它的特点是基类的公有成员和保护成员被子类继承时,都会成为子类的保护成员,子类的子类可通过保护成员函数或友元访问;基类的私有成员被继承下来仍然是私有的,依旧不能被子类访问。
private能够对外部和子类保密,即除了成员所在的类本身可以访问之外,别的都不能直接访问。protected能够对外部保密,但允许子类直接访问这些成员。不难看出protected限定符是因为继承才能表现出作用。
各种继承方式下各种成员关系变化如下图
继承方式就像一张‘网’,被继承后都成了跟‘网’权限“相同的”和比“网” ’“小”的。
总结一下:
1. public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
2. protetced/private继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。
3. 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,但是基类的私有成员存在但是在子类中不可见(不能
访问)。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好要显示地写出继承方式。
2.赋值兼容规则
要点:
1. 子类对象可以赋值给父类对象(支持对象切片)
2. 父类对象不能赋值给子类对象
3. 父类的指针/引用可以指向子类对象
4. 子类的指针/引用不能指向父类对象(可通过强制类型转换完成)
例子:
#include <iostream>
#include <string>
using namespace std; class Person
{
public:
void Show()
{
cout<<_name<<endl;
}
protected:
string _name;
}; struct Student : public Person
{
public:
void Print()
{
cout<<_name<<endl;
} //private:
public:
string _id;
}; void test1()
{
Person p;
Student s; p = s; // 不是隐式类型转换 -- 切片处理-编译器天然支持 -- is-a
Person* pP = &s; //通过
Person& p1 = s; //通过
//上面都属于一种向上类型的转换,编译器默认支持。
//s = p; //报错,类型不匹配
Student* p3 = (Student*)&p; //编译通过,通过p3进行操作会出错
//p3->_id = 10; //该句执行完虽给p3->_id赋了值,但在整个程序结束时程序会崩掉,因为强转为了子类的指针,那么编译器就会按子类
//的大小对该指针作解释,这样就多‘占用’一块内存,而它可能是用来执行其他的活动的,但p3->id却指向这块非法的内存。
Student& r3 = (Student&)p; //编译通过,用r3进行操作出错
//Student& r3 = p; //出错
//r3._id = 10; //运行出错,同上面道理 } 注:p = s操作时会将子类对象独有的(非继承的部分)函数和变量自动“切去”,子类只留下继承来的基类原有的“切片”来对基类的对象进行赋值。
3.多继承
多继承是指 一个子类有两个或以上直接父类时称这个继承关系为多继承。这种继承方式使一个子类可以继承多个父类的特性。多继承可以看作是单继承的扩展。派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承。
多继承下派生类的构造函数与单继承下派生类构造函数相似,但是注意几点:
①子类构造函数必须同时负责该派生类所有基类构造函数的调用。
②派生类的参数个数必须包含完成所有基类初始化所需的参数个数。
③子类继承来的部分在内存中是按照声明定义的顺序存放的
4.棱形继承& 虚继承
棱形继承如下图
#include <iostream>
#include <string>
using namespace std; class A
{
public:
int _a;
}; class B : public A
{
public:
int _b;
}; class C : public A
{
public:
int _c;
}; class D : public B, public C
{
public:
int _d;
}; void test4()
{
D d;
d.B::_a = ; //添加域作用限定符指向修改_a
d.C::_a = ; //同理 //cout<<sizeof(D)<<endl;
//cout<<sizeof(B)<<endl; }
棱形继承
从图来看它带来两个问题:①数据冗余 ②二义性
D的对象模型里面保存了两份A,当我们想要改动从A里继承的_a时就会出现指向不明确问题,并且还存在数据冗余的问题,明明可以只要一份_a就好,但却保存了两份,浪费空间。
所以想要改动_a便要加上域作用限定符,但是这难免有些繁琐,有一个更好的办法来实现——虚继承
class A
{
public:
int _a;
}; class B : virtual public A
{
public:
int _b;
}; class C : virtual public A
{
public:
int _c;
}; class D : public B, public C
{
public:
int _d;
};
虚继承
关于虚继承:
虚继承即让B和C在继承A时 加上
virtural
关键字,注意不是D继承B、C使用虚继承。虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗和浪费空间的问题。
虚继承和虚函数虽然用的是同一个关键字,然而它俩之间没半毛钱关系。
- 虚继承体系比较复杂,在实际应用我们通常不会定义如此复杂的继承体系。应尽量避免定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗
既然说虚继承解决数据冗余问题,那怎么还说虚继承带来了损耗?
还是针对上面代码,先来看看使用虚继承和不使用虚继承 类D的大小,
//普通继承
cout<<sizeof(D)<<endl;
//结果: 20 //使用虚继承
cout<<sizeof(D)<<endl;
//结果: 24
虚继承后的结果确实大了,这就比较奇怪了,到底大在了啥地方。看看内存
从上图不难看出两点:
①派生对象d确实存放了继承来的各个成员(_a, _b, _c),同时初始基类属性_a被提出来放在了高地址处
②多出来了两个值,68 58 2d 01 和 60 58 2d 01 似乎找到了d变大原因,看看它们所指向的地方会存放什么。
d._a = ;
d._b = ;
d._c = ;
d._d = ;
一个是数值12,一个是20。这时候看看D的对象模型,发现d._b(2
)
的地址和 d._a(1)
地址之差是20,d._c(3)
的地址和 d._a(1)
地址之差是12。大胆猜测一下,这也许就是某个偏移量啊。
事实上,确实是的。每一个继承自父类对象的实例中都存有一个指针,这个指针指向虚基表中的某一项,表项里面存的是一个偏移量。对象的实例可以通过自身的地址加上这个偏移量找到存放继承自父类属性的地址。
虚继承基于这种机制虽然解决了冗余和二义性,但是也增大性能的开销,应尽量避免使用。
【隐藏和重写】
因为有类的继承机制,在父类和子类之间就会隐藏和重写这两个东西。而隐藏(重定义)和重写因字面上又有着相近的意思,导致它们比较容易被我们搞混淆。但其实重写是建立在隐藏的基础上的,它比隐藏多了一些限制。
对于隐藏,它的出现是在父子类拥有相同的成员(成员函数,成员属性)的时候,此时父类的成员就会被隐藏起来(还存在),子类的成员得到访问或调用
举个例子,
class Person
{
public:
void Show()
{
cout<<"Person::"<<_name<<endl;
}
protected:
int _id;
string _name;
}; struct Student : public Person
{
public:
void Show()
{
_id = ;
//Person::_id = 10; 要访问加上类作用符
cout<<"Student::"<<_name<<endl;
}
public:
int _id;
};
void test1()
{
Person p;
Student s;
s.Show();
} 结果:
注意到此时只为子类的_id赋了值,并未给继承来的_id赋值,如果需要访问,则要加上类作用符“::”。通常不建议定义同名的成员。
再看看隐藏了函数的情况,
class AA
{
public:
void f()
{
cout<<"AA::f()"<<endl;
}
}; class BB : public AA
{
public:
void f(int a)
{
cout<<"BB::f()"<<endl;
}
}; void test2()
{
AA aa;
BB bb; aa.f();
bb.f(); //会报错
}
这个例子,乍一看,感觉AA::f() 和BB::f(int)两个函数构成了重载,但其实BB类对象bb将f() 继承下来,两函数是构成了隐藏。因为它们压根就不在同一作用域,所以就不存在重载这么一说了,此时bb.f()调用的是自己的 f(int) ,但却未传参数,所以这个程序就会报错。
虚函数和重写
虚函数:类的成员函数前面加virtual关键字,则这个成员函数称为虚函数。
作用:基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。实际上就是用来实现多态的
关于虚函数:
1. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
2. 只有类的成员函数才能定义为虚函数。
3. 静态成员函数不能定义为虚函数。(原因戳这里)
4. 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。
5. 构造函数不能为虚函数 (虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为使用时容易引起混淆)
7. 不要在构造函数和析构函数里面调用虚函数,(在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为)具体原因戳这里
8. 最好把基类的析构函数声明为虚函数。(why?继承时,如果派生类有资源需要自己手动释放,我们可以通过基类的指针或引用去释放子类的资源,防止内存泄露。)
重写就用到了虚函数。对于重写(覆盖),它是指在父类中有一个虚函数,然后子类重写出一个和它形式(函数名,参数列表,返回值)完全相同的一个虚函数,
class Human
{
public :
virtual void Drink()
{
cout<<"喝白开水"<< endl;
} protected :
string _name ; // 姓名
}; class Boss: public Human
{
public :
virtual void Drink() //virtual 不写同样构成覆盖
{
cout<<"喝咖啡"<<endl;
}
protected :
int _position; //职称
};
重写
来小结什么时候隐藏,什么时候重写
隐藏的情况:
构成隐藏的两成员函数都不在同一作用域
函数名相同,参数不同(无论有无virtual关键字,父类函数被隐藏)
- 如果派生类函数与基类函数参数相同,但是在基类函数中没有virtual关键字,发生函数隐藏
重写的情况:
不在同一作用域
子类的函数与父类的函数同名,并且参数列表也相同
父类函数必须virtual关键字,使之成为虚函数。(否则,父类的函数在子类中将被隐藏)
C++因继承引发的隐藏与重写的更多相关文章
- C++继承引入的隐藏与重写
在区分隐藏和重写之前,先来理一理关于继承的东西... [继承] 继承是面向对象复用的重要手段,是类型之间的关系建模.通过继承一个类,共享公有的东西,实现各自本质不同的东西.简单的说,继承就是指一个对象 ...
- 继承中的隐藏(hide)重写(Override)和多态(Polymorphism)
继承中的隐藏:(不要使用隐藏,语法没有错误但是开发项目时会被视为错误) 在继承类中完全保留基类中的函数名 //基类,交通工具 class Vehicle { public void Run() { C ...
- Unity属性的封装、继承、方法隐藏
(一)Unity属性封装.继承.方法隐藏的学习和总结 一.属性的封装 1.属性封装的定义:通过对属性的读和写来保护类中的域. 2.格式例子: private string departname; // ...
- C#学习笔记_11_方法的隐藏和重写
11_方法的隐藏和重写 方法的隐藏 需要使用到关键字:new 方法的重写 虚函数: 使用关键字virtual修饰的函数 虚函数可以被子类隐藏,也可以被子类重写 非虚函数只能被子类隐藏 关键字:over ...
- c#隐藏和重写基类方法的异同
最近正在学习c#,对其中的方法重写和隐藏的概念很是模糊,现在将其归纳如下: 1:方法重写:就是在基类中的方法用virtual关键字来标识,然后在继承类中对该类进行重写(override),这样基类中的 ...
- 包、继承、Super、方法重写
1 包_继承 1.1 包 包(package) 用于管理程序中的类,主要用于解决类的同名问题.包可以看出目录. 包的作用 [1] 防止命名冲突. [2] 允许类组成一个单元(模块),便于管理和维护 [ ...
- C++父子类继承时的隐藏、覆盖、重载
存在父子类继承关系时,若有同名成员函数同时存在,会发生隐藏.覆盖和重载这几种情况.对于初学者也比较容易混淆,为此,我整理了一下我的个人看法,仅供参考.希望对大家理解有帮助,也欢迎指正. 1.父子类继承 ...
- 实现继承+接口继承+虚方法+隐藏方法+this/base+抽象类+密封类/方法+修饰符
概念: 在上一节课中学习了如何定义类,用类当做模板来声明我们的数据. 很多类中有相似的数据,比如在一个游戏中,有Boss类,小怪类Enemy,这些类他们有很多相同的属性,也有不同的,这个时候我们可以使 ...
- java基础疑难点总结之成员变量的继承,方法重载与重写的区别,多态与动态绑定
1.成员变量的继承 1.1要点 子类用extends关键字继承父类.子类中可以提供新的方法覆盖父类中的方法.子类中的方法不能直接访问父类中的私有域,子类可以用super关键字调用父类中的方法.在子类中 ...
随机推荐
- shell之九九乘法表
echo -n 不换行输出 $echo -n "123" $echo "456" 最终输出 123456 而不是 123 456 echo - ...
- Intellij Idea下tomcat设置自动编译
*eclipse默认tomcat下是自动完成编译:而Intellij Idea默认tomcat下不是自动完成编译,从如下开始设置: 进入"settings",如下图找到" ...
- 2017级C语言教学总结
一个学期下来,对于这门课教学还是感受挺多,多个教学平台辅助,确实和我前10年的教学方式区别很多,也辛苦很多. 一.课堂教学方面 1.课堂派预习作业 主要借助课堂派平台,每次课前发布预习作业.而预习作业 ...
- 团队作业4——第一次项目冲刺(Alpha版本)11.14
a. 提供当天站立式会议照片一张 举行站立式会议,讨论项目安排: PM对整个项目的需求进行讲解: 全队对整个项目的细节进行沟通: 对整个项目的开发计划进行分析,分配每天的任务: 统一确定项目的开发环境 ...
- Java的HelloWorld程序
Java的HelloWorld程序 1.新建文本文档,编写HelloWorld程序,最后保存时记得保存成.java格式 2.在D盘新建一个HelloJava文件夹用于保存java程序 3.使用WIN+ ...
- 201621123043 《Java程序设计》第8周学习总结
1. 本周学习总结 2. 书面作业 1. ArrayList代码分析 1.1 解释ArrayList的contains源代码 contains的源代码如下 public boolean contain ...
- NOIP2017 列队
https://www.luogu.org/problemnew/show/P3960 p<=500 50分 模拟 每个人的出队只会影响当前行和最后一列 p<=500,有用的行只有500行 ...
- poj2029 Get Many Persimmon Trees
http://poj.org/problem?id=2029 单点修改 矩阵查询 二维线段树 #include<cstdio> #include<cstring> #inclu ...
- c# gridview 新增行
string[] newRow = {"long","d","b"}; Gridview.Rows.Insert(Gridview.Rows ...
- django三种文件下载方式
一.概述 在实际的项目中很多时候需要用到下载功能,如导excel.pdf或者文件下载,当然你可以使用web服务自己搭建可以用于下载的资源服务器,如nginx,这里我们主要介绍django中的文件下载. ...