🎈个人主页:🎈 :✨✨✨初阶牛✨✨✨
🐻强烈推荐优质专栏: 🍔🍟🌯C++的世界(持续更新中)
🐻推荐专栏1: 🍔🍟🌯C语言初阶
🐻推荐专栏2: 🍔🍟🌯C语言进阶
🔑个人信条: 🌵知行合一
🍉本篇简介:>:讲解C++中有关继承中多继承,菱形继承.
金句分享:
✨如果事事都如意,那就不叫生活了.✨
目录
- 前言
- 一、隐藏
- 二、默认成员函数
- 三、多继承
- (1)菱形继承
- 虚继承原理图:
- 四、继承中的静态成员变量
- 一、结语
前言
C++
中多继承是指一个子类可以从多个父类中继承属性和行为.
其中涉及菱形继承和虚拟继承,显得复杂很多.
需要理解原理.
一、隐藏
继承体系中,子类和父亲类是两个不同的作用域,即子类和父类分别有自己的作用域.
<<同名成员变量>>
由于是两个不同的作用域,所以语法上是在子类和父类中可以定义同名的成员变量的.
但是,在访问子类时,子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问子类自己的成员,这种情况叫隐藏,也叫重定义。
如果不想访问子类的同名成员,可以在子类成员函数中显示调用父类的成员.
显示调用格式: 基类: 基类成员
出现相同的名称的变量终究是容易让人混乱的,还是不建议在子类和父类中定义同名成员变量.
<<同名成员函数>>
在基类和父类中定义同名的成员函数.
问题:下面的void Print(int a)
函数和void Print(double b)
函数构成函数重载吗?
class People{
public:
void Print(){
cout << "People()" << endl;
cout << "num=" << _num << endl;
}
protected:
int _num=30;
};
class Student :public People{
public:
void Print(int a){
cout << "Student()" << endl;
cout << "num=" << _num << endl;
cout << "num=" << People::_num << endl;
}
protected:
int _num = 66;
};
答案:
不构成,因为函数重载是指在同一个作用域下的同名函数,这里是构成隐藏,并不能直接调用基类的成员函数.
子类和父类中的成员函数只要函数名相同,就会构成隐藏.
示例:
class People{
public:
void Print(){
cout << "People()" << endl;
cout << "num=" << _num << endl;
}
protected:
int _num=30;
};
class Student :public People{
public:
void Print(double b){
cout << "Student()" << endl;
cout << "num=" << _num << endl;
cout << "num=" << People::_num << endl;
}
protected:
int _num = 66;
};
int main()
{
People p1; //父类对象
Student s1; //子类对象
s1.Print(1); //调用子类的函数
cout << endl;
s1.Print(); //会报错,因为基类和父类不构成函数重载,而子类的函数需要传参
cout << endl;
s1.People::Print(); //显示调用父类的函数
cout << endl;
p1.Print(); //父类对象调用父类的函数
cout << endl;
return 0;
}
二、默认成员函数
还记得C++
的六大天选之子吗?
那在派生类中,这几个成员函数是如何生成的呢?
(1) 构造函数:
派生类的构造函数必须调用基类的构造函数,利用基类的构造函数去初始化基类的部分.并且是先调用基类的构造之后,再去构造派生类的成员.
如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用(即带参的构造).
(2)析构函数:
在进行派生类析构时,应当先析构派生类的成员,再析构基类的成员.
为了保证这个顺序,派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
(3拷贝构造与赋值运算符重载:
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
派生类的operator=
必须要调用基类的operator=
完成基类的复制。
重点:
派生类对象初始化先调用基类构造再调派生类构造。
派生类对象析构清理先调用派生类析构再调基类的析构。
测试:
class People
{
public:
People(string name,int age=13)
:_name(name)
,_age(age)
{
cout << "People(string name,int age=13) " << endl;
}
People(const People& p1) //拷贝构造
:_name(p1._name)
,_age(p1._age)
{
cout << "People(People& p1)" << endl;
}
People& operator =(const People& p1) //赋值运算符重载
{
cout << "People& operator =(const People& p1)" << endl;
_name = p1._name;
_age = p1._age;
return *this;
}
~People()
{
cout << "~People" << endl;
}
protected:
string _name;
int _age;
};
class Student : public People
{
public:
Student(const char* name="王也", int num=20214567)
: People(name)
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s) //拷贝构造
: People(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)
{
People::operator =(s);
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num;
};
int main()
{
Student s1("初阶牛", 20); //创建一个Student对象
cout << endl;
Student s2(s1); //使用拷贝构造
cout << endl;
Student s3;
s3 = s2; //使用赋值运算符重载
cout << endl;
return 0;
}
运行结果:
三、多继承
单继承:
C++
中的单继承是指一个子类只能继承一个父类的特性。单继承的好处在于它可以保证类之间的关系更加清晰和简单,并且可以减少代码的冗余和复杂度。
多继承:
在C++中,多继承是指当一个类继承自多个父类时的继承方式。多继承可以让一个类拥有多个不同父类的成员函数和成员变量,提高代码复用性。同时,多继承也会带来一些问题和挑战,例如菱形继承问题,需要合理使用。
(1)菱形继承
多继承中的特殊情况:菱形继承
由于Student
和Teacher
类都继承了People
类,
而MY
类又同时继承了Student
和Teacher
,则MY
类里面有两份People
类.
菱形继承的问题:
一方面造成了数据冗余,另一方面也造成的访问数据的二义性,在MY
类在访问People
类的成员时,会产生二义性,不知道是访问哪一份.
示例:
class People
{
public:
int _a;
};
class Student : public People
{
public:
int _b;
};
class Teacher : public People
{
public:
int _c;
};
class MY : public Student ,public Teacher
{
public:
int _d;
};
int main()
{
MY m;
m.Student::_a = 7; //不可直接访问,要指定显示访问
m.Teacher::_a = 5;
m.Student::_b=12;
m.Teacher::_c=15;
m._d = 10;
return 0;
}
可以通过内存窗口进行观察,在内存窗口输入:&m
观察m
的存储结构.
不难发现,m
中,People
类有两个,也就意味着m
对象里面有两个_a
,这也就导致了数据冗余,和数据访问的二义性.
那菱形继承的这两个问题该如何解决呢?
猜测祖师爷在这里也没想到多继承可能会引发菱形继承,为了解决这个问题,祖师爷应该也很头痛.
解决方法:虚继承
虚继承
的用法,在中间的类(这里可能描述的不清楚)继承前面增加 关键字virtual
class People
{
public:
int _a;
};
class Student :virtual public People //加上关键字virtual 成为虚继承
{
public:
int _b;
};
class Teacher :virtual public People //加上关键字virtual 成为虚继承
{
public:
int _c;
};
class MY : public Student, public Teacher
{
public:
int _d;
};
int main()
{
MY m;
m.Student::_a = 7;
m.Teacher::_a = 5;
m.Student::_b = 12;
m.Teacher::_c = 15;
m._d = 10;
return 0;
}
虚拟继承
:解决数据冗余和二义性的原理.
其实在虚继承中,MY
对象中将People
放到的了对象组成的最下面,这个People
同时属于Student
和Teacher
,那么Student
和Teacher
如何去找到公共的People
呢?这里是通过了Student
和Teacher
的两个指针(虚基表指针),分别指向两张张表,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面公共People
虚基表:
偏移量的作用: 找到公共的People
虚继承原理图:
会不会小伙伴有疑问:为啥是非要搞一个虚基表出来,直接在对象中存放People
的地址或者偏移量不好吗?,一定要搞一个夹在中间的虚基表干嘛?
如果细心的小伙伴就会发现,虚基表的第一个地址并没有存放东西,而是第二个位置存放了偏移量,至于第一个位置不存放东西,这里暂时不解释.
四、继承中的静态成员变量
在所有派生类和基类中,静态成员变量始终为一份,所有类公用.
我们可以通过静态成员变量实现求创建的派生类+基类对象的个数.
class People
{
public:
People() { ++_count; }
People(const People& p1){ ++_count; }
~People() { --_count; }
static int _count;
};
int People::_count = 0;
class Student : public People
{
protected:
int _num;
};
class Teacher : public People
{
protected:
string_subject;
};
void test1(People p)
{
cout << "cout2=" << People::_count << endl;
Student s1;
cout << "cout3=" << People::_count << endl;
}
void test2(People& p)
{
cout << "cout4=" << People::_count << endl;
Student s1;
cout << "cout5=" << People::_count << endl;
}
int main()
{
People p1;
Student s1;
Student s2;
cout << "cout1=" << People::_count << endl;
test1(s1);
test2(s1);
Teacher t1;
cout << "cout6=" << People::_count << endl;
return 0;
}
运行结果:
cout1=3
cout2=4
cout3=5
cout4=3
cout5=4
cout6=4
一、结语
对于多继承,菱形继承,可能还有些地方讲解的不是很清楚,牛牛已经很尽力的去讲解了.
C++
语法十分复杂,多继承,菱形继承就是一种体现.
由于多继承和菱形继承的复杂性,后来的很多语言就跳过了这个大坑,比如隔壁Java
.
C++
开发中我们一般也不建议设计出多继承,特别是菱形继承,如果设计出奇奇怪怪的菱形继承,我觉得和你在一起工作的小伙伴可能会把你吃掉!
继承
的耦合度很高!
继承允许你根据基类的实现来定义派生类的实现。在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言,即对基类的细节暴露出来了.
组和
的耦合度相对就比较低了!
对象组合是类继承之外的另一种复用选择,他可以使用组合类的接口,但不暴露组合类的内部细节.
这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。
具体情况选择适合的复用方式,一般优先选择组合
,因为组合的耦合度低,代码可维护性高.