当前位置: 首页 > news >正文

cpp中的继承

一、继承概念

在cpp中,封装、继承、多态是面向对象的三大特性。这里的继承就是允许已经存在的类(也就是基类)的基础上创建新类(派生类或者子类),从而实现代码的复用。

如上图所示,Person是基类,Stu与Tea是派生类,Stu与Tea分别继承了基类中的对象,同时也有自己的类对象。


1.1派生类对基类的修改

派生类对象可以赋值给基类对象、基类指针、基类引用,这里的赋值只是把派生类中原本继承于父类的类对象赋值回去,对于派生类对象自己的类对象不会赋值。但是基类对象不能赋值给派生类对象。

如上图,派生类只能将基类中原有的(或者说继承过来的)_name和_gender赋值给父类,其余的无法赋值,如果是引用或指针,也是将派生类中基类对应的对象引用给或地址传给基类,基类修改时,子类也会受影响。

如上图,代码验证。注意,以上代码是在public继承时才会生效,如果换成protected时代码就会报错,protected继承下来的父类对象就是protected而非public,不支持修改的,private继承同理。


1.2父子类类成员变量、函数重名

当父类类成员变量名与子类成员变量名冲突时,默认时优先使用子类的。其实子列中也继承了父类中的重名变量,只不过将其隐藏,可以通过指定类成员名::变量名的方式访问。

再提一点,如果子类中没有实现Print函数而是依靠父类中的Print函数,那么打印结果会是这样的,如下图。

这是因为返回给父类的是一个Person类型的this指针,解引用访问的就是Person类中的_val.


当存在同名的函数名时,子类会调用自身的函数,也可以通过类名指定的方式进行访问。


如上图,这里A::func与B::func关系是隐藏,注意与函数重载区分(函数重载条件是同一作用域内函数名相同,参数列表不同构成重载)。


1.3派生类的默认成员函数

#include <iostream>
using namespace std;
class Person {
public://构造函数Person() :_name("张三") {cout << "Person()" << endl;}//析构函数~Person() {cout << "~Person()" << endl;}//拷贝构造Person(const Person& p1):_name(p1._name) {cout << "Person(copy construct)" << endl;}Person& operator=(const Person& p1) {cout << "operator=" << endl;if (this != &p1) {this->_name = p1._name;}return *this;}public:string _name;
};class Son :public Person {
public:Son(const char* name = "", const string id = "111"):_id(id){}void display() {cout << _name << " " << _id << endl;}
private:string _id;
};int main() {Son s1;s1.display();return 0;
}

子类继承父类时会调用父类的构造函数来初始化继承过来的成员,然后子类在初始化自己的成员,同理对于析构、拷贝构造、赋值重载等都是同理。

如上图,s1会对继承的成员调用其对应的类的构造函数,当然,这也是我没有自定义时会调用父类的构造函数对其进行构造。

那么如何进行自定义构造_name呢?

如上图所示,通过son的构造函数对s1进行实例化构造,但是对于从父类继承下来的_name进行自定义时需要注意的是,在初始化_name时我们不能通过直接初始化的方式进行构造(如38行代码,这是错误的),而是通过父类的构造函数对父类成员进行初始化。在上图中也可以看见代码在初始化列表时(代码36行)就会调用父类的构造函数对_name进行初始化。

当然,也可以不自定义,此时_name就调用父类默认的构造函数对其进行初始化(前提是父类要有全缺省的构造函数,不然代码就会报错)。也可以使用初始化匿名对象的方式完成。

如上图所示,同时也要在父类中定义相对应的构造函数类型。

实现子类对象的拷贝构造函数

如上图,在实现子类的拷贝构造函数时,可以用子类类型的s来实例化Person,(这就是切片:父类可以提取子类中从父类继承来的_name进行初始化通过参数来初始化基类成员)

实现子类对象的赋值重载函数

如上图,实现子类对象的赋值重载函数时需要指明具体是哪一个重载函数,否则就会出现死循环,因为子类和父类出现同名函数时会优先调用子类的函数。代码第61行将Son类对象s进行切片,然后调用父类Person的重载函数将s中父类的部分切给Person完成赋值重载。

1.4继承与友元的关系

如上图所示,父类A的友元函数为display,子类B继承了父类A,此时友元函数只能访问子类的公开成员,对于受保护和私有的则无法访问。

1.5继承与静态成员

如上图,父类A中定义的静态成员变量在整个继承体系中都是存在的。

1.6菱形继承

如下图,A是B和C的父类,D又同时继承了B和C,此时D中含有基类成员_d和父类B(_b)和父类C(_c),同时B和C又同时含有A(_a),因此我们在访问_a时需要指定类域。

在上述图中可见,在开辟空间时,内存中64~68是父类B的空间,其中存放了B::_a和B::_b,对应的值就是1和3;而6C~70是父类C的空间,其中存放的就是C::_a和C::_C,对应的值就是2和4,最后一个位置就是D::_d。

如上图,整个44~54是类对象D的空间。

造成代码冗余与二义性问题

在上述代码中,子类D会同时存储了两份A的继承,分别是继承B和C的,这个就造成了代码冗余与内存消耗;其次当D访问A中成员时必须要指定具体哪个类中的(无法通过d._a方式访问)。解决方法就是虚拟继承。


如上图,通过虚拟继承的方法可以直接访问d._a,其实这里的B和C共享同一分A的继承,也就是说代码第91和92行对_a的修改是对同一个对象的修改(这一点在代码运行过程中可以看出)。

如上图所示,不难发现虽然_a是类中共享的一份区域,但是C和B区域与非虚拟继承相比又多出一块区域(如上图中绿色区域所示)。在分析内存时,0x0078FEAC指向的位置是0x00929bf4,0x0078FEC0指向的位置是0x00929c00,其内存图如下图所示

如上图所示,虽然0x00929bf4与0x00929c00指向的位置内容为空,但是其后一个位置的0000000c从十六进制转换为十进制刚好是12,其实这也就是C到_a的偏移量,这个表叫做虚基表,而只想虚机表的指针叫做虚机表指针。


http://www.mrgr.cn/news/92249.html

相关文章:

  • 如何手动设置u-boot的以太网的IP地址、子网掩码、网关信息、TFTP的服务器地址,并进行测试
  • 计算机网络与通讯知识总结
  • 部署若依微服务遇到的坑
  • Android之图片保存相册及分享图片
  • blender bpy渲染禁用日志
  • 【前端基础】Day 1 HTML
  • 6层高速PCB设计入门第1~10讲
  • 首次使用WordPress建站的经验分享(一)
  • SQL笔记#函数、谓词、CASE表达式
  • 运行测试用例
  • Orange 开源项目 - 集成阿里云大模型
  • Redis速成(1)
  • 【Python LeetCode 专题】位运算
  • 图论算法篇:BFS宽度优先遍历
  • 【数据结构】链表中快指针和慢指针
  • Zap:Go 的高性能日志库
  • Ollama部署本地大模型DeepSeek-R1-Distill-Llama-70B
  • JavaWeb开发入门:从前端到后端的完整流程解析
  • Fetch API 与 XMLHttpRequest:深入剖析异步请求的利器
  • BUU40 [CSCCTF 2019 Qual]FlaskLight1【SSTI】