C++(进阶) 第1章 继承
文章目录
- 前言
- 一、继承
- 1.什么是继承
- 2.继承的使用
- 二、继承方式
- 1.private成员变量的(3种继承方式)继承
- 2. private继承方式
- 3.继承基类成员访问⽅式的变化
- 三、基类和派生类间的转换
- 1.切片
- 四、 继承中的作⽤域
- 1.隐藏规则:
- 2.考察继承作⽤域相关选择题
- 五、派⽣类的默认成员函数
- 1.默认成员函数
- 2.派生类的默认成员函数
- 1.构造函数
- 2.析构函数
- 3.拷贝构造
- 4.赋值
- 总结
前言
在初级篇提过面向对象的三大特性:封装继承多态,在初阶篇可以非常直观的感受到封装是什么那么继承到底是什么呢?
一、继承
1.什么是继承
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触函数层次的复⽤,继承是类设计层次的复⽤。
举个例子:
假如我现在有三个类分别是:骑手 商家 用户 ,这三个类都有下面这些基础信息,这些基础信息太过于冗余重复,这个时候就可以定义一个公共类然后让这三个类去继承
就像这样
2.继承的使用
class person
{
public:void print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "张三";int _age = 18;
};
class Student: public person
{
protected:int _stuid;
};
假如现在有上面这俩个类,可以看到student
类里面是没有print函数的
可以看到这里s也是可以去调用print函数的
假如现在我们成员变量修改成public
这里可以看到俩个_name并不是一个,但是这里的成员函数是一个,可见父类和子类并不是同一个成员
但是成员函数是同一个,因为函数并不是存在一个对象里面,他们都是公用的
总结:继承本质上其实也是一种复用
二、继承方式
继承方式有三种和类里面的访问限定符是一样的
他们之间可以这样9组组合
这里可以把这表格堪称俩个部分
下面这个蓝色的可以看见都是不可见的,那么不可见是什么意思呢?
1.private成员变量的(3种继承方式)继承
private三种继承方式都是不可见这里只演示一个
class person
{
public:void print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
//private:string _name = "张三";int _age = 18;
};
class Student: public person
{
public:void func(){cout << _age << endl;}
protected:int _stuid;
};
int main()
{Student s;s.func();return 0;
}
比如说上面的代码,是可见的,运行以后就是这样,可以去访问
现在我把private加上,这里就不让继承的类用了,这个就叫不可见
这里有一个容易混淆的概念,不可见不代表没有继承
可以看到上面还是给继承了的,但是就是无法直接调用,但是这里可以简直的去调用它,这里就可以直接去调用父类的成员函数实现间接的去调用
2. private继承方式
在1的代码的基础上,我把public的基础方式改成private
可以看到原本的public的成员函数也会变成private
3.继承基类成员访问⽅式的变化
通过上面的例子可以发现访问权限和继承权限他们是取权限小的那个
public < protected < private
现在在回头看原来的那个表格
可以看到最后的结果全部都是取到了最小的权限哪里
三、基类和派生类间的转换
1.切片
现在假如有一个父类和一个子类
class person
{
public:void print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "张三";int _age = 18;};
class Student: public person
{protected:int _stuid;
};
现在思考这样几个问题
- 能否把子类赋值给父类呢?
- 子类的指针能否赋值给父类?
- 子类的引用能否赋值给父类?
答案是可以的
这里引入一个新的概念赋值兼容转换
为了方便理解可以看下面这个图片
一般情况下父类的东西会比子类的少,因为父类一般都是放基本信息的,子类一般都是继承了父类信息的基础上在添加了一些东西就会像上面这样
#define _CRT_SECURE_NO_WARNINGS
#include<bits/stdc++.h>
using namespace std;class person
{
public:void print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}string _name = "张三";
protected:int _age = 18;};
class Student: public person
{protected:int _stuid;
};
int main()
{person p;Student s;p = s;person* ptr = &s;person& ref = s;return 0;
}
现在我把保护去掉
可以发现上面的代码是没有报错的
值得注意的是这里并不是之前说的隐式类型转换这里是一个新的概念赋值兼容转换
如果子类赋值给父类那么编译器就会走特殊处理,他并不是传统类型的转换 ,但是这里父类不能给子类赋值
四、 继承中的作⽤域
1.隐藏规则:
- 在继承体系中基类和派⽣类都有独⽴的作⽤域。
- 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成藏。
- 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
假如现在我代码是这样的
class person
{
public:void print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:int _age = 18;string _name = "张三";};
class Student: public person
{protected:string _name = "李四";int _stuid;
};
我这里student继承了父类,那么子类其实这里的函数并不会直接拷贝过来,他们其实还是共用一个那么,这里再用子类去调用print函数,假如这个时候刚好子类和父类里面刚好有同名变量那么这个print函数会去调用那个呢?
首先这样写并不会报错这里会按照就近原则去查找
2.考察继承作⽤域相关选择题
class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){cout << "func(int i)" <<i<<endl;}
};
int main()
{B b;b.fun(10);b.fun();return 0;
};
1. A和B类中的两个func构成什么关系(B)
A. 重载 B. 隐藏 C.没关系2.下⾯程序的编译运⾏结果是什么(A)
A. 编译报错 B. 运⾏报错 C. 正常运⾏
五、派⽣类的默认成员函数
首先第一个问题:父类的构造函数子类能不能用?答案是:能
1.默认成员函数
这里就只考虑前四个后面俩个没有那么重要
2.派生类的默认成员函数
1.构造函数
假如现在这么一个代码
父类:
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 Studen :public Person
{
protected:int _x;string _addrss;
};
派生类在原来的基础上在加俩个成员变量一个内置类型一个自定义类型
派生类的默认成员函数可以这样看
- 父类成员(整体)
- 子类自己的自定义类型
- 子类自己的内置类型
只会调用默认构造函数
有变化的就只有1 下面的俩个都和普通的类一样
可以看到这里定义了一个子类,但是这里调用的是一个父类的构造和析构
从上面代码可以发现子类是会继承父类的构造函数和析构函数的
走完上面的1就直接去走23,内置初始化成随机值(不同编译器处理方式不一样)自定义就去找自定义的处理方式
值得注意的是这里只会调用默认构造带参数的构造是不会掉用的
假如说我现在要显示的写这个构造函数
它是不可以这么写的,这里报错是因为_name 是父类的成员函数
这里是编译器的规定记住就行了
假如说这里非要去显示调用构造函数这里就要这么写
2.析构函数
如果不写析构函数系统就会生成默认的析构函数,但是如果要显示的写
但是这里有这一个问题这里根本就编译不过
由于多态的原因析构函数会被编译器统一处理成destructorr()
,但是如果他们俩个都叫这个那么这里就会构成隐藏,所以这里连续调用了俩次就会报错
下面全部都是错误演示
所以这里要指定作用域
但是过了以后这里又开始扯淡了这里直接调用了这么多
这里可以发现多调用了,这里不得不说一个机制就是这里编译器做了处理子类析构函数里面会自动调用父类的析构,这里就是析构特殊的地方,其他几个都是显示切片调用就只有它是隐藏
3.拷贝构造
如果我们在子类里面不写拷贝构造那么编译器就会去调用父类的拷贝构造
所以一般情况下子类不需要写拷贝构造除非是设计到深拷贝
子类的就要这么写,子类调用拷贝构造就要去调用父类的拷贝构造就要这样去切片,因为这里要传一个父类过去但是我们没有所以我们这里就要用到上面说的复制兼容转换转一个自己(子类)编译器就会自己去切割
4.赋值
这里就是一个典型的隐藏错误
这里我是要子类切片去调用父类的拷贝,然后这里触发了上面说的隐藏,这里变成了自己调用自己,所以这里要指定作用域
Studen& operator=(const Studen& st)
{if (this != &st){operator=(st);_x = st._x;_addrss = st._addrss;}return *this;
}
Studen& operator=(const Studen& st)
{if (this != &st){Person:: operator=(st);_x = st._x;_addrss = st._addrss;}return *this;
}
总结
构造的顺序是先父后子,析构的顺序是先子后父,这里编译器做了强制处理