news 2026/4/25 12:24:08

C++继承机制详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++继承机制详解

C++中的继承是面向对象编程的核心概念之一,它允许新的类(派生类)获取已有类(基类)的属性和行为,从而实现代码的复用和扩展。本文将结合生动的比喻和详细的代码示例,全面讲解C++继承的机制,帮助你深入理解这一重要概念。

1. 继承的基本概念与意义

继承是面向对象编程的基石,它体现了类与类之间的"是一种"(is-a)关系。就像在现实生活中,孩子会继承父母的特征一样,在C++中,派生类(子类)可以继承基类(父类)的成员变量和成员函数。这种机制使得我们可以在保持原有类特性的基础上进行扩展,增加新功能,从而构建出层次化的类结构

继承的基本语法很简单,使用冒号:表示继承关系,后跟访问说明符(public、protected或private)和基类名:

class 派生类名 : 访问说明符 基类名 { // 派生类新增的成员 };

C++提供了三种继承方式,它们决定了基类成员在派生类中的访问权限:

  • public继承​:表示"是一种"关系,是最常用的继承方式
  • protected继承​:较少使用,基类的公有和保护成员在派生类中变为保护权限
  • private继承​:表示"依据...实现"的关系,基类的公有和保护成员在派生类中变为私有权限

不同的继承方式会导致基类成员在派生类中的访问权限发生变化,我们可以用以下表格总结这种变化规律:

基类成员权限public继承protected继承private继承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
private不可访问不可访问不可访问

表格:不同继承方式下基类成员在派生类中的访问权限变化

从表格中可以看出,基类的private成员在派生类中永远是不可访问的,无论采用何种继承方式。这就像家族秘密,只存在于基类内部,连派生类也无法直接接触。而其他成员的访问权限则遵循"​取更严格权限​"的规则,即比较成员在基类中的权限和继承方式,选择两者中更严格的那个

为了更好地理解这三种继承方式,让我们看一个具体的例子:

class Animal { public: int m_pub; // 公有成员,任何地方都可访问 protected: int m_pro; // 保护成员,仅类内和派生类可访问 private: int m_pri; // 私有成员,仅类内可访问 }; // public继承 - "是一种"关系 class Cat : public Animal { public: Cat() { m_pub = 1; // 正确:基类公有成员在派生类中仍为公有 m_pro = 1; // 正确:基类保护成员在派生类中仍为保护 // m_pri = 1; // 错误:基类私有成员在派生类中不可访问 } }; // protected继承 - 不常用 class Dogs : protected Animal { public: Dogs() { m_pub = 1; // 正确:基类公有成员在派生类中变为保护 m_pro = 1; // 正确:基类保护成员在派生类中仍为保护 // m_pri = 1; // 错误:基类私有成员在派生类中不可访问 } }; // private继承 - "依据...实现"关系 class Pig : private Animal { public: Pig() { m_pub = 1; // 正确:基类公有成员在派生类中变为私有 m_pro = 1; // 正确:基类保护成员在派生类中变为私有 // m_pri = 1; // 错误:基类私有成员在派生类中不可访问 } };

在实际编程中,​public继承最常用的方式,因为它真正体现了"是一种"关系。其他两种继承方式使用较少,因为它们会改变基类接口的访问权限,可能导致设计上的混乱

2. 继承中的构造与析构

在继承体系中,对象的构造和析构过程遵循特定的顺序。这就像建房子,需要从地基开始一层层向上建造,而拆除时则需要从屋顶开始一层层向下拆除

2.1 构造与析构的顺序

当创建派生类对象时,构造函数的调用顺序是:​基类构造函数 → 派生类构造函数。而析构函数的调用顺序则完全相反:​派生类析构函数 → 基类析构函数。这样的顺序确保了对象的正确初始化和清理

考虑以下代码示例:

class Animal { public: Animal() { cout << "Animal构造" << endl; } ~Animal() { cout << "Animal析构" << endl; } }; class Cat : public Animal { public: Cat() { cout << "Cat构造" << endl; } ~Cat() { cout << "Cat析构" << endl; } }; class CatBoss : public Cat { public: CatBoss() { cout << "CatBoss构造" << endl; } ~CatBoss() { cout << "CatBoss析构" << endl; } }; void Test() { CatBoss a; // 创建三层继承的对象 }

运行上述代码后,输出结果将是:

Animal构造 Cat构造 CatBoss构造 CatBoss析构 Cat析构 Animal析构

这个结果清晰地展示了构造的由内而外和析构的由外而内的顺序。在多重继承的情况下,构造函数的调用顺序取决于继承列表中基类的声明顺序,而不是它们在初始化列表中的顺序

2.2 派生类中构造函数的调用规则

在派生类的构造函数中,我们需要注意以下几点:

  • 派生类的构造函数必须调用基类的构造函数来初始化基类部分成员
  • 如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表中显式调用基类的构造函数
  • 派生类的拷贝构造函数和赋值运算符也需要调用基类的相应函数来完成基类部分的拷贝
class Person { public: Person(const char* name = "peter") : _name(name) { cout << "Person()" << endl; } Person(const Person& p) : _name(p._name) { cout << "Person(const Person& p)" << endl; } Person& operator=(const Person& p) { if (this != &p) _name = p._name; return *this; } ~Person() { cout << "~Person()" << endl; } protected: string _name; }; class Student : public Person { public: Student(const char* name, int num) : Person(name), _num(num) { cout << "Student()" << endl; } Student(const Student& s) : Person(s), _num(s._num) { cout << "Student(const Student& s)" << endl; } Student& operator=(const Student& s) { if (this != &s) { Person::operator=(s); // 调用基类的赋值运算符 _num = s._num; } return *this; } ~Student() { // 注意:不需要显式调用基类析构函数,它会自动调用 cout << "~Student()" << endl; } protected: int _num; };

需要特别注意的是,​析构函数不需要显式调用基类的析构函数,编译器会自动完成这一操作。而且析构函数应该声明为虚函数,以确保正确调用派生类的析构函数

3. 同名成员与多重继承

在继承体系中,有时会出现派生类与基类成员同名的情况,或者一个派生类继承自多个基类(多重继承)。这就像在一个大家族中,不同辈分的人可能有相同的名字,或者一个人同时从父母那里继承了不同的特征。

3.1 同名成员的隐藏规则

当派生类定义了与基类同名的成员(变量或函数)时,派生类的成员会隐藏基类的同名成员。这种情况下,要访问基类的成员,必须使用作用域解析运算符::来明确指定

class Animal { public: Animal() { m_date = 1; } int m_date; // 基类成员变量 void eat() { cout << "动物在吃东西" << endl; } }; class Cat : public Animal { public: Cat() { m_date = 8888645; } int m_date; // 派生类同名成员变量,隐藏基类的m_date void eat() { // 派生类同名成员函数,隐藏基类的eat() cout << "Cat在吃东西" << endl; } }; void Test() { Cat a; cout << a.m_date << endl; // 输出:8888645(派生类成员) cout << a.Animal::m_date << endl; // 输出:1(基类成员) a.eat(); // 调用派生类的eat() a.Animal::eat(); // 调用基类的eat() }

这种隐藏规则也适用于成员函数:只要函数名相同,就会形成隐藏,​不管参数列表是否相同。这与函数重载不同,重载要求在同一作用域内

3.2 多重继承的挑战与应对

C++支持多重继承,即一个派生类可以同时继承多个基类。这提供了更大的灵活性,但也带来了复杂性

class BaseA { public: int m_datea; int a; // 与BaseB中的a同名 BaseA() : m_datea(520) {} }; class BaseB { public: int a; // 与BaseA中的a同名 int m_dateb; BaseB() : m_dateb(1314) {} }; class BaseC { public: int a; // 与前两个基类中的a同名 int m_datec; BaseC() : m_datec(0) {} }; class Son : public BaseA, public BaseB, public BaseC { // 同时继承三个基类 }; int main() { Son s; s.m_datea = 1; // 正确:访问BaseA的成员 s.m_dateb = 2; // 正确:访问BaseB的成员 s.m_datec = 3; // 正确:访问BaseC的成员 // s.a = 10; // 错误:不明确,不知道访问哪个基类的a s.BaseA::a = 20; // 正确:明确指定访问BaseA的a s.BaseB::a = 10; // 正确:明确指定访问BaseB的a cout << &(s.BaseA::a) << endl; // 输出BaseA::a的地址 cout << &(s.BaseB::a) << endl; // 输出BaseB::a的地址(与上一个地址不同) cout << sizeof(s) << endl; // 输出对象s的大小 return 0; }

在这个例子中,Son类同时继承了三个基类,其中两个基类都有名为a的成员。当我们尝试访问s.a时,编译器会报错,因为它无法确定我们要访问哪个基类的a。解决方案是使用作用域解析运算符来明确指定。

多重继承可能引发的问题不仅仅是同名成员冲突,更严重的是菱形继承问题,我们将在下一节详细讨论。

4. 虚继承与菱形问题

菱形继承是C++多继承中的一个特殊问题,它就像家族树中的近亲结婚,可能导致基因紊乱。在编程中,这个问题表现为数据冗余和二义性。

4.1 菱形继承的问题

考虑以下继承结构:

class Animal { public: int age; }; class Cat : public Animal { // 继承自Animal }; class Dog : public Animal { // 继承自Animal }; class CatDog : public Cat, public Dog { // 同时继承Cat和Dog };

在这个结构中,CatDog类通过CatDog两条路径间接继承了两个Animal类。这意味着CatDog对象中会包含两份Animal的成员,导致以下问题:

  1. 数据冗余​:CatDog对象中有两份age成员,浪费内存空间
  2. 二义性​:当访问age时,编译器无法确定要访问哪一份age
CatDog cd; // cd.age = 5; // 错误:不明确,不知道访问Cat继承的age还是Dog继承的age cd.Cat::age = 3; // 需要明确指定 cd.Dog::age = 5; // 需要明确指定

4.2 虚继承的解决方案

C++提供了虚继承机制来解决菱形继承问题。虚继承确保无论虚基类在继承体系中出现多少次,派生类中都只包含一个共享的虚基类子对象

使用虚继承修改上述代码:

class Animal { public: int age; }; class Cat : virtual public Animal { // 虚继承 // ... }; class Dog : virtual public Animal { // 虚继承 // ... }; class CatDog : public Cat, public Dog { // ... };

现在,CatDog对象中只有一份Animal的成员,可以直接访问而不会产生二义性:

CatDog cd; cd.age = 5; // 正确:只有一份age成员,不存在二义性

虚继承的底层实现通常是通过虚基表指针来实现的。每个虚继承的派生类对象中包含一个指向虚基表的指针,表中存储了到虚基类子对象的偏移量。这样,无论派生层次多深,都能通过相同的机制找到共享的虚基类子对象

需要注意的是,虚继承会带来一定的性能开销,因为访问虚基类成员需要通过额外的间接寻址。因此,应该仅在解决菱形继承问题时使用虚继承,而不是作为常规继承方式

5. 继承的使用建议

在实际开发中,我们需要谨慎使用继承,特别是多重继承。以下是一些实用建议:

5.1 继承与组合的选择

  • 优先使用组合而非继承​:组合(在一个类中包含另一个类的对象)通常比继承更能降低类之间的耦合度
  • public继承表示"是一种"关系​:只有在派生类真正是一种基类的情况下才使用public继承
  • 谨慎使用多继承​:多继承会增加代码复杂度,优先使用单继承加组合的方式替代多继承

5.2 实用技巧与最佳实践

  1. 将析构函数声明为虚函数​:当类可能被继承时,应该将析构函数声明为虚函数,以确保正确释放资源

  2. 避免同名成员隐藏​:在继承体系中尽量避免定义同名成员,以免引起混淆。如果必须定义,使用作用域解析运算符明确调用

  3. 使用final关键字禁止继承​:C++11提供了final关键字,可以禁止类被继承

class A final { // 该类不能被继承 // ... };
  1. 接口隔离原则​:定义专门的接口类(只包含纯虚函数的类),让派生类实现这些接口,而不是继承庞大的具体类

  2. 合理使用虚继承​:只有在真正需要解决菱形继承问题时才使用虚继承,因为它会带来额外的性能开销

继承是C++面向对象编程的强大工具,但也需要谨慎使用。正确使用继承可以构建出清晰、灵活的类层次结构,而滥用继承则会导致代码难以维护。希望通过本文的讲解,你能更加深入地理解C++继承机制,并在实际项目中灵活运用。

"编程的本质是创造,而继承是我们创造的工具之一。掌握工具的使用方法,但不要被工具所限制。"

6.源代码(杂糅版选取复制)

#include<iostream> using namespace std; //继承 //语法:子类:public 父类 //子类->派生类 //父类->基类 /* public继承方式 公共:public 私有:private 保护:protect 3*3=9 | public | protected | private public: | public | protected |无法访问 protected | protected | protected |无法访问 private | private | private |无法访问 public:类内可以访问 protected:类内可以访问,类外不可访问,且子类可以访问 private:类内可以访问,类外不可访问,且子类不可以访问 */ /* 动物 / \ 猫 狗 */ class Animal { public: int m_pub; protected: int m_pro; private: int m_pri; public: void eat() { cout << "吃" << endl; } }; class Cat:public Animal { public: void sayHi() { cout << "喵喵喵" << endl; } Cat() { m_pub=1; m_pro = 1; //m_pri;//私有无法访问,父类私有,子公有,无法访问 } }; class BossCat :public Cat { BossCat() { m_pro = 2;//父类保护成员,子类还是保护成员 } }; void testCat() { Cat c; c.m_pub = 3; //c.m_pro;//私有,类内部可以访问,类外无法访问 } class Dogs:protected Animal { public: Dogs() { m_pro = 1; m_pub = 2;//如果私有那么 pro 无法访问 //m_pri = 3;// } }; class ProDog :public Dogs { public: ProDog() { m_pro = 1; m_pub = 2; } }; void testDogs() { Dogs c; /*c.m_pub = 3*/;//保护类外访问,保护 //c.m_pro;//私有,类内部可以访问,类外无法访问 } class Dog:public Animal {public: void sayHi() { cout << "汪汪汪" << endl; } }; class Pig :private Animal { public: Pig() { m_pub = 1; m_pub = 2; //m_pro;//私有,类内部可以访问,类外无法访问 } }; class Wildpig :public Pig { public: Wildpig() { /* m_pub = 1; m_pro = 2;*///都是私有是保护pub可以访问 } }; int main() { Animal a; Dog b; Cat c; a.eat(); b.sayHi(); c.sayHi(); return 0; } //构造和析构 //继承中,构造链里先构造后析构 class Animal {public: Animal() { cout << "Animal构造" << endl; } ~Animal() { cout << "Animal析构" << endl; } }; class Cat :public Animal {public: Cat() { cout << "Cat构造" << endl; } ~Cat() { cout << "Cat析构" << endl; } }; class CatBoss :public Cat {public: CatBoss() { cout << "CatBoss构造" << endl; } ~CatBoss() { cout << "CatBoss析构" << endl; } }; void Test() { /*Animal a;*/ /* Cat a;*/ CatBoss a; } int main() { Test(); return 0; } //继承同名属性访问,储存的内存不一样所以互不影响 class Animal { public: Animal() { m_date = 1; } int m_date; }; class Cat :public Animal { public: Cat() { m_date = 8888645; } int m_date; }; void Test() { Cat a; cout << a.m_date << endl; cout << a.Animal::m_date << endl; cout << "-------------------" << endl; cout << &(a.m_date) << endl; cout << &(a.Animal::m_date) << endl; } int main() { Test(); return 0; } //继承中同名函数的访问,调用看的是你实例化的是谁 class Animal { public: Animal() {}; void eat() { cout << "动物在吃东西" << endl; } }; class Cat:public Animal { public: Cat() {}; void eat() { cout << "Cat在吃东西" << endl; } }; int main() { Cat a; a.eat(); a.Animal::eat(); return 0; } //多继承 class BaseA { public: int m_datea; int a; BaseA() :m_datea(520) {}; }; class BaseB { public: int a; int m_dateb; BaseB() :m_dateb(1314) {}; }; class BaseC { public: int a; int m_datec; BaseC() :m_datec(0) {}; }; class Son :public BaseA, public BaseB, public BaseC { }; int main() { Son s; s.m_datea = 1; s.m_dateb = 2; s.m_datec; /* s.a = 10;*///不明确 //正确形式: s.BaseA::a = 20; s.BaseB::a = 10; cout << &(s.BaseA::a) << endl;//两个地址不同!!! cout << &(s.BaseB::a) << endl; cout << sizeof(s) << endl; return 0; }

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 20:58:38

解放双手:PT站一键转载工具完全指南

在PT&#xff08;Private Tracker&#xff09;社区中&#xff0c;内容分享和转载是日常运营的重要环节。然而&#xff0c;手动在不同站点之间转载内容往往耗时耗力。auto-feed项目应运而生&#xff0c;这是一个基于用户脚本的强大工具&#xff0c;专门为PT站点设计的一键转载解…

作者头像 李华
网站建设 2026/4/16 21:52:19

【C++】CMake 构建系统选择指南:从 MinGW 到 Ninja

CMake 构建系统选择指南&#xff1a;从 MinGW 到 Ninja 前言 在使用 CMake 进行 C 项目构建时&#xff0c;选择合适的构建系统至关重要。本文记录了从遇到 MinGW Makefiles 中文路径问题&#xff0c;到切换到 Ninja 构建系统的完整过程&#xff0c;并对比了三种主流构建系统的特…

作者头像 李华
网站建设 2026/4/23 17:19:56

学习日记day48

Day48_1211专注时间&#xff1a;6H33min&#xff0c;破纪录了&#xff0c;非常好每日任务&#xff1a;1h二刷2道力扣hot100(如果是hard&#xff0c;只做一道就好&#xff0c;完成情况及时长&#xff1a;1.5)&#xff1b;【学习资源&#xff1a;PyTorch官方文档&#xff1a;http…

作者头像 李华
网站建设 2026/4/23 19:21:55

谷歌开源“深度研究”Agent体系:碾压级性能、成本仅GPT‑5 Pro一成

【摘要】谷歌发布由Deep Research Agent、DeepSearchQA基准与Interactions API构成的完整技术栈&#xff0c;以SOTA性能与极低成本&#xff0c;重塑AI Agent的开发与应用范式。引言AI领域的发展正从模型能力的军备竞赛&#xff0c;转向应用价值的深度挖掘。当基础大模型的性能逐…

作者头像 李华
网站建设 2026/4/25 12:11:57

Venera漫画阅读器:打造你的个人数字漫画馆

Venera漫画阅读器&#xff1a;打造你的个人数字漫画馆 【免费下载链接】venera A comic app 项目地址: https://gitcode.com/gh_mirrors/ve/venera 还在为分散的漫画资源而苦恼&#xff1f;Venera漫画阅读器为你提供一站式解决方案&#xff0c;将本地收藏与在线资源完美…

作者头像 李华
网站建设 2026/4/23 18:52:46

boost库中boost::hash_combine和boost::hash_range使用

boost::hash_combine 1. boost::hash_combine 的作用 boost::hash_combine 是 Boost 库中用于组合多个哈希值的辅助函数。它通常用于自定义类型&#xff08;struct/class&#xff09;的哈希函数&#xff0c;用于像 std::unordered_map 或 std::unordered_set 这样的哈希容器。 …

作者头像 李华