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

继承(Inheritance)

继承的基本概念

在面向对象编程(OOP)中,继承(Inheritance) 是一种允许一个类(称为派生类 / 子类)复用另一个类(称为基类 / 父类)的属性和方法,并可以在此基础上添加新特性或修改现有特性的机制。它是面向对象的三大核心特性(封装、继承、多态)之一,核心目的是代码复用建立类之间的层次关系

继承的核心概念

  • 基类(Base Class):被继承的类,也称为父类或超类(Superclass),包含可复用的属性和方法。
  • 派生类(Derived Class):继承基类的类,也称为子类(Subclass),可以继承基类的成员(成员变量和成员函数),并可新增自己的成员或重写基类的方法。
  • 继承关系:派生类与基类之间形成 “is-a”(是一个)的逻辑关系。例如,“狗” 是 “动物” 的一种,因此Dog类可以继承Animal类。

继承的基本语法

// 基类:动物
class Animal {
public:void eat() { cout << "动物吃东西" << endl; }void sleep() { cout << "动物睡觉" << endl; }
protected:string name; // 受保护的成员,派生类可访问
};// 派生类:狗(继承自动物)
class Dog : public Animal { // 公有继承
public:void bark() { cout << "狗叫:汪汪" << endl; } // 新增方法void setName(string n) { name = n; } // 访问基类的protected成员
};

在这个例子中:

  • Dog类通过public继承方式继承了Animal类的eat()sleep()方法和name成员。
  • Dog类新增了自己的方法bark(),并能访问基类的protected成员name

继承的主要作用

  1. 代码复用
    派生类无需重复编写基类中已有的代码,直接继承并使用基类的成员,减少冗余。例如,DogCat等类都能复用Animal类的eat()sleep()方法。

  2. 建立类层次
    通过继承可以构建类的层次结构,清晰表达类之间的关系。例如:
    生物 → 动物 → 哺乳动物 → 狗
    上层类更通用,下层类更具体。

  3. 支持多态
    继承是多态的基础。派生类可以重写(override)基类的方法,使得不同派生类对同一方法有不同实现,通过基类指针 / 引用调用时能动态选择具体实现。

继承的类型

根据继承方式的不同,基类成员在派生类中的访问权限会发生变化(如前文 “访问权限交集” 规则),主要有 3 种继承方式:

  • 公有继承(public):基类的public成员在派生类中仍为publicprotected成员仍为protected(最常用)。
  • 保护继承(protected):基类的publicprotected成员在派生类中均变为protected
  • 私有继承(private):基类的publicprotected成员在派生类中均变为private
基类成员原权限公有继承(public)保护继承(protected)私有继承(private)
publicpublic(取更宽松)protectedprivate
protectedprotectedprotectedprivate
private不可访问(基类私有成员任何继承方式都无法在派生类外访问)不可访问不可访问

继承方式相当于 “权限上限”,基类成员原权限若高于继承方式的权限,则会被 “降级” 到继承方式的权限;若低于或等于,则保持原权限。最终权限是两者中更严格的那个(private > protected > public)。

总结:派生类的访问权限如下:

  1. 不管什么继承方式,派生类内部都不能访问基类的私有成员;

  2. 不管什么继承方式,派生类内部除了基类的私有成员不可以访问,其他的都可以访问;

  3. 不管什么继承方式,派生类对象在类外除了公有继承基类中的公有成员可以访问外,其他的都不能访问。

保护继承和私有继承之间有什么区别呢?

以三层继承为例,如果中间层采用保护继承的方式继承顶层基类,那么在底层派生类中也能访问到顶层基类的公有成员和保护成员。

如果中间层采用私有继承的方式继承顶层基类,那么底层派生类中对顶层基类的任何成员都无法访问了。

继承关系的局限性

创建、销毁的方式不能被继承 —— 构造、析构

复制控制的方式不能被继承 —— 拷贝构造、赋值运算符函数

空间分配的方式不能被继承 —— operator new 、 operator delete

友元不能被继承(友元破坏了封装性,为了降低影响,不允许继承)

单继承下派生类对象的创建和销毁

有这样一种说法:创建派生类对象时,先调用基类构造函数,再调用派生类构造函数,对吗?

错误,创建派生类对象,一定会先调用派生类的构造函数,在此过程中会先去调用基类的构造

来看下面这段代码

#include <iostream>
using std::cout;
using std::endl;
class Base{
public:Base(){cout << "Base()" << endl;}~Base(){cout << "~Base()" << endl;}private:int _a = 10;int _b = 20;
};
class Derived : public Base{
public:Derived(){cout << "Derived()" << endl;}~Derived(){cout << "~Derived()" << endl;}
private:int _c = 30;int _d = 40;
};void test(){Derived d;cout << sizeof(Base) << endl;//8cout << sizeof(Derived) << endl;//16Derived * p = &d;int * pInt = (int *)p;cout << *pInt << endl;//10cout << *(pInt + 1) << endl;//20cout << *(pInt + 2) << endl;//30cout << *(pInt + 3) << endl;//40
}int main()
{test();return 0;
}

这段代码直观地证明了:

  • 继承时,派生类对象的内存包含基类成员(在前)和自身成员(在后)。派生类对象的内存空间是基类成员 + 自身成员的总和,基类成员在派生类对象中占据独立的内存空间。
  • 构造 / 析构顺序严格遵循 “基类先构造,派生类后构造;派生类先析构,基类后析构”。创建派生类对象会马上调用派生类的构造函数,但在初始化列表的最开始调用基类的构造函数, 可以理解为派生对象中包含了一个基类对象
  • 访问控制不改变成员的内存存在性,仅限制编译期的访问权限。

创建派生类对象时调用基类构造的机制

语法规则

1.当派生类中没有显式调用基类构造函数时,会自动调用基类的默认无参构造(或者所有参数都有默认值的有参构造);

2.此时如果基类中没有默认无参构造,Derived类的构造函数的初始化列表中也没有显式调用基类构造函数,编译器会报错

——不允许派生类对象的创建;

3.当派生类对象调用基类构造时,希望使用非默认的基类构造函数,必须显式地在初始化列表中写出。

#include <iostream>
using std::cout;
using std::endl;class Base{public:Base(int baseNumber):_baseNumber(baseNumber){cout << "Base(int)" << endl;}private:int _baseNumber;
};class ThirdObject{public:ThirdObject(){cout << "ThirdObject(int)" << endl;}};class Derived :public Base{public:Derived(int derivedNumber, int thirdNumber, int baseNumber):Base(baseNumber),_derivedNumber(derivedNumber),_base(baseNumber){cout << "Derived(int,int,int)" << endl;}private:ThirdObject _thirdObj;//持有基类对象成员Base _base;int _derivedNumber;
};void test(){Derived d(1, 2, 3);
}int main()
{test();return 0;
}

output

Base(int)          // 基类Base的构造(因Derived继承自Base)
ThirdObject(int)   // 成员对象ThirdObject的构造
Base(int)          // 成员对象Base(_base)的构造
Derived(int,int,int) // 派生类Derived自身的构造

这段代码的核心作用是演示对象构造的优先级顺序
基类构造 → 成员对象构造(按声明顺序) → 派生类自身构造

派生类对象的销毁

当派生类析构函数执行完毕之后,会自动调用基类析构函数,完成基类部分所需要的销毁(回收数据成员申请的堆空间资源)。

记忆:创建一个对象,一定会马上调用自己的构造函数;一个对象被销毁,也一定会马上调用自己的析构函数。

当派生类对象中包含对象成员        

如果此时派生类对象中包含对象成员,那么此时的调用顺序满足:

  • 1.完成派生类对象所占内存空间的开辟
  • 2.调用基类的构造函数,完成从基类吸收的数据成员的初始化
  • 3.完成子对象、const数据成员、引用数据成员的初始化
  • 4.执行派生类构造函数的函数体

对基类成员的隐藏

派生类中声明了和基类的数据成员同名的数据成员,就会对基类的这个数据成员形成隐藏。

派生类对象无法通过数据成员的名字直接访问基类的这个数据成员。

1. 成员变量的隐藏

当派生类定义了与基类同名的成员变量时,基类的同名变量会被隐藏。派生类中直接访问该变量时,默认访问的是派生类自己的变量;若要访问基类的同名变量,需用 基类名:: 限定。

示例:

    #include <iostream>
    using namespace std;class Base {
    public:int x = 10; // 基类的x
    };class Derived : public Base {
    public:int x = 20; // 派生类的x,隐藏基类的x
    };int main() {Derived d;cout << d.x << endl;       // 输出20(访问派生类的x)cout << d.Base::x << endl; // 输出10(显式访问基类的x)return 0;
    }

    如果一定要访问基类的这个数据成员,需要加上作用域,但是这种写法不符合面向对象的原则,不推荐实际使用

    2. 成员函数的隐藏

    当派生类定义了与基类同名的成员函数时,无论参数列表是否相同,基类的同名函数都会被隐藏。

    (1)函数名相同,参数列表不同(隐藏)
    class Base {
    public:void func(int a) { // 基类的func(带int参数)cout << "Base::func(int): " << a << endl;}
    };class Derived : public Base {
    public:void func() { // 派生类的func(无参数),隐藏基类的func(int)cout << "Derived::func()" << endl;}
    };int main() {Derived d;d.func();       // 正确:调用派生类的func()// d.func(10);  // 错误:基类的func(int)被隐藏,无法直接调用d.Base::func(10); // 正确:显式调用基类的func(int)return 0;
    }

    派生类对基类的成员函数构成隐藏,只需要派生类中定义一个与基类中成员函数同名的函数即可(函数的返回类型、参数情况都可以不同,依然能隐藏)。

    (2)函数名相同,参数列表相同(非虚函数时为隐藏,虚函数时为覆盖)
    • 若基类函数不是虚函数:派生类同名同参数函数会隐藏基类函数。
    • 若基类函数是虚函数:派生类同名同参数函数会重写(覆盖) 基类函数(多态的基础)。
    class Base {
    public:void func() { cout << "Base::func()" << endl; } // 非虚函数virtual void vfunc() { cout << "Base::vfunc()" << endl; } // 虚函数
    };class Derived : public Base {
    public:void func() { cout << "Derived::func()" << endl; } // 隐藏基类func()void vfunc() { cout << "Derived::vfunc()" << endl; } // 重写基类vfunc()
    };int main() {Derived d;Base* b = &d;b->func();  // 输出Base::func()(隐藏:非虚函数,按指针类型调用)d.func();   // 输出Derived::func()(直接访问派生类)b->vfunc(); // 输出Derived::vfunc()(重写:虚函数,按对象类型调用)return 0;
    }

    隐藏的核心特点

    1. 触发条件:派生类成员与基类成员同名(与参数、返回值无关,虚函数重写是特殊情况)。
    2. 访问规则:派生类中直接访问同名成员时,默认指向派生类自己的;访问基类同名成员需用 基类名:: 限定。
    3. 与重写的区别:重写仅针对虚函数且要求签名完全一致,隐藏则对所有同名成员生效。

    总结

    继承中的隐藏是编译器的名字查找规则导致的:当在派生类中查找某个名称时,编译器会先在派生类内部查找,找到后就停止向上查找基类的同名成员。这一机制可能导致意外屏蔽基类成员,因此实际开发中应尽量避免在派生类中定义与基类同名的成员(除非有意隐藏)。

    多继承

    在 C++ 中,多继承(Multiple Inheritance) 是指一个派生类可以同时继承多个基类的特性(成员变量和成员函数)。与单一继承(一个类只继承自一个基类)相比,多继承能更灵活地组合不同类的功能,但也会带来一些复杂性。

    多继承的基本语法

    派生类声明时,在基类列表中用逗号分隔多个基类,语法如下:

    class Derived : access-specifier Base1, access-specifier Base2, ... {// 派生类成员
    };
    

    其中 access-specifier 是继承方式(publicprotectedprivate),每个基类可以指定独立的继承方式。

    多继承的示例

    假设我们有两个基类 Student(学生)和 Worker(工人),可以通过多继承创建 StudentWorker(工读生)类,同时拥有两者的特性:

    #include <iostream>
    #include <string>
    using namespace std;// 基类1:学生
    class Student {
    protected:string _school; // 学校
    public:Student(string school) : _school(school) {}void study() { cout << "在" << _school << "学习" << endl; }
    };// 基类2:工人
    class Worker {
    protected:string _company; // 公司
    public:Worker(string company) : _company(company) {}void work() { cout << "在" << _company << "工作" << endl; }
    };// 派生类:工读生(同时继承学生和工人)
    class StudentWorker : public Student, public Worker {
    private:string _name; // 姓名
    public:// 初始化列表需同时初始化所有基类StudentWorker(string name, string school, string company): Student(school), Worker(company), _name(name) {}void introduce() {cout << "我是" << _name << "," << endl;study(); // 调用Student的方法work();  // 调用Worker的方法}
    };int main() {StudentWorker sw("小明", "清华大学", "字节跳动");sw.introduce();return 0;
    }
    

    输出结果:

    我是小明,
    在清华大学学习
    在字节跳动工作
    

    这个例子中,StudentWorker 同时继承了 Student 的 study() 方法和 Worker 的 work() 方法,实现了功能的组合。

    多继承的问题与解决

    多继承虽然灵活,但会引入一些特殊问题:

    1. 菱形继承(钻石问题)

    当两个基类继承自同一个间接基类,而派生类同时继承这两个基类时,会形成 “菱形” 结构,导致间接基类的成员在派生类中存在多份拷贝,引发二义性。

    // 间接基类
    class Person {
    protected:int _age;
    public:Person(int age) : _age(age) {}
    };// 基类1(继承自Person)
    class Student : public Person {
    public:Student(int age) : Person(age) {}
    };// 基类2(继承自Person)
    class Worker : public Person {
    public:Worker(int age) : Person(age) {}
    };// 派生类(同时继承Student和Worker)
    class StudentWorker : public Student, public Worker {
    public:// 需初始化两个基类,间接导致Person被初始化两次StudentWorker(int age1, int age2) : Student(age1), Worker(age2) {}void printAge() {// cout << _age << endl; // 错误:_age有两份,二义性cout << Student::_age << endl; // 需显式指定来自哪个基类cout << Worker::_age << endl;}
    };
    

    解决方法:使用虚继承(Virtual Inheritance)
    在基类继承间接基类时,用 virtual 关键字声明,确保间接基类在派生类中只存在一份拷贝:

    // 基类1:虚继承自Person
    class Student : virtual public Person { ... };// 基类2:虚继承自Person
    class Worker : virtual public Person { ... };
    

    此时 StudentWorker 中 _age 只有一份,初始化时需直接初始化间接基类 Person

    StudentWorker(int age) : Person(age), Student(age), Worker(age) {}
    

    采用虚拟继承的方式处理菱形继承问题,实际上改变了派生类的内存布局。B类和C类对象的内存布局中多出一个虚基类指针,位于所占内存空间的起始位置,同时继承自A类的内容被放在了这片空间的最后位置。D类对象中只会有一份A类的基类子对象。

    2. 成员名冲突

    当多个基类拥有同名成员时,派生类访问该成员会产生二义性,需显式指定来源。

    class A {
    public:void func() { cout << "A::func()" << endl; }
    };class B {
    public:void func() { cout << "B::func()" << endl; }
    };class C : public A, public B {
    public:void callFunc() {A::func(); // 显式调用A的func()B::func(); // 显式调用B的func()// func(); // 错误:二义性}
    };
    

    多继承的适用场景

    多继承适合 “组合多个独立功能” 的场景,例如:

    • 一个类需要同时具备多个不相关类的特性(如 “工读生” 同时具备 “学生” 和 “工人” 的特性)。
    • 实现 “接口组合”(在 C++ 中,接口通常是纯虚函数类,多继承多个接口可以组合不同的行为规范)。

    总结

    多继承的核心是让派生类同时拥有多个基类的功能,但也可能带来菱形继承(需用虚继承解决)和成员名冲突(需显式指定来源)等问题。实际开发中应谨慎使用多继承,优先考虑组合(将多个类作为成员对象)而非继承,以降低代码复杂度。

    基类与派生类之间的转换

    一般情况下,基类对象占据的空间小于派生类对象。

    (空继承时,有可能相等)

    1:可否把一个基类对象赋值给一个派生类对象?可否把一个派生类对象赋值给一个基类对象?

    2:可否将一个基类指针指向一个派生类对象?可否将一个派生类指针指向一个基类对象?

    3:可否将一个基类引用绑定一个派生类对象?可否将一个派生类引用绑定一个基类对象?

    引用和指针同理

    派生类对象间的复制控制(重点)

    复制控制函数就是 拷贝构造函数、赋值运算符函数

    原则:基类部分与派生类部分要单独处理

    (1)当派生类中没有显式定义复制控制函数时,就会自动完成基类部分的复制控制操作;

    (2)当派生类中有显式定义复制控制函数时,不会再自动完成基类部分的复制控制操作,需要显式地调用;

    如果Derived类的数据成员申请了堆空间,那么必须手动写出Derived类的复制控制函数,此时就要考虑到基类的复制控制函数的显式调用。

    (如果只是Base类的数据成员申请了堆空间,那么Base类的复制控制函数必须显式定义,Derived类自身的数据成员如果没有申请堆空间,不用显式定义复制控制函数)

    #include <string.h>
    #include <iostream>
    using std::ostream;
    using std::cout;
    using std::endl;
    //继承体系下的复制控制函数
    //这部分代码也需要会写,和之前学习的赋值运算符拷贝构造函数
    //同等的地位
    class Base{
    public:Base(const char * p):_pbase(new char[strlen(p) + 1]()){cout << "Base()" << endl;strcpy(_pbase, p);}~Base(){cout << "~Base()" << endl;if(_pbase){delete [] _pbase;_pbase = nullptr;}}//拷贝构造函数Base(const Base & rhs):_pbase(new char[strlen(rhs._pbase) + 1]()){cout << "Base(const Base &)" << endl;strcpy(_pbase, rhs._pbase);}//赋值运算符函数Base & operator=(const Base & rhs){cout << "Base::operator=" << endl;if(this != &rhs){delete [] _pbase;_pbase = new char[strlen(rhs._pbase) + 1]();strcpy(_pbase, rhs._pbase);}return *this;}//private:
    protected://采取深拷贝char * _pbase;
    };
    //第二阶段:派生类中定义一个指针数据成员
    class Derived : public Base{
    public:Derived(const char * base, const char * derived):Base(base),_pderived(new char[strlen(derived) + 1]()){strcpy(_pderived, derived);cout << "Derived(const char *)" << endl;}~Derived(){cout << "~Derived" << endl;if(_pderived){delete [] _pderived;_pderived = nullptr;}}//需要显示写出派生类的拷贝构造函数和赋值运算符函数//为什么在派生类的拷贝构造函数中,会去调用基类的无参构造函数呢?//在没有显示写出派生类的拷贝构造函数时,编译器会自动帮助//我们调用基类的拷贝构造函数完成对应的操作//但是一旦我们显示写出派生类的拷贝构造函数,但是却没有主动调用基类的//拷贝构造函数时,那么只能够调用基类的无参构造函数来进行初始化
    #if 0Derived(const Derived & rhs):Base(rhs)//调用基类的拷贝构造函数,向上转型,_pderived(new char[strlen(rhs._pderived) + 1]()){strcpy(_pderived, rhs._pderived);cout << "Derived(const Derived &)" << endl;}//赋值运算符函数//一定需要显式调用基类的赋值运算符函数Derived & operator=(const Derived & rhs){if(this != &rhs){//调用基类的赋值运算符函数Base::operator=(rhs);delete [] _pderived;_pderived = new char[strlen(rhs._pderived) + 1]();strcpy(_pderived, rhs._pderived);}return *this;}#endif   friend ostream & operator<<(ostream & os, const Derived & rhs);
    private:char * _pderived;
    };//输出流运算符一般需要加上const
    //1.不会在函数中被修改数据成员
    //2.可以绑定右值,可以输出右值
    //如果需要去访问函数,那么只能够访问const成员函数
    ostream & operator<<(ostream & os, const Derived & rhs){if(rhs._pbase){os << rhs._pbase;}if(rhs._pderived){os << "," << rhs._pderived;}return os;
    }void test(){Derived d1("hello", "world");cout << d1 << endl;Derived d2 = d1;cout << d2 << endl;
    }
    void test2(){Derived d1("hello", "world");Derived d2("c++", "python");d1 = d2;cout << d1 << endl;
    }int main()
    {test();return 0;
    }

    如果派生类没有定义自己的拷贝构造函数,那么对应的内存图示如上;此时派生类部分会采取浅拷贝的方式,但是随后会自动调用基类的拷贝构造函数完成基类部分的处理

    如果派生类拥有自己的指针数据成员,但是同时没有定义赋值运算符函数,那么便会发生上述现象,会存在内存泄漏以及double free的情况(问题是由于派生类部分的浅拷贝导致的,基类的部分是没有问题的)

    http://www.xdnf.cn/news/18331.html

    相关文章:

  • 机器学习集成算法与K-means聚类
  • Pytest 插件怎么写:从0开发一个你自己的插件
  • 14. 多线程(进阶1) --- 常见的锁策略和锁的特性
  • 【Protues仿真】基于AT89C52单片机的数码管驱动事例
  • Windows下,将本地视频转化成rtsp推流的方法
  • strcasecmp函数详解
  • AI模型部署 - 大语言模型(LLM)部署技术与框架
  • js来比较两个对象内容有误差异
  • mysql数据库学习
  • 想在手机上操作服务器?cpolar让WaveTerminal终端随身携带,效率倍增
  • 【Springboot进阶】Java切面编程对性能的影响深度分析
  • 【Ruoyi解密-02.登录流程:】登录-找密码不抓瞎
  • selenium3.141.0执行JS无法传递element解决方法
  • Linux的奇妙冒险——进程间通信(管道、SystemV IPC)
  • 完全背包(模板)
  • webrtc中win端音频---windows Core Audio
  • 2025图表制作完全指南:设计规范、工具选型与行业案例
  • Chrome/360 浏览器扩展深度解析:内置扩展与普通扩展的实现机制对比
  • (栈)Leetcode155最小栈+739每日温度
  • 力扣 30 天 JavaScript 挑战 第37天 第九题笔记 知识点: 剩余参数,拓展运算符
  • Spring Boot集成腾讯云人脸识别实现智能小区门禁系统
  • 【C++去除整数某一位数字求新数和倍数保留2位小数控制】2022-10-22
  • 人工智能 -- 循环神经网络day1 -- 自然语言基础、NLP基础概率、NLP基本流程、NLP特征工程、NLP特征输入
  • 打造医疗新质生产力
  • 如何用算力魔方4060安装PaddleOCR MCP 服务器
  • visual studio更改git提交的用户名和邮件
  • Seaborn数据可视化实战:Seaborn基础与实践-数据可视化的艺术
  • 高效处理NetCDF文件经纬度转换:一个纯CDO驱动的Bash脚本详解
  • [大模型微调]基于llama_factory用 LoRA 高效微调 Qwen3 医疗大模型:从原理到实现
  • WPF中UI线程频繁操作造成卡顿的处理