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

Python + BS4实战:手把手带你爬取商业数据

Python + BS4实战:手把手带你爬取商业数据

目录 一、bs4篇 1.bs4介绍 1.1 什么是BeautifulSoup4? 1.2 为什么选择BeautifulSoup4?       核心优势 2.bs4详解 2.1 首先下载bs4 2.2 接下来引入一个使用bs4的例子让我们快速熟悉它 2.3 运行结果 3.bs4使用实战案例 3.1 完整代码 3.2 为什么会影响翻页 3.3 反爬机制 3.4 已知信息 3.5 解决思路 3.6 结果展示 3.7 容易混淆的一点 3.8 图片爬虫 🌟 Hello,

By Ne0inhk
Python + AI:打造你的智能害虫识别助手

Python + AI:打造你的智能害虫识别助手

Python + AI:打造你的智能害虫识别助手 在农业生产中,病虫害是影响作物产量和品质的“隐形杀手”。传统的害虫识别依赖人工巡查,不仅耗时耗力,还容易因经验不足导致误判、漏判。而随着智慧农业的普及,AI技术正成为破解这一难题的关键——今天,我们就用Python从零搭建一个智能害虫识别助手,让电脑替你“火眼金睛”辨害虫,轻松搞定农作物病虫害预警! 一、为什么要做这个项目? 智慧农业的核心是“精准、高效、低成本”,而害虫识别正是其中的典型场景: * 对农户:无需专业植保知识,拍照就能识别害虫种类,快速匹配防治方案; * 对开发者:这是一个“小而美”的实战项目,覆盖AI开发全流程,从数据处理到模型部署,学完就能落地; * 技术价值:融合Python、深度学习、Web部署,是入门AI+垂直领域应用的绝佳案例。 这个项目不需要你有深厚的AI功底,只要掌握Python基础,跟着步骤走,就能做出一个能实际使用的智能识别工具。 二、项目核心技术栈 先明确我们要用到的工具,都是行业主流、

By Ne0inhk

Clang 17正式发布:C++26十大新特性你必须马上掌握

第一章:Clang 17正式发布:C++26新特性的整体概览 Clang 17 的正式发布标志着对 C++26 标准早期特性的全面支持迈出了关键一步。作为 LLVM 项目的重要组成部分,Clang 17 不仅提升了编译性能与诊断能力,更率先实现了多项处于提案阶段的 C++26 核心语言特性,为开发者提供了前沿的实验平台。 核心语言特性的演进 C++26 正在推进一系列旨在提升代码简洁性与安全性的变更。Clang 17 已初步支持以下关键特性: * 类模板参数推导(CTAD)在别名模板中的扩展应用 * 隐式移动的进一步放宽规则,减少不必要的拷贝操作 * 基于范围的循环支持初始化语句(类似 if 和 switch 的 init-statement) 模块化系统的增强 Clang 17 深化了对 C++20 模块的支持,并为 C+

By Ne0inhk
C++ 面试题常用总结 详解(满足c++ 岗位必备,不定时更新)

C++ 面试题常用总结 详解(满足c++ 岗位必备,不定时更新)

📚 本文主要总结了一些常见的C++面试题,主要涉及到语法基础、STL标准库、内存相关、类相关和其他辅助技能,掌握这些内容,基本上就满足C++的岗位技能(红色标记为重点内容),欢迎大家前来学习指正,会不定期去更新面试内容。  Hi~!欢迎来到碧波空间,平时喜欢用博客记录学习的点滴,欢迎大家前来指正,欢迎欢迎~~ ✨✨ 主页:碧波 📚 📚 专栏:C++ 系列文章 目录 一、C ++ 语法基础 🔥 谈谈变量的使用和生命周期,声明和初始化 🔥 谈谈C++的命名空间的作用 🔥  include " " 和 <> 的区别 🔥 指针是什么? 🔥 什么是指针数组和数组指针 🔥 引用是什么? 🔥 指针和引用的区别 🔥 什么是函数指针和指针函数以及区别 🔥 什么是常量指针和指针常量以及区别 🔥 智能指针的本质是什么以及实现原理 🔥 weak_ptr 是否有计数方式,在那分配空间? 🔥 类型强制转换有哪几种? 🔥 函数参数传递时,

By Ne0inhk