文章目录
- 1. 继承的概念和定义
- 1.1 继承的概念
- 1.2 继承定义
- 1.2.1定义格式
- 1.2.2 继承关系和访问限定符
- 2. 基类和派生类对象赋值转换
- 3.继承中的作用域
- 4. 派生类的默认成员函数
- 5. 继承和友元
- 6. 继承与静态成员
- 7. 菱形继承即菱形虚拟继承
- 菱形虚拟继承
- 8. 继承的总结与反思
1. 继承的概念和定义
1.1 继承的概念
继承是面向对象程序设计使代码可以复用的最重要的手段,允许程序员在保持原有类特性的基础上进行扩展,增加功能,通过继承产生的类,称为派生类。 继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。 在继承之前我们接触的复用只是函数复用,而继承是设计层面的复用。
其用法如下:
#include <iostream>
using namespace std;class person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name = "";int _age = 0;
};class student : public person
{
protected:int _id;
};class teacher : public person
{int _jobid;
};//其中,student和teacher就属于派生类(子类),而person就是这两个类的基类
//我们可以通过监视窗口查看student和teacher对象就可以观察到变量的复用,调用print可以喊道成员函数的复用。
int main()
{student s;teacher t;s.Print();t.Print();return 0;
}
1.2 继承定义
1.2.1定义格式
从下面就可以看到person是父类,也称为基类,而student是子类,也称为派生类。
1.2.2 继承关系和访问限定符
在c++中,有三种继承方式,分别是:public继承, protected继承,private继承。
而访问限定符也有三种,public访问,protected访问,private访问。
这两个一组合就导致了继承的机制极为复杂!
类成员/继承方式 | public继承 | protected继承 | private 继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
- 基类private成员在派生类外不管是什么方式继承都是不可见的,这里的不可见是指基类成员虽然还是被继承到了派生类中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 ==
min(成员在基类的访问限定符,继承方式)
,public > protected > private - 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
2. 基类和派生类对象赋值转换
- 派生类对象可以赋值给基类的对象/指针/引用。形象的说法可以说是切片或切割,意思是把派生类中父类那部分切过来赋值过去。
需要注意的是在赋值的时候并没有调用拷贝构造,而是直接将里面的内容拷贝过去。
- 基类对象不能赋值给派生类对象
这也很容易理解,如果能赋值过去,那么子类对象多的成员不会知道应该赋值成什么
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但必须是基类的指针指向派生类对象时才安全。
3.继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域
- 子类和父类中有同名成员,派生类将屏蔽基类对同名成员的直接访问,这种情况叫做**隐藏(也叫做重定义)。**如果在定义有重名成员变量或函数的时候想要访问基类的成员,可以使用访问限定符来显示访问。
class person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name = "";int _age = 0;
};class student : public person
{
protected:string _name;int _id = 0;
public:void print(){cout << _id << endl;}
};
int main()
{//在这种情况下想要访问基类的print()函数,应该使用访问限定符student s;s.person::print();//所以在设计继承时最好不要定义同名成员,否则会出现二义性问题
}
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际使用中继承体系里面最好不要定义同名成员。
4. 派生类的默认成员函数
在学习c++类和对象的时候,我们知道对于类来说有六个我们不写也会自动生成的默认成员函数,那么在派生类中,这些默认成员函数是如何生成的呢?
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。(也就是说基类的成员变量只能通过基类构造函数构造,如果没有显示调用,则编译器会自动调用默认构造函数)。如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员(也就是不需要显示调用基类的析构函数)。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 编译器会对析构函数名进行特殊处理,处理程destrutor(),便于后面的重写做铺垫(注意,重写和重定义是两个不同的概念!!),因此父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
5. 继承和友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
通俗一点理解,父亲的朋友不是你的朋友。
class student;
class person
{friend void display(const person& p, const student& s);
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name = "";int _age = 0;
};class student : public person
{
protected:int _id = 0;
};class teacher : public person
{int _jobid = 0;
};void display(const person& p, const student& s)
{cout << p._name << endl;cout << s._id << endl;
}
6. 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个成员。无论派生出多少个子类,都只有一个static成员实例。
#include <iostream>
using namespace std;
class person
{
public:static int _num;person() { ++_num; }void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "";int _age = 0;
};int person::_num = 0;class student : public person
{
protected:int _id = 0;
};class teacher : public person
{int _jobid = 0;
};int main()
{student s;teacher t;person p;cout << person::_num << endl;return 0;
}
7. 菱形继承即菱形虚拟继承
**单继承:**一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类友两个或以上直接父类时称这个继承关系为多继承(只要保证父类中都没有同名成员就不会二义性问题)。
**菱形继承:**菱形继承时多继承的一种特殊情况
这种继承方式存在的问题是:可能会存在数据冗余和二义性的问题!看下图:
class person
{
public:string _name;int _age;
};class student: public person
{
protected:int _id;
};class teacher : public person
{
protected:int _num;
};class assistant : public student, public teacher
{
protected:string _course;
};int main()
{assistant s;cout << s._name << endl;}
然后我们可以通过观看对象模型就可以看到问题出现在哪
如果想要解决二义性问题,可以使用指定访问,如s.student::_name
,s.teacher::_name
,但是,这样的方法仍然不能解决数据冗余的问题,想要解决这个问题,就需要使用虚拟继承。
菱形虚拟继承
虚拟继承可以解决虚拟继承的二义性和数据冗余的问题。如上面的继承关系,在student和teacher继承person时使用虚拟继承(在继承体系的腰部),即可解决问题。
class person
{
public:int _age = 0;
};class student: virtual public person
{
public:int _id = 0;
};class teacher : virtual public person
{
public:int _num = 0;
};class assistant : public student, public teacher
{
public:int _t = 0;
};int main()
{assistant s;cout << s._age << endl;return 0;}
通过使用虚继承,就可以使得assistant对象中只存在一个person成员,但是虚拟继承是如何做到的呢?我们可以通过借助内存窗口观察对象模型得知。
通过分析可以看到,assistant对象将person对象的数据放到了对象组成的最下面单独出来,而本来student成员和teacher成员里面的person成员则变成了两个指针,而这两个指针指向的是什么呢?同样我们通过监视窗口观察一下。
通过观察我们就可以发现,这两个指针指向的地方存的下一个位置储着一个数据指向从该地方到虚继承成员的数据存储地址,存的是偏移量。通过测试,我们就知道了虚继承是如何解决数据冗余的问题的了。
虚拟继承中student和teacher对象模型中的这两个指针叫做虚基表指针,这两个表叫做虚基表。虚基表中存的是偏移量,通过偏移量找到下面的person。
下面是对菱形虚拟继承的原理解释:
8. 继承的总结与反思
- 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱 形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
- 多继承可以认为是C++的缺陷之一,很多后来的OO(object oritanted)语言都没有多继承,如Java
- 继承和组合
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 优先使用对象组合,而不是类继承 。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用称为白箱复用。术语“白箱”是相对可视性而言:在继承关系中,基类的大部分细节对派生类可见。继承一定程度的破坏了基类的封装,基类的改变对派生类的影响有可能很大,派生类和基类之间的依赖关系较强,耦合度很高。
- 对象组合是继承之外的另一种复用选择。新的更复杂的功能可以通过组装或者组合对象来获得。对象组合要求被组合的对象具有良好的接口。这种复用风格被称为黑箱复用,因为对象的内部细节是不可见的。对象只以“黑箱”的方式呈现。组合类之间的依赖关系较弱,耦合度第。优先使用组合有助于保持每个类的独立性。
- 实际中应尽量多的取用组合。组合的耦合度低,代码维护性好。不过继承也有其用处,并且多态也只能通过继承实现。简单来说,只有组合无法完成的任务我们才使用继承。
以上就是关于c++继承的主要内容了,如果大家对本内容还有什么疑惑或者博主哪里说法有误的话,欢迎大家在评论区指出!