【C++】继承(1):深入理解和使用
目录
一 继承的概念与定义
1 继承的概念
2 示例
3继承定义
(1)定义格式
(2)继承方式与访问限定
(3)继承基类成员访问方式的变化
4 继承类模板
二 基类和派生类之间的转换
1 基础类型转换
2 基类和派生类转换
3 示例
三 继承中的作用域
1 隐藏规则
2 考察继承作用域相关选择题
四 派生类的默认成员函数
1 四个常见默认成员函数
2 核心生成机制
3 构造函数与析构函数
一 继承的概念与定义
1 继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复⽤。
继承的本质是类层次的复用
2 示例
假如我们需要写学生管理系统或者门禁管理系统,我们就需要定义多个类:如学生,老师,食堂阿姨等等。

我们发现类中间有些成员是重复的,可以理解为是公用的。我们把这部分公共类叫做父类/基类。
把每个部分独有的叫做子类/派生类
公共的特性,抽取出来,放到一个公共类(父类/基类)里面——

我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有⼀些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。
我们来实现一下:
class Student
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 void identity(){// ...}// 学习 void study(){// ...}
protected:string _name = "peter"; // 姓名 string _address; // 地址 string _tel; // 电话 int _age = 18; // 年龄 int _stuid; // 学号
};class Teacher
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 void identity(){// ...}// 授课 void teaching(){//...}
protected:string _name = "张三"; // 姓名 int _age = 18; // 年龄 string _address; // 地址 string _tel; // 电话 string _title; // 职称
};
int main()
{return 0;
}
我们把公共的类放到person中去,Student和teacher都继承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
1{
public:// 学习 void study(){// ...}
protected:int _stuid; // 学号
};class Teacher : public Person
{
public:// 授课 void teaching(){//...}
protected:string title; // 职称
};
int main()
{Student s;Teacher t;s.identity();t.identity();return 0;
}
3继承定义
(1)定义格式
下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以既叫基类/派⽣类,也叫⽗类/⼦类)

(2)继承方式与访问限定

(3)继承基类成员访问方式的变化

1. 基类的private成员在派生类中,无论采用何种继承方式,都是不可见的。这里的“不可见”意味着:基类的私有成员虽然会被继承到派生类对象中,但从语法层面,无论是在派生类内部还是外部,都不允许访问该成员。
2. 由于基类的private成员在派生类中无法访问,若希望某成员不在类外被直接访问,却能在派生类中被访问,可将其定义为protected。由此可见,保护成员限定符是因继承需求而产生的。
3. 对上述内容总结后可知:基类的私有成员在派生类中始终不可见;基类的其他成员在派生类中的访问方式,等于“成员在基类的访问限定符”与“继承方式”二者中的较小值(访问权限等级:public > protected > private)。
4. 使用class关键字时,默认的继承方式为private;使用struct时,默认的继承方式为public。不过,为清晰起见,最好显式写出继承方式。
5. 在实际应用中,通常采用public继承,很少且不提倡使用protected或private继承。因为通过protected/private继承得到的成员,仅能在派生类内部使用,会导致实际扩展和维护性较差。
4 继承类模板
namespace bit
{//template<class T>//class vector//{};// stack和vector的关系,既符合is-a,也符合has-a template<class T>class stack : public std::vector<T>{public:void push(const T& x){// 基类是类模板时,需要指定⼀下类域, // 否则编译报错:error C3861: “push_back”: 找不到标识符 // 因为stack<int>实例化时,也实例化vector<int>了 // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到 vector<T>::push_back(x);//push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}
int main()
{bit::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}
模版是按需实例化的,调用了哪个成员函数,就实例化哪个。
像这里,构造/析构/push_back会实例化,其他成员函数就不会实例化
二 基类和派生类之间的转换
1 基础类型转换
通常情况下我们把一个类型的对象赋值给另一个类型的指针或者引用时,存在类型转换,中间会产生临时对象,所以需要加const,如:
int a=l;
const double&d = a;
2 基类和派生类转换
public继承中,是一个例外,派生类对象可以赋值给基类的指针/基类的引用,而不需要加const,这里的指针和引用绑定的是派生类对象中的基类部分。也就意味着一个基类的指针或者引用,可能指向基类对象,也可能指向派生类对象。
public 继承的派生类对象可以赋值给基类的指针或基类的引用。这里有个形象的说法叫切片或者切割,意思是把派生类中属于基类的那部分切出来,基类指针或引用指向的就是派生类中切出来的基类部分。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但必须保证基类的指针指向的是派生类对象时,这种转换才是安全的。如果基类是多态类型,可以使用 RTTI(Run-Time Type Information,运行时类型信息)的 dynamic_cast 来进行识别后再进行安全转换。(注:这部分内容我们会在后面的类型转换章节单独专门讲解,这里先简单提及)


3 示例
class Person
{
protected :string _name; // 姓名 string _sex; // 性别 int _age; // 年龄
};
class Student : public Person
{
public :int _No ; // 学号
};
int main()
{Student sobj ;// 1.派⽣类对象可以赋值给基类的指针/引⽤ Person* pp = &sobj;Person& rp = sobj;// 派⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的 Person pobj = sobj;//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错 sobj = pobj;return 0;
}
三 继承中的作用域
1 隐藏规则
1. 在继承体系中,基类和派生类各自拥有独立的作用域。
2. 当派生类与基类存在同名成员时,派生类的成员会屏蔽基类中同名成员的直接访问,这种情况称为隐藏。(在派生类的成员函数中,可通过“基类::基类成员”的方式显式访问基类的同名成员)
3. 需要注意的是,对于成员函数的隐藏,只要函数名称相同就会构成隐藏,与参数列表等无关。
4. 实际应用中,在继承体系里最好不要定义同名成员,以避免混淆和错误。
示例:
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆
class Person
{
protected :string _name = "⼩李⼦"; // 姓名 int _num = 111; // ⾝份证号
};
class Student : public Person
{
public:void Print(){cout<<" 姓名:"<<_name<< endl;cout<<" ⾝份证号:"<<Person::_num<< endl;cout<<" 学号:"<<_num<<endl;}
protected:int _num = 999; // 学号
};
int main()
{Student s1;s1.Print();return 0;
};
2 考察继承作用域相关选择题
1、A和B类中的两个func构成什么关系()
A. 重载 B. 隐藏 C. 没关系
2、下面程序的编译运行结果是什么()
A. 编译报错 B. 运行报错 C. 正常运行

答案是:B A
解析:第一题很有可能会判断成函数重载,但是注意!:函数重载要求在同一作用域。但是显然,基类和派生类不在同一作用域。
第二题:b对应的是class B ,当发现参数不匹配的时候,会直接编译报错,而不是去基类中找
四 派生类的默认成员函数
1 四个常见默认成员函数

在派生类中,这几个函数怎么生成呢?
2 核心生成机制
1、派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2、派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3、派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的
operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域4、派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5、派生类对象初始化先调用基类构造再调派生类构造。
6、派生类对象析构清理先调用派生类析构再调基类的析构。
7、因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

3 构造函数与析构函数
class Person
{
public:Person(const char* name):_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; // 姓名
};
1、我们不写,默认生成的函数行为是什么?是否符合需求
2、不符合,我们要自己实现,如何实现?
本质:可以把派生类当做多一个的自定义类型成员变量(基类)的普通类——
class Student : public Person
{
public:Student(const char* name = "张三", int num = 18, const char* address = "武汉"):Person(name),_num(num),_address(address){cout << "Student()" << endl;}// 本质:可以把派生类当做多一个的自定义类型成员变量(基类)的普通类Student(const Student& s):Person(s), _num(s._num), _address(s._address){// 涉及到深拷贝,需要自己实现}Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_num = s._num;_address = s._address;}return *this;// 涉及到深拷贝,需要自己实现}~Student(){//// 不用显示调用基类析构,编译器会在派生类析构结束之后自动调用析构//Person::~Person();//// ...// 隐式原因:为了安全性cout << "~Student()" << endl;}protected:int _num; // 学号string _address; // 地址//int* _ptr;
};
1 继承的基类成员变量(整体对象)+ 自己的成员变量(遵循普通的规则,跟类和对象部分一样)
2 默认生成的构造,派生类自己的成员,内置类型不确定,自定义类型调用默认构造,基类部分调用默认构造
3 本质上可以把派生类当做多了自定义类型成员变量(基类)的普通类总,跟普通类原则一样。
4 派生类一般要自己的实现构造,不显示写析构、拷贝析构、赋值重载,除非派生类有深拷贝的资源。
析构构成了隐藏,需要指定类域(见处理机制第七点)---->为了补多态的坑

构造先父后子 析构先子后父

构造先父后子是因为:初始化列表的时候,是按照放在内存中的顺序放的。
析构先子后父是因为:如果先父的话,析构完了父就会销毁,但是子还能访问父,就会造成野指针
