1.继承的概念和定义
1.1继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
简单的说,就好比之前c语言把常用的一个函数功能,单独定义成一个函数,而继承则是把不同类中相同的属性和方法,定义到一个基类(父类)中,比如下面中把老师类和学生类,相同的属性和方法都放到Person基类中,就可以复用这些成员,不需要重复定义,省去了很多麻烦。
class Person { public: // 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 void identity() { cout << "void identity()" << _name << endl; } protected: string _name = "张三"; // 姓名 string _address; // 地址 string _tel; // 电话 int _age = 18; // 年龄 }; class Student : public Person { public: // 学习 void study() { // ... } protected: int _stuid; // 学号 }; class Teacher : public Person { public: // 授课 void teaching() { //... } protected: string title; // 职称 };1.2继承的定义
上面我们看到Person是基类(父类),Student,Teacher是派生类(子类)则
定义格式如下:
其中继承方式有三种:public、protected、private。
访问方式也有三种:public、protected、private。
由于继承方式和访问方式的不同就会影响在派生类中对基类成员的访问,一个有下面九种对应关系
| 类成员 / 继承方式 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| 基类的 public 成员 | 派生类的 public 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 protected 成员 | 派生类的 protected 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
- 基类
private成员,无论哪种继承,派生类都不能直接访问 - public 继承:访问权限保持不变
- protected 继承:基类 public → protected
- private 继承:基类 public/protected 全部变成 private
1.3继承类模板
当基类是一个类模板的时候,在派生类中使用基类的方法要指定类域,否则会报错,这和模板按需实例化有关,具体看下面代码,栈的实现继承库里面的vector实现;
//类模板继承 namespace gxy { template<class T> class stack:public vector<T> { public: void push(const T& x) { //push_back(x); error C3861: “push_back”: 找不到标识符 // 基类是类模板时,需要指定一下类域, // 否则编译报错:error C3861: “push_back”: 找不到标识符 // 因为stack<int>实例化时,也实例化vector<int>了 // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到 vector<T>::push_back(x); } void pop() { vector<T>::pop_back(); } const T& top() { return vector<T>::back(); } bool empty() { return vector<T>::empty(); } }; }2.基类和派生类之间的转换
通常情况下我们把一个类型的对象赋值给另一个类型的指针或者引用时,存在类型转换,中间会产
生临时对象,所以需要加const,如:int a=l;const double& d=a;public继承中,就是一个特殊处理的例外,派生类对象可以赋值给基类的指针/基类的引用,而不需要加const,这里的指针和引用绑定是派生类对象中的基类部分,如下图所示。也就意味着一个基类的指针或者引用,可能指向基类对象,也可能指向派生类对象。
派生类对象赋值给基类对象是通过基类的拷贝构造函数或者赋值重载函数完成的(这两个函数的细节后面小节会细讲),这个过程就像派生类自己定义部分成员切掉了一样,所以也被叫做切割或者切片,如下图中所示。
但是基类对象不能赋值给派生类对象
3.继承中的作用域
隐藏规则:
1.在继承体系中基类和派生类都有独立的作用域。不能简单理解成是直接把基类添加到派生类
2.派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
(在派生类成员函数中,可以使用基类:基类成员显示访问)
3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(注意不是函数重载,因为函数重载必须在同一个做用域)
4.注意在实际中在继承体系里面最好不要定义同名的成员。
4.派生类的默认成员函数
4.1 4个常见的默认成员函数
默认成员函数我们写编译器会默认生成一个,那么在派生类中,他们是如何实现的?
此时我们应该理解继承的作用域:基类与派生类各自拥有独立的作用域,不能简单地认为基类被复制进了派生类。派生类可以访问基类中的成员,在逻辑上可以把基类的成员看作一个整体来使用。
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3.派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5.派生类对象初始化先调用基类构造再调派生类构造。
6.派生类对象析构清理先调用派生类析构再调基类的析构。
// 基类:人 class Person { public: // 基类构造函数:初始化姓名,创建Person对象时调用 Person(const char* name) :_name(name) { cout << "Person()" << endl; } // 基类拷贝构造函数:用一个已存在的Person对象,拷贝新建一个Person对象 Person(const Person& p) { _name = p._name; _sex = p._sex; _age = p._age; cout << "Person(const Person& p)" << endl; } // 基类赋值运算符重载:把一个Person对象的数据赋值给另一个已经存在的Person对象 Person& operator=(const Person& p) { _name = p._name; _sex = p._sex; _age = p._age; cout << "Person& operator=(const Person& p)" << endl; return *this; } // 基类析构函数:销毁Person对象时自动调用,释放资源 ~Person() { cout << "~Person()" << endl; } protected: string _name = "张三"; // 姓名,protected:派生类可以直接访问,外部不能访问 string _sex; // 性别 int _age; // 年龄 }; // 派生类:学生,公有继承Person class Student : public Person { public: // 派生类构造函数 // 初始化列表先调用基类Person的构造,初始化基类部分成员;再初始化自己的学号_No Student(const char* name, int id=2) :Person(name) ,_No(id) { } // 派生类拷贝构造函数 // 初始化列表:调用基类Person的拷贝构造,拷贝继承来的基类成员;再拷贝派生类自己的_No // 这里Person(s):Student对象s向上转型,传给Person的拷贝构造的const Person&形参 Student(const Student& s) :Person(s) ,_No(s._No) { } // 派生类赋值重载函数 Student& operator=(const Student& s) { // 显式调用基类Person的赋值重载,完成基类部分成员的赋值 Person::operator=(s); // 赋值派生类自己新增的成员学号 _No = s._No; return *this; } // 派生类析构函数 // 派生类析构执行完后,编译器会自动调用基类析构,这里手动写Person::~Person()是多余的(仅演示原理) ~Student() { Person::~Person(); } private: int _No; // 学号,派生类自己独有的成员,基类Person没有 };4.2实现一个不能被继承的类
方法一:在类的定义讲到继承的方式和访问方式,如果基类的构造函数定义成私有,就不能在派生类调用,而在派生类构造是必须调用基类的构造,那么派生类就无法实例化出对象。
方法二:c++11 后增加了一个新的关键字final,final修饰基类,派生类就不能继承了。
//c++ 11 class Base final { public: void func5() { cout << "Base::func5" << endl; } protected: int a = 1; private: //Base() //{} }; class Derive :public Base { void func4() { cout << "Derive::func4" << endl; } protected: int b = 2; };5.继承和友元
友元关系不能被继承。基类的友元函数仅拥有访问基类内部成员的权限,不会自动成为派生类的友元;如果要让同一个友元函数访问派生类的私有成员,必须在派生类中单独声明该函数为友元。
class Student; class Person { public: friend void Display(const Person& p, const Student& s); protected: string _name; }; class Student : public Person // Student公有继承Person { private: int _num; }; void Display(const Person& p, const Student& s) { cout << p._name << endl; cout << s._num << endl; //编译报错!Display只是Person友元,不是Student友元,访问不了_num }6.继承和静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个static成员实例。
可以通过代码验证一下:
#include <iostream> #include <string> using namespace std; class Person { public: string _name; static int _count; // 静态成员属于整个Person类,不属于某个对象 }; // 静态成员变量必须在类外定义初始化 int Person::_count = 0; class Student : public Person { protected: int _stuNum; }; int main() { Person p; Student s; cout << &p._name << endl; cout << &s._name << endl; // 静态成员属于Person类,只有一份内存,所有对象共享 cout << &p._count << endl; cout << &s._count << endl; // 公有继承下,派生类可以通过类域访问基类的静态成员 cout << Person::_count << endl; cout << Student::_count << endl; return 0; }vs下运行结果
总结:普通成员随对象,一份对象一份拷贝;静态成员属于基类,只有一份,继承不会生成副本,派生类只是有权限去访问它
7.多继承及其菱形继承问题
7.1继承模型
单继承:一个派生类只有一个直接基类时称这个继承关系为单继承
多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。
菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
可以看下面模型图理解一下:
7.2虚继承
菱形继承的数据冗余和二义性是因为相当继承两份相同的类,那有没有什么办法做到只继承一份,虚继承就可以做到。
在中间两层子类(Student、Teacher)继承 Person 的时候,加上virtual,变成虚继承。
class Person {}; class Student : virtual public Person{}; // 虚继承 class Teacher : virtual public Person{}; // 虚继承 class Assistant : public Student, public Teacher{};Assistant对象只保留一份 Person 基类,不再重复拷贝,消除冗余,同时解决访问二义性,因为虚继承不会在子类里面直接嵌入父类成员,而是用虚基表指针指向唯一的公共虚基类 Person。
注意:virtual是加在谁去继承顶层的公共的父类的继承列表上。
8.继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种haS-a的关系。假设B组合了A,每个B对象中都有一个A对象。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用
(white-boxreuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类可
见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依
赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复(black-boxreuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太
那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。
以上就是cpp继承相关的知识点。