C++第十讲:继承
C++第十讲:继承
- 1.什么是继承
- 2.继承的使用
- 2.1定义格式
- 2.2继承基类成员访问访问方式的变化
- 2.3继承类模板
- 3.基类和派生类的转换
- 4.继承中的作用域
- 5.派生类的默认成员函数
- 5.1常见的默认成员函数
- 5.2构造函数
- 5.3析构函数
- 5.4拷贝构造
- 5.5赋值运算符重载
- 5.6实现一个不能被继承的类
- 6.继承与友元
- 7.继承与静态成员
- 8.多继承和菱形继承
- 8.1虚继承
- 8.2菱形继承的相关问题
- 8.3多继承中的指针偏移问题
- 9.继承和组合
1.什么是继承
继承是面向对象程序设计使代码可以复用的重要手段,它允许我们在保持原有类特性的基础上进行拓展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称为派生类:
//两个类中都有的变量:
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;
};
2.继承的使用
2.1定义格式
2.2继承基类成员访问访问方式的变化
我们知道,访问限定符和继承方式都有三种,所以这里就存在了9中组合方式:
下面我们要详细介绍一下关于继承的知识点:
1.基类private成员不管是以什么样的方式继承都是不可见的,这里的指的是在子类中不可用,但是物理上还是继承到了子类中,只是语法上限制了无论是在类外还是在类内都无法进行访问:
2.protected成员与private成员的不同在这里进行了体现:
protected成员不可以在类外进行访问,但是可以在子类中进行访问:
3.我们可以总结一下上面的表格中的规律:
基类的私有成员在派生类中都是不可见的,基类的其它成员在派生类中的访问方式为 == Min(成员在基类的访问限定符,继承方式),public > protected > private:
4.但是,表格到底是什么意思呢?我们来详细讲解一下:
5.使用关键字class时默认继承方式是private,使用public的默认继承方式是public,不过最好显式写出继承方式:
6.在实际应用中一般都是使用public继承,一般不是用protected和private继承,因为这两种继承方式继承下来的成员都只能在派生的类里面使用,实际中扩展维护性不强
2.3继承类模板
我们不仅仅可以继承普通类,也可以继承模板类,比如我们之前使用的vector实现栈:
这时我们就可以考虑使用继承来替代适配器的使用了(看上图),但是这里会出现的问题是:
这是因为:stack< int >实例化时,vector< int >也被实例化了,但是模板是按需实例化的,虽然vector< int >实例化了,但是里面的函数并没有实例化!所以如果需要指定类域使得vector中的push函数进行实例化,告诉编译器有这个函数,才可以使用这个函数!
我们再拓展一个例子:
3.基类和派生类的转换
public继承的派生类对象可以赋值给基类的指针/基类的引用,这个操作有个形象的说法叫做切片或者切割,意思就是把派生类中基类的那部分切出来,基类的指针或者引用指向的是派生类中切出来的基类那一部分
但是,基类对象不能够赋值给派生类对象:
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但是必须是基类的指针指向派生类的对象时才是安全的,这里我们后续再讲,我们现在只需要认为是不能够进行赋值就可以了:
4.继承中的作用域
我们之前知道,不同的作用域中可以创建同名变量,相同的作用域下是千万不能够创建同名的变量的
1.在继承体系中,基类和派生类都有独立的作用域
2.那么基类和派生类中可以存在同名成员,这时,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫做隐藏
这时如果我们想要访问到父类中的num的话需要指定作用域:
3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏:
4.所以说,在继承体系中,我们最好不要定义同名的成员
5.派生类的默认成员函数
总结:
5.1常见的默认成员函数
我们在之前就了解过了几个默认的成员函数,也就是自己不写,编译器也会帮忙生成的函数,我们先回忆一下之前的默认成员函数,然后再分析一下,在派生类中,这些默认成员函数是如何生成的:
5.2构造函数
知道了默认构造的使用了,那么默认构造的实现是怎么实现的呢?:
5.3析构函数
而且析构是先析构子类,再析构父类,构造相反,先构造父类
5.4拷贝构造
拷贝构造的使用和默认构造的使用是相同的:
5.5赋值运算符重载
5.6实现一个不能被继承的类
下面我们看一道题:实现一个不能被继承的类:
6.继承与友元
友元关系不能被继承,也就是说基类友元,无法访问派生类友元和保护成员:
7.继承与静态成员
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;
// 这里的运行结果可以看到非静态成员_name的地址是不一样的
// 说明派生类继承下来了,父派生类对象各有一份
cout << &p._name << endl;
cout << &s._name << endl;
// 这里的运行结果可以看到静态成员_count的地址是一样的
// 说明派生类和基类共用同一份静态成员
cout << &p._count << endl;
cout << &s._count << endl;
// 公有的情况下,父派生类指定类域都可以访问静态成员
cout << &Person::_count << endl;
cout << &Student::_count << endl;
return 0;
}
8.多继承和菱形继承
C++的继承,不仅仅可以一个子类继承一个父类,还可以一个子类继承多个父类,但是多继承就会产生菱形继承的问题:
8.1虚继承
为了解决菱形继承的问题,又整出了一个虚继承的概念,菱形继承的效率比较低,而且还不好用,所以我们千万不要设计出菱形继承!:
其实在IO库中,就有菱形继承的使用:
它们的底层实现为:
但是我们一定不要写!
那上面的这个继承是不是菱形继承呢?是的,并不只是规范的菱形的继承才是菱形继承,而是伪菱形继承也是菱形继承,那么这里的virtual关键字应该放在哪里呢?:
8.2菱形继承的相关问题
class Person
{
public:
Person(const char* name)
:_name(name)
{}
string _name; // 姓名
};
class Student : virtual public Person
{
public:
Student(const char* name, int num)
:Person(name)
, _num(num)
{}
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
public:
Teacher(const char* name, int id)
:Person(name)
, _id(id)
{}
protected:
int _id; // 职工编号
};
// 不要去玩菱形继承
class Assistant : public Student, public Teacher
{
public:
Assistant(const char* name1, const char* name2, const char* name3)
:Person(name3)
, Student(name1, 1)
, Teacher(name2, 2)
{}
protected:
string _majorCourse; // 主修课程
};
int main()
{
// 思考一下这里a对象中_name是"张三", "李四", "王五"中的哪一个?
Assistant a("张三", "李四", "王五");
return 0;
}
我们看上面的题,我们下面来分析一下:
所以菱形继承的坑很多,所以尽量不要使用菱形继承!
8.3多继承中的指针偏移问题
我们继续画图来理解一下:
9.继承和组合
1.继承是一种 is-a 的关系,也就是每一个派生类对象都可以说是一个特殊的基类对象
2.而组合是一种 has-a 的关系,假设B组合了A,每一个B种有的是一个A的对象
其它的具体事项我们看下面: