C++ 继承
前提引入
假如我们定义了两个类:狗类、猫类
class Dog {
public:
void Eat()
{
cout << _name << "在吃饭!" << endl;
}
void Sleep()
{
cout << _name << "在睡觉!" << endl;
}
void Bark()
{
cout << _name << "在狂吠~~~~" << endl;
}
public:
string _name; //名字
string _gender; //性别
string _color; //颜色
};
class Cat {
public:
void Eat()
{
cout << _name << "再吃饭!" << endl;
}
void Sleep()
{
cout << _name << "在睡觉!" << endl;
}
void Mew()
{
cout << _name << "喵喵喵~~~" << endl;
}
public:
string _name;
string _gender;
string _temper; //脾性
};
运行没有任何问题,那么我们在仔细观察这两个类:
为了提升代码效率,我们考虑能否将不同类中相同的代码部分提取出来供这些类来共同使用
这也就引出了继承这个概念。
继承
定义:
继承机制是面向对象程序设计使代码可以进行复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类称为派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。
对于开篇我们所举的栗子,我们发现猫类与狗类存在相同的成员函数和成员变量,那么我们可以将这部分相同的板块提取出来形成一个单独的动物类,显然动物是对猫和狗的高度抽象概括,则动物类称为基类/父类,而细致的分析出来的猫、狗组成的类称为 派生类/子类
class Animal { //动物类
public:
void Eat()
{
cout << _name << "在吃饭!" << endl;
}
void Sleep()
{
cout << _name << "在睡觉!" << endl;
}
public:
string _name;
string _gender;
};
层次结构:
基类是对子类的抽象,派生类是对基类的进一步细分的描述,并且可能在基类的基础上增加了新的成员函数(功能),由上图可以看出来,我们将猫与狗的共有部分提取出来形成新的动物类,从上往下明显看出是划分成了三个层次等级,并且越往下层走抽象度越小。
即针对这个示例,我们可以说猫和狗都是属于动物,但是不能说动物就是猫和狗
继承定义格式
回到我们开篇的例子:
class Dog:public Animal //定义继承关系
{
public:
void Bark()
{
cout << _name << "在狂吠~~~~" << endl;
}
public:
string _color;
};
class Cat :public Animal //定义继承关系
{
public:
void Mew()
{
cout << _name << "喵喵喵~~~" << endl;
}
public:
string _temper;
};
则在子类中可以调用到父类中的成员:
继承权限一共有三种:public、protected、private
当我们没有显式给出继承权限时候,class 默认的继承权限是 private,而 struct 默认的继承权限是 public。
C++ 中class 与 struct
(1)class 中默认的成员访问权限是 private,而 struct 默认的成员访问权限是 public;
(2)class 默认的继承权限是 private ,而 struct 默认继承权限是 public ;
(3)在模板的使用中,参数的类型可以使用 class 或者 typename 关键字来定义,但是不能够使用 struct;
那么,不同的继承权限又有什么不同呢?
接下来我们来探讨看看吧~
class A { //定义 A 类为基类
public:
void SetA()
{
cout << "SetaA()" << endl;
}
public: // 定义具有不同访问权限的成员变量
int _a;
protected:
int _b;
private:
int _c;
};
public 继承
由此可见,public 继承中:
子类内可以访问基类的 public 和 protected 成员,但不能访问到 private 成员,而在类外只能访问到 public 成员;
那么原本在基类中的成员 public 继承在子类中之后的访问权限会发生变化吗?
在上边测试中,我们发现子类不能访问到基类的 private 成员,则说明原本在基类中的 private 成员在子类中访问权限依旧是 private
而原本在基类中访问权限为 public 和 protected 的变量在public继承之后的访问权限没有发生变化
protected 继承
protected 继承中,原本在基类中的public 成员和 protected 成员的访问权限在派生类中都会变为 protected,基类中访问权限为 private 的成员依旧不可见
private 继承
private 继承,会使原本在基类中访问权限为 public 和 protected 的成员访问权限在派生类中变为 private,且原本在基类中访问权限为 private 的成员在派生类中依旧不可见
小总结:
public 继承下的对象赋值转换
首先定义好基类与派生类:
class A {
public:
void SetA(int a)
{
_a = a;
cout << "SetA()" << endl;
}
public:
int _a;
};
class B :public A
{
public:
void SetB(int a , int b)
{
SetA(a);
_b = b;
cout << "SetB()" << endl;
}
public:
int _b;
};
1、派生类对象可以给基类对象进行赋值,反之不行
我们来分析为什么:
2、基类引用可以引用派生类对象,反之不行
3、基类指针可以指向派生类对象,反之不行(或者使用类型强转,但这种做法不安全)
分析:
使用派生类指针指向基类对象时,指针指向空间的大小应该与派生类中成员变量大小一致,因此会造成空间的非法获取
继承中作用域
1、在继承体系中基类与派生类都有独立的作用域;
2、子类与基类具有同名成员时,子类成员将优先调用自己的成员,而不能直接访问到基类的成员(隐藏 / 重定义),但可以通过限定作用域来访问基类中同名的成员;
3、成员函数的隐藏,只要函数名相同就可以构成隐藏
//测试代码:
class A {
public:
void SetA(int a, int d)
{
_a = a; _d = d;
}
void Func()
{
cout << "A::Func()" << endl;
}
public:
int _a;
int _d;
};
class B:public A
{
public:
void SetB(int b,int d)
{
_b = b; _d = d;
}
void Func(int b) //与 A 成员函数同名
{
cout << "B::Fun()" << endl;
}
public:
int _b;
int _d; //与 A 成员变量同名
};
注意
这里的两个 Func 函数并不能构成函数重载!!!!
函数重载是指:
在同一个作用域内,函数名相同但参数列表不同(参数个数、类型、参数次序)的函数
派生类的默认成员函数
构造方法
1、若基类中并未定义构造方法,则子类中可以根据实际需要确定是否需要定义构造方法
class A {
public:
void Func()
{
cout << "A::Func()" << endl;
}
public:
int _a;
};
class B :public A
{
public:
B(const int b) :_b(b) //有没有都可以
{}
void Func()
{
cout << "B::Func()" << endl;
}
public:
int _b;
};
2、若基类中定义了无参或全缺省构造方法,则派生类可以根据需要来确定是否需要定义
class A {
public:
A(const int a=10) :_a(a) //全缺省构造
{}
void Func()
{
cout << "A::Func()" << endl;
}
public:
int _a;
};
class B :public A
{
public:
/*B(const int b) :A(b) //有没有都可以,默认会调用基类的缺省构造方法
{}*/
void Func()
{
cout << "B::Func()" << endl;
}
public:
int _b;
};
3、若基类中定义了带参且非全缺省构造,则派生类中必须定义自己的构造方法,并在初始化列表调用基类的构造方法
class A {
public:
A(const int a) :_a(a)
{}
void Func()
{
cout << "A::Func()" << endl;
}
public:
int _a;
};
class B :public A
{
public:
B(const int a , const int b) :A(a),_b(b) //初始化列表中调用基类构造方法
{}
void Func()
{
cout << "B::Func()" << endl;
}
public:
int _b;
};
拷贝构造方法
1、基类中拷贝构造没有实现,子类中可以选择性实现
2、基类的拷贝构造定义了,子类中没有定义可以正常定义,若子类定义了拷贝构造则必须在初始化列表调用基类的拷贝构造方法
class A {
public:
A(int a = 10) :_a(a) //缺省构造
{}
A(const A& a) :_a(a._a) //拷贝构造
{}
void Func()
{
cout << "A::Func()" << endl;
}
public:
int _a;
};
class B :public A
{
public:
B(int b) :_b(b) //构造--------因为基类 A 中存在缺省构造
{}
B(const B& bb):A(bb),_b(bb._b) //拷贝构造
{}
void Func()
{
cout << "B::Func()" << endl;
}
public:
int _b;
};
赋值运算符重载
1、基类的赋值运算符没有定义,子类也可以不用定义------------浅拷贝
2、对子类对象赋值之前,需要先调用基类的赋值运算符重载函数—继承给子类的基类赋值,然后再给子类自己的成员进行赋值操作
class A {
public:
A(int a = 10) :_a(a)
{}
A& operator=(A a)
{
if (this != &a) {
_a = a._a;
}
return *this;
}
public:
int _a;
};
class B :public A
{
public:
B& operator=(B b) {
if (this != &b) {
A::operator=(b); //调用 基类的赋值运算符重载函数
//operator=(b);
//直接调用赋值运算符重载,会屏蔽基类中赋值运算符重载函数而优先调用自己的赋值运算符重载函数---------------------造成死循环
_b = b._b;
}
return *this;
}
public:
int _b;
};
析构函数
1、若子类中未涉及资源管理时,则可以选择性定义析构函数,因为编译器会给子类生成一份默认的析构函数,并在该析构方法中自动会调用基类的析构方法
//测试代码
class A {
public:
A()
{
_a = new int[5];
}
~A() //析构方法
{
delete[] _a;
_a = nullptr;
cout << "~A()" << endl;
}
public:
int* _a;
};
class B :public A
{
public:
B(int b):A(), _b(b)
{}
//子类 B 中并未显式定义析构方法
public:
int _b;
};
2、若子类中存在资源管理时,必须显式定义析构方法
//测试代码
class A {
public:
A()
{
_a = new int[5];
}
~A() //析构方法
{
delete[] _a;
_a = nullptr;
cout << "~A()" << endl;
}
public:
int* _a;
};
class B :public A
{
public:
B(int b):A(), _b(b)
{
_d = new int[10];
}
~B() //当子类中存在资源管理时,需要显式定义子类的析构方法
{
delete[] _d;
_d = nullptr;
cout << "~B()" << endl;
}
public:
int _b;
int* _d;
};
倘若在该测试中,子类 B 中存在资源管理,但是我们并未显式定义析构方法,则会存在内容泄漏:
创建哪个对象,就调用哪个对象的构造方法
在创建子类对象时,会先执行初始化列表,并且在初始化列表中完成基类对象的构造,其次完成子类自己对象的构造
销毁哪个对象,就调用哪个对象的析构方法
在销毁子类对象时,会先析构子类对象,并在子类的析构方法最后进行基类对象的销毁
继承与友元
友元关系不能继承
要验证这个结论成立,则我们可以给基类 A 声明一个友元函数关系 Func,则友元函数 Func 可以访问到 A 类中的受保护和私有的成员,那么 作为 A 类的派生类 B ,是否也可以在 A 的友元函数 Func 中访问到 B 类的成员??
若可以,则说明友元关系可以继承,若不可以则说明友元关系不能继承
继承与静态成员
静态成员可以被继承,并在整个继承体系中只存在一份
静态成员在类外进行初始化
class A {
public:
static int _a;
};
//静态成员在类外初始化
int A::_a = 10;
class B :public A
{
protected:
int _b;
};
不同继承方式下的对象模型
对象模型:类中成员的存贮方式
单继承
每个子类只有一个基类
class A {
public:
int _a;
};
class B :public A //单继承
{
public:
int _b;
};
sizeof(B) == 8
多继承
每个子类至少有两个基类
多个继承,必须在每个基类前都加继承权限
class A1 {
public:
int _a1;
};
class A2 {
public:
int _a2;
};
class B :public A1,public A2
{
public:
int _b;
};
sizeof(B) == 12
若存在多个继承,则对象模型中基类成员的先后次序与继承列表的先后次序一致
菱形继承/钻石继承
将单继承与多继承组合起来形成的继承方式
class A {
public:
int _a;
};
class B1 :public A{
public:
int _b1;
};
class B2 :public A {
public:
int _b2;
};
class C :public B1,public B2
{
public:
int _c;
};
注意:菱形继承的二义性
更改后的正确测试:
(1)访问明确化----------》增加访问作用域限定符
从该代码中可以看出最顶层的基类中成员在底层子类中存在了多份----------造成空间浪费
要想解决这种空间浪费问题,引入了虚拟继承
(2)虚拟继承
在继承权限前加虚拟关键字 virtual
普通单继承:
//测试代码
class A {
public:
int _a;
};
class B :public A {
public:
int _b;
};
int main()
{
//cout << sizeof(B) << endl;
B bb;
cout << sizeof(bb) << endl;
bb._a = 1;
bb._b = 2;
return 0;
}
虚拟继承:
//测试代码
class A {
public:
int _a;
};
class B :virtual public A {
public:
int _b;
};
在虚拟继承中,我们发现子类大小多了 4 个字节大小,并且基类与子类对象模型的顺序与普通继承中是不一样的
在测试之后,我们发现多出来的四个字节是指的一个地址空间,中间存放了元素 0 8,这代表的是距离子类对象 bb 地址的偏移量
我们来观察汇编代码:
虚拟继承可以有效解决菱形继承中的数据二义性,菱形虚拟继承:
//菱形虚拟继承
class A {
public:
int _a;
};
class B1 : virtual public A {
public:
int _b1;
};
class B2 :virtual public A {
public:
int _b2;
};
class C :public B1, public B2
{
public:
int _c;
};
int main()
{
C cc;
cout << sizeof(cc) << endl;
cc._a = 1;
cc._b1 = 2;
cc._b2 = 3;
cc._c = 4;
return 0;
}
我们来查看存储方式:
可以发现,在菱形继承中父类继承而来的成员对象模型在上,而子类自己的成员的对象模型在下
在虚继承中我们提到,在子类中会多出几个字节空间来存储一个地址空间,同样的我们在该代码的内存监视窗口可以发现继承给子类的两个父类对象模型中都多出来 4 个字节的空间,那这个空间中 虚基表 存放的是什么地址信息:
我们可以在监视窗口来查看对应信息:
ps:
欢迎读者评论留言呀~~
文章评论