【C++】继承(下)

个人主页~

继承(上)~


继承

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

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

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

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

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

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

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

需要值得注意的是,构造时要先父后子,析构时要先子后父

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

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

cpp 复制代码
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挨个析构,先子后父

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

五、继承与友元

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

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

cpp 复制代码
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成员实例

cpp 复制代码
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、菱形继承

继承分为单继承多继承

单继承:一个子类只有一个直接父类

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

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

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

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

cpp 复制代码
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;
}

继承关系如下图

这里可以看到,间接访问的方式就是指定访问哪个父类成员,这样虽然可以解决二义性的问题,但数据冗余仍然存在

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

cpp 复制代码
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、菱形虚拟继承

cpp 复制代码
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对象

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

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

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

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


今日分享就到这了~

相关推荐
MikelSun5 分钟前
梳理一下C语言中的格式说明符
c语言·开发语言·c++·单片机·物联网·算法
茶卡盐佑星_18 分钟前
window.onload什么时候执行
开发语言·前端·javascript
加蓓努力我先飞19 分钟前
WebAPI编程(第三天,第四天)
开发语言·前端·javascript
@haihi25 分钟前
Qt中常用类和函数解释
开发语言·c++·qt
Pandaconda27 分钟前
【计算机网络 - 基础问题】每日 3 题(二十八)
开发语言·网络·经验分享·笔记·后端·计算机网络·面试
初级代码游戏28 分钟前
C# winforms DataGridView 隐藏行 解决“与货币管理器的位置关联的行不能设置为不可见”
开发语言·c#·datagridview·隐藏行
z2014z31 分钟前
系统架构设计师教程 第11章 11.1 信息物理系统技术概述 笔记
笔记·系统架构
fieldsss34 分钟前
JAVA基础语法 day07
java·开发语言
芜婳36 分钟前
JVM 内存模型
java·开发语言·jvm
夏河始溢37 分钟前
一六九、go使用泛型封装一个可以应用于任何字段的模糊匹配
开发语言·后端·golang