一、继承的概念及定义
概念:
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用。
例如,一个人有名字、年龄等属性,而一个学生也是人,他就可以直接继承人的特性,再加上自己独有的特性即可
格式:
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类,而继承方式根据访问限定符的不同被分为了三种方式
继承并不是派生类就可以访问基类的所有成员,不同继承方式下,对基类成员访问方式也会发生变化,例如对于基类的private成员,不管派生类以什么方式继承,都只能在基类内部才能访问。
权限:public>protected>private ,派生类继承方式和基类的成员权限取小的权限就是派生类对基类不同成员的访问权限
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的protected成员 | 派生类的protected 成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可 见 |
总结:
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
二、基类和派生类
基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。
注意:基类对象是不能赋值给派生类对象的,派生类对象可以赋值给基类对象是因为派生类的成员一定包含了基类的成员,只需要按需切片赋值给基类即可,但是基类可能不存在派生类的一些成员
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
void Test ()
{
Student sobj ;
Person pobj = sobj ; //对象
Person* pp = &sobj; //指针
Person& rp = sobj; //引用
}
继承中的作用域
在继承体系下,基类与派生类都有自己独立的作用域,例如当基类与派生类存在同名成员,子类成员会屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。 所以在实际中在继承体系里面最好不要定义同名的成员。如果想访问基类的同名成员,可以通过基类::基类成员 显示访问
三、派生类的默认成员函数
1.派生类对象初始化先调用基类构造再调派生类构造。派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序(派生类在调用完自己的析构函数后会自动调用基类的析构函数,所以我们不需要在派生类的析构函数内部显示调用析构函数了)
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
补充:
- 友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
- 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。
四、单继承与多继承
C++在继承体系中除了可以单继承外,还支持多继承,但是多继承也带来了许多问题,例如菱形继承
- 单继承:一个子类只有一个直接父类时称这个继承关系为单继承
- 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
- 菱形继承:存在多继承就存在菱形继承,菱形继承是多继承的一种特殊情况
class A
{
protected:
int _a;
};
class B:public A
{
private:
int _b;
};
class C :public A
{
protected:
int _c;
};
class D :public B,public C
{
protected:
int _d;
};
int main()
{
D d;
return 0;
}
菱形继承存在许多问题,由于此时D类型对象d中存在了两个a成员,当我们想访问d对象中的_a时就不知道应该访问B中的_a,还是C中的_a,存在数据冗余和二义性的问题,所以菱形继承建议不要使用!
那如何解决这些问题呢?
- 方法一:
加修饰限定,这样做虽然解决了二义性的问题但是还是没有解决数据冗余的问题
B::d._a
C::d._a
- 方法二:
虚继承:在继承方式前加上virtual
class A
{
public:
int _a;
};
class B:virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D :public B,public C
{
public:
int _d;
};
int main()
{
D d;
d._a = 1;
d._a = 2;
return 0;
}
这里可以分析出,D对象将A对象放到了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指 向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量,通过偏移量 可以找到下面的A