首页 > 编程语言 >【C++】继承(下)

【C++】继承(下)

时间:2024-09-29 14:54:38浏览次数:8  
标签:继承 基类 C++ class int 派生类 public

在这里插入图片描述

个人主页~

继承(上)~


继承

四、派生类的默认成员函数

派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用

派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

派生类的operator=必须要调用基类的operator=完成基类的复制

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员,因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序

派生类对象初始化先调用基类构造再调派生类构造

派生类对象析构清理先调用派生类析构再调基类的析构

需要值得注意的是,构造时要先父后子,析构时要先子后父
对于构造来说,因为子类是继承来的,所以一定是先父后子,对于析构来说,在子类中可能会有访问父类成员的成员在,当父类先析构了,再析构子类就会存在风险,所以析构要先子后父

这个程序中的打印信息可以帮助我们确认构造的过程

class Person
{
public:
    Person(const char* name = "little_monster")
        : _name(name)
    {
        cout << "Person()" << endl;
    }

    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()
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name; // 姓名
};

class Student : public Person
{
public:
    Student(const char* name, int num)
        : Person(name)
        , _num(num)
    {
        cout << "Student()" << endl;
    }

    Student(const Student& s)
        : Person(s)
        , _num(s._num)
    {
        cout << "Student(const Student& s)" << endl;
    }

    Student& operator = (const Student& s)
    {
        cout << "Student& operator= (const Student& s)" << endl;
        if (this != &s)
        {
            Person::operator =(s);
            _num = s._num;
        }
        return *this;
    }

    ~Student()
    {
        cout << "~Student()" << endl;
    }
protected:
    int _num; //学号
};

void Test()
{
    Student s1("little", 18);
    Student s2(s1);
    Student s3("monster", 17);
    s1 = s3;
}

在这里插入图片描述
分析:

当构造s1时先构造父类Person然后构造Student

拷贝构造s2是先调用Person的拷贝构造再调用Student的拷贝构造

然后构造s3与构造s1相同,先构造父类Person然后构造Student

=也是先调用父类Person然后调用子类Student

最后s3、s2、s1挨个析构,先子后父

并且父类析构函数不需要显示调用,子类析构函数结束时会自动调用父类析构

五、继承与友元

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

记住一句话:父亲的朋友不是我的朋友而是我的叔叔

class B;
class A
{
public:
	friend void C(const A& a, const B& b);

protected:
	int _a;
};

class B : public A
{
protected:
	int _b;
};

void C(const A& a,const B& b)
{
	cout << a._a << endl;
	cout << b._b << endl;
}

void test()
{
	A a;
	B b;
	C(a, b);
}

在这里插入图片描述

可以看出C是基类A的友元函数,而C是无法访问派生类B的内容的

六、继承与静态成员

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

class A 
{
protected:
	int _a;
public:
	static int _d;
};

int A :: _d = 1;

class B : public A
{
protected:
	int _b;
};

int main()
{
	A a;
	A::_d++;
	B b;
	B::_d++;
	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

七、复杂的菱形继承以及菱形虚拟继承

1、菱形继承

继承分为单继承多继承

单继承:一个子类只有一个直接父类
多继承:一个子类有两个及以上直接父类

有了多继承的继承方式,就会产生一种继承方式叫做菱形继承,这是多继承的一种特殊形式

菱形继承:菱形继承是指一个派生类(孙子类)同时继承自两个直接或间接基类(子类),而这两个基类又都继承自同一个更基础的基类(父类),由于这种继承关系在图形上类似于菱形,因此得名菱形继承

菱形继承会出现一个问题:菱形继承有数据冗余和二义性的问题,也就是说,按照上面的说法,孙子类的对象中会有两份父类对象,多了一份即数据冗余,访问父类时无法确定访问的是两个子类对象的哪一个

class A
{
public:
	int _a;
};

class B : public A
{
protected:
	int _b;
};

class C : public A
{
protected:
	int _c;
};

class D : public B, public C //多继承的方式
{
protected:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 0;
	d.C::_a = 0;

	d._a;
	return 0;
}

继承关系如下图
在这里插入图片描述
在这里插入图片描述
这里可以看到,间接访问的方式就是指定访问哪个父类成员,这样虽然可以解决二义性的问题,但数据冗余仍然存在

这段代码跟上面那段不一样!

class A
{
public:
	int _a;
};

class B : public A
{
public:
	int _b;
};

class C : public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

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

这张内存图可以清楚地看到内存分布的现象
在这里插入图片描述
首先,内存是分区的,挨在一起的是一个类的实例化成员,我们看到前两行是B类中的成员_a,_b,中间两行是C类中的成员_a,_c,由于D类没有实例化_a,所以只有一个_d,说到这里我们发现_a有两个,且存储在不同的地方,这就是内存冗余,解决办法之一就是虚拟继承

2、菱形虚拟继承

class A
{
public:
	int _a;
};

class B : virtual public A
{
public:
	int _b;
};

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.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

在这里插入图片描述
在这里插入图片描述
通过上面两张调试图可以分析出来:虚拟继承后的父类被孙子类调用时都是同一个,存放在同一块内存当中
在这里插入图片描述
当孙子类调用子类对象时,如果父类成员被实例化,那么存储数据的上方位置会有一个指针,通过解析我们发现这个指针指向的位置存储着一个数据,而这个数据正是存放父类成员的位置地址与这个指针的位置地址的差,也就是说,这个指针存储的是到父类成员地址的距离(偏移量),通过解析这些数据,可以得到父类成员的值

八、继承的总结与反思

建议不要使用菱形继承,难搞

继承和组合

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

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

优先使用对象组合,类继承次之,因为类继承的耦合性太强,我们追求低耦合、高内聚,也就是对象之间的联系少,对象内的成员联系紧密,对象组合比起类继承的耦合性低,其中一个改变对另一个的影响较小

继承允许你根据基类的实现来定义派生类的实现,这种通过生成派生类的复用通常被称为白箱复用,术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 ,继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响,派生类和基类间的依赖关系很强,耦合度高

对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或组合对象来获得,对象组合要求被组合的对象具有良好定义的接口,这种复用风格被称为黑箱复用,因为对象的内部细节是不可见的,对象只以“黑箱”的形式出现,组合类之间没有很强的依赖关系,耦合度低,优先使用对象组合有助于保持每个类被封装

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


今日分享就到这了~

在这里插入图片描述

标签:继承,基类,C++,class,int,派生类,public
From: https://blog.csdn.net/s_little_monster/article/details/142144544

相关文章

  • C++ -引用-详解
    博客主页:【夜泉_ly】本文专栏:【C++】欢迎点赞......
  • 分享C++程序员面试八股文(十三)
    以下是C++常见八股文(十三):一、C++中的命名空间和模块的高级用法(AdvancedUsageofNamespacesandModules)解释命名空间别名和嵌套命名空间的作用及使用场景命名空间别名:作用:命名空间别名可以为一个较长或复杂的命名空间提供一个更简洁的名称,提高代码的可读性和可......
  • C++-练习-46
    题目:许多州的彩票发行机构都使用如下所示程序的简单彩票的变体。在这些玩法中,玩家从一组被称为域号码的号码中选择几个。列如,可以从域号码1~47中选择5个号码;还可以从第二个区间(如1~27)选择一个号码(称为特选号码)。要赢得头奖,必须正确猜中所有的号码。中头奖的几率是选择所有域号......
  • C++ 静态顺序表和动态顺序表
    对比静态顺序表与动态顺序表特性静态顺序表动态顺序表大小固定动态内存管理简单复杂随机访问快速快速插入/删除效率较低较低(需移动元素)扩展能力不可扩展可扩展C++静态顺序表概述定义:静态顺序表是一种线性表的实现方式,采用一段连续的内存空间存储数据元素,具有固定的大小。在......
  • 南沙C++信奥老师解一本通题 1221:分成互质组
    ​ 【题目描述】给定n个正整数,将它们分组,使得每组中任意两个数互质。至少要分成多少个组?【输入】第一行是一个正整数n。1≤n≤10。第二行是n个不大于10000的正整数。【输出】一个正整数,即最少需要的组数。【输入样例】6142033117143175【输出样例】3......
  • C++:模板初级
    一.泛型编程。1.1如何实现一个交换函数呢?voidSwap(int&left,int&right){ inttemp=left; left=right; right=temp;}voidSwap(double&left,double&right){ doubletemp=left; left=right; right=temp;}voidSwap(char&left,......
  • C++ 学习,标准库
    C++标准库是C++语言的重要组成部分,它提供了一系列的类、函数和模板,使得开发者能够更加高效地进行编程。C++标准库包括一组头文件,头文件提供了各种功能和工具,涵盖了输入输出、容器、算法、多线程、正则表达式等。C++标准库可以分为两部分:标准函数库: 由通用的、独立的、......
  • 【C++掌中宝】用最少的话让你全方位理解内联函数
    文章目录引言1.什么是内联函数2.工作原理3.内联函数的编程风格4.使用限制5.内联函数与宏的比较6.优缺点7.何时使用内联函数8.补充9.总结结语引言在C++编程中,函数的调用开销是程序运行效率的一个重要影响因素。为了解决频繁调用函数时的性能问题,C++提供了内......
  • 【C++标准模版库】map和set的介绍及使用
    map和set一.序列式容器和关联式容器二.set系列的使用1.set和multiset参考文档2.set类的介绍3.set的构造和迭代器4.set的增删查5.insert和迭代器遍历使用6.find和erase的使用7.erase迭代器失效问题8.lower_bound与upper_bound9.multiset和set的差异10.set解决:leetcode例题......
  • C/C++语言基础--C++面向对象之继承、继承限制、多继承、拷贝继承等知识讲解
    本专栏目的更新C/C++的基础语法,包括C++的一些新特性前言通过前面几节课,我们学习了抽象、封装相关的概念,接下来我们将讲解继承;C语言后面也会继续更新知识点,如内联汇编;本人现在正在写一个C语言的图书管理系统,1000多行代码,包含之前所学的所有知识点,包括链表和顺序表等数据......