C++中的父继子承(2)多继承菱形继承问题,多继承指针偏移,继承组合分析+高质量习题扫尾继承多态
🎬 胖咕噜的稞达鸭:个人主页
实现一个不能被继承的类
方法一:父类的构造函数私有化,子类的构成必须调用父类的构造函数,但是父类的构成函数私有化之后,子类看不到就不能调用了,子类就无法实例化出对象。
代码演示:把父类foundaTion的构造函数放进private:中,就实现了父类的构造函数私有化。代码会出现问题。
#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


友元和继承
友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。
派生类可以继承基类,但是基类中的友元声明关系不会被继承下来
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
,但是这个子类Teacher
的name
跟Student
是不一样的,如果在父类中定义了static
的静态成员adddress
,那么父类和子类中就会有同一个static
的成员address
,是同一个成员函数。
不管这个学生作为学生还是作为老师,他的住址都是在雄安新区。
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的特性。这种会形象构成菱形关系。
怎么样:
-
造成二义性问题:如果在主函数
Assistant
创建一个对象a
,去调用_name
,那么就会有二义性问题,到底调的是Student
还是Teacher
的_name
,那么该怎么解决这样的问题?
需要显示指定访问哪个父类的成员可以解决二义性问题,比如:a.Student::_name="小章同学";a.Teacher::_name="小章老师"
;从学生继承过来或者从老师继承过来指定类域。
-
数据冗余,存储了两份数据,
Assistant
存储了Student
和Teacher
的两份数据,一定程度上造成了内存浪费数据冗余,Person
类中创建的数据愈多,内存就越大。
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
所以:菱形继承尽量不要使用,底层要比我们想象的复杂很多,所以不建议使用菱形继承。
多继承中指针偏移问题:
#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* p1
,Base2* p2
)指向派生类对象(d)
时,指针会指向对应基类子对象的起始地址;而派生类指针(Derive* p3)
会指向派生类对象本身的起始地址。
- 分析
在内存中的分布是按照声明的顺序来进行分布的,先继承的就在前面。
对于Base1* p1=&d
; p1会指向派生类对象d中Base1子对象的起始地址。
对于Base2* p2=&d
;p2指向派生类对象d中Base2子对象的起始地址,由于Base1子对象已经在内存中占用了一定的空间,所以Base1和Base2的起始地址是不同的,因此p1和p2的指向不同。
对于Derive* p3=&d
,p3指向派生类对象d本身的起始地址,因为Base1是Derive继承的第一个基类,派生类对象的起始地址和第一个基类子对象(Base1子对象)的起始地址是重合的,所以p3和p1指向的地址相同。
所以:p1==p3!=p2
- 题目改编:
改编将这道题改一下:
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对象。栈里面有一个链表。
//继承
class stack:public list
{}
//组合
class stack
{list _lt;
}
继承允许根据父类的实现来定义子类的实现,这种通过生成子类的复用通常被称为白箱复用。
白箱:是相对可视性而言的:在继承方式中,父类的内部细节对于子类是可见的。继承一定程度上破坏了父类的封装,父类的改变对于子类有很大的影响。子类和父类间的依赖关系很强,耦合度很高。
对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组合或组合对象来获得,对组合对象要求被组合的对象具有良好定义的窗口,这种复用风格被称为黑箱复用,因为对象的内部细节都是不可见的。对象只能以“黑箱”的形式存在。组合类中没有很强的依赖关系,耦合度很低。优先使用对象组合有助于保持每个类都被封装。
黑盒测试:不了解底层实现,从功能角度测试;
白盒测试:了解底层实现,从代码运行逻辑测试。
低耦合,高内聚。
来做一道题:结合继承和多态两种机制:
要做这道题:
要结合博主的两篇博客:
第一篇:
C++中的父继子承:继承方式实现栈及同名隐藏和函数重载的本质区别, 派生类的4个默认成员函数
第二篇:
多态:(附高频面试题)虚函数重写覆盖,基类析构重写,重载重写隐藏对比,多态原理,虚表探究一文大全
说明下面一道题的打印结果是?
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
执行过程拆解:
- 构造基类A:
通过 初始化列表m_iVal(0)
,将m_iVal
初始化为0;构造函数体中调用test(
),test()
内部调用func()
,由于此时再基类的构造阶段,虚函数没有动态绑定,调用基类的func()
,打印出0;
- 构造派生类B:
B的构造函数要调用test()
,此时B已经构造完成了,虚函数动态绑定,调用派生类B中的func()
,m_iVa
l自增为1;
- 指针p指向B对象,调用
test()
时,虚函数动态绑定,再次调用B类的func():m_iVal
自增为2。