概念:
继承机制是面向对象程序设计使代码可以服用的最重要手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类。
父类也叫基类。
子类也叫派生类。
定义格式:
class/struct 派生类名称 :继承方式 基类名称
注:
- 基类的private成员在派生类中无论以什么样的方式继承都是不可见的,指派生类对象不管在类里面还是类外面都不能访问它
- 如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protetced成员。
- 使用关键字class时默认的继承方式是private,使用struct是默认的继承方式是public。
class Person
{
protected:
string _name = "Qyuan"; // 姓名
private:
int _age = 18; // 年龄
};
class Student : public Person
{
public:
void Printf()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;//报错
//成员 "Person::_age" (已声明) 不可访问
cout << "age:" << _stuid << endl;
}
private:
int _stuid;
};
基类和派生类对象之间的赋值
派生类对象可以赋值给基类的对象、指针、引用。
基类对象不能赋值给派生类对象(即使强转可以,但最好不要)
![](https://i-blog.csdnimg.cn/blog_migrate/cf35e1f9b5410a601557a91f2baab6ab.png)
继承中的作用域
- 在继承体系基类和派生类都有自己独立的作用域
- 如果基类和派生类中有同名成员,派生类成员将屏蔽父类成员的直接访问(这种现象称为隐藏或者重地位)
- 针对上一种情况,在子类成员函数中,可以使用 基类名称::基类成员 显示访问同名成员
- 只要函数名相同,不管返回值和参数列表是否相同都会造成隐藏现象。基类和派生类的构造和析构会造成隐藏现象。(与函数名修饰有关)
派生类的默认成员函数
我们只要记住一句:自己的事情自己干
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝构造
- 派生类的赋值重载函数必须调用基类的赋值重载函数完成基类的复制
- 派生类的析构函数会在调用完成之后自动调用基类的析构函数清理基类成员。(这样保证了派生类对象先清理派生类成员再清理基类成员的顺序)
既然自己完成自己那部分的任务,那么构造和析构的顺序是什么呢?
在默认调用的情况下:先完成基类的构造,在进行派生类的构造;先进行派生类的析构,在完成基类的析构
注:
此处默认调用的情况下,构造和析构必须是默认函数,否则编译器直接报错。
无论你是否在派生类的析构函数中调用了基类的析构函数,编译器都会派生类的析构函数会在调用完成之后调用基类的析构函数清理基类成员
关于友元和静态成员
友元关系不能被继承,即基类的友元不能访问子类的私有和保护成员。
基类当中定义了一个静态成员,则整个继承体系中无论派生出多少个子类,都只有一个该静态成员(与内存中储存的位置有联系)
class Person
{
public:
int _age; // 姓名
static int num;
};
int Person::num = 0;
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Person p;
p._age = 18;
p.num = 1;
Student s;
s.num = 2;
s._age = 15;
return 0;
}
非静态成员,每个类都有一份;静态成员,整个继承体系中,仅有一份;
继承关系:
单继承:一个子类只有一个直接父类;
多继承:一个子类有两个或两个以上直接父类
菱形继承:多继承的一种特殊情况。(存在数据冗余和二义性的问题)
菱形的数据冗余及二义性问题
class A
{
public:
int _a;
};
class B : public A //虚拟继承 class B: virtual public A
{
public:
int _b;
};
class C : public A //虚拟继承 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 = -1;
d._b = 2;
d._c = 3;
d._d = 4;
return 0;
}
内存分布:
我们可以看到:B,C类都继承了A类;D类继承B,C类,本意是包含一个A类中所表示的数据。但在内存分布中,D类中存在着两个_a,所以这造成了数据的冗余(本意存放一个相同的数值)和二义性(表示的含义)。
怎么解决掉菱形继承的数据冗余问题呢?
虚拟继承(关键字virtual)
菱形虚拟继承的内存对象成员模型:
我们可以看出,D对象将_a放到了对象组成的最下面。这个_a同时属于B和C类。
那么B和C类如何找到公共的_a呢?
B和C类中有两个指针,各指向一张表(虚基表),表中存放偏移量,通过偏移量就可以找到_a.
这么设计的目的之一:切片后B类还可以找到_a.
组合:
class A
{
public:
int _a;
};
class B
{
A _a;
int _b;
};
继承和组合的区别:
继承允许根据基类的实现来定义派生类的实现。在继承方式中,基类的内部细节对派生类可见,在一定程度上破坏了基类的封装性,并且基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
组合要求被组合的对象具有良好定义的接口,(对象的内部细节是不可见的),此外,组合类之间没有很强的依赖关系,耦合度低。使用对象组合有助于每个类的封装。
继承是is -a的关系,每个派生类都是一个基类对象
组合是has-a的关系,每个B对象都有一个A对象
一个类如何设计不能被继承
- 构造函数设为私有;
- 将这个类声明为final类