C++继承

本文详细解释了C++中的继承概念,包括继承的格式、赋值兼容规则、派生类构造函数的使用、静态成员函数、菱形继承和虚拟继承的复杂性。作者强调了避免多继承和菱形继承的重要性,提倡组合而非继承作为更好的复用策略。
摘要由CSDN通过智能技术生成

目录

一、继承的概念

二、继承的格式

三、赋值兼容

四、派生类的构造函数

五、继承里的静态成员函数

六、菱形继承和虚拟继承


一、继承的概念

继承 (inheritance) 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员在
持原有类特性的基础上进行扩展 ,增加功能,这样产生新的类,称派生类。继承 呈现了面向对象
程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,
承是类设计层次的复用。

二、继承的格式

 

 这里我们不要通过背的方式去记,因为这里的访问方式都是按最小的来

顺序是public > protected > private。就比如说,我们public继承,然后基类里面的protected成员,因为protected  < public,所以这里的基类在派生类里的访问方式是按protected来进行的。


我们如果不显示的写继承方式,用的是class的,默认按照private方式继承,struct的默认继承方式是public,但建议最好是把继承方式写出来。


三、赋值兼容

赋值兼容其实就是一种赋值,我们以前进行类型转化时,有强制类型转化和隐式类型转化,这些在转化的过程中其实都是通过生成一个中间变量来实现,而这个中间变量具有常性,所以我们在进行类型转化的数据进行引用时要加上一个const。

 如果我们没有加const,编译器会给我们报错,因为在类型转化过程中会产生一个具有常性的临时变量,不加const会造成权限提升,就变成一个可以改变的值,不符合常性的特点。所以就会报错,加了const,就变成了不能修改的变量,也相当于具有了常性。


但是我们在继承的时候,我们的就不叫类型转化,而叫做赋值兼容,它与类型转化是有区别的,它只能由子类(派生类)赋值给基类对象,并且它不会产生一个临时变量,而是做一个切片,对属于基类的成员切片出来。

 

 我们可以看到,编译器并没有报错,说明没有权限放大的问题,赋值兼容不产生临时变量,而是直接将属于基类成员的部分分割出去。

为什么基类就不能赋值给派生类呢?

因为这种行为不安全,派生类肯定会拥有基类的全部成员,但是基类不一定有派生类的所有成员,因此如果能够赋值,那么派生类中的某些成员可以没有被赋值到,这时候就会生成一个随机数,如果里面还有指针,那么就会产生野指针,这是非常危险的行为,因此这种基类赋值给派生类的行为被编译器禁止了。


四、派生类的构造函数

在派生类中,我们定义一个构造函数使用初始化列表初始化时,如果没有显示的调用基类的构造函数,编译器就会先去调用基类的默认构造函数,而当我i们需要调用基类的某个特定的构造函数时,我们可以通过初始化列表的方式,来调用我们需要的基类构造函数。

class Base {
public:
    Base() {
        // Base 类的构造函数
    }
};

class Derived : public Base {
public:
    Derived() {
        // Derived 类的构造函数
    }
};

就比如我们创建Derived的对象时,首先调用的是Base(),然后才是Derived()。


五、继承里的静态成员函数
基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员 。无论派生出多少个子
类,都只有一个 static 成员实例 。这个基类静态成员函数是属于类的,不是属于某个对象的。

六、菱形继承和虚拟继承

 菱形继承是一个十分复杂的继承关系,我们在继承类的时候,要尽可能去避免菱形继承的发生,因为它会带来数据冗余和二义性的问题。

 就比如我们继承多个有共同基类的派生类时,最后的派生类会将父类的数据全部拷贝一份,

class Animal {
public:
    int age;
};

class Mammal : public Animal {
};

class Bird : public Animal {
};

class Bat : public Mammal, public Bird {
};

比如Bat 类继承自两个基类,这两个基类都有一个共同的基类 Animal。当我们创建 Bat 类的对象时,Bat 类实际上包含了两个 Animal 类的实例,一个来自 Mammal,另一个来自 Bird。这样就造成了 Bat 类中对于 age 变量的冗余,即存在两份相同的 age 变量。

就是说会有多份相同数据的Animal,实际上只需要一份就够了,这就是数据冗余,它会造成内存空间的浪费。

二义性:二义性是指在多继承中,由于派生类从多个基类继承了同名的成员函数或数据成员,导致在调用这些成员时产生歧义,编译器无法确定使用哪个基类的成员。当发生二义性时,编译器会产生错误或警告,因为它无法确定要调用哪个基类的成员。

也就是说会有多个相同类型相同名字的基类成员变量,在调用的时候,编译器就不知道该去用哪个比较好。然后我们对其进行修改,编译器也不知道应该改哪一个。

class A
{
public:
	A()
	{
		_a = 0;
	}
	int _a;
};


class B : public A
{
public:
	B()
	{
		_b = 1;
	}
	int _b;
};


class C : public A
{
public:
	C()
	{
		_c = 2;
	}
	int _c;
};


class D : public B, public C
{
public:
	D()
	{
		_d = 3;
	}
	int _d;
};

int main()
{
	D d;
	cout << d._a << endl;
	return 0;
}

 虽然我们可以通过指定类域去访问


int main()
{
	D d;
	cout << d.B::_a<< endl;
	cout << d.C::_a << endl;
	return 0;
}

但是数据冗余的问题依然没有解决

 第一行存的是B中的_a,

第二行存的是B中的_b,

第三行存的是C中的_a,

第四行存的是C中的_c,

第五行存的是D中的_d,

这样造成了多份a的存在,虽然这张图看起来没什么,但是假如A类中的成员变量是个数组呢?造成的空间浪费就会变大很多。


所以c++引入了一个虚拟继承。

虚拟继承是在菱形继承的腰部添加virtual

class A
{
public:
	A()
	{
		_a = 0;
	}
	int _a;
};


class B : virtual public A
{
public:
	B()
	{
		_b = 1;
	}
	int _b;
};


class C :virtual public A
{
public:
	C()
	{
		_c = 2;
	}
	int _c;
};


class D : public B, public C
{
public:
	D()
	{
		_d = 3;
	}
	int _d;
};

 虚拟继承可以让我们的B和C有一个公共的A类数据。

下图是菱形虚拟继承的内存对象成员模型:这里可以分析出 D 对象中将 A 放到的了对象组成的最下
面,这个 A 同时属于 B C ,那么 B C 如何去找到公共的 A 呢? 这里是通过了 B C 的两个指针,指
向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量
可以找到下面的 A

 我们可以看到A类中的_a被放在了最后,它的地址是0x005EF770,第一行放的是虚基表指针,里面的偏移量是14,转化成10进制是20。它的地址是0x005EF75C。5C转化成10进制就是92,70转化成10进制是112.所以我们很容易看出来,这个偏移量是让B找到_a的。

那么为什么B和C要去找A类对象的成员呢?

因为假如发生这种情况

D d;
B b = d;
C c = d;

我们d赋值给b,要发生切割,就要把d中属于B类对象的部分给切割赋值给b,b中包含有A类的成员,这时候就必须找到A类的成员才能赋值给b。


最后总结

1. 很多人说 C++ 语法复杂,其实多继承就是一个体现。有了多继承 ,就存在菱形继承,有了菱
形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设
计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是 C++ 的缺陷之一,很多后来的 OO 语言都没有多继承,如 Java
3. 继承和组合
  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语白箱是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 (black-box reuse),因为对象的内部细节是不可见的。对象只以黑箱的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被 封装。
  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ck837

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值