C++继承

C++ 继承从入门到踩坑:访问控制、对象切片、隐藏、构造析构、菱形与虚继承

继承 (inheritance)机制是面向对象程序设计使代码可以复用 的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展 ,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

下面我们用一篇"通俗但不糊弄"的方式,把继承里最常用、最容易混淆、最容易面试翻车的点一次讲透。


1. 继承的基本写法:派生类、基类、继承方式

最常见写法:

cpp 复制代码
class Person
{
public:
	Person(){}
	void Print()const
	{
		cout << _name << endl;
		cout << _age << endl;
	}
protected:
	string _name="peter";
private:
	size_t _age=18;
};

class Student:public Person
{

protected:
	int _stuid;
};
class Teacher :public Person
{

protected:
	int _teaid;
};

int main()
{

	Person p;
	Student s;
	Teacher t;
	s.Print();
	t.Print();
}
  • Person:基类(父类)
  • Student:派生类(子类)
  • : public Person继承方式 (public/protected/private)

实际工程里几乎总是 public 继承,protected/private 继承会显著降低可扩展性与可维护性。


2. 访问控制:public / protected / private 到底怎么"传递"?


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

一个非常好记的总结:
派生类中成员的可访问级别 = min(基类成员访问限定符, 继承方式) ,并且 基类的 private 成员在派生类里始终不可直接访问(注意:不可访问 ≠ 不存在,它仍然占据对象内存)。

2.1 protected 的意义是什么?

  • private:类外不能访问,派生类内部也不能访问
  • protected:类外不能访问,但派生类内部可以访问
  • public:大家都能访问

因此:如果你不想让外部直接碰到成员,但希望派生类能用,就用 protected( protected 是"因继承而出现的"非常典型的需求)。

工程建议:数据成员尽量保持 private,即使要给派生类用,也更推荐"protected 的函数接口",而不是暴露 protected 数据(减少耦合)。


3. 基类与派生类对象的赋值转换:对象切片(切割)是啥?

  • 派生类对象 可以赋值给 基类对象 / 基类指针 / 基类引用
    这种把"派生类中属于基类的那部分"拷贝/绑定出去的行为叫:切片/切割(slicing)
  • 基类对象 不能赋值给 派生类对象
  • 基类指针/引用强转成派生类指针/引用:只有当它实际指向派生类对象 时才安全;多态场景建议 dynamic_cast

3.1 一个"看得见坑"的例子

cpp 复制代码
class Person {
public:
    std::string name{"peter"};
    int age{18};
};

class Student : public Person {
public:
    int no{0};  // 学号
};

void SlicingDemo() {
    Student s;
    s.name = "alice";
    s.age = 20;
    s.no = 1001;

    Person p = s; // ✅ 发生切片:p 只保留 Person 那部分
    // p.no = 123; // ❌ Person 没有 no
}

切片不是 bug,它是语义 :你明确写了 Person p = s;,那就表示"只需要 Person 视角"。

3.2 指针强转:能转 ≠ 安全

cpp 复制代码
void CastDemo() {
    Student s;
    Person* pp = &s;

    // ✅ pp 实际指向 Student,对应强转"恰好"安全(但仍不推荐 C 风格)
    Student* ps1 = static_cast<Student*>(pp);
    ps1->no = 10;

    Person p;
    pp = &p;

    // ❌ pp 实际指向 Person,对应强转会产生越界/未定义行为风险
    Student* ps2 = static_cast<Student*>(pp);
    // ps2->no = 10; // 别这么干
}

工程建议(尤其面向多态时):

  • 基类需要至少一个 virtual 函数(成为多态类型)才能安全使用 dynamic_cast
  • 优先使用 dynamic_cast<Derived*>(basePtr) 并检查结果是否为空

4. 继承中的作用域:同名成员"隐藏"比你想得更阴险

核心点:基类和派生类有各自独立作用域。当派生类出现同名成员时,会**屏蔽(隐藏)**基类同名成员;函数隐藏只看"名字",不看参数列表,不看返回值。

4.1 数据成员隐藏

cpp 复制代码
class Person {
protected:
    int num_{111}; // 身份证号(示例)
};

class Student : public Person {
public:
    void Print() const {
        std::cout << "Person::num_  = " << Person::num_ << "\n";
        std::cout << "Student::num_ = " << num_ << "\n";
    }

private:
    int num_{999}; // 学号(同名:隐藏基类 num_)
};

给出建议:继承体系里尽量不要写同名成员,可读性非常差。

4.2 成员函数隐藏(更常见的面试点)

cpp 复制代码
class A {
public:
    void fun() { std::cout << "A::fun()\n"; }
};

class B : public A {
public:
    void fun(int x) {
        A::fun(); // 显式调用基类版本
        std::cout << "B::fun(int): " << x << "\n";
    }
};
void Test()
{
	
	B b;
	b.fun(10);
	//b.fun();//产生编译错误
};

很多人会误以为这是"重载"。但它们不在同一作用域里,所以不是重载,而是 隐藏

工程化补救 :如果你确实想把基类同名函数引入派生类作用域,可以用 using

cpp 复制代码
class B : public A {
public:
    using A::fun;     // 把 A::fun() 引入 B 的作用域
    void fun(int x) { /* ... */ }
};

5. 派生类的默认成员函数:构造、拷贝、赋值、析构的调用链


总结:派生类在生成"默认成员函数"(构造/拷贝构造/赋值/析构等)时,必须正确处理基类那部分,例如:

  • 派生类构造函数:必须调用基类构造初始化基类子对象;基类没有默认构造时必须在初始化列表显式调用
  • 派生类拷贝构造:必须调用基类拷贝构造 完成基类的拷贝初始化
  • 派生类 operator=:必须调用基类 operator= 完成基类部分赋值
  • 析构顺序:先派生类后基类 ;构造顺序相反 先基类后派生类

5.1 一份"规范写法"的示例

cpp 复制代码
class Person 
{
public:
    explicit Person(std::string name = "peter")
        : name_(std::move(name)) {}

    Person(const Person&) = default;
    Person& operator=(const Person&) = default;

    virtual ~Person() = default; // ⭐ 多态基类:强烈建议虚析构

protected:
    std::string name_;
};

class Student : public Person 
{
public:
    Student(std::string name, int num)
        : Person(std::move(name)), num_(num) {}

    Student(const Student& other)
        : Person(other), num_(other.num_) {}

    Student& operator=(const Student& other) {
        if (this != &other) {
            Person::operator=(other); // 基类部分赋值
            num_ = other.num_;
        }
        return *this;
    }

    ~Student() override = default;

private:
    int num_{0};
};

提醒:析构函数在语法层面也会发生"隐藏",并且在多态场景如果基类析构不加 virtual,可能导致通过基类指针删除派生类对象时只调用基类析构,资源泄漏/未定义行为风险极高。

工程里:只要一个类准备被继承且可能通过基类指针析构,就给它虚析构。


6. 继承与友元:友元关系不能继承

友元关系不能继承,基类的友元并不会自动获得访问派生类私有/保护成员的权限。

简言之:友元是"点对点授权",不是"血统继承"。

cpp 复制代码
class Person
{
public:
	friend void Display(const Person& p,const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
void Display(const Person& p,const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
}

7. 继承与静态成员:整个继承体系只有一份

如果基类定义了 static 成员,那么在整个继承体系里只有一个实例,不管派生多少层、多少个子类。

cpp 复制代码
class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
void TestPerson()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
}

8. 菱形继承:数据冗余 + 二义性(以及虚继承怎么救)

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

  • 菱形继承是多继承的一种特殊情况

  • 问题:数据冗余 (基类子对象出现两份)+ 二义性(不知道访问哪一份)

  • 解决:在中间层对共同基类使用 虚继承virtual public),让最底层只保留一份共同基类子对象

8.1 菱形继承产生二义性的直观示例

cpp 复制代码
class Person 
{
public:
    std::string name;
};

class Student : public Person {};
class Teacher : public Person {};

class Assistant : public Student, public Teacher {};

void AmbiguityDemo() 
{
    Assistant a;
    // a.name = "peter"; // ❌ 二义性:到底是 Student::Person 还是 Teacher::Person?
}

你可以通过 a.Student::name / a.Teacher::name 消除二义性,但冗余依然存在

8.2 用虚继承解决:只保留一份 Person

cpp 复制代码
class Person 
{
public:
    std::string name;
};

class Student : virtual public Person {};
class Teacher : virtual public Person {};

class Assistant : public Student, public Teacher {};

void VirtualInheritanceDemo() 
{
    Assistant a;
    a.name = "peter"; // ✅ 不再二义性,因为只有一份 Person
}

8.3虚拟继承解决数据冗余和二义性的原理

cpp 复制代码
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
  • 菱形继承
  • 下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。

9. 最重要的工程反思:能用组合就别用继承

  • 多继承尤其菱形会让复杂度飙升、性能和维护性变差;一般不建议设计出多继承/菱形继承
  • public 继承是 is-a ;组合是 has-a
  • 优先使用对象组合而不是类继承:组合更"黑箱复用",耦合更低,基类改动对外影响更小;继承更"白箱复用",对子类暴露内部细节,耦合更强

9.1 is-a vs has-a:一眼看懂

cpp 复制代码
// is-a:BMW 是一种 Car
class Car 
{
public:
    void Run() {}
};

class BMW : public Car 
{
public:
    void DriveFun() {}
};

// has-a:Car 有一个 Tire
class Tire 
{
public:
    int size{17};
};

class CarWithTire 
{
private:
    Tire tire_; // 组合
};

一句话建议

  • 为了"多态"或"天然 is-a"关系:用继承
  • 为了"复用实现"但又不想强耦合:优先组合

10. 小结:写继承代码的"安全清单"

  1. 默认使用 public 继承;不要为了"复用代码"就随便继承

  2. 避免同名成员(数据/函数都尽量别重名);需要时用 Base::using Base::func

  3. 明确对象切片:Base b = derived; 只保留基类部分是正常语义

  4. 需要多态时:

    • 基类析构函数加 virtual
    • 派生类重写加 override(可读性 + 防手滑)
  5. 菱形结构尽量不要设计;真碰上再用虚继承"定点拆弹"

  6. 设计层面:优先组合,其次继承


相关推荐
阿华hhh1 小时前
day4(IMX6ULL)<定时器>
c语言·开发语言·单片机·嵌入式硬件
YE1234567_1 小时前
从底层零拷贝到分布式架构:深度剖析现代 C++ 构建超大规模高性能 AI 插件引擎的实战之道
c++·分布式·架构
没有bug.的程序员1 小时前
Java锁优化:从synchronized到CAS的演进与实战选择
java·开发语言·多线程·并发·cas·synchronized·
初九之潜龙勿用1 小时前
C#实现导出Word图表通用方法之散点图
开发语言·c#·word·.net·office·图表
脏脏a2 小时前
C++ 容器的两把利器:优先级队列与反向迭代器
c++·反向迭代器·优先级队列
历程里程碑2 小时前
Linux 2 指令(2)进阶:内置与外置命令解析
linux·运维·服务器·c语言·开发语言·数据结构·ubuntu
王燕龙(大卫)2 小时前
rust入门
开发语言·rust
无心水2 小时前
2、Go语言源码文件组织与命令源码文件实战指南
开发语言·人工智能·后端·机器学习·golang·go·gopath
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 主题切换实现
android·开发语言·javascript·python·flutter·游戏·django