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

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk