C++-继承

继承

  • 继承是使得代码实现最大程度上的复用,是类设计层次的复用

  • 设计一个基础类,将大多数相同属性的放到一个父类里面

  • 继承方式:总共9种关系.父子各自有3种:public protected private

image-20230203211946134

  1. 父类中私有的在子类中不可见不代表没有继承,在子类里面的类外面都不可用.

  2. 子类继承父类的权限,取访问方式和访问限定符小的那个.

  3. 访问限定符不写时,class 默认是私有,struct默认是共有.

切片(赋值兼容规则)

同类型的可以互相赋值,那么不同类型之间如何赋值?相关的可以进行隐士类型转换.

  • 那么父子类如何进行赋值呢?限于公有继承–>切片

拿子类中继承的一部分切割出来赋值给父类,还可以给给父类的指针和引用,指向子类父类公有的部分,子类的私有父类其实还是看不见的.

image-20230203213914200

  • 注意:不存在类型转换,要不然无法实现引用,是语法天然支持的。类型转换是会产生临时变量,临时变量具有常性,引用临时变量前面得加const.

  • 基类指针可以通过强转直接指向子类对象,子类的指针和引用不能调用基类的
    student肯定是一个Person(基类可以调用子类的对象)
    Person不非得是student(子类并不能完全代表Person基类)

  • 为甚么私有继承不支持切割切片呢?

    私有继承父类的公有和保护,在子类中是私有的,如果切片之后,和父类中会存在权限矛盾.将派生类切片为其私有基类没有任何意义。想一想“私人”是什么意思。这意味着外界不应该关心它。允许对私人基地进行切片(铸造)意味着外界会关心。

  • 父类不能给子类(少的给多的给不了)

  • 对象不行,但是指针和引用可以。反正就是一个指针指向,但是会存在会越界的风险,子类指针指向了父类的私有就出现了越界的问题。

#include<iostream>
#include<cstdio>
using namespace std;
class Person
{
public:
	string _name; // 姓名
	string _sex; // 性别
private:
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _No; // 学号
};
void Test2()
{
	//子类对象给给父类对象-切片
	Student s;
	Person p = s;
	Person* p = &s;//指针
	Person& p = s;//引用
	
	Person p1;
	Student s;
	//s = p1;//父类对象不可以给给子类对象
	Person* ptr = &p1;//父类对象的指针可以通过强转的方式赋值给子类指针
	Student* sptr = (Student*)ptr;
	//子类指针可修改父类公有无法修改私有(越界风险,都看不见)
	sptr->_name = "12345";
}

继承-作用域

基类和派生类都有独立的作用域.

当有同名成员时,局部优先原则,就近原则.

  • 怎样先访问外面的?指定作用域。

同理在子类当中存在同名的变量时,先访问自己的。
如果想要访问父类的,指定父类的就行。

隐藏

子类会隐藏父类的同名成员(成员变量,成员函数).

父类和子类中相同名字的函数是不构成函数重载的,因为在同一作用域才构函数重载,成员函数名相同就构成隐藏,和参数的区别无关。

image-20230204094300973

隐藏之后就无法直接调用父类继承来的内个同名函数,被隐藏的,想调得指定基类作用域

例如:反向迭代器是对正向迭代器的一层封装,在lsit类中typedef 之后会就近找同名,造成bug.可以指定作用域名或者根据向上寻找的特点调换位置.

继承-成员函数

派生类的默认成员函数(总共6个,有两个不重要):构造函数,析构函数,拷贝构造函数,赋值函数operator=(4个重要的)

  • 我们不写,默认生成的派生的构造和析构?

父类继承的(调用父类的默认构造和析构处理) 和自己的(内置类型和自定义类型成员)

  • 我们不写,默认生成的派生的拷贝构造和赋值

从父类继承的(调用父类的拷贝构造和赋值) 和自己的(拷贝构造和赋值)

image-20230204103016081

  • 小结:继承来的调用父类处理,自己的按照普通类基本规则处理.
  • 什么时候需要自己写?
    • 父类没有默认构造时,需要我们自己显示写构造
    • 如果子类有开辟的资源需要释放,就需要自己显示写析构函数.
    • 如果子类存在浅拷贝,就需要自己实现拷贝构造和赋值解决浅拷贝的问题
  • 怎么写?
    • 父类的成员调用父类的拷贝构造和赋值

析构函数的名字会统一为destructer,名字相同,
子类的析构函数和父类的就构成隐藏,把父类的给隐藏了。

class Person
{
public:
	Person(const string name)
	{
		_name = name;
	}
	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
	Person& operator=(const Person& p)
	{
		cout << "Person& operator=(const Person& p)" << endl;

		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
	~Person()
	{}
public:
	string _name=""; // 姓名
};
class Student : public Person
{
public:
	//当父类没有默认的拷贝构造时:
	Student(const char* name="张三", int No=10)
		:Person(name)
		, _No(No)
	{}
	Student(const Student& s)
		:Person(s)//切片
		,_No(s._No)
	{}
	//当父类没有默认的赋值构造时:
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			//不指定会因为就近原则无线递归栈溢出
			Person::operator=(s);//切片
			_No = s._No;
		}
		return *this;
	}
	~Student()
	{
		//只能指定作用域才能调用析构函数?
		//析构函数的名字会统一为destructer,名字相同(为什么?--多态)
		//子类的析构函数和父类的就构成隐藏,把父类的给隐藏了。
		Person::~Person();
		//delete[] _ptr;
	}
public:
	int _No; // 学号
	//int* _ptr=new int[10];
};
void Test()
{
	Student s;
	Student s1(s);
	Student s2 = s;
}
  • 但是会自动调用两次析构函数(如果存在空间释放两次造成崩溃)为啥呢?

子类的析构函数不需要显示调用一下父类的。子类析构函数结束时,会自动调用父类的析构函数。
栈里面的变量是先进后出,后初始化构造的先析构。也就是子类先析构
如果自己显示调用,会先析构父类的,不符合先后关系。

继承和友元

友元关系是不能够继承的,不能够访问子类的私有成员。

继承和静态成员

统计一个父类到底派生了多少个子类,采用静态变量加加的方式。无论派生多少个子类,这些类指向的静态变量是同一个。

image-20230204112001423

class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
void TestPerson()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;

	cout << &s4._count << endl;
	cout << &s3._count << endl;
	cout << &s2._count << endl;
}

菱形继承

单继承:一个子类只有一个直接父类时的继承关系

多继承:有两个或者以上的直接父类的继承关系,会出现数据的冗余二义性

菱形继承是多继承的一种特殊情况:

image-20230204112617726

  • 指定类还能确定对象。但是开空间就是二倍开,那如何解决这种数据冗余呢?-虚继承
class Person
{
public:
	string _name; // 姓名
	int a[10000];
};
class Student : public Person
{
public:
	int _num; //学号
};
class Teacher : public Person
{
public:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
public:
	string _majorCourse; // 主修课程
};
void Test()
{
	Assistant a;
	//a._name = "张三";//"Assistant::_name" 不明确
	a.Student::_name = "张三";
	a.Teacher::_name = "李四";
	//如果存在开辟一个大数组,就会存在数据冗余
}

虚继承使用:

image-20230204114001969

虚继承 virtual public

虚继承设置在距离二义性和数据冗余的基类的第一级子类上.

image-20230204124748540

  • 不是虚继承时
    在内存的存储中,先继承的类的成员放在前面储存。
class D : public B, public C//先B后C

image-20230204114157148

image-20230204115212853

  • 是虚继承时:
    • 将冗余的变量放到一个公共的地方,也就是最后的地方

image-20230204115024537

但是会多出现不认识的东西:在内存中输入之后得到16进制数字

image-20230204115634413

就是B和C公共A成员的偏移量也及时相对距离。(小端存储,低地址存小值)

A叫虚基类,找虚继类的表叫虚基表.在D中,A放在一个公共的位置,那么B要找A,就需要通过虚基表中的偏移量进行计算.

image-20230204122002127

  • 场景
B b=d;
C c=d;
//发生切片时,切片的过程中,_a已经被切除,但是如果要在子类中通过B找到A,就需要这个偏移量的表,虚基表找到_a.

B* pb=&d;
pb->_b=20;//b类找到,很容易
pb->_a=10;//通过b找到就需要相对a偏移量找到a

image-20230204120625611

继承-组合

  • 继承

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用

(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。

继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关

系很强,耦合度高。

// Car和BMW Car和Benz构成is-a的关系
class Car
{
protected:
	string _colour = "白色"; // 颜色
	string _num = "ABIT00"; // 车牌号
};
class BMW : public Car {
public:
	void Drive() { cout << "好开" << endl; }
};

class Benz : public Car {
public:
	void Drive() { cout << "舒适" << endl; }
};
  • 组合

组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对

象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),

因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,

耦合度低。优先使用对象组合有助于你保持每个类被封装。

// Tire和Car构成has-a的关系
 class Tire{
 protected:
 string _brand = "Miche"; // 品牌
 size_t _size = 17; // 尺寸
 
 };
 
 class Car{
 protected:
 string _colour = "白色"; // 颜色
 string _num = "ABIT00"; // 车牌号
 Tire _t; // 轮胎
 };

优先使用对象组合,而不是类继承 。

实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

类之间/模块之间的关系最好是低耦合高内聚.

要点陈列:
  • 继承的功能(代码复用的体现):继承可以使用现有类的所有功能,并在无需重新编写原来类的情况下对这些功能进行扩展。

  • 子类不一定要比基类大,有可能子类只是改写父类的方法而已,并没有增加其自身的数据成员,则大小一样.

  • 狗是一种动物,体现了继承的思想,属于is-a的关系。

  • 访问权限在类内部,继承权限在类外部。

  • 派生类在继承基类时,可以不指定继承方式,默认是private.

  • class 声明的类默认是private

  • 菱形继承(相似就行,都会出现数据冗余和二义性)接继承冗余类的地方,是内个虚继承的地方。

  • C++缺陷:没有垃圾回收器,多继承

  • 继承可以访问基类的public 和protected.组合只可以访问基类的public
    继承的关联关系更强,组合的自由性更高。低耦合高内聚。
    多态是建立在继承的基础之上的。

  • 当构成子类对于父类函数的重定义(隐藏)后,调用函数必须按照子类的来,要不然就会编译报错

  • 子类构造函数的定义又是需要参考基类构造函数

  • 当父类没有默认构造函数时,在派生构造函数初始化列表的位置必须显式调用基类构造函数.

  • 子类成员变量中不包含静态变量,静态变量不属于任何类,属于整个继承体系

  • 基类指针可以直接指向子类对象

  • 基类不能直接给给子类对象,因为类型并不完全

  • p1 p2都是父类,但是在子类的内存模型当中位置不同,所以p1p2指向的子类的位置不相同,p1!=p由于p1对象是第一个被继承的父类类型,所有的地址与子类对象的地址.先继承谁的,谁就在内存中排在前面。

  • 子类实例化对象,由于继承的有父类。所以会先构造父类,然后在构造子类,析构顺序完全按照构造的相反顺序进行析构.

  • 菱形继承中子类对象不能直接访问最顶层基类B中继承下来的b成员,因为在D对象中,b有两份,一份是从C1中继承的,一份是从C2中继承的,直接通过D的对象访问b会存在二义性问题,在访问时候,可以加类名::b,来告诉编译器想要访问C1还是C2中继承下来的b。

  • 子类对象可以传给父类,切片切割。各种子类的返回值类型都能够赋值给父类的类型.运算符重载不能够改变内置类型。

  • 如果是虚继承,公有的部分存储时候放在最后,但是调用声明的是确实最前面的.因为他属于公共部分,既不属于B也不属于C.

  • 基类不能直接给给子类对象,因为类型并不完全

  • p1 p2都是父类,但是在子类的内存模型当中位置不同,所以p1p2指向的子类的位置不相同,p1!=p由于p1对象是第一个被继承的父类类型,所有的地址与子类对象的地址.先继承谁的,谁就在内存中排在前面。

  • 子类实例化对象,由于继承的有父类。所以会先构造父类,然后在构造子类,析构顺序完全按照构造的相反顺序进行析构.

  • 菱形继承中子类对象不能直接访问最顶层基类B中继承下来的b成员,因为在D对象中,b有两份,一份是从C1中继承的,一份是从C2中继承的,直接通过D的对象访问b会存在二义性问题,在访问时候,可以加类名::b,来告诉编译器想要访问C1还是C2中继承下来的b。

  • 子类对象可以传给父类,切片切割。各种子类的返回值类型都能够赋值给父类的类型.运算符重载不能够改变内置类型。

  • 如果是虚继承,公有的部分存储时候放在最后,但是调用声明的是确实最前面的.因为他属于公共部分,既不属于B也不属于C.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值