C++ 继承详解(上):从代码复用到切片与隐藏

发布时间:2026/5/28 14:58:56

C++ 继承详解(上):从代码复用到切片与隐藏 本文是我系统整理 C 面向对象三大特性的第一篇继承笔记。继承是多态的基础也是整个 OOP 体系里最容易写错、面试最爱考的部分。写这篇文章的目的一方面是把自己学到的东西梳理清楚另一方面也希望对看到这篇文章的人有所帮助。内容分上下两篇本篇覆盖继承基础、权限变化、切片原理、作用域与隐藏下篇覆盖派生类默认成员函数、虚继承底层、菱形继承以及继承与组合的设计取舍。为什么需要继承在没有继承之前假设我们要设计Student和Teacher两个类会发现两个类里有大量重复的成员姓名、地址、电话、年龄以及身份认证函数identity()。每个类都单独写一遍这叫代码冗余改一处就要改两处维护起来很痛苦。class Student { public: void identity() { /* 刷二维码认证 */ } void study() { /* ... */ } protected: string _name peter; string _address; string _tel; int _age 18; int _stuid; // 学号Student 独有 }; class Teacher { public: void identity() { /* 刷二维码认证 */ } // 和 Student 完全重复 void teaching() { /* ... */ } protected: string _name 张三; int _age 18; string _address; string _tel; string _title; // 职称Teacher 独有 };继承就是为了解决这个问题。把公共部分抽到基类PersonStudent和Teacher各自继承Person只需要写自己独有的部分公共成员自动复用。class Person { public: void identity() { cout void identity() _name endl; } protected: string _name 张三; string _address; string _tel; int _age 18; }; class Student : public Person { public: void study() { /* ... */ } protected: int _stuid; // 学号 }; class Teacher : public Person { public: void teaching() { /* ... */ } protected: string _title; // 职称 }; int main() { Student s; Teacher t; s.identity(); // 直接调用从 Person 继承来的函数 t.identity(); return 0; }继承的本质函数层次的复用是把代码抽成函数继承是类设计层次的复用把公共接口和属性抽到基类。继承的定义语法class 派生类名 : 继承方式 基类名Person叫基类也叫父类Student叫派生类也叫子类。两种叫法都对是翻译来源不同导致的。继承方式有三种public、protected、private。class Student : public Person { /* ... */ }; // public 继承最常用 class Student : protected Person { /* ... */ }; // protected 继承 class Student : private Person { /* ... */ }; // private 继承一个容易忽略的默认行为class关键字定义的类默认继承方式是privatestruct定义的类默认是public。实际写代码时建议显式写出继承方式不要依赖默认值可读性更好也不容易出错。继承后成员访问权限的变化这是继承里最基础也最容易搞混的部分先看完整的变化表基类成员 / 继承方式public 继承protected 继承private 继承基类 public 成员派生类 public派生类 protected派生类 private基类 protected 成员派生类 protected派生类 protected派生类 private基类 private 成员派生类不可见派生类不可见派生类不可见表格看起来复杂其实记一个规律就够了派生类成员的访问权限 Min(基类成员权限, 继承方式)其中public protected private。举几个例子验证基类public成员用protected继承Min(public, protected) protected所以在派生类里变成 protected。基类protected成员用public继承Min(protected, public) protected仍然是 protected。基类private成员无论什么继承方式结果永远是不可见。关于 private 成员不可见的准确含义不可见不等于没继承过来。基类的 private 成员实际上已经存在于派生类对象的内存中只是语法上不允许派生类在类内或类外访问它。这个区别在面试里经常被问到。protected 存在的意义如果一个成员既不想被外部直接访问不能是 public又需要在派生类内部访问不能是 private就用 protected。可以说 protected 限定符是专门为继承而生的。实际开发里几乎只用 public 继承。protected 和 private 继承下来的成员只能在派生类内部使用对外完全封闭扩展性差实际项目里极少见到。继承类模板基类是类模板时派生类在调用基类成员函数时需要显式指定类域否则编译器找不到。namespace bit { templateclass T class stack : public std::vectorT { public: void push(const T x) { // 必须写 vectorT::push_back(x) // 不能直接写 push_back(x)编译器会报错找不到标识符 vectorT::push_back(x); } void pop() { vectorT::pop_back(); } const T top() { return vectorT::back(); } bool empty() { return vectorT::empty(); } }; }原因在于模板是按需实例化的。stackint实例化时虽然也实例化了vectorint但push_back等成员函数在没有显式调用的情况下未被实例化编译器在当前作用域找不到必须通过vectorT::指定类域告诉编译器去哪里找。基类与派生类之间的转换这部分有三条规则方向不对称很容易混淆。规则一派生类对象可以赋值给基类的指针或引用切片class Person { protected: string _name; string _sex; int _age; }; class Student : public Person { public: int _No; // 学号 }; int main() { Student sobj; Person* pp sobj; // OK基类指针指向派生类对象 Person rp sobj; // OK基类引用绑定派生类对象 Person pobj sobj; // OK派生类对象赋值给基类对象调用基类拷贝构造 return 0; }这个过程有个形象的名字叫**切片slice**或切割。意思是把派生类对象中属于基类的那部分切出来让基类指针或引用指向这一部分。切片只在 public 继承下成立。private 和 protected 继承不支持这种隐式转换因为它们破坏了派生类 is-a 基类的语义。规则二基类对象不能赋值给派生类对象sobj pobj; // 编译报错道理很简单基类对象里没有派生类独有的成员比如_No强行赋值过去那部分数据从哪来所以编译器直接拒绝。规则三基类指针或引用可以强转为派生类指针或引用但有条件Person* pp sobj; Student* ps (Student*)pp; // 安全因为 pp 本来就指向 Student 对象只有当基类指针本来就指向的是派生类对象时这种强转才安全。如果基类指针指向的是一个纯基类对象强转后访问派生类成员会产生未定义行为。如果基类是多态类型可以用dynamic_cast做运行时安全识别这个放到类型转换章节再讲。继承中的作用域与隐藏继承体系中基类和派生类各自有独立的作用域。当两个类里出现同名成员时就会触发隐藏规则。什么是隐藏派生类中的同名成员会屏蔽基类同名成员的直接访问这种情况叫隐藏也叫重定义。class Person { protected: string _name 小李子; int _num 111; // 身份证号 }; class Student : public Person { public: void Print() { cout 姓名 _name endl; cout 身份证号 Person::_num endl; // 加类域才能访问基类的 _num cout 学号 _num endl; // 直接访问的是 Student 自己的 _num } protected: int _num 999; // 学号和 Person::_num 同名构成隐藏 };Student::_num把Person::_num隐藏了。在Student内部直接写_num访问到的是学号 999如果要访问身份证号 111必须显式加上Person::_num。隐藏和重载的区别重载要求在同一作用域且函数名相同但参数列表不同。隐藏发生在不同作用域父子类只要名字相同就构成隐藏参数列表是否相同不影响结论。成员函数的隐藏只看函数名不看参数class A { public: void fun() { cout func() endl; } }; class B : public A { public: void fun(int i) { cout func(int i) i endl; } // B::fun(int) 隐藏了 A::fun()两者构成隐藏而非重载 }; int main() { B b; b.fun(10); // OK调用 B::fun(int) b.fun(); // 编译报错A::fun() 被隐藏直接调用找不到 return 0; }这道题的答案是编译报错原因是B::fun(int)隐藏了基类的A::fun()b.fun()在 B 的作用域里找到的是fun(int)但没有传参参数不匹配编译失败。如果想调用基类版本需要写b.A::fun()。很多人看到 B 里有fun(int i)、A 里有fun()参数不同就以为构成重载。这是典型的误区——重载必须在同一作用域父子类是两个独立作用域不存在重载只有隐藏。考察题详解题目一A 和 B 类中的两个 func 构成什么关系答案是B隐藏。理由上面已经分析过不同作用域名字相同即构成隐藏参数不同不影响。题目二下面程序的编译运行结果是什么class A { public: void fun() { cout func() endl; } }; class B : public A { public: void fun(int i) { cout func(int i) i endl; } }; int main() { B b; b.fun(10); b.fun(); // 这里是关键 return 0; }答案是A编译报错。编译器在 B 的作用域里找到了fun(int)确定了候选函数但b.fun()没有传参不匹配报错。编译器不会再去父类里找因为B::fun已经把A::fun隐藏掉了。这两道题放在一起是因为它们考的是同一个知识点的两面第一道考你认不认识隐藏第二道考你理不理解隐藏之后名字查找的行为。易错点总结1. private 成员不可见不等于没有继承派生类对象里实际存在基类 private 成员的内存只是语法上无法访问。sizeof 一个派生类对象可以验证这一点。2. 切片只在 public 继承下有效Person* pp sobj这种隐式转换只对 public 继承成立。写代码时如果用了 private 继承又尝试切片编译器会直接报错但报错信息可能比较难懂不容易定位原因。3. 隐藏和重载的混淆不同作用域永远不构成重载只要函数名相同就是隐藏。实际开发里要尽量避免在继承体系中定义同名成员非常容易引发混淆。4. class 继承默认是 privateclass Student : Person { /* ... */ }; // 等价于 private 继承不是 public这个默认行为很容易踩坑明明想用 public 继承漏写了关键字切片就失效了但编译器不会直接告诉你是继承方式的问题。5. 基类指针强转派生类指针的安全前提强转本身不会报错但如果基类指针指向的不是派生类对象访问派生类成员就是未定义行为运行时可能崩溃也可能静默出错非常难调试。小结本篇覆盖了继承的基础部分为什么需要继承、三种继承方式及权限变化规则Min 公式、切片的原理与单向性、继承类模板的类域指定、以及隐藏规则和两道经典考察题的深度分析。下篇将深入派生类的六个默认成员函数构造/析构顺序、拷贝构造、赋值运算符在继承中的特殊处理、友元与静态成员的继承行为、菱形继承的数据冗余与二义性问题以及虚继承在底层是如何通过 vbptr 和虚基表解决这些问题的。

相关新闻