C++ 继承详解:从入门到深入
继承是面向对象中实现类层次代码复用的机制在保留父类特性的基础上扩展出新功能由此产生的新类称为派生类。一、为什么需要继承1.1 问题引入假设我们要设计学生和老师两个类你会发现它们有很多共同的属性姓名、年龄、地址、电话等。如果没有继承代码会大量重复#include iostream #include string using namespace std; // 没有继承的写法 - 代码冗余严重 class Student { public: void identity() { cout 身份认证 _name endl; } void study() { cout _name 正在学习 endl; } protected: string _name 张三; int _age 18; string _address; string _tel; int _stuId; // 学号学生特有 }; class Teacher { public: void identity() { cout 身份认证 _name endl; } void teach() { cout _name 正在授课 endl; } protected: string _name 李老师; int _age 35; string _address; string _tel; string _title; // 职称老师特有 }; int main() { Student s; Teacher t; s.identity(); t.identity(); return 0; }1.2 继承解决方案使用继承将公共部分提取到基类Person中#include iostream #include string using namespace std; // 基类父类- 存放公共部分 class Person { public: Person(const string name 无名, int age 0) : _name(name), _age(age) { //cout Person构造函数: _name endl; } void identity() { cout 身份认证 _name endl; } void showInfo() { cout 姓名 _name 年龄 _age endl; } protected: string _name; int _age; string _address; string _tel; }; // 派生类子类- 继承Person class Student : public Person { public: Student(const string name, int age, int stuId) : Person(name, age), _stuId(stuId) { cout Student构造函数: _name endl; } void study() { cout _name 学号 _stuId 正在学习 endl; } protected: int _stuId; // 学号学生特有 }; class Teacher : public Person { public: Teacher(const string name, int age, const string title) : Person(name, age), // 直接一步到位 _title(title) { cout Teacher构造函数: _name endl; } void teach() { cout _name 职称 _title 正在授课 endl; } protected: string _title; // 职称老师特有 }; int main() { cout 创建学生 endl; Student s(小明, 18, 2024001); s.identity(); // 继承自Person s.study(); // Student自己的函数 s.showInfo(); // 继承自Person cout \n 创建老师 endl; Teacher t(王老师, 35, 教授); t.identity(); // 继承自Person t.teach(); // Teacher自己的函数 t.showInfo(); // 继承自Person return 0; }注意必须调用父类构造函数让父类自己初始化自己的成员// ✅ 正确唯一写法 Student(const string name, int age, int stuId) : Person(name, age), // 调用父类构造让父类初始化 _name _age _stuId(stuId) // 子类初始化自己的成员 {}我自己写的错误示例// ❌ 错误绝对不能这么写 Student(const string name, int age, int stuId) :_name(name), _age(age), _stuId(stuId)子类不能在初始化列表里直接给父类的成员变量赋值_name和_age是父类 Person 的成员父类成员必须由父类的构造函数来初始化子类无权在初始化列表里直接初始化父类成员二、继承的三种方式访问权限和继承权限是不同的概念1.访问权限成员本身的权限作用控制 “外面能不能直接用”public谁都能访问protected自己 子类能访问private只有自己能访问这是成员自己的属性跟继承没关系。2.继承权限继承方式作用控制 “继承过来后成员变成什么权限”三种继承方式public继承protected继承private继承它只改变成员在子类里的最终权限不改变基类本身。在类的继承中有以下几点特性1 基类的构造函数与析构函数不能被继承2 派生类对基类成员的继承没有选择权不能选则继承或不继承某些成员3 派生类中可以添加新成员用于实现新功能让派生类的功能在基类上有所扩展。2.1 访问限定符与继承方式的关系#include iostream #include string using namespace std; class Base { public: int pub 1; // 公有成员 protected: int pro 2; // 保护成员 private: int pri 3; // 私有成员 }; // 1. public继承 class PubDerived : public Base { public: void test() { cout pub endl; // ✅ 可以访问pub → public cout pro endl; // ✅ 可以访问pro → protected // cout pri endl; // ❌ 错误基类private成员在派生类不可见 } }; // 2. protected继承 class ProDerived : protected Base { public: void test() { cout pub endl; // ✅ 可以访问pub → protected cout pro endl; // ✅ 可以访问pro → protected // cout pri endl; // ❌ 错误不可见 } }; // 3. private继承 class PriDerived : private Base { public: void test() { cout pub endl; // ✅ 可以访问pub → private cout pro endl; // ✅ 可以访问pro → private // cout pri endl; // ❌ 错误不可见 } }; int main() { PubDerived pubObj; pubObj.pub 10; // ✅ public成员在类外可访问 // pubObj.pro 20; // ❌ protected成员类外不可访问 ProDerived proObj; // proObj.pub 10; // ❌ 变成protected后类外不可访问 PriDerived priObj; // priObj.pub 10; // ❌ 变成private后类外不可访问 cout 结论 endl; cout 实际开发中99%的情况使用public继承 endl; return 0; }总结1. private 成员子类永远无法直接访问不管什么继承方式。2. public 继承父类的访问权限原样保留public → 依然 publicprotected → 依然 protectedprivate → 依然不能直接访问你这句话说得特别到位“公有继承就是基类的成员在子类也有一份子类的访问权限和基类一样。”只是父类 private 永远碰不到构造析构不继承。3. protected 继承父类 public、protected → 在子类里都变成 protected正确父类 private子类永远无法直接访问4. private 继承父类所有可继承成员 → 在子类都变成 private 正确父类 private子类永远无法直接访问虽然父类 private子类永远无法直接访问但子类的成员函数可以调用父类的public,protected权限的 成员函数 通过父类间接访问例子class A { private: int c 100; // 父类私有 public: void show_c() { // 父类自己的函数可以访问 c cout c endl; } }; class B : public A { public: void func() { // c 200; ❌ 不能直接访问父类 private show_c(); // ✅ 可以调用父类 public 函数 // 间接访问 c } }; int main() { B a; a.show_c(); a.func(); }三、基类和派生类之间的转换切片#include iostream #include string using namespace std; class Person { public: Person(const string name ) : _name(name) {} string _name; int _age 0; }; class Student : public Person { public: Student(const string name, int stuId) : Person(name), _stuId(stuId) {} int _stuId; }; int main() { Student s(小明, 2024001); // 1. 子类对象赋值给父类对象切片 Person p1 s; // 只拷贝Person部分 cout p1._name: p1._name endl; // 小明 // 2. 子类对象赋值给父类指针指向子类中的父类部分 Person* p2 s; cout p2-_name: p2-_name endl; // 小明 // p2-_stuId; // ❌ 错误父类指针不能访问子类成员 // 3. 子类对象赋值给父类引用 Person p3 s; cout p3._name: p3._name endl; // 小明 // 4. ❌ 父类对象不能赋值给子类对象 // Student s2 p1; // 编译错误 // 5. 父类指针指向子类时可以强制转换回子类指针需要确保确实指向子类 Person* p4 s; Student* s2 (Student*)p4; // C风格强制转换 cout s2-_stuId: s2-_stuId endl; // 2024001 cout \n 切片示意图 endl; cout 子类对象: [Person部分][Student部分] endl; cout 父类指针: 指向 [Person部分] ← 切片 endl; return 0; }派生类对象可以直接赋值/初始化给基类对象 / 引用 / 指针派生类中独有的成员会被切掉只保留基类部分这就是对象切片。派生类切片 → 切掉独有部分只留基类成员安全向上转换。四、继承中的作用域隐藏规则#include iostream #include string using namespace std; class Person { public: Person() : _num(111) {} void print() { cout Person::print() endl; } void func(int x) { cout Person::func(int) x endl; } protected: string _name Person; int _num; // 身份证号 }; class Student : public Person { public: Student() : _num(999) {} void print() { cout Student::print() endl; } void func() { // 函数名相同参数不同也构成隐藏 cout Student::func() endl; } void show() { // 同名成员子类会隐藏父类的 cout 子类_num: _num endl; // 999 cout 父类_num: Person::_num endl; // 111显式指定 // 同名函数子类隐藏父类 print(); // 调用子类的 Person::print(); // 调用父类的 func(); // 调用子类的无参 // func(10); // ❌ 错误被隐藏了找不到 Person::func(10); // ✅ 显式指定可以调用 } protected: int _num; // 学号与父类同名 }; int main() { Student s; s.show(); cout \n 隐藏规则总结 endl; cout 1. 子类和父类有同名成员变量/函数子类会隐藏父类 endl; cout 2. 函数同名即构成隐藏不看参数 endl; cout 3. 可以用 父类::成员 显式访问被隐藏的成员 endl; cout 4. 建议尽量不要在子类定义同名成员 endl; return 0; }子类和基类的作用域不一样所以子类有和基类一样的成员变量或者成员函数时构不成重载重载要作用域一样根据就近原则用子类的除非显示调用父类的作用域不同 → 不能重载 → 同名就隐藏 → 就近用子类 → 父类加::五、派生类的默认成员函数完整示例这是面试中的高频考点#include iostream #include string using namespace std; class Person { public: // 构造函数 Person(const string name 无名) : _name(name) { cout Person构造函数: _name endl; } // 拷贝构造函数 Person(const Person p) : _name(p._name) { cout Person拷贝构造函数: _name endl; } // 赋值运算符重载 Person operator(const Person p) { cout Person赋值运算符: p._name - _name endl; if (this ! p) { _name p._name; } return *this; } // 析构函数 ~Person() { cout Person析构函数: _name endl; } protected: string _name; }; class Student : public Person { public: // 构造函数必须调用基类构造函数初始化基类部分 Student(const string name, int stuId) : Person(name) // 显式调用基类构造函数 , _stuId(stuId) { cout Student构造函数: name , 学号: _stuId endl; } // 拷贝构造函数必须调用基类拷贝构造 Student(const Student s) : Person(s) // 切片Student对象赋值给Person引用 , _stuId(s._stuId) { cout Student拷贝构造函数: _stuId endl; } // 赋值运算符必须显式调用基类的赋值运算符 Student operator(const Student s) { cout Student赋值运算符 endl; if (this ! s) { Person::operator(s); // 显式调用基类赋值运算符 _stuId s._stuId; } return *this; } // 析构函数会自动调用基类析构无需显式调用 ~Student() { cout Student析构函数: 学号 _stuId endl; // 会自动调用 ~Person() } protected: int _stuId; }; int main() { cout 1. 创建对象构造 endl; Student s1(小明, 1001); cout \n 2. 拷贝构造 endl; Student s2(s1); cout \n 3. 赋值运算 endl; Student s3(小红, 1002); s1 s3; cout \n 4. 销毁对象析构 endl; // 对象会按创建顺序逆序析构 return 0; }1子类对象 父类部分 子类自己部分子类构造函数初始化列表必须先调用父类构造函数用匿名对象的方法:父类名值不然父类的成员变量无法初始化上面说的的子类不能直接初始化父类成员除非你不用父类的成员变量。// 构造函数必须调用基类构造函数初始化基类部分 Student(const string name, int stuId) : Person(name) // 显式调用基类构造函数 , _stuId(stuId) { cout Student构造函数: name , 学号: _stuId endl; }name要初始化成学生自己的名字2 拷贝构造时s2 里面也包含父类的 name 变量父类部分必须由父类的拷贝构造来初始化所以子类必须调用父类的拷贝构造// 拷贝构造函数 Person(const Person p) : _name(p._name) { cout Person拷贝构造函数: _name endl; }// 拷贝构造函数必须调用基类拷贝构造 Student(const Student s) : Person(s) // 切片Student对象赋值给Person引用 , _stuId(s._stuId) { cout Student拷贝构造函数: _stuId endl; } cout \n 2. 拷贝构造 endl; Student s2(s1);这里的Person(s)还顺便复习了切片s是Student的引用参数匹配父类的拷贝构造这里的p看到的只是s里面的Person 部分切掉子类部分只保留父类部分如果不写Person(s)会发生什么编译器会自动调用父类的【默认构造】Person()而不是拷贝构造Student(const Student s) : Person() // 编译器自动插入 , _stuId(s._stuId) {}结果s2 的名字 无名s2 的学号 1001这就错了3构造子类必须在初始化列表调用父类构造拷贝构造子类必须调用父类拷贝构造赋值重载子类必须显式调用父类赋值重载Person::operator(s)析构不用调用编译器自动调用父类析构3赋值重载编译器不会自动帮你调用父类赋值必须手动写// 赋值运算符重载 Person operator(const Person p) { cout Person赋值运算符: p._name - _name endl; if (this ! p) { _name p._name; } return *this; } // 赋值运算符必须显式调用基类的赋值运算符 Student operator(const Student s) { cout Student赋值运算符 endl; if (this ! s) { Person::operator(s); // 显式调用基类赋值运算符 _stuId s._stuId; } return *this; }子类赋值 父类赋值 子类赋值缺一不可①切片发生了s是Student传给父类的operator(const Person p)编译器自动切片 →只把父类部分传给父类赋值函数②调用父类的赋值重载把_name正确赋值过去。如果不写子类学号赋值成功父类名字根本没赋值还是原来的值注意Person::operator(s); // 显式调用基类赋值运算符不加Person::子类和父类有同名函数 → 父类被直接隐藏发生无限递归栈溢出总结构造顺序基类 → 派生类析构顺序派生类 → 基类拷贝构造必须调用基类拷贝构造赋值运算符必须显式调用基类赋值运算符六、菱形继承与虚继承重点难点6.1 菱形继承的问题#include iostream #include string using namespace std; class Person { public: string _name Person; int _age 0; }; // Student和Teacher都继承Person class Student : public Person { public: int _stuId 0; }; class Teacher : public Person { public: int _teacherId 0; }; // Assistant同时继承Student和Teacher class Assistant : public Student, public Teacher { public: string _major 计算机; }; int main() { Assistant a; // ❌ 二义性_name有两个副本一份来自Student一份来自Teacher // a._name 张三; // 编译错误对_name的访问不明确 // ✅ 需要显式指定从哪个路径访问 a.Student::_name 张三学生身份; a.Teacher::_name 张三老师身份; // 数据冗余同一个Person对象有两份 cout Student::_name地址: a.Student::_name endl; cout Teacher::_name地址: a.Teacher::_name endl; cout 两个地址不同说明有两份数据 endl; // 内存大小包含两份Person的成员 cout \n对象大小: sizeof(Assistant) 字节 endl; // string(32字节) * 2份 64字节 其他成员 return 0; }对象内存布局分析Person / \ / \ Student Teacher \ / \ / Assistant编译器看见class Assistant : public Student, public Teacher它就严格按从左到右执行1. 先构造左边第一个Student构造 Student 必须先构造它的父类Person输出Person 构造然后构造 Student 自己输出Student 构造2. 再构造右边第二个Teacher构造 Teacher 必须先构造它的父类Person输出Person 构造又来一次然后构造 Teacher 自己输出Teacher 构造3. 最后构造自己Assistant输出Assistant 构造所以:_name存在两个不同的地址编译器不知道你要哪一个 →二义性错误数据冗余、浪费空间、容易出错6.2 虚继承解决菱形继承#include iostream #include string using namespace std; // 使用虚继承virtual class Person { public: Person() : _name(Person) { cout Person构造函数 endl; } string _name; int _age 0; }; // 虚继承Person class Student : virtual public Person { public: Student() : _stuId(0) { cout Student构造函数 endl; } int _stuId; }; // 虚继承Person class Teacher : virtual public Person { public: Teacher() : _teacherId(0) { cout Teacher构造函数 endl; } int _teacherId; }; class Assistant : public Student, public Teacher { public: Assistant() : _major(计算机) { cout Assistant构造函数 endl; } string _major; }; int main() { cout 菱形虚继承 endl; Assistant a; // ✅ 可以直接访问没有二义性 a._name 张三; cout 姓名: a._name endl; // ✅ 只有一份Person数据 cout a._name: a._name endl; cout a.Student::_name: a.Student::_name endl; cout a.Teacher::_name: a.Teacher::_name endl; cout 三个地址相同说明只有一份数据 endl; cout \n 构造顺序虚继承 endl; // 输出会显示构造顺序最远的基类先构造 return 0; }虚继承写在Student和Teacher上它的作用只有一个告诉编译器Student 和 Teacher 不要各自复制一份 Person而是共享同一份 Person所以cout a._name: a._name endl; cout a.Student::_name: a.Student::_name endl; cout a.Teacher::_name: a.Teacher::_name endl;他们地址完全一样。a.Student::_name意思是我要访问的是从 Student 继承下来的 _name作用虚继承 只有一份公共的 Person 基类让最终子类 Assistant 里Person 只保留一份不再有两份_name不再有二义性6.3 虚继承的构造顺序重要#include iostream using namespace std; class Grand { public: Grand() { cout Grand构造 endl; } }; class Base1 : virtual public Grand { public: Base1() { cout Base1构造 endl; } }; class Base2 : virtual public Grand { public: Base2() { cout Base2构造 endl; } }; class Derived : public Base1, public Base2 { public: Derived() { cout Derived构造 endl; } }; int main() { cout 虚继承构造顺序最远的虚基类最先构造 endl; Derived d; // 输出顺序 // Grand构造 ← 最远的基类 // Base1构造 // Base2构造 // Derived构造 return 0; }因为虚继承保证最高层基类只构造一份所以必须让最终孙子类Derived直接负责构造爷爷Grand不能让 Base1、Base2 各自去构造否则又会重复最远的基类由最终派生类直接构造只构造一次不会重复构造七、继承与友元、静态成员#include iostream using namespace std; class Person { public: friend void show(const Person p); static int cnt; Person() { cnt; } protected: string name Person; }; int Person::cnt 0; class Student : public Person {}; void show(const Person p) { cout 访问 p.name endl; } int main() { // 静态整个继承体系共用一个 Person p1, p2; Student s1, s2; cout 总数 Person::cnt endl; // 4 cout Student::cnt endl;; couts2.cntendl; cout 地址一样 Person::cnt Student::cnt endl; // 友元只作用于 Person不继承给 Student Person p; show(p); // 可以 // Student s; // show(s); // 不行友元不继承 }1 静态成员属于类不属于对象存贮在静态变量/全局区独一份所有对象共享父类、子类全都共用这一个static int cnt;不是每个 Person 自带一个 cnt是整个 Person 类、整个继承体系共用这一个所以创建 p1、p2、s1、s2 时每构造一次同一份 cnt结果自然就是42友元关系不能被继承父类友元函数不是子类的友元函数不能访问子类的私有保护变量八、继承 vs 组合设计原则#include iostream #include string #includevector using namespace std; // 组合示例has-a关系 // 轮胎类 class Tire { public: Tire(const string brand 米其林, int size 17) : _brand(brand), _size(size) { cout Tire构造: _brand _size 寸 endl; } void show() { cout 轮胎品牌: _brand , 尺寸: _size 寸 endl; } private: string _brand; int _size; }; // 发动机类 class Engine { public: Engine(int power 200) : _power(power) { cout Engine构造: _power 马力 endl; } void show() { cout Engine构造: _power 马力 endl; } private: int _power; }; class Car { public: // 给 tire 和 engine 也写上初始化 Car(const string brand) : _brand(brand), _tire(), _engine() { cout Car构造: _brand endl; } void show() { cout 汽车品牌: _brand endl; _tire.show(); _engine.show(); } private: string _brand; Tire _tire; Engine _engine; }; // 继承示例is-a关系 // 交通工具类 class Vehicle { public: virtual void run() { cout 交通工具在运行 endl; } }; // 汽车也是一种交通工具 - is-a关系 class Benz : public Vehicle { public: void run() override { cout 奔驰汽车在飞驰 endl; } void luxury() { cout 提供豪华配置 endl; } }; class BMW : public Vehicle { public: void run() override { cout 宝马汽车在狂飙 endl; } void sport() { cout 提供运动模式 endl; } }; // 实际开发建议 // 栈的实现既可以用继承也可以用组合 templatetypename T class StackByInheritance : public vectorT { // 继承方式 public: void push(const T val) { vectorT::push_back(val); } void pop() { vectorT::pop_back(); } T top() { return vectorT::back(); } }; templatetypename T class StackByComposition { // 组合方式推荐 public: void push(const T val) { _v.push_back(val); } void pop() { _v.pop_back(); } T top() { return _v.back(); } private: vectorT _v; // 组合一个vector }; int main() { cout 组合示例has-a endl; Car myCar(特斯拉); cout \n 继承示例is-a endl; Vehicle* v1 new Benz(); Vehicle* v2 new BMW(); v1-run(); // 多态 v2-run(); cout \n 设计原则 endl; cout 1. 优先使用组合耦合度低更灵活 endl; cout 2. 只有当确实是is-a关系时才使用继承 endl; cout 3. 需要多态时必须使用继承 endl; cout 4. 类之间的关系既适合继承也适合组合时优先用组合 endl; delete v1; delete v2; return 0; }注意为什么继承 vector 时必须写vectorT::push_back(val)模板本身不是真正的类 / 函数只是一张图纸。因为编译时父类还没生成所以调用父类函数必须写 父类T时编译器才会根据图纸生成真正的代码也就是按需实例化1. 两种关系最核心① 组合has-a有一个汽车有一个轮胎汽车有一个发动机写法类里面包含另一个类对象构造顺序组合:先构造成员对象 → 再构造自己1. 两种关系最核心 ① 组合has-a 有一个 汽车 有一个 轮胎 汽车 有一个 发动机 写法类里面包含另一个类对象② 继承is-a是一个奔驰是一种交通工具宝马是一种交通工具写法class Benz : public Vehicle2. 设计原则最重要优先使用组合少用继承组合耦合低、安全、灵活继承耦合高、会继承所有接口容易被乱用例子栈Stack用组合只开放 push/pop/top用继承能调用 vector 所有函数不安全终极口诀is-a 用继承has-a 用组合能组合绝不继承九、练习