【C++】继承


需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。


 目录

一、继承的概念

二、被继承成员访问方式的变化

三、赋值兼容规则(切片) 

四、继承中的作用域

五、子类的默认成员函数

1、父、子类中各自的成员处理方式

2、需要自己写默认成员函数的情况

3、子类中默认成员函数的写法

3.1父类没有默认构造函数,需要在子类的构造函数里补充

3.2在子类中显式写拷贝构造

3.3在子类中显式写赋值运算符重载

3.4不需要显式调用析构函数

六、继承和友元、静态成员的关系

七、菱形继承和菱形的虚拟继承

1、菱形继承

2、二义性和数据冗余

3、虚拟继承解决二义性和数据冗余

4、virtual关键字解决二义性和数据冗余的方法

4.1未使用virtual关键字

4.2使用virtual关键字

4.3虚继承+重写问题

八、继承和组合的区别

1、组合的使用场景

2、继承和组合的区别


一、继承的概念

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

        现在有学生类和老师类,类中均有年龄、性别等相同的属性,这些相同的属性在每个类中都写一份就会出现代码的冗余。可以使用一个父类包含这些共有的成员变量和成员函数,让学生类、老师类作为子类对父类进行继承即可。

二、被继承成员访问方式的变化

public继承

protected继承

private继承

父类的public成员

public

protected

private

父类的protected成员

protected

protected

private

父类的private成员

子类不可见

子类不可见

子类不可见

        1、父类的private成员被子类继承后是不可见的。不可见指的是父类的private成员会被继承到子类,但是子类无论是在类里面还是类外面,都无法对这些被继承的私有成员进行访问。(但是可以使用继承的“获取成员变量的偷家函数”对这些不可见的成员变量进行修改、访问)

        2、除了父类中的private成员,其他成员被子类继承后最终的访问方式取决于该成员在父类中的权限和继承权限两者权限较小的那个。

        3、可以看出protected限定符是因为继承才出现的。保护和私有在当前类中没有区别,但子类继承时,父类中的私有成员子类是不可见的,而保护成员是可见的。

        4、在实际中一般使用都是public继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。父类一般是公有和保护,不使用私有

        5、class默认私有继承,struct默认公有继承。但好习惯是写明继承方式。

struct Student : public person
{

};
struct Teacher :person//不提倡,最好写明继承方式
{
};

三、赋值兼容规则(切片) 

class Person
{
protected:
	string _name;
	string _sex;
	int _age;
};
class Student : public Person
{
public:
	int _num;//学号
};
int main()
{
	Person p;
	Student s;
	//父类=子类 赋值兼容/切割/切片
	p = s;//父类对象
	Person* ptr = &s;//父类的指针
	Person& ref = s;//父类的引用
	return 0;
}

        1、子类对象可以赋值给父类对象父类的指针父类的引用。这种操作叫做赋值兼容/切割/切片,意为将子类对象中继承于父类的成员切割下来赋值给父类对象。这不是类型转换,是天然的赋值行为。(切片仅限公有继承举例:父类为公有,子类保护或私有继承后,成员变为保护和私有,子类再切片给父类,那么被继承的成员权限会变,所以切片仅限公有继承)

        2、只能将子类对象赋值给父类,父类对象不能给子类赋值。但是指针和引用却可以,不过存在越界风险。

int main()
{
	Person p;
	Student s;
	//s = (Student)p;//父类对象无法赋值给子类,强转也不行
	Student* ptr = (Student*)& p;//类型强转,有越界风险
	Student& ref = (Student&)p;//类型强转,有越界风险
	return 0;
}

        3、基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。

四、继承中的作用域

        1、在继承中父类和子类都有自己独立的类域。

        2、当子类和父类中存在同名成员时,子类将会屏蔽继承于父类的同名成员,这种情况被称为隐藏或重定义。(子类内部优先使用自己类域的同名成员,外部可使用stu.Person::_name进行显示访问)

        3、子类和父类中的同名成员函数并不构成函数重载,因为它们所处于不同的类域,子类会隐藏父类同名函数。

        4、父类和子类尽量不要使用同名成员。

五、子类的默认成员函数

1、父、子类中各自的成员处理方式

        子类中有两部分成员,一类是子类原生的成员,另一类是继承于父类的成员。

        对于原生成员,按照普通类调用默认成员函数的规则进行处理;对于继承于父类的成员,将会调用父类中的默认成员函数进行处理。(各管各的)

2、需要自己写默认成员函数的情况

        1、父类没有默认构造函数,需要自己显式写构造。

        2、子类存在浅拷贝问题,需要自己显式写拷贝构造和赋值。

        3、子类有资源需要释放,需要自己写显式析构。

3、子类中默认成员函数的写法

3.1父类没有默认构造函数,需要在子类的构造函数里补充

class Person
{
public:
	Person(const char* name)
		: _name(name)
	{}
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student(const char* name = "张三", int num = 10)
		: Person(name)//必须调用父类的构造函数进行初始化
		, _num(num)
	{}
protected:
	int _num; //学号
};

        父类有提供默认构造函数就可以不用在子类写了。

3.2在子类中显式写拷贝构造

class Person
{
public:
	Person(const Person& p)//形参:引用切片对象
		: _name(p._name)
	{}
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student(const Student& s)
		:Person(s)//切片
		,_num(s._num)
	{}
protected:
	int _num; //学号
};

        利用切片传入父类对象构造父类。Student s1(s2),在初始化列表中,利用s2中的父类成员去拷贝构造s1中的父类成员。

3.3在子类中显式写赋值运算符重载

class Person
{
public:
	Person& operator=(const Person& p)
	{
		if (this != &p)
			_name = p._name;
		return *this;
	}
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student& operator=(const Student& s)
	{
		if (this != &s)//防止自己给自己赋值
		{
			Person::operator=(s);//切片传入父类赋值运算符重载中
			//根据子类成员进行深浅拷贝
			_num = s._num;
		}
		return *this;
	}
protected:
	int _num; //学号
};

        在子类赋值运算符重载中调用父类赋值运算符重载,通过切片,完成父类成员的赋值。

3.4不需要显式调用析构函数

错误代码:

~Student()
{
    Person::~Person();
}
//子类析构函数结束后会调用一次父类的析构函数

        ~Person前必须加类域Person。因为析构函数的名字会被编译器统一处理为destructor(),子类的析构函数和父类的析构函数之间构成隐藏,所以这里需要写明类域。

        但是,我们并不需要显式调用父类的析构函数,因为出了子类析构函数的作用域,编译器会自动调用父类的析构。手动调用父类析构将会造成重复析构。

六、继承和友元、静态成员的关系

        1、友元关系不能被继承

        2、父类中的静态成员也会被继承,但是整个继承关系中共用这个静态成员

七、菱形继承和菱形的虚拟继承

1、菱形继承

        从上图可以看出,Assistant中会有两份Person成员,调用时存在二义性和数据冗余。

2、二义性和数据冗余

int main()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	//a._name = "peter";//不能这么写,因为a中有两个_name成员,需要指定类域
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

        由于Assistant中有两个_name成员,直接调用存在二义性,需要在成员之前指定类域。

        _name这个成员变量有两个问题不大,毕竟一个人可以叫蒋灵瑜,在其他场合也可以叫小蒋。但如果这个成员变量是一个int _arr[50000]的数组呢,一个类中同时有两份这么大的数组,将会导致数据冗余。

3、虚拟继承解决二义性和数据冗余

        产生二义性和数据冗余的本质就是子类继承了多份相同成员。

        解决方法是在“腰部”类增加virtual关键字。

class Person
{
public:
	string _name; // 姓名
};
class Student :virtual public Person
{
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

4、virtual关键字解决二义性和数据冗余的方法

        先来一段菱形继承的代码,_a、_b、_c、_d分别是类A、类B、类C、类D中的原生成员。        

class A
{
public:
	int _a;
};
 class B : public A
//class B : virtual public A
{
public:
	int _b;
};
 class C : public A
//class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d._b = 2;
	d.C::_a = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

4.1未使用virtual关键字

通过调用内存,会发现对象d中存在两份的_a,存在二义性和数据冗余。

4.2使用virtual关键字

        当使用了虚拟继承,通过调用内存,发现对象d中仅有一份_a,但是继承于B类和C类的_b和_c上方多了一串地址,再次要通过内存查找这串地址,发现这串地址之后的位置存放一个数字0x14,这个数字就是继承于B的成员到_a的偏移量,通过这个偏移量,对象d便能到d.B::_a。这样就解决了菱形继承成员冗余的问题。

        这里的A叫做虚基类,在对象d中,将虚基类的成员放到一个公共的位置,继承的B、C类需要找到A的成员,通过虚基表中的偏移量进行计算。我们看到虚基表中第一行还空置了4个字节,这块空间存放的也是一个偏移量,它用于寻找d对象中的虚函数指针表。

        实际使用时,尽量不要使用用菱形继承,因为它本质就是C++设计的一个坑!

4.3虚继承+重写问题

        1、如果A类中还存在一个虚函数,那么对象d会在_a后面存放虚函数指针表;

        2、如果A类中存在一个虚函数,并且B、C类均对这个虚函数进行了重写,那么D类中必须对这个函数进行重写,否则将会发生虚函数表重命名的问题。

八、继承和组合的区别

        组合也是一种类复用的手段。

1、组合的使用场景

        适用组合的代码:轮胎和车的关系

class Tire
{
protected:
   string _brand = "Michelin";  // 品牌
   size_t _size = 18;         // 尺寸
};
   
class Car{
protected:
    string _colour = "白色"; // 颜色
    string _num = "xxxxx"; // 车牌号
    Tire _t; // 轮胎
};  

2、继承和组合的区别

public继承是一种is-a的关系,每个子类对象都是一个父类对象,例如“学生”“人”(子类学生,父类人)

组合是一种has-a的关系,B组合了A,每个B对象中都有一个A,例如“车”包含“轮胎”

如果两个类既可以是is-a,又可以是has-a的关系,那么优先使用组合

        继承是一种白盒复用,父类内部的细节对子类可见,破坏了封装。子类将会继承父类的公有和保护成员,一旦父类修改了这些成员的实现,将会影响子类的功能。子类和父类之间的依赖关系强,耦合度高。

        组合是一种黑盒复用,父类内部的细节对子类不可见,子类仅可使用父类的公有成员,只要父类的公有成员的实现细节不变,子类影响较小。父子之间没有很强的依赖关系,耦合度较低。

  • 38
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

蒋灵瑜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值