C++中的父继子承(2)多继承菱形继承问题,多继承指针偏移,继承组合分析+高质量习题扫尾继承多态

🎬 胖咕噜的稞达鸭个人主页
🔥 个人专栏 : 《数据结构《C++初阶高阶》《算法入门》

⛺️技术的杠杆,撬动整个世界!


实现一个不能被继承的类

方法一:父类的构造函数私有化,子类的构成必须调用父类的构造函数,但是父类的构成函数私有化之后,子类看不到就不能调用了,子类就无法实例化出对象。

代码演示:把父类foundaTion的构造函数放进private:中,就实现了父类的构造函数私有化。代码会出现问题。

cpp 复制代码
#include<iostream>
using namespace std;

class foundaTion
{
public:
	void func(){ cout << "foundaTion::func" << endl; }
protected:
	int a = 1;
private:
	foundaTion()
	{ }
};

class branCh :public foundaTion
{
public:
	void func() { cout << "branCh::func" << endl; }
};

int main()
{
	branCh b;
}

方法二:用final修改父类,子类就不能被继承了。class foundaTion final


友元和继承

友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。

派生类可以继承基类,但是基类中的友元声明关系不会被继承下来

cpp 复制代码
class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name;
};
class Student :public Person
{
	//friend void Display(const Person& p, const Student& s);//注释[^1]
protected:
	int _stuNum;
};
void Display(const Person& p, const Student& s)
//Display是Person的友元,Person中其他的函数会被继承下来,但是友元的声明关系不会被继承下来
//解决方法:在student中也继承友元即可,如:注释[^1]
{
	cout << p._name << endl;
	cout << s._name << endl;//派生类中没有继承基类的友元这里会报错
}

int main()
{
	Person p;
	Student s;
	Display(p, s);

	return 0;
}

继承和静态成员

父类定义了一个static静态成员,则是整个继承体系里面只有一个的成员,无论派生出多少个子类,都只有一个static成员。

假如说父类Student中成员有一个name,继承下来的子类中也有name,但是这个子类TeachernameStudent是不一样的,如果在父类中定义了static 的静态成员adddress,那么父类和子类中就会有同一个static的成员address,是同一个成员函数。

不管这个学生作为学生还是作为老师,他的住址都是在雄安新区。

cpp 复制代码
class Student
{
public:
	int _id;
	static string _address;
};

string Student:: _address = "雄安新区";

class Teacher :public Student
{
protected:
	string title;
};

int main()
{
	Student s;
	Teacher t;

	cout << &s._id << endl;
	cout << &t._id << endl;

	cout << &s._address << endl;
	cout << &t._address << endl;

	cout << Student::_address << endl;
	cout << Teacher::_address << endl;

	return 0;
}

继承模型

多继承及其菱形继承问题
是什么:

单继承:一个子类只有一个直接父类时称这个继承关系为单继承;

多继承:一个子类有两个及以上的父类,这个继承关系就是多继承。一个类具有很多个特性,就是多继承,形象一点说,黄瓜既是水果也是蔬菜。那这个关系就是多继承。

菱形继承:

是什么:假设有一个助教,他既是学生也是老师,在继承Person的大类,还分别满足Student和Teacher的特性,而Student和Teacher满足Person的特性。这种会形象构成菱形关系。

怎么样:

  1. 造成二义性问题:如果在主函数Assistant创建一个对象a,去调用_name,那么就会有二义性问题,到底调的是Student还是Teacher_name那么该怎么解决这样的问题?

需要显示指定访问哪个父类的成员可以解决二义性问题,比如:a.Student::_name="小章同学";a.Teacher::_name="小章老师";从学生继承过来或者从老师继承过来指定类域。

  1. 数据冗余,存储了两份数据,Assistant存储了StudentTeacher的两份数据,一定程度上造成了内存浪费数据冗余,Person类中创建的数据愈多,内存就越大。

cpp 复制代码
class Person
{
public:
	string _name;
};
class Student :public Person
{
protected:
	int _num;
};
class Teacher :public Person
{
protected:
	int _id;
};

class Assistant :public Student, public Teacher
{
protected:
	string _majorCourse;
};

int main()
{
	Assistant a;
	//a._name = "Keda";//会报错

	//改正:指定类域
	a.Student::_name = "小章同学";
	a.Teacher::_name = "小章老师";

	cout << sizeof(a) << endl;//136
}

BUT!!!菱形继承是不可避免的,支持多继承就会有菱形继承。

怎么解决:

虚继承:哪个类(公共基类)产生了数据冗余和二义性,继承时用虚继承。只有构成菱形继承才会加virtual

所以:菱形继承尽量不要使用,底层要比我们想象的复杂很多,所以不建议使用菱形继承。

多继承中指针偏移问题:

cpp 复制代码
#include<iostream>
using namespace std;

class Base1 { public:int _b1; };
class Base2 { public:int _b2; };
class Derive : public Base2 ,public Base1 { public:int _d; };

int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;

	Derive* p3 = &d;
	
	return 0;
}

这里引入一道题目来帮助我们理解,

说明p1,p2p3的地址是一样的吗?

多继承中指针的指向规则:

在多继承中,派生类对象的内存会包含多个基类子对象(比如Base1中的_b1,Base2中的_b2)的区域。当用基类指针(Base1* p1Base2* p2)指向派生类对象(d)时,指针会指向对应基类子对象的起始地址;而派生类指针(Derive* p3)会指向派生类对象本身的起始地址。

  1. 分析

在内存中的分布是按照声明的顺序来进行分布的,先继承的就在前面。

对于Base1* p1=&d; p1会指向派生类对象d中Base1子对象的起始地址。

对于Base2* p2=&d;p2指向派生类对象d中Base2子对象的起始地址,由于Base1子对象已经在内存中占用了一定的空间,所以Base1和Base2的起始地址是不同的,因此p1和p2的指向不同。

对于Derive* p3=&dp3指向派生类对象d本身的起始地址,因为Base1是Derive继承的第一个基类,派生类对象的起始地址和第一个基类子对象(Base1子对象)的起始地址是重合的,所以p3和p1指向的地址相同。

所以:p1==p3!=p2

  1. 题目改编:

改编将这道题改一下:class Derive: public Base2,public Base1{int _d;} 其他部分不做变化,这个时候p1==p3!=p2这个结果还是对的吗?

不对!!!一定要注意:先继承的在前面, Derive这个类在继承的时候先继承的是Base2,再继承Base1,所以内存分布是先给Base2分配空间,然后再分配地址给Base1,此时p2和p3指向同一块地址,而不等于p1:p2=p3!=p1

继承和组合

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

组合,是一种has-a的关系,假设stack组合了list,每个stack中都有一个list对象。栈里面有一个链表。

cpp 复制代码
//继承
class stack:public list
{}
cpp 复制代码
//组合
class stack
{
	list _lt;
}

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

白箱:是相对可视性而言的:在继承方式中,父类的内部细节对于子类是可见的。继承一定程度上破坏了父类的封装,父类的改变对于子类有很大的影响。子类和父类间的依赖关系很强,耦合度很高。
对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组合或组合对象来获得,对组合对象要求被组合的对象具有良好定义的窗口,这种复用风格被称为黑箱复用,因为对象的内部细节都是不可见的。对象只能以"黑箱"的形式存在。组合类中没有很强的依赖关系,耦合度很低。优先使用对象组合有助于保持每个类都被封装。

黑盒测试:不了解底层实现,从功能角度测试;

白盒测试:了解底层实现,从代码运行逻辑测试。

低耦合,高内聚。

来做一道题:结合继承和多态两种机制:

要做这道题:

要结合博主的两篇博客:

第一篇:
C++中的父继子承:继承方式实现栈及同名隐藏和函数重载的本质区别, 派生类的4个默认成员函数

第二篇:
多态:(附高频面试题)虚函数重写覆盖,基类析构重写,重载重写隐藏对比,多态原理,虚表探究一文大全

说明下面一道题的打印结果是?

cpp 复制代码
class A
{
public:
    A() :m_iVal(0) { test(); }
    virtual void func() { std::cout << m_iVal << " "; }
    void test() { func(); }
public:
    int m_iVal;
};

class B : public A
{
public:
    B() { test(); }
    virtual void func()
    {
        ++m_iVal;
        std::cout << m_iVal << " ";
    }
};

int main(int argc, char* argv[])
{
    A* p = new B;
    p->test();
    return 0;
}


这道题打印出来为什么是 0 1 2 呢?

解释:

首先new B的时候要调用父类A的构造函数,因为AB要构成继承关系,只有父类和子类都构造好了才可以执行操作。

A类构造 :A类的构造函数中,只有一个成员变量m_iVal被初始化成了0,这个成员变量是Int类型的。然后在构造函数的函数体中执行test()操作。

A类中test()函数体中要执行func()操作,也就是打印出现阶段的m_iVal的值为0;

B类构造 :A类构造好了之后,整体就形成了多态的机制(构成虚函数,AB类的函数名返回值类型及参数列表相同,且父类A的指针p调用虚函数)所以调用test()函数,再去执行func(),继承下来在B类中的m_iVal是0,自增打印之后是1;

然后再分析p->test() ,此时AB类已经构造好了,而且完全形成多态的机制。由于基类的指针p,调用的是B类对象,所以这个test()应该调用的是B类中的,执行test()操作,执行子类的func(),所以打印出来再次自增的m_iVal的值,二次自增最后是2。

总结:构造派生类对象B的时候(new B),会先构造基类,再构造派生类B

执行过程拆解:

  1. 构造基类A:

通过 初始化列表m_iVal(0),将m_iVal初始化为0;构造函数体中调用test(),test()内部调用func(),由于此时再基类的构造阶段,虚函数没有动态绑定,调用基类的func(),打印出0;

  1. 构造派生类B:

B的构造函数要调用test(),此时B已经构造完成了,虚函数动态绑定,调用派生类B中的func(),m_iVal自增为1;

  1. 指针p指向B对象,调用test()时,虚函数动态绑定,再次调用B类的func():m_iVal自增为2。
相关推荐
JAVA学习通4 小时前
Replication(下):事务,一致性与共识
大数据·分布式·算法
蓝色汪洋4 小时前
Completed String easy
算法
铭哥的编程日记4 小时前
贪心算法精选30道编程题 (附有图解和源码)
算法·贪心算法
CoovallyAIHub4 小时前
顶刊新发!上海交大提出PreCM:即插即用的旋转等变卷积,显著提升分割模型鲁棒性
人工智能·算法·计算机视觉
JAVA学习通4 小时前
基本功 | 一文讲清多线程和多线程同步
java·开发语言·多线程
啦啦9117144 小时前
如何理解Java中的并发?
java·开发语言
超级大只老咪4 小时前
哈希表(算法)
java·算法·哈希算法
时时三省4 小时前
【时时三省】(C语言基础)用格式化的方式读写文本文件
c语言
api_180079054604 小时前
异步数据采集实践:用 Python/Node.js 构建高并发淘宝商品 API 调用引擎
大数据·开发语言·数据库·数据挖掘·node.js