C++中的父继子承:继承方式实现栈及同名隐藏和函数重载的本质区别, 派生类的4个默认成员函数

C++中的父继子承:继承方式实现栈及同名隐藏和函数重载的本质区别, 派生类的4个默认成员函数

🎬 胖咕噜的稞达鸭个人主页

🔥 个人专栏: 《数据结构《C++初阶高阶》《算法入门》

⛺️技术的杠杆,撬动整个世界!


在这里插入图片描述


在这里插入图片描述

学习完本文,你将知道:(各位大佬预知答案几何请移步文章结尾!)

1. 当子类继承了父类,父类的私有成员在子类中是不可见的,所以父类的私有成员在子类中有没有被继承下来?
2. 子类对象一定比父类大?
3. 函数重载和函数隐藏的区别是什么?同名了有什么影响?
4. 派生类构造函数初始化列表的位置必须显式调用基类的构造函数,已完成基类部分成员的初始化?
5. 派生类构造函数先初始化子类成员,再初始化基类成员?派生类对象构造函数先调用子类构造函数,在调用基类构造函数?

接着来步入今天的正文:
面向对象三大特性:封装,继承,多态
我们之前学过了封装,类的定义是一个封装,迭代器实现也是一个封装,屏蔽了底层的实现细节。模板的使用也是一个封装。接下来讲解面向对象第二大特性:继承。

继承的定义:

假设大学学生和大学的老师,作为一个人的共性,都有姓名,住址和电话号码,但是不同的是,老师授课有职称,学生有学号,这是老师和学生不同的地方。所以我们可以将姓名,住址和电话号码封装在一个大类Person里面,将职称和学号分别封装在一个小的类Student,teacher里面。下面来实现代码:

classPerson{public:// 进入校园/图书馆/实验室刷二维码等身份认证voididentity(){ cout <<"void identity()"<<_name<< endl;}protected: string _name ="张三";// 姓名 string _address;// 地址 string _tel;// 电话private:int _age =18;// 年龄};classStudent:publicPerson{public:// 学习voidstudy(){// ...}protected:int _stuid;// 学号};classTeacher:publicPerson{public:// 授课voidteaching(){//...}protected: string title;// 职称};intmain(){ Student s; Teacher t;}
在这里插入图片描述

定义格式:

  1. 父类的private成员在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类外面还是类里面都不能去访问它。
    比如说:上方代码中,age在父类中被定义为私有成员,想要在子类中访问私有成员,代码会出现报错,所以父类的private在子类中不可见。
  2. 父类的private成员在子类中是不能被访问的,如果父类成员不想在类外直接被访问,但是需要在子类中能访问,就定义为protected。可见保护成员限定符是因为继承才出现的。
  3. 总结:父类的私有成员在子类中是不可见的,父类的其他成员在子类的访问方式==Min(成员在父类的访问限定符,继承方式),public>protected>private。
  4. 在实践中,一般都是public继承,很少使用protected/private继承。
  5. 使用class时默认的继承方式是private,使用struct时默认的继承方式是public
在这里插入图片描述

继承的类模板

之前用容器适配器实现了一个栈,在这里我们也可以用继承的类模板去实现一个栈。

如果在push成员函数中不指定类域会怎么样?

主函数中实例化了stack,同时也就实例化了vector,Keda::stack<int>st;只会实例化栈的构造函数,这里构造函数编译器自动产生了,所以在stack<int>实例化的时候,同时也实例化了vector <T>,将vector实例化为int 类型了,vector<int>,模板需要按需实例化,如果在void push(const T& x)中写push_back(x)会报错,push_back会向上找父类,没有找到就会报错,所以父类是类模板的时候,需要指定一下类域,否则编译器会报错,“push_back找不到标识符”。将push_back(x)改为vector<int>::push_back(x).
这里也就涉及模板的按需实例化
也即是说在测试中没有写栈的删除测试,而在成员变量中没有在pop()下面指定类域,反而不会报错。如果对删除进行测试,但是没有指定类域,就会出现问题。编译器用到了那个地方,就会调用哪个地方。

#include<vector>usingnamespace std;namespace Keda {template<classT>classstack:public std::vector<T>{public:voidpush(const T& x){vector<T>::push_back(x);}voidpop(){vector<T>::pop_back();}const T&top(){returnvector<T>::back();}boolempty(){returnvector<T>::empty();}};}intmain(){ Keda::stack<int>st; st.push(1); st.push(2);while(!st.empty()){ cout << st.top()<<" "; st.pop();}return0;}
在这里插入图片描述

用继承的方式实现一个栈,实现vector的栈,实现list的栈,实现deque的栈,我们可以用宏来进行替换。宏的原理就是一种替换,预处理之后就没有CONTAINER了,预处理之后就直接是相对应的栈了。想要实现vector就是vector,list就是list,deque就是deuqe.,不会存在CONTAINER。

//#define CONTAINER std::vector//#define CONTAINER std::list#defineCONTAINERstd::deque#include<vector>#include<list>#include<deque>usingnamespace std;namespace Keda {template<classT>classstack:publicCONTAINER<T>{public:voidpush(const T& x){CONTAINER<T>::push_back(x);}voidpop(){CONTAINER<T>::pop_back();}const T&top(){returnCONTAINER<T>::back();}boolempty(){returnCONTAINER<T>::empty();}};}intmain(){ Keda::stack<int>st; st.push(1); st.push(2);while(!st.empty()){ cout << st.top()<<" "; st.pop();}return0;}

父类和子类对象赋值兼容转换

  1. 前提是公有继承条件下,把子类对象中的父类对象的一部分切割出来拷贝给父类对象,也可以给父类的指针或者引用,引用会变成子类当中父类对象一部分的别名,也叫切片或者切割。切出来让父类去引用。
  2. 父类对象是不能赋值给子类对象的。
  3. 父类的指针或者解引用可以通过强制类型转换赋值给子类的指针或者引用,但是必须是父类的指针是指向子类对象时才是安全的。

继承中的作用域

#include<iostream>#include<string>usingnamespace std;classPerson{protected: string _name ="Keda";//姓名int _num =111;};classStudent:publicPerson{public:voidPrint(){ cout << _num << endl;//999//cout << Person::_num << endl;//111}protected:int _num =999;};intmain(){ Student s; s.Print();return0;}

上面代码会打印出999,而不是111,为什么?

如果子类和父类中有同名成员,这段代码中是_num,在Person中有,在Student中也是有的,子类成员将屏蔽父类对同名成员的直接访问,这种情况就叫隐藏。那如何打印出111,那就需要在子类中加:cout<<Person::_num<<endl.本质影响的是编译时的查找规则。

函数重载,要求在同一作用域,同一个域里面不能有同名的变量和函数,不同的域里面可以有不同的变量和函数,list中有Push_back,vector中也有push_back,不会互相影响。因为他们在不同的域里面。

同名隐藏:如果是成员函数的隐藏,只需要函数名相同就构成隐藏,不管参数,因为在不同的作用域。如果想调就需要指定作用域。
所以尽量不要定义同名成员!!!

来一道常考的选择题:

classA{public:voidfun(){ cout <<"func()"<< endl;}};classB:publicA{public:voidfun(int i){ cout <<"func(int i)"<< i << endl;//func(int i)10}};intmain(){ B b; b.fun(10); b.fun();return0;};

问题:
1.题目中AB的关系是什么?
隐藏关系,在同一个类中,出现同名函数就是同名隐藏,此时派生类会隐藏基类。

2.编译结果?
在类B指定一个对象b,用对象b去调func函数,初始化为10,结果打印出来是func(int i)10
因为基类被隐藏了,编译器调用的是派生类中的。
b.fun()会报错:这里想要去调用的是基类中的func(),由于被隐藏了,掉不出来就会报错,所以最好指定类域:修改成b.A::func();,程序才不会报错!!!

子类的默认成员函数

  1. 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用
    什么意思?
    类的默认生成的构造函数的行为:
    1.内置类型,默认构造函数不会对其进行初始化,其值是不确定的;
    2.对于自定义类型:默认构造函数会调用该自定义类型的默认构造函数来进行初始化;
    3.对于继承自父类的成员,默认构造函数会将父类成员视为一个整体,调用父类的默认构造 函数来进行初始化。
Student(constchar* name,int num,constchar* address):Person(name),_num(num),_address(address){}
  1. 拷贝构造时,对于内置类型会去调用值拷贝,对于自定义类型调用自定义类型的拷贝构造,对于父类调用父类的拷贝构造。父类调用父类的构造。
    严格说student拷贝构造默认生成的就够用了。如果有深拷贝的资源才需要自己实现。
Student(const Student& s):Person(s)//把子类对象传给父类的引用,_num(s._num){ cout <<"Student(const Student& s)"<< endl;}

赋值兼容规则:
在这一块子类对象要传给父类对象的引用,调用父类的拷贝构造,就要传父类的对象过去,但是在父类中只有_num,没有这个对象,所以我们传过去它也会自然切的。这里就是赋值兼容规则的体现。

那么我们要是在父类的初始化中写上Person(const char* name = "xxxxxx")_name(name)
{
cout << “Person()” << endl;
}
在拷贝构造的时候初始化列表不写 Person(s),是达不到拷贝构造的目标的,因为在传递的时候没有进行对于父类调用父类的拷贝构造。

  1. 子类的Operator=必须要调用父类的operator=完成父类的赋值。需要注意的是子类的operator=隐藏了父类的operator=,需要指定父类作用域。
    严格说student赋值默认生成的就够用了。如果有深拷贝的资源才需要自己实现。
Student&operator=(const Student& s){ cout <<"Student& operator= (const Student& s)"<< endl;if(this!=&s){// 构成隐藏,所以需要显示调用 Person::operator=(s);//这里要是不写Person::会报错!!! _num = s._num;}return*this;}

父类和子类的operator赋值会构成同名隐藏,所以要在operator=(s)前加Person::,指定类域。

  1. 析构,严格来说,子类析构默认生成的就够用了,自定义类型会调用自己的析构,父类可以看作一个特殊的自定义类型进行析构。
    父类中已经有了析构,在子类中继续析构有问题:析构函数都会被特殊处理成destructor(),所以在子类中的析构~Student()和 ~Person()会被处理成destructor(),这就构成了隐藏,所以子类的析构和父类的析构也构成隐藏关系。

规定: 不需要显示调用,子类析构之后,会自动调用父类进行析构。这样就保证了析构顺序,先子后父,显示调用取决于实现的人,不能保证。基类先初始化,派生类再初始化,析构的时候先析构派生类,接着析构基类。
后定义的需要先析构,
5. 子类的初始化先调用父类构造再调子类构造;
6. 子类对象析构清理先调用子类析构再调父类的析构;子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员,因为这样才可以保证子类对象先清理子类成员再清理父类成员的顺序。

classPerson{public:Person(constchar* 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;// 姓名};classStudent:publicPerson{public:Student(constchar* name,int num,constchar* address):Person(name),_num(num){}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); _num = s._num;}return*this;}~Student(){//~Person();}protected:int _num=1;//学号};intmain(){ Student s1();//Student s2(s1);//Student s3("rose", 17);//s1 = s3;return0;}

总结这一部分:构造,拷贝构造,赋值重载都要显示调用父类,但是析构不需要,因为析构顺序是先子后父,先定义的后析构

问答答案揭晓:

1. 答:私有的成员继承下来了,但是在子类中不可见。基类私有成员不能直接访问不是没有被继承,而是权限问题
2. 答:不一定,有可能子类只是改写父类的方法而已,并没有增加其自身的数据成员,则大小一样,故错误
3. 答:函数重载:在同一个类里面,函数必须是同名函数,但是函数参数类型不同,且成员变量不能同名,即使类型不同
函数隐藏:在不同类中使用同名函数,在基类和派生类中都使用同名,但是参数不同,派生类中会隐藏基类中同名的函数名
4. 如果父类有默认构造函数,此时就不需要
5. 顺序相反,先初始化父类,再是子类,在派生类对象构造时,先调用基类构造函数,后调用子类构造函数

在这里插入图片描述

Read more

Java 大视界 -- Java 大数据机器学习模型在电商用户画像构建与精准营销中的应用

Java 大视界 -- Java 大数据机器学习模型在电商用户画像构建与精准营销中的应用

Java 大视界 -- Java 大数据机器学习模型在电商用户画像构建与精准营销中的应用 * 引言: * 正文: * 一、电商用户画像构建的底层逻辑与数据基石 * 1.1 用户画像的四维数据体系 * 1.2 数据采集与预处理架构设计 * 二、Java 驱动的机器学习模型构建用户画像核心能力 * 2.1 协同过滤算法的工程化实现 * 2.2 聚类算法实现用户分群 * 三、精准营销系统的工程实践与行业案例 * 3.1 京东 “京准通” 智能营销平台 * 3.2 阿里巴巴 “千人千面” 推荐系统 * 四、系统性能优化与工程落地细节 * 4.1 高并发场景下的性能调优策略 * 4.2 模型全生命周期管理体系 * 结束语: * 🗳️参与投票和联系我: 引言: 嘿,亲爱的 Java

By Ne0inhk
java入门----JDK和IDEA下载安装环境搭建保姆级教学

java入门----JDK和IDEA下载安装环境搭建保姆级教学

文章目录 * 一、初识Java * 1.1什么是Java? * 1.2为什么要学Java? * 二、JDK的下载和安装 * 2.1环境的搭建 * 2.2检测是否安装成功 * 2.3环境变量 * 三、IDEA的下载和安装 * 四、第一个java程序 * 4.1先创建一个包 * 4.2编写第一个java代码 * 五、结语 一、初识Java 1.1什么是Java? Java是一门面向对象的编程语言,由Sun公司于1995年正式发布,其设计理念源于对C 语言的改进,摒弃了多继承和指针等复杂概念,实现了功能强大与简单易用的结合。(摘自百度百科) [百科链接]https://baike.baidu.com/item/Java/85979 1.2为什么要学Java? Java是一门成熟的编程语言,java的应用领域广: 1. 大数据开发

By Ne0inhk
Java WebFlux技术在百度地图深度检索集成中的实践应用

Java WebFlux技术在百度地图深度检索集成中的实践应用

目录 前言 一、WebFlux技术简介 1、WebFlux是什么 2、WebFlux有哪些组件 3、WebFlux的使用场景 二、WebFlux集成百度深度检索 1、Maven资源引入 2、业务层实现 3、控制层实现 4、程序启动 三、成果输出及对比 1、百度深度检索输出 2、DeepSeek检索输出 3、Kimi检索输出 四、总结 前言         随着地理信息技术的飞速发展以及移动互联网的普及,地图服务已成为人们日常生活中不可或缺的一部分。从出行导航到位置查询,从周边设施搜索到地理信息分析,地图服务的应用场景日益丰富。百度地图凭借其庞大的地理数据资源、精准的定位技术和强大的检索功能,为用户提供了全方位的地理信息服务。然而,对于众多企业和开发者而言,如何将百度地图的深度检索能力与自身业务系统或应用进行高效集成,以满足用户对地理信息检索的个性化需求,是一个极具挑战性且意义重大的课题。在之前的博文中,我们对百度地图的深度检索服务进行了详细的介绍,对如何使用DeepSeek和地图的结合进行了很好的实践,智绘未来:当 DeepSeek

By Ne0inhk
飞算 JavaAI 转 SpringBoot 项目沉浸式体验:高效开发在线图书借阅平台

飞算 JavaAI 转 SpringBoot 项目沉浸式体验:高效开发在线图书借阅平台

标签#JavaAI 在软件开发领域,高效且高质量的开发工具一直是开发者们追求的目标。飞算 JavaAI 作为一款新兴的 AI 辅助开发工具,以其独特的能力为 Java 开发带来了新的可能。本次,我借助飞算 JavaAI 进行在线图书借阅平台的开发,并将其转换为 SpringBoot 项目,沉浸式体验了飞算 JavaAI 在开发流程中的便捷与高效。 一、飞算 JavaAI 操作流程:从需求到项目的顺畅之旅 飞算 JavaAI 的操作流程非常清晰且人性化,极大地简化了传统开发中从需求分析到项目构建的繁琐步骤。 首先是理解需求阶段。我将在线图书借阅平台的需求进行拆解,包括用户管理、图书资源管理、借阅管理等 8 个关键点。飞算 JavaAI 能够快速识别这些需求要点,为后续的接口设计和表结构设计奠定基础。这一步给整个项目提供了清晰的蓝图,让我对项目的整体轮廓有了明确的认识,避免了后续开发中因需求不明确而产生的反复修改。 接着进入设计接口阶段,基于之前拆解的需求,飞算 JavaAI 自动生成了

By Ne0inhk