目录
继承的概念
继承的使用
继承方式
protected
继承类模板
赋值兼容转换
隐藏
子类的默认成员函数
构造函数
拷贝构造函数
赋值重载函数
析构函数
不能被继承的类
方法1:父类的构造函数私有
方法2:final
继承与友元
继承与静态成员
多继承
菱形继承
虚继承
继承和组合
is-a
has-a
继承的概念
继承是面向对象语言程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类的特性基础上进行扩展,增加成员函数或者成员变量,这样产生新的类称为子类
继承的使用
首先我们要先有一个类,这个类是有需要被继承的属性,这个类就被叫做基类(父类)
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;int _major;
};
这里的Person就是基类,而下面还有一个Student类,将Person类给继承了,这个类就叫做派生类(子类)
具体的使用继承的方法就是派生类 : 继承方式 基类
这个时候的派生类里面就会有基类的成员变量和成员函数(二者都有)
那么这里的继承方式又是什么呢?
继承方式
和类里面一样,有三种继承方式:public继承、protected继承、private继承
protected
在没学习继承的时候protected访问限定符的作用就和private一样,但现在就不一样了
在基类中protected限定的变量或函数只有派生类中能访问,在外界不能访问
而private是派生类和外界都不能访问!
例如上面Person类中的那四个成员变量都是protected限定,那么这四个成员变量只能在派生类Student中使用,在外界是不能使用的
继承方式相当于给基类里面的所有属性设置一个权限最大值
例如上面的继承方式是public,那么基类中的属性在派生类中可以是public、protected、private
但是如果继承方式是private,那么基类中的属性在派生类中就都是private,无论该属性在基类中是以什么访问限定符限定的
具体如下表所示:
总结:
1. 父类private成员在子类中无论以什么方式继承都是不可见的
这里的不可见指的是父类的私有成员被继承到了子类对象中,但是语法上限制了子类对象无论是在类内还是类外都无法访问它
2. 父类的其它成员在子类中的访问方式==Min(成员在父类中的访问限定符,继承方式),public > protected > private
3. 使用关键字class时默认的继承方式是private,使用struct关键字时默认的继承方式是public,最好显示写出继承方式
4. 在实际运用中建议使用public继承,因为protected和private继承下来的成员使用范围小,实际中扩展性不强
继承类模板
当需要继承类模板的时候,需要明确的指明类域
#include <iostream>
#include <vector>using namespace std;namespace lyw
{template<class T>class stack : public std::vector<T>{public:void push(const T& x){vector<T>::push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}int main()
{return 0;
}
这里使用继承也可以完成stack类的实现
但是我们在复用vector类模板里面的函数的时候,必须要明确的指明类域
因为在运行实例化stack的函数时,vector类模板里面的函数是还没有实例化的,所以我们需要指明类域依次能实例化出特定函数
赋值兼容转换
首先这里有两个类,Person(基类)和Student(派生类)
class Person
{
protected:string _name; string _sex; int _age;
};class Student : public Person
{
public:int _No;
};
派生类是可以赋值给基类的
int main()
{Student s;Person p;p = s;return 0;
}
但是基类不可以赋值给派生类!
int main()
{Student s;Person p;s = p;return 0;
}
父类的指针是可以指向子类的,引用也可以引用子类,如下:
int main()
{Student s;Person p;Person* pp = &s;Person& rp = s;return 0;
}
但是子类的指针却不能指向父类,引用也是
int main()
{Student s;Person p;Student* ps = &p;Student& rs = p;return 0;
}
但是如果我们稍加处理(强制类型转换),是可以的
int main()
{Student s;Person p;Student* ps = (Student*)&p;Student& rs = (Student&)p;return 0;
}
这里有一个说法叫做切片或者切割,它可以帮助我们很好的理解基类和派生类的赋值规则
因为子类中就有父类的属性,所以子类是可以赋值给父类的,但是父类缺没有子类的一些属性,所以不能赋值回去
总结:
public继承的子类对象可以赋值给父类的对象、指针、引用
父类对象不能赋值给子类对象,但是通过强转就可以实现指针和引用的赋值
隐藏
隐藏是当父类和子类有同名的成员,子类将屏蔽父类的成员(隐藏起来),我们可以通过访问限定符来访问父类的隐藏成员
class Person
{
protected:string _name = "张三";int _num = 111;
};
class Student : public Person
{
public:int GetNum(){return _num;}
protected:int _num = 999;
};int main()
{Student s;cout << s.GetNum() << endl;return 0;
}
若是我们在GetNum中加上类域
int GetNum()
{return Person::_num;
}
通过以上实验不难看出这里的Person确实被隐藏起来了,但是我们仍然能访问
总结:
1. 在继承中子类和父类都有独立的作用域
2. 当子类和父类中有同名成员(函数名或者变量名),那么它们将构成隐藏关系
3. 我们可以使用父类::父类成员显示访问
子类的默认成员函数
下面是Person类中的四个默认成员函数
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}
protected:string _name;
};
那么我们将父类继承给子类之后,该怎么样调用父类的
class Student : public Person
{
public:
protected:int _num;
};
构造函数
Student(int num): _num(num)
{cout << "Student()" << endl;
}
我们可以这样写
父类的那部分怎么初始化呢?
因为父类是有默认构造函数的,所以它会调用父类的默认构造函数,将子类的name构造为"peter"
若是没有这个"peter"会报错!
Person(const char* name): _name(name)
{cout << "Person()" << endl;
}
所以父类尽量有一个默认构造函数
但是如果就是没有默认构造函数怎么办?我们可以在初始化列表中显示调用父类的构造函数
Student(const char* name, int num): Person(name), _num(num)
{cout << "Student()" << endl;
}
这样就不会有问题了
拷贝构造函数
Student(const Student& s): Person(s), _num(s._num)
{cout << "Student(const Student& s)" << endl;
}
和构造函数一样,我们需要在初始化列表中显示调用Person的构造函数
这样就可以使用Person的构造函数来构造从Person继承出来的那部分属性了
赋值重载函数
Student& operator=(const Student& s)
{cout << "Student& operator= (const Student& s)" << endl;if (this != &s){Person::operator=(s);_num = s._num;}return *this;
}
一样需要调用父类的赋值重载函数,但是这里由于函数名都相同,所以构造隐藏关系,既然构成隐藏关系,就需要类域限定符来指定Person类域访问Person的赋值重载函数
析构函数
~Student()
{cout << "~Student()" << endl;
}
析构函数只需要处理Student自己的属性即可,如果没有空间需要释放我们也可以什么都不做
因为子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员,因为只有这样才能保证子类对象先清理子类成员--->清理父类成员这样的顺序
这里父类的析构函数和子类的析构函数其实是构成隐藏关系
隐藏不是函数名相同吗?为什么会构成隐藏关系呢?
从代码层看起来它们的函数名不相同,但是从底层来看它们的名字都是一个叫destructor()的函数,这是编译器特殊处理的结果
所以它们是构成了隐藏关系的
不想要这个关系可以加上virtual关键字在父类析构上
virtual ~Person() {cout << "~Person()" << endl; }
总结:
1. 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员,如果父类没有默认构造函数那么子类需要显示调用父类的构造函数
2. 子类的拷贝构造函数必须调用父类的拷贝构造函数完成父类的拷贝初始化
3. 子类的operator=必须要调用父类的operator=完成父类的复制,需要注意的是它们两个构成隐藏关系,子类调用时需要指定父类作用域
4. 子类的析构函数会在被调用后自动调用父类的析构函数,确保顺序是子类析构--->父类析构
5. 子类和父类的析构函数由于编译器特殊处理的原因,它们也构成隐藏关系
具体处理流程图如下:
不能被继承的类
如何实现不能被继承的类?
方法1:父类的构造函数私有
这是C++98时期常用的写法
当父类的构造函数私有时,子类的构成必须要调用父类的构造函数,但是私有化后,子类看不见,所以就不能调用了,子类就无法实例化出对象
class Base
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:Base(){}
};
方法2:final
C++11新增了一个final关键字,final修改父类,子类就不能继承父类了
class Base final
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private://Base()//{}
};
继承与友元
友元的关系不能继承,也就是说父类的友元不能访问子类私有和保护成员
class Student; // 提前声明class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name;
};class Student : public Person
{
protected:int _stuNum;
};void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}
Display函数中可以访问处于父类protected保护中的_name,因为Display是Person类的友元
但是却不能访问子类中protected保护中的_stuNum,因为它们不是友元,没有关系
父类的朋友可不一定是你子类的朋友
继承与静态成员
若是父类定义了static静态成员,则整个继承体系中只有一个这样的成员,无论生出多少个子类,都只有一个static成员实例
class Person
{
public:string _name;static int _count; // 声明
};int Person::_count = 0; // 定义class Student : public Person
{
protected:int _stuNum;
};int main()
{Person p;Student s;cout << &p._count << endl;cout << &s._count << endl;return 0;
}
在X86的环境下我们打印出了父类Person的_count和子类Student继承下来的_count的地址
最终结果显而易见它们的地址是相同的
说明它们是同一个变量
所以证实了static成员只有一个!
多继承
继承的关系并不是只可以有父和子的,还可以有“爷孙”关系(多继承)
Student继承Person,PostGraduate继承Student,这就是一个简单的多继承关系
Assistant同时继承了Student和Teacher,这也是一个简单的多继承关系
单继承:一个子类只有一个直接父类时,这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时,这个继承关系为多继承
多继承对象在内存中的模型是:先继承的父类在前面,后面继承的父类在后面,子类成员放到最后面
菱形继承
菱形继承是多继承中的一种特殊的情况
从上图就可以看出菱形继承有数据冗余和二义性问题
在Assistant的对象中Person的成员会有两份
只要支持多继承就一定会有菱形继承的问题
在Java中就不支持多继承,直接的规避掉了这里的问题,这里也是C++比Java语法相对难一些的地方
数据冗余:Person的成员在Assistant中有两份
二义性:Person的成员在Assistant中的两份代表的含义是什么?怎么能有两个?
具体示例代码如下:
class Person
{
public:string _name;
};class Student : public Person
{
protected:int _num;
};class Teacher : public Person
{
protected:int _id;
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse;
};
若是我们直接在Assistant的对象中直接访问从Person继承下来的成员,编译就会报错
int main()
{Assistant a;a._name = "peter";return 0;
}
这就是二义性的问题
二义性的问题我们可以依靠指定类域来解决
int main()
{Assistant a;a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}
但是数据冗余的问题我们依然无法解决,a对象的名字怎么能有两个?依照我们前面类中的设定name是只能有一个的!
那多继承的二义性和数据冗余到底该如何彻底解决呢?
虚继承
虚继承相当于让菱形继承下来的那个类只能有一个相同的父类属性
class Person
{
public:string _name;
};class Student : virtual public Person
{
protected:int _num;
};class Teacher : virtual public Person
{
protected:int _id;
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse;
};
使用方法是在最前面的父类继承下了的两个子类的继承方式前面加一个virtual关键字
这样当我们再次在Assistant对象中访问继承下了的_name的时候就不会有二义性和数据冗余的问题了!
int main()
{Assistant a;a._name = "peter";return 0;
}
继承和组合
is-a
public继承是一种is-a的关系
template<class T>
class stack : public vector<T>
{};
也就是说每个子类对象都是一个父类对象
has-a
组合是一种has-a的关系
template<class T>
class stack
{
public:vector<T> _v;
};
若B组合A,那么每个B对象中都有一个A对象
继承是根据父类的实现来定义子类的实现
这种通过生成子类的复用通常被称为白箱复用
白箱:父类内部细节对子类可见(公开透明)
继承⼀定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系很强,耦合度高,这不是一个很好的方式
对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或者组合对象来获得
这种复用风格被称为黑箱复用
黑箱:对象的内部细节是不可见的,对象只以“黑箱”的方式出现(不公开)
组合之间没有很强的依赖关系,耦合度低
建议优先使用组合,而不是继承,为什么?
因为组合的耦合度低,代码维护性好
当然还是需要看两者属于什么关系,如果是is-a则使用继承,是has-a则使用组合
另外,如果想要实现多态也得使用继承
小tips:
C++最常用的iostream库中使用的也是菱形继承
是由istream和ostream继承而来的
完