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

【C++】继承机制全解析

1.继承的概念

继承机制是面向对象程序设计使代码可以复用的最重要的手段。它允许一个类(派生类)继承另一个类(基类)属性(成员变量)和方法(成员函数),同时可以在此基础上添加新的属性和方法,或重写基类的方法,从而实现代码复用和扩展。

class 派生类名 : 继承方式 基类名 {// 派生类新增的成员
};

2.继承的方式

C++通过继承方式控制基类成员在派生类中的访问权限,有三种继承方式,继承方式对应的访问权限如下表所示:

类成员 / 继承方式public 继承protected 继承private 继承
基类的 public 成员派生类的 public 成员派生类的 protected 成员派生类的 private 成员
基类的 protected 成员派生类的 protected 成员派生类的 protected 成员派生类的 private 成员
基类的 private 成员在派生类中不可见在派生类中不可见在派生类中不可见

总结一下,基类的私有成员不管什么继承方式在派生类中都是不可见;基类的其他成员在派生类的访问方式就是派生类的继承方式。在使用class关键字时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。在实际使用当中,一般使用public继承,几乎很少使用protetced/private继承,也不提倡使用,因为protetced/private继承下来的成员都只能在派生类的类中进行使用,实际中扩展维护性不强。

3.继承类模板

类模板是一种通用的类定义,它允许在创建类的时候使用类型参数。继承类模板指的是一个类模板从另一个类模板继承属性和方法,或者一个普通类继承类模板,以及类模板继承普通类等情况。这里就不详细展开讲解了,内容比较好理解,如果需要更深入的学习可在网上搜索。

4.基类和派生类间的转换

当派生类以public方式继承基类时,派生类对象可以赋值给基类指针或绑定到基类引用。这就是多态的基础!通过基类指针/引用统一操作派生类对象的“基类部分”,配合虚函数可实现“运行时多态”。

#include <iostream>
#include <string>
using namespace std;// 基类 Person
class Person
{
protected:string _name; // 姓名string _sex;  // 性别int _age;     // 年龄
};// 派生类 Student,public 继承 Person
class Student : public Person
{
public:int _No; // 学号
};int main()
{Student sobj; // 1. 派生类对象可以赋值给基类的指针/引用Person* pp = &sobj;Person& rp = sobj;// 派生类对象可以赋值给基类的对象(通过基类的拷贝构造)Person pobj = sobj;// 2. 基类对象不能赋值给派生类对象,此处会编译报错// sobj = pobj; return 0;
}

上述代码就演示了:派生类对象向基类指针/引用的隐式转换派生类对象向基类对象的拷贝构造(切片操作)基类对象无法直接赋值给派生类对象的限制。值得注意的是并非不能支持sobj=pobj的操作,如果需要可以为Student类自定义赋值运算符重载函数来实现显式转换逻辑。

5.继承的作用域

5.1继承机制的隐藏规则

在继承体系中基类和派生类都有独立的作用域;派生类和基类如果有同名函数,派生类成员将屏蔽对基类同名成员的直接访问,这种情况叫做隐藏。

class Base {
public:void func(int x) { cout << "Base::func(int)" << endl; }
};class Derived : public Base {
public:// 隐藏基类的 func(int),即使参数不同!void func(double x) { cout << "Derived::func(double)" << endl; }
};int main() {Derived d;d.func(10);   // 调用 Derived::func(double)(int 被隐式转 double)// d.func(10) 本想调用基类的 func(int),但被派生类的 func(double) 隐藏了!// 若要调用基类函数,需显式:d.Base::func(10);return 0;
}

上面的这段代码说明了派生类只要声明同名函数,就会隐藏基类的所有同名函数,必须显式调用才能访问基类版本。

总结一下继承的“隐藏规则”
1)派生类同名成员(变量 / 函数)会隐藏基类的同名成员,默认访问派生类自己的。
2)隐藏是作用域层级的覆盖,而非 “删除” 基类成员,需显式用(基类名::)访问被隐藏的基类成员。
3)实际开发中,应尽量避免同名成员,减少代码混淆风险;若无法避免,务必显式指定作用域。

6.派生类的默认成员函数

派生类默认成员函数的行为与基类的默认成员函数密切相关,其核心规则是派生类必须先初始化 / 清理基类部分,再处理自身成员。

6.1生成和调用规则

派生类的默认成员函数会自动调用基类对应的成员函数,以保证基类部分的正确初始化,拷贝或清理。
1)派生类构造函数:编译器生成的默认构造函数会先调用基类的默认构造函数(无参构造),在初始化派生类自己的成员变量(内置类型不初始化,自定义类型调用其默认构造)。注意:若基类没有默认构造函数(如用户定义了带参构造,编译器不在自动生成),派生类必须显式在初始化列表中调用基类的某个构造函数,否则会编译报错。

2)派生类析构函数:编译器生成的默认析构函数会先清理派生类自己的成员,再自动调用基类的析构函数(与构造函数顺序相反)。注意:基类析构函数建议声明为virtual(虚析构函数),确保删除派生类对象时,基类部分能被正确析构(避免内存泄露),虚函数在后面的多态会详细讲解。

3)派生类拷贝构造函数:编译器生成的默认拷贝构造函数会自动调用基类的拷贝构造函数(完成基类部分的拷贝),再拷贝派生类自己的成员变量。注意:若用户显式定义派生类拷贝构造,必须在初始化列表中显式调用基类的拷贝构造,否则会默认调用基类的默认构造函数(导致基类部分未正确拷贝)。

4)派生类赋值运算符重载:同理,会先调用基类的赋值运算符,再对派生类自己的成员变量赋值。跟拷贝构造同理,若用户显式定义派生类赋值运算符,必须在函数体内显式调用基类的赋值运算符,否则基类部分不会被赋值。

上面让注意必须显式调用的时候,其实原因就是我们上一个话题所说的继承的作用域隐藏问题。

7.继承注意

1)基类的构造函数私有,派生类构造必须调用基类的构造函数,但是基类的构造函数私有化以后,派生类看不见就不能调用了,派生类就无法实例化对象。C++11新增一个fonal,final修改基类,派生类就能继承该基类了。

2)注意基类的友元关系不能被继承,也就是基类友元不能访问派生类私有和保护成员。

3)基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论现在派生出多少个派生类,都只有一个static实例。原因时静态成员在本质上属于类而非类对象,在继承中,派生类会继承基类的成员,对于静态成员也不例外,虽然派生类继承了基类的静态成员,但本质时共享基类在静态存储区中已经分配好的那个静态成员实例,而不是创建一个新的静态成员。

8.多继承问题

一个派生类有两个或以上继承基类时称为多继承。虽然能提高代码复用性,当随之而来会带来一些问题。

8.1会出现的问题

1)数据冗余:当类A是基类,类B和类C都继承自A,类D同时继承B和C,此时D会间接拥有两份A的成员(分别来自B和C),A的成员在D中被存储了两次,浪费内存。

2)访问二义性:场景就是数据冗余的那个场景,当通过D访问A的成员时,编译器无法确定来自B还是C的继承路径,导致编译错误。

3)成员名冲突:当多个基类中存在同名成员(变量或函数),即使没有菱形结构,也会导致访问二义性。(想要解决就显式指定基类)

为了解决菱形继承相关问题:C++引入了虚继承机制,让派生类间接继承同一基类时,只保留一份基类成员,原理是通过一个间接指针(虚表类表指针)指向唯一的基类实例,确保派生类(D)中只存在一份A的成员。例如:

class A { public: int x; };// B和C虚继承A(关键:在中间类添加virtual)
class B : virtual public A {}; 
class C : virtual public A {}; class D : public B, public C {}; // D继承B和C

D中只会保留一份A的成员x,访问d.x不再有歧义。

虚继承的底层原理实现是通过虚基类表与偏移量虚继承的中间类(B和C)不再直接存储基类A的成员,将虚基类表存储在程序的只读数据段中,它是在编译期生成的静态只读数据,然后在对象中增加一个虚基类表指针指向虚基类表;虚基类表中存储了当前类对象到顶层基类(A)成员的偏移量,通过偏移量可找到唯一的A成员实例

不过我们要注意虚继承仅用于解决菱形继承问题,非菱形结构的多继承无需使用,否则会增加内存开销(虚基类表指针)和代码复杂度

我们要注意避免过多使用多继承优先使用单继承+接口:通过接口(纯虚函数)实现多态,减少多继承带来的复杂度;用组合替代继承:当需要复用多个类的功能时,可在派生类中定义其他类的对象(组合),而非继承它们,更灵活且降低耦合。

最后总结一下多继承问题,多继承的核心问题就是二义性和结构复杂性,通过虚继承、显式指定基类等方式可缓解,但是更推荐在设计中优先考虑单继承和组合,减少多继承的使用场景

指针自动偏移机制:

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

上述代码中三个指针的关系是啥?答案是p1==p3!=p2。三个指针明明都指向d,为什么不相同呢?这是因为Base2指针其实指向的是d对象的Base2子对象的起始位置,而Base1作为主基类,指针地址和派生类地址相同。这就是在多继承问题中指针自动偏移机制


本篇文章我们主要学习了C++中关于继承机制的知识,本文系作者本人根据自己学习笔记所作,如有错误,希望你能帮忙指出!万分感谢!希望我们共同进步!

http://www.dtcms.com/a/323017.html

相关文章:

  • Spring-rabbit使用实战七
  • 48伏电气系统—— 铺就电动出行之路的关键技术
  • 大语言模型中的幻觉
  • 24SpringCloud黑马商城微服务整合Seata重启服务报错的解决办法
  • 使用SymPy lambdify处理齐次矩阵的高效向量化计算
  • Poetry与UV——现代Python依赖管理的革新者
  • GitHub 趋势日报 (2025年08月08日)
  • java10学习笔记
  • EPI2ME分析软件测试
  • Java 8 特性
  • PG靶机 - Shiftdel
  • 计算机网络:CIDR地址块划分子网可以使用VLSM吗?
  • 使用 Vuepress + GitHub Pages 搭建项目文档(2)- 使用 GitHub Actions 工作流自动部署
  • [激光原理与应用-206]:光学器件 - SESAM - 基本结构与工作原理
  • “高大上“的SpringCloud?(微服务体系入门)
  • 关于灰度图像相似度的损失函数(笔记)
  • 【Datawhale AI夏令营】基于多模态RAG的企业财报问答系统
  • MySQL弹幕内容字段设计总结
  • Linux Makefile解析
  • 元宇宙技术如何改变社交方式?
  • MyBatis联合查询 - 注解篇
  • QT系统相关
  • gpt-oss 全量技术解读
  • Alibaba Cloud Linux 3 安装 git
  • 【Spring Boot启动流程底层源码详解】
  • kubectl get node k8s-node01 -o yaml | grep taint -B 5 -A 5
  • 如何理解SA_RESTART”被信号中断的系统调用自动重启“?
  • 腾讯COS云存储入门
  • 笔试——Day33
  • 基于遗传优化的稀疏线阵最优排布算法matlab仿真