C++继承:友元、静态成员、多继承与组合

继承的基本语法不难,比较容易让人绕进去的,反而是一些边界情况:为什么基类的友元不能访问派生类的私有成员?静态成员被继承之后到底有几份?多个基类同时拥有同名成员时怎么办?菱形继承又为什么需要虚继承?

这些问题都和继承体系的结构有关,理解以后,在设计类时就能少踩不少坑。

一、友元关系不能继承

先看一个例子:

cpp 复制代码
class Student;

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; // 编译错误
}

Display() 是 Person 的友元,因此它可以访问 Person 的私有和保护成员。但是,它并不是 Student 的友元,所以不能因为 Student 继承了 Person,就顺便访问 Student 自己的保护成员。

如果确实需要让 Display() 访问 Student 中的受保护或私有成员,就需要在 Student 中也声明它为友元。

cpp 复制代码
class Student : public Person
{
    friend void Display(const Person& p,
                        const Student& s);

protected:
    int _stuNum;
};

这里要记住:友元关系不具备继承性。 基类的友元不会自动成为派生类的友元,派生类也不会自动拥有基类的友元关系。

友元本身是为了给特定函数或类开放访问权限,不应该把它理解成普通成员的继承规则。

二、继承中的静态成员

再来看 static 成员。

cpp 复制代码
class Person
{
public:
    static int _count;
};

int Person::_count = 0;

class Student : public Person
{
};

静态成员与普通成员不同。普通成员变量通常属于每个对象,而静态成员属于类相关的共享状态。

如果 Person 定义了一个静态成员 _count,那么 Student 继承它之后,并不会为继承体系中的每个派生类对象再额外创建一份 _count。

例如:

cpp 复制代码
Person p;
Student s;

p._count = 10;

cout << s._count << endl;
cout << Person::_count << endl;
cout << Student::_count << endl;

在没有其他修改的情况下,这几个访问表达式指向的是同一个继承来的静态成员,因此后面两个结果都是 10。

静态成员也可以通过类名访问:

cpp 复制代码
Person::_count;
Student::_count;

不过,后者并不意味着 Student 有一份独立的 _count。从设计上看,使用 Person::_count 通常更能体现这个静态成员的定义归属。

三、多继承是什么?

之前的继承例子都是一个派生类只继承一个基类,这叫单继承。

C++ 还支持多继承,也就是一个派生类可以拥有多个直接基类。

例如,一个助教既具有学生的身份,也具有老师的一部分身份:

cpp 复制代码
class Student
{
public:
    int _stuNum;
};

class Teacher
{
public:
    int _teacherId;
};

class Assistant : public Student, public Teacher
{
public:
    string _majorCourse;
};

Assistant 同时继承了 Student 和 Teacher,因此可以使用这两个基类中的公有成员:

cpp 复制代码
Assistant a;

a._stuNum = 1001;
a._teacherId = 2001;
a._majorCourse = "C++";

多继承在表达多个基类关系时比较灵活,但它也会让对象布局和成员访问更加复杂。

在通常的多继承布局中,派生类对象包含各个基类子对象,以及派生类自己的成员。不同基类子对象在对象内存中的位置可能不同,因此从同一个派生类对象转换成不同基类指针时,指针值也可能不同。

多继承最容易暴露的问题,是不同基类中存在同名成员。

四、菱形继承有什么问题?

下面是一个典型的菱形继承结构:

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

class Student : public Person
{
protected:
    int _stuNum;
};

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

class Assistant : public Student, public Teacher
{
protected:
    string _majorCourse;
};

结构关系如下:

text 复制代码
        Person
        /    \
   Student  Teacher
        \    /
       Assistant

Assistant 同时继承了 Student 和 Teacher,而这两个类又分别继承了 Person。

问题就在这里:Assistant 会包含两份 Person 子对象。

一份来自 Student,另一份来自 Teacher。因此,当访问 _name 时,编译器无法知道应该选择哪一份:

cpp 复制代码
Assistant a;

// a._name = "Peter"; // 二义性,编译错误

可以通过明确指定基类来解决访问时的二义性:

cpp 复制代码
a.Student::_name = "Peter";
a.Teacher::_name = "Mary";

但这种方法只解决了"访问哪一份"的问题,并没有解决 Person 数据重复的问题。一个 Assistant 对象仍然存在两份独立的 _name。

如果设计上希望学生身份和老师身份共享同一份 Person 数据,就需要考虑虚继承。

五、虚继承如何解决菱形继承?

虚继承的写法是在继承时增加 virtual:

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

class Student : virtual public Person
{
protected:
    int _stuNum;
};

class Teacher : virtual public Person
{
protected:
    int _teacherId;
};

class Assistant : public Student, public Teacher
{
protected:
    string _majorCourse;
};

这里的 Student 和 Teacher 都使用虚继承继承 Person。

当 Assistant 同时继承它们时,整个对象就共享同一份虚基类 Person 子对象。这样既消除了重复的 Person 数据,也解决了访问 _name 时的二义性:

cpp 复制代码
Assistant a;
a._name = "Peter";

这段代码可以正常使用同一份 _name。

不过,虚继承也会让对象布局和构造过程更复杂一些,不能只把它当成一个普通的语法修饰符。

虚继承下的构造函数

虚继承还有一个需要注意的地方:在含有虚基类的继承体系中,负责构造虚基类的是最派生类。

例如:

cpp 复制代码
class Person
{
public:
    Person(const string& name)
        : _name(name)
    {
    }

    string _name;
};

class Student : virtual public Person
{
public:
    Student(const string& name, int num)
        : Person(name), _stuNum(num)
    {
    }

protected:
    int _stuNum;
};

class Teacher : virtual public Person
{
public:
    Teacher(const string& name, int id)
        : Person(name), _teacherId(id)
    {
    }

protected:
    int _teacherId;
};

class Assistant : public Student, public Teacher
{
public:
    Assistant(const string& name1,
              const string& name2,
              const string& name3)
        : Person(name3),
          Student(name1, 1),
          Teacher(name2, 2)
    {
    }

protected:
    string _majorCourse;
};

创建 Assistant 对象时,Person 是虚基类,所以由最派生类 Assistant 负责初始化这份 Person 子对象。这里最终使用的是 Person(name3)。

虽然 Student 和 Teacher 的构造函数初始化列表中也写了 Person(name),但当它们作为 Assistant 的基类子对象构造时,这些虚基类初始化会被忽略。

这点比较容易让人疑惑:既然 Student 和 Teacher 都调用了 Person 构造函数,为什么最终只有一个名字?原因就是虚继承确保共享一份虚基类子对象,而它的初始化由最派生类负责。

虚继承能解决菱形继承中的重复基类问题,但实现和理解成本都更高,因此更应该从类之间的关系出发,谨慎决定是否需要多继承。

六、继承和组合有什么区别?

写类的时候,并不是所有代码复用都应该依靠继承。C++ 中还有组合这一种常见的设计方式。

两者最核心的区别是:

  • 公有继承体现 is-a 关系:派生类是一种基类。
  • 组合体现 has-a 关系:一个类拥有另一个类对象作为自己的成员。

比如汽车和轮胎之间就更适合使用组合关系。汽车不是一种轮胎,而是拥有轮胎。

cpp 复制代码
class Tire
{
public:
    string _brand;
    size_t _size;
};

class Car
{
private:
    Tire _t1;
    Tire _t2;
    Tire _t3;
    Tire _t4;
};

这里 Car 通过成员对象拥有四个 Tire,这就是组合。

而汽车和某个具体车型之间就可以体现继承关系:

cpp 复制代码
class Car
{
public:
    void Drive()
    {
        cout << "Car driving" << endl;
    }
};

class BMW : public Car
{
public:
    void Drive()
    {
        cout << "BMW driving" << endl;
    }
};

从关系上讲,BMW 是一种 Car,因此用继承表达比较自然。

为什么经常建议优先考虑组合?

继承虽然能复用基类的实现,但也会让派生类依赖基类的内部设计。基类的变化有可能影响所有派生类,导致类之间耦合较强。

组合则更多通过稳定的接口使用另一个对象。只要接口没有变化,内部实现可以调整,组合类一般不需要跟着改动。

所以,在设计类的时候,可以先问自己:这两个类真的是"是一种"的关系,还是一个类只是"拥有"另一个对象?

如果关系符合 is-a,就可以考虑继承;如果符合 has-a,通常更适合组合。需要扩展多态行为时,也可能需要使用继承,但不应该为了复用几行代码而强行建立继承关系。

七、总结

继承不只是把一个类的成员拿到另一个类里使用。进入多继承之后,还要考虑成员来自哪个基类、同名成员该如何访问,以及基类子对象究竟有几份。

友元关系不会自动继承,静态成员在继承体系中共享其原来的静态对象;菱形继承可能导致重复基类数据和二义性,虚继承则可以让多个路径共享一份虚基类。

最后,继承和组合要根据类之间的真实关系来选择。能够复用代码并不代表就适合继承,合理的类设计应该尽量让接口清晰、依赖关系可控。

相关推荐
科学熊1 小时前
OBS Studio 新手入门
1024程序员节
贵州诚鑫致达科技有限公司1 小时前
PHP两个9.2分RCE漏洞,速查版本
服务器·php·1024程序员节
漫天转悠1 小时前
Vue3白屏排查实录-Transition与多根模板
vue.js·1024程序员节
不老刘1 小时前
异构双 Agent 协同:构建无人值守的长任务自动化研发流水线
运维·人工智能·自动化·ai编程·1024程序员节
lusklusklusk1 小时前
Python使用pymysql连接Mysql数据库操作笔记:连接、游标与 execute、executemany——含手动管理与 with 自动管理两种写法
1024程序员节
迷途呀1 小时前
强化学习概念
人工智能·深度学习·机器学习·强化学习·1024程序员节
技术小甜甜1 小时前
【Cursor】Skill 被标黄色警告,怎么改成合格的 SKILL.md
开发语言·javascript·ai编程·cursor·1024程序员节·ai agent·skills
syounger1 小时前
SAP季度营收首次突破100亿欧元:云增长强劲,AI贡献仍需进一步量化
1024程序员节
qz_Serene1 小时前
练习算法题:双指针 -cpp
c++·算法·leetcode·1024程序员节