同名成员到底调用谁?C++ 隐藏规则你真的会吗?
欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到say−fall的文章

🌈say-fall:个人主页🚀专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》💪格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。
前言:
对于c++来说,有三大核心特性,是面向对象编程(OOP)的经典三要素:封装、继承、多态。这三个特性是 C++ 区别于纯面向过程语言(如 C)的核心,也是理解 C++ 面向对象思想的关键。之前利用类和对象的思想和STL中的适配器:queue和stack了解过封装,本篇文章就详细介绍一下继承这个特性
文章目录
正文:
一、 什么是继承?
继承(inheritance) 机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类( 基类 )特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称 派⽣类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤( 模板 ),继承是类设计层次的复⽤。
下面我们设计了两个类:Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣的独有成员函数是学习,⽼师的独有成员函数是授课。
classStudent{public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 voididentity(){// ...}// 学习 voidstudy(){// ...}protected: string _name ="peter";// 姓名 string _address;// 地址 string _tel;// 电话 int _age =18;// 年龄 int _stuid;// 学号 };classTeacher{public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 voididentity(){// ...}// 授课 voidteaching(){//...}protected: string _name ="张三";// 姓名 int _age =18;// 年龄 string _address;// 地址 string _tel;// 电话 string _title;// 职称 };intmain(){return0;}显然,有大量的重复代码出现,我们将这些重复代码,或者说公共成员放入一个person类中,用继承的方法来处理,就不需要重复定义了
classPerson{public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 voididentity(){ cout <<"void identity()"<< _name << endl;}protected: string _name ="张三";// 姓名 int _age =18;// 年龄 string _address;// 地址 string _tel;// 电话 };classStudent:publicPerson{public:voidstudy(){ cout <<"void study()"<< endl;}protected:int _stuid;};classTeacher:publicPerson{public:voidtesching(){ cout <<"void tesching()"<< endl;}protected: string _title;};intmain(){ Student s; Teacher t; s.identity(); t.identity();return0;}二、 继承的定义
定义的格式
下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以既叫基类/派⽣类,也叫⽗类/⼦类)

其实子类和父类比较好理解,那么继承方式是什么呢?


继承方式如图,和访问限定符有点类似,都有三种,下面我们来看一下继承基类访问方式的变化:

- 看起来花里胡哨的,其实规则蛮简单的:
public > protect > private 选其中小一点的 - 还有一些我们都知道的规则:使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。
- 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强。
以上我们继承的Person是一个普通的类,继承不只能继承普通类,还能继承类模板
继承类模板
namespace say_fall {//template<class T>//class vector//{};// stack和vector的关系,既符合is-a,也符合has-a template<classT>classstack:public std::vector<T>{public:voidpush(const T& x){// 基类是类模板时,需要指定⼀下类域, // 否则编译报错:error C3861: “push_back”: 找不到标识符 // 因为stack<int>实例化时,也实例化vector<int>了 // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到 vector<T>::push_back(x);//push_back(x);---->没有实例化,所以找不到}voidpop(){vector<T>::pop_back();}const T&top(){returnvector<T>::back();}boolempty(){returnvector<T>::empty();}};}intmain(){ say_fall::stack<int> st; st.push(1); st.push(2); st.push(3);while(!st.empty()){ cout << st.top()<<" "; st.pop();}return0;}注意到,这里用继承实现了一个stack类,而不是之前适配器的方法:用函数封装,这里就要解释一下,原来适配器实现用的是组合的方法,还可以用继承实现。
三、 基类和派生类之间的转换
继承就像是私有制极其严格的父子关系一样,有这么几条规则:
- public继承的派⽣类对象 可以赋值给 基类的指针 / 基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
- 基类对象不能赋值给派⽣类对象。
基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type Information)的dynamic_cast 来进⾏识别后进⾏安全转换。

- 类比:父亲给孩子的财产父亲是有权使用的,而孩子不能直接使用父亲的财产,必须经过父亲允许才可以
classPerson{protected: string _name;// 姓名 string _sex;// 性别 int _age;// 年龄 };classStudent:publicPerson{public:int _No;// 学号 };intmain(){ Student sobj;// 1.派生类对象可以赋值给基类的指针/引用 Person* pp =&sobj; Person& rp = sobj;// 派生类对象可以赋值给基类的对象是通过调用基类的拷贝构造完成的 Person pobj = sobj;//2.基类对象不能赋值给派生类对象,这里会编译报错 sobj = pobj;//报错:没有与这些操作数匹配的 "=" 运算符return0;}四、 继承的作用域
隐藏规则与重载比较
重载规则:
在同一作用域下的同名函数,在参数不同的情况下构成函数重载。
隐藏规则
- 在继承体系中基类和派⽣类都有 独⽴的作⽤域。
- 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
(在派⽣类成员函数中,可以使⽤基类::基类成员显⽰访问) - 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆 classPerson{protected: string _name ="小李子";// 姓名 int _num =111;// ⾝份证号 };classStudent:publicPerson{public:voidPrint(){ cout <<" 姓名:"<< _name << endl; cout <<" ⾝份证号:"<< Person::_num << endl; cout <<" 学号:"<< _num << endl;}protected:int _num =999;// 学号 };intmain(){ Student s1; s1.Print();return0;};相关选择题
classA{public:voidfun(){ cout <<"func()"<< endl;}};classB:publicA{public:voidfun(int i){ cout <<"func(int i)"<< i << endl;}};intmain(){ B b; b.fun(10); b.fun();return0;};1. A和B类中的两个func构成什么关系()
A. 重载 B. 隐藏 C.没关系
- 答案是 B ,回顾重载,其定义是一个作用域中的同名函数,而继承出来的子类和父类实际上是在两个作用域的,所以是隐藏
2. 上⾯程序的编译运⾏结果是什么()
A. 编译报错 B. 运⾏报错 C. 正常运⾏
- 首先我们直到调用的
func()是构成隐藏的,在调用b.fun();时候,却没有传入参数,很显然是编译错误。
五、 派生类默认成员函数

自己手动写一个类的过程中,我们了解过这6个默认成员函数,也就是即使我们自己不实现,编译器也会自己实现的,而继承的类中会自动生成吗?生成的规则又是什么样子的?
4个常⻅默认成员函数

这里说的四个默认构造函数是指构造、析构、赋值重载,拷贝构造:
- 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
- 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
- 派⽣类的
operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域 - 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。
- 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
- 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
- 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
classPerson{public:Person(constchar* 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;// 姓名 };classStudent:publicPerson{public:Student(constchar* 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;}//拷贝构造则要看类型,内置类型不需要自己实现,而有资源释放的则必须需要自己实现 Student&operator=(const Student& s){ cout <<"Student& operator= (const Student& s)"<< endl;if(this!=&s){// 构成隐藏,所以需要显⽰调⽤ Person::operator=(s);//operator=(s); 会触发递归,因为调用的是自己的operator=() _num = s._num;}return*this;}//赋值重载和拷贝构造是一样的~Student(){ cout <<"~Student()"<< endl;}//析构同样也和拷贝构造一样//有关于析构:析构子类时候会连带父类一起析构,所以父类不需要显式析构protected:int _num;//学号 };intmain(){ Student s1("jack",18); Student s2(s1); Student s3("rose",17); s1 = s3;return0;}- 本节完…