C++继承与派生——(8)多继承
归纳编程学习的感悟,
记录奋斗路上的点滴,
希望能帮到一样刻苦的你!
如有不足欢迎指正!
共同学习交流!
🌎欢迎各位→点赞 👍+ 收藏? + 留言?📝
苦难和幸福一样,都是生命盛开的花朵!
一起加油!
目录
一、前言:
????????根据派生类继承基类的个数,将继承分为单继承和多继承。之前我们主要以单继承为例学习了派生类的定义以及使用中应注意的问题。多继承可以看成是单继承的组合,它们有很多相似的特征。
二、多继承的定义:
????????多继承的基类不只一个,而是有多个,派生类与每个基类之间的关系可以看作是一个单继承。多继承的定义格式如下:
class <派生类名>:<继承方式><基类名 1>,..,<继承方式><基类名?n>
????????????????????????????????{
????????????????????????????????????????????????<派生类新定义的成员>
????????????????????????????????}
三、多继承的构造函数以及调用顺序:
????????在多继承方式下,派生类构造函数要负责为每一个基类构造函数传入初始化的参数,派生类的构造函数格式如下:
class ?<派生类名>(<总参数表>):<基类名 1>(<参数表 1>),...,<基类名 n>(<参数表 n>)
????????????????????????????????????????{
派生类数据成员的初始化
????????????????????????????????????????}
????????其中,<总参数表>必须包含完成所有基类初始化所需的参数。
????????由于存在多个基类,多继承规定派生类包含多个基类时,构造函数的调用顺序是:先调用所有基类的构造函数,再调用对象成员的构造函数(如果有对象成员 ),最后调用派生类自己的构造函数。其中,处于同一继承层次的各基类构造函数的调用顺序取决于定义派生类时所指定的基类的顺序,与派生类构造函数中所定义的成员初始化列表顺序无关。如果类中有对象成员,那么,对象成员构造函数的调用顺序与对象在类中声明的顺序一致。?
💦例:多继承下构造函数和析构函数的调用顺序?。
#include<iostream>
using namespace std;
class Base1
{
public:
Base1(int i)
{
b1=i;
cout<<"construct Base1"<<endl;
}
void display()
{
cout<<"b1="<<b1<<endl;
}
~Base1()
{
cout<<"destruct Base1"<<endl;
}
private:
int b1;
};
class Base2
{
public:
Base2(int i)
{
b2=i;
cout<<"construct Base2"<<endl;
}
void display()
{
cout<<"b2="<<b2<<endl;
}
~Base2()
{
cout<<"destruct Base2"<<endl;
}
private:
int b2;
};
class Derive:public Base2,public Base1
{
public:
Derive(int m):Base1(m+2),Base2(m-2)
{
d=m;
cout<<"construct Derive"<<endl;
}
void display()
{
Base1::display();
Base2::display();
cout<<"d="<<d<<endl;
}
~Derive()
{
cout<<"destruct Derive"<<endl;
}
private:
int d;
};
int main()
{
Derive d(10);
d.display();
return 0;
}
🔑说明:
????????构造函数的调用顺序是: Base2、Basel、Derive,析构函数的调用顺序是: Derive、Base1、Base2。
四、多继承中的同名隐藏和二义性问题:?
? ? ? ? 上例中派生类中定义了与基类同名的函数 display,对于在不同作用域声明的标识符,可见性原则是:如果存在两个或多个包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名标识符,那么外层标识符在内层依然可见;如果在内层声明了同名标识符,则外层标识符在内层不可见,此时称内层标识符隐藏了外层同名标识符,这种现象被称为同名隐藏规则。
????????在类的派生层次结构中,基类的成员和派生类新增加的成员都具有类作用域,两者的作用范围不同,是相互包含的两个层,派生类在内层。这时,在基类 Basel、Base2 中都定义了 display函数,在派生类中也定义了 display 函数。如果在类外通过派生类对象 d 去调用 display 函数,派生类新成员就会隐藏外层同名的成员,直接使用成员名只能访问派生类的成员。若派生类中声明了与基类成员函数同名的新函数,即使函数的形参表不同,也不构成重载的关系,从基类继承过来的同名函数也会被隐藏。若要访问被隐藏的成员,就需要使用作用域操作符和基类名来限定。
?注意:
????????当派生类中定义了与基类同名但具有不同的形参的函数时(形参个数不同,或者形参类型不同),不属于函数重载,这时派生类中的函数使基类中的函数隐藏,调用父类中的函数必须使用父类名称来限定。只有在相同作用域中定义的函数才可以构成重载。?
💦例:多继承同名隐藏示例。?
#include<iostream>
using namespace std;
class Base1
{
public:
Base1(int i)
{
b1=i;
cout<<"construct Base1"<<endl;
}
void display()
{
cout<<"b1="<<b1<<endl;
}
~Base1()
{
cout<<"destruct Base1"<<endl;
}
private:
int b1;
};
class Base2
{
public:
Base2(int i)
{
b2=i;
cout<<"construct Base2"<<endl;
}
void display()
{
cout<<"b2="<<b2<<endl;
}
~Base2()
{
cout<<"destruct Base2"<<endl;
}
private:
int b2;
};
class Derive:public Base2,public Base1
{
public:
Derive(int m):Base1(m+2),Base2(m-2)
{
d=m;
cout<<"construct Derive"<<endl;
}
void display()
{
cout<<"d="<<d<<endl;
}
~Derive()
{
cout<<"destruct Derive"<<endl;
}
private:
int d;
};
int main()
{
Derive d(10);
d.Base1::display();
d.Base2::display();
d.display();
return 0;
}
🔑说明:
????????在主函数中、定义了派生类对象 d,根据同名隐藏规则,如果通过派生类对象访问display 函数,只能访问派生类新添加的成员,从基类继承过来的成员由于处于外层作用域而被隐藏。此时要通过d 访问从基类继承过来的成员,就必须使用类名和作用域操作符;访问 Base1中的 display,使用 Base1::display;访问 Base2 中的 display,使用Base2::display。
????????通过作用域操作符,明确且唯一地标识了派生类中由基类继承过来的成员,解决了同名隐藏的问题。
??思考:
????????如果在派生类中没有定义 display,是不是就不存在同名隐藏,那么,通过d可以访问到的是 Base1的display,还是 Base2的display?请改写程序,验证你的想法。
????????假如我们把派生类定义的 display 函数删除,此时派生了继承了来自 Base1的 display 和 Base2的 display,由于 display 存在二义性,依然无法直接通过派生类对象d直接访问基类的成员 display。
????????如果某个派生类的部分或者直接基类是从另一个共同的基类派生而来,在这些间接基类中从上一级基类继承来的成员拥有相同的名称,在派生类中也会产生同名的现象。这种同名也需要通过作用域操作符来进行标识,而且必须用直接基类来进行限定。?
🔑说明:
????????基类 A 中声明了数据成员a、构造函数、析构函数和函数 fun0,A 派生出了 B1和B2,再以 B1、B2 作为基类共同派生出新类 C,在派生类中都没有添加新的同名成员。这时的C类,包含通过B1,B2 继承过来的基类A 中的同名成员 fun0,类的关系图及派生类的结构图如图所示,其中“+”号表示公有,“-”号表示私有,保护的用“#”号表示。
????????对于派生类中成员 a 和 fun0 的访问,只能通过直接基类 B1或者 B2 的名称来限定才可以不能通过基类A 来限定,因为通过 A 限定无法表明成员是从 B1继承的,还是从 B2 继承的。
💦例:复杂版本的多继承同名隐藏示例。
#include<iostream>
using namespace std;
class A
{
public:
int a;
void fun0(){ cout<<"A function is called"<<endl;}
A(){cout<<"construct A"<<endl;}
~A(){cout<<"destruct A"<<endl;}
};
class B1:public A
{
public:
int b1;
B1(){cout<<"construct B1"<<endl;}
~B1(){cout<<"destruct B1"<<endl;}
};
class B2:public A
{
public:
int b2;
B2(){cout<<"construct B2"<<endl;}
~B2(){cout<<"destruct B2"<<endl;}
};
class C:public B1,public B2
{
public:
int c;
void fun0(){ cout<<"C function is called"<<endl;}
C(){cout<<"construct C"<<endl;}
~C(){cout<<"destruct C"<<endl;}
};
int main()
{
C c;
c.B1::a=10;
c.B1::fun0() ;
c.B2::a=20;
c.B2::fun0() ;
return 0;
}
????????在主函数中定义了派生类对象 c,如果只通过成员名称来访问该类的成员 a 和 fun0,系统就无法唯一确定要引用的成员。这时必须通过作用域操作符,通过直接基类来确定要访问的从基类
继承来的成员。
????????此时,在内存中,派生类对象同时拥有两个 a 的空间,这两个a 可以分别通过 B1和B2调基类 A 的构造函数进行初始化,能够存放不同的数值。也可以使用作用域操作符通过直接基类行区分,分别进行访问。但是,在大多数情况下,我们不需要两个同名副本,只需要保留一个即可,C++提供了虚基类技术来解决此问题。?
五、虚基类:
? ? ? ? 上例中当我们定义一个派生类对象 c 时,它会构造 B1 类,B2 类,B1,B2 类都有一个父类,因此A 类被构造了两次,在 c中,A 中的数据成员 a 有两个副本,A 中的成员函数 fun0 也有两个映射。一般可以将共同基类 A 设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个空间,同一个函数名也只有一个映射,在构造派生类对象 c 时,A 类只会构造一次。
💦例:虚基类应用示例。
#include<iostream>
using namespace std;
class A
{
public:
int a;
void fun0(){ cout<<"A function is called"<<endl;}
A(){cout<<"construct A"<<endl;}
~A(){cout<<"destruct A"<<endl;}
};
class B1:virtual public A
{
public:
int b1;
B1(){cout<<"construct B1"<<endl;}
~B1(){cout<<"destruct B1"<<endl;}
};
class B2:virtual public A
{
public:
int b2;
B2(){cout<<"construct B2"<<endl;}
~B2(){cout<<"destruct B2"<<endl;}
};
class C:public B1,public B2
{
public:
int c;
void fun0(){ cout<<"C function is called"<<endl;}
C(){cout<<"construct C"<<endl;}
~C(){cout<<"destruct C"<<endl;}
};
int main()
{
C c;
c.a=10;
c.fun0();
return 0;
}
?注意:
????????虚基类并不是将基类声明为虚基类,只是在类的派生过程中使用了 virtual 关键字?
????????在具体程序设计过程中,如果不需要重复的副本,可以选择虚基类,如果需要更多副本空间存在不同数据,则可以采用作用域操作符方式区别访问。一般采用虚基类可以使得程序更加简洁同时节省更多内存空间。?
?六、总结:
- 根据派生类继承基类的个数,将继承分为单继承和多继承。
- 多继承可以看成是单继承的组合。
- 处于同一继承层次的各基类构造函数的调用顺序取决于定义派生类时所指定的基类的顺序,与派生类构造函数中所定义的成员初始化列表顺序无关。
- 如果在内层声明了同名标识符,则外层标识符在内层不可见,此时称内层标识符隐藏了外层同名标识符。
- 若派生类中声明了与基类成员函数同名的新函数,即使函数的形参表不同,也不构成重载的关系。
- 只有在相同作用域中定义的函数才可以构成重载。?
- 通过作用域操作符,明确且唯一地标识了派生类中由基类继承过来的成员,解决了同名隐藏的问题。
- 虚基类并不是将基类声明为虚基类,只是在类的派生过程中使用了 virtual 关键字 。
- 一般采用虚基类可以使得程序更加简洁同时节省更多内存空间。?
七、共勉:
????????以上就是我对C++继承与派生——(8)多继承的理解,希望本篇文章对你有所帮助,也希望可以支持支持博主,后续博主也会定期更新学习记录,记录学习过程中的点点滴滴。如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对C++继承与派生的理解,请持续关注我哦!!!?
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!