C++ 继承

继承作为 C++ 面向对象三大特性之一,是实现代码复用与扩展的核心机制,也是面试与工程开发中的高频考点。本文将从继承的基本概念、语法规则、成员访问控制、默认成员函数、多继承与菱形继承问题,到继承与组合的选型,带你系统性吃透 C++ 继承,彻底搞懂底层原理与实战坑点。


1. 继承的概念及定义

1.1 继承的概念

继承(inheritance)是面向对象程序设计中代码复用的重要手段,允许在保持原有类特性的基础上进行扩展,产生新的类(派生类/子类)。它体现了面向对象的层次结构,实现了类设计层次的复用。

示例场景:

• 未使用继承时,Student和Teacher类中存在大量冗余代码(如姓名、地址、电话、年龄等成员变量,以及身份认证identity()成员函数)。

• 使用继承后,可将公共成员提取到Person基类中,Student和Teacher作为派生类继承Person,避免重复定义。

cpp 复制代码
// 未使用继承的冗余代码
class Student {
public:
    void identity() { /* ... */ } // 身份认证
    void study() { /* ... */ }    // 独有功能:学习
protected:
    string _name = "peter";
    string _address;
    string _tel;
    int _age = 18;
    int _stuid; // 独有成员:学号
};

class Teacher {
public:
    void identity() { /* ... */ } // 身份认证
    void teaching() { /* ... */ } // 独有功能:授课
protected:
    string _name = "张三";
    int _age = 18;
    string _address;
    string _tel;
    string _title; // 独有成员:职称
};
// 使用继承后,代码复用
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(); // 复用基类功能
    t.identity();
    return 0;
}

1.2 继承定义

1.2.1 定义格式

cpp 复制代码
class 派生类 : 继承方式 基类 {
    // 新增成员
};

• 基类(父类):被继承的类,如Person。

• 派生类(子类):继承得到的新类,如Student。

• 继承方式:public、protected、private,默认继承方式为private(class定义时)或public(struct定义时),建议显式写出。

1.2.2 继承基类成员访问方式的变化

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

核心规则:

  1. 基类private成员在任何继承方式下,在派生类中均不可见(语法限制访问,实际仍被继承)。

  2. 若基类成员需在派生类中访问但禁止外部直接访问,应定义为protected。

  3. 派生类成员访问权限 = Min(基类成员访问限定符, 继承方式),优先级:public > protected > private。

  4. 实际开发中优先使用public继承,protected/private继承因扩展性差极少使用。

cpp 复制代码
// 三种继承方式示例
class Person {
public:
    void Print() { cout << _name << endl; }
protected:
    string _name;
private:
    int _age;
};

// public继承
class Student : public Person {
protected:
    int _stunum;
};

// protected继承
// class Student : protected Person { ... };

// private继承
// class Student : private Person { ... };

1.3 继承类模板

当基类是类模板时,派生类需指定基类的类型,否则编译器无法识别标识符。

cpp 复制代码
namespace gxy {
    // stack通过public继承vector实现,既符合is-a,也符合has-a
    template<class T>
    class stack : public std::vector<T> {
    public:
        void push(const T& x) {
            // 需指定类域,否则编译报错:error C3861: "push_back": 找不到标识符
            vector<T>::push_back(x);
        }

        void pop() {
            vector<T>::pop_back();
        }

        const T& top() {
            return vector<T>::back();
        }

        bool empty() {
            return vector<T>::empty();
        }
    };
}

int main() {
    gxy::stack<int> st;
    st.push(1);
    st.push(2);
    st.push(3);
    while (!st.empty()) {
        cout << st.top() << " ";
        st.pop();
    }
    return 0;
}

可适配不同底层容器的栈(stack)

cpp 复制代码
namespace gxy
{
    // 模板参数 CONTAINER 表示栈所使用的底层容器
    // 它必须支持 push_back、pop_back、back、empty 等接口
    template<class T, class CONTAINER = std::vector<T>>
    class stack
    {
    public:
        // 入栈:在容器尾部添加元素
        void push(const T& x)
        {
            // 显式调用底层容器的 push_back
            CONTAINER::push_back(x);
        }

        // 出栈:移除容器尾部元素
        void pop()
        {
            CONTAINER::pop_back();
        }

        // 获取栈顶元素:返回容器尾部元素的引用
        const T& top()
        {
            return CONTAINER::back();
        }

        // 判断栈是否为空
        bool empty() const
        {
            return CONTAINER::empty();
        }

        // 可选:获取栈中元素个数
        size_t size() const
        {
            return CONTAINER::size();
        }

    private:
        // 底层容器对象
        CONTAINER _c;
    };
}

关键说明:

  1. 模板参数设计:

T:栈中存储的元素类型。

CONTAINER:底层容器类型,默认使用 std::vector<T>,也可以指定为 std::deque<T>、std::list<T> 等。

  1. 使用示例:
cpp 复制代码
#include <iostream>
#include <vector>
#include <deque>

int main()
{
    // 使用 vector 作为底层容器
    gxy::stack<int, std::vector<int>> st1;
    st1.push(1);
    st1.push(2);
    st1.push(3);
    while (!st1.empty())
    {
        std::cout << st1.top() << " ";
        st1.pop();
    }
    std::cout << std::endl;

    // 使用 deque 作为底层容器
    gxy::stack<int, std::deque<int>> st2;
    st2.push(4);
    st2.push(5);
    st2.push(6);
    while (!st2.empty())
    {
        std::cout << st2.top() << " ";
        st2.pop();
    }
    return 0;
}

用宏切换底层容器

cpp 复制代码
// 用宏切换底层容器:vector / list / deque
//#define CONTAINER std::vector
//#define CONTAINER std::list
#define CONTAINER std::deque
// 在预处理阶段,编译器会把所有 CONTAINER<T> 直接替换成 std::deque<T>

#include <iostream>
#include <vector>
#include <list>
#include <deque>
using namespace std;

// 自己命名空间
namespace gxy
{
	// stack 继承自底层容器(教学演示用)
	template<class T>
	class stack : public CONTAINER<T>
	{
	public:
		// 入栈:调用容器的尾插
		void push(const T& x)
		{
			// 必须加 容器名<T>:: 因为是依赖基类模板
			CONTAINER<T>::push_back(x);
		}

		// 出栈:调用容器的尾删
		void pop()
		{
			CONTAINER<T>::pop_back();
		}

		// 获取栈顶:返回最后一个元素
		const T& top()
		{
			return CONTAINER<T>::back();
		}

		// 判断栈是否为空
		bool empty()
		{
			return CONTAINER<T>::empty();
		}
	};
}

// 测试主函数
int main()
{
	// 定义一个栈对象
	gxy::stack<int> st;

	// 入栈
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);

	// 遍历打印栈(栈只能从栈顶取)
	while (!st.empty())
	{
		// 取栈顶
		cout << st.top() << " ";
		// 出栈
		st.pop();
	}
	cout << endl;

	return 0;
}

2. 基类和派生类间的转换

• public继承下的赋值兼容规则:

  1. 派生类对象可以赋值给基类的对象/指针/引用(切片/切割,仅复制基类部分)。

  2. 基类对象不能赋值给派生类对象。

  3. 基类指针/引用可通过强制类型转换赋值给派生类指针/引用,但仅当基类指针实际指向派生类对象时才安全,可使用dynamic_cast进行安全识别。

cpp 复制代码
class Person {
protected:
    string _name;
    string _sex;
    int _age;
};

class Student : public Person {
public:
    int _No;
};

int main() {
    Student sobj;
    // 1. 派生类对象赋值给基类指针/引用
    Person* pp = &sobj;
    Person& rp = sobj;
    Person pobj = sobj; // 调用基类拷贝构造

    // 2. 基类对象不能赋值给派生类对象,编译报错
    // sobj = pobj;

    return 0;
}
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 父类
class Person
{
    // 虚函数,后面多态会用
	virtual void func()
	{}

public:
	string _name; // 姓名
	string _sex;  // 性别
	int _age;     // 年龄
};

// 子类:公有继承 Person
class Student : public Person
{
public:
	int _No; // 学号
};

int main()
{
	Student sobj;  // 子类对象

    // ==========================
    // 1. 赋值兼容规则(最核心)
    // 子类对象 → 可以赋值给 父类对象 / 父类指针 / 父类引用
    // 这个动作叫:切片 / 切割(slicing)
    // ==========================

    // 子类对象 赋值给 父类对象
    // 只会拷贝父类那部分成员,子类独有成员会被切掉
	Person pobj = sobj;

    // 父类指针 指向 子类对象
	Person* pp = &sobj;

    // 父类引用 引用 子类对象
	Person& rp = sobj;
	rp._name = "张三"; // 通过父类引用修改子类对象里的父类部分


    // ==========================
    // 对比理解:普通类型隐式转换
    // ==========================
	int i = 1;
	double d = i;           // int 隐式转 double
	const double& rd = i;   // 引用也支持隐式转换(会产生临时量)


    // ==========================
    // 2. 父类对象 → 不能赋值给子类对象
    // 编译报错!
    // 因为父类少成员(没有_No),塞不进子类
    // ==========================
	// sobj = (Student)pobj;  // 错误!


    // ==========================
    // 3. dynamic_cast 安全向下转型
    // ==========================

    // pp 原本指向子类对象 sobj
    // 可以安全转成 Student*
	Student* ps1 = dynamic_cast<Student*>(pp);
	cout << ps1 << endl; // 输出有效地址,非空


    // pp 现在指向父类对象 pobj
    // 强转成 Student* 会失败,返回 nullptr
	pp = &pobj;
	Student* ps2 = dynamic_cast<Student*>(pp);
	cout << ps2 << endl; // 输出 0(空指针)

	return 0;
}

最核心的 3 个知识点

  1. 子类对象 → 父类对象/指针/引用 ✅ 允许,这叫 切片 / 切割(object slicing)

子类是"父类+自己扩展"; 赋值给父类时,只拷贝父类那部分成员; 语法天然支持,不需要强转。

Person pobj = sobj; Person* pp = &sobj; Person& rp = sobj;

  1. 父类对象 → 子类对象 ❌ 不允许

父类成员少,没有子类的成员(如 _No);强行赋值会缺数据,语法直接报错。// sobj = pobj; // 错误

  1. dynamic_cast<子类*>(父类指针):安全的向下转型;如果父指针真的指向子类对象 → 转换成功,返回有效地址;如果父指针指向父类对象 → 转换失败,返回 nullptr。

总结:子类 → 父类:天然允许(切片) 父类 → 子类:不允许,不安全

dynamic_cast 用来安全判断是不是真的指向子类

3. 继承中的作用域

3.1 隐藏规则

  1. 基类和派生类拥有独立的作用域。

  2. 若派生类与基类存在同名成员,派生类成员会隐藏基类成员的直接访问。

  3. 在派生类中可通过基类::成员显式访问被隐藏的基类成员。

  4. 成员函数隐藏仅需函数名相同,与参数列表无关。

  5. 实际开发中应避免在继承体系中定义同名成员。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 父类
class Person
{
protected:
	string _name = "小李子"; // 姓名
	int _num = 111;         // 身份证号(父类的 _num)
};

// 子类公有继承 Person
class Student : public Person
{
public:
	void Print()
	{
		// 这里直接访问 _num:访问的是【子类自己】的 _num
		// 因为子类和父类同名成员,子类会【隐藏】父类
		cout << _num << endl;         // 输出:999

		// 想访问父类的 _num:必须加 【父类名::】
		cout << Person::_num << endl; // 输出:111
	}

protected:
	int _num = 999; // 学号(子类自己的 _num,和父类同名)
};

int main()
{
	Student s;
	s.Print();

	return 0;
}

运行结果:

cpp 复制代码
999
111

核心知识点:同名成员的隐藏规则

  1. 只要同名,就会构成隐藏

子类有 _num,父类也有 _num; 名字相同 = 隐藏,跟参数、类型都没关系

  1. 直接访问 _num

编译器查找顺序:先在自己类(Student)里找; 找到了,就用自己的 → 999

  1. 想访问父类的同名成员

必须写:父类名::成员名 Person::_num 这样才能强制访问到父类的 111

总结:子类和父类成员同名 → 子类隐藏父类。 直接访问是子类,加 父类:: 访问父类。

3.2 考察继承作用域相关选择题

1)题目1

A和B类中的两个func构成隐藏关系(B继承A,且func同名)。

以下程序编译运行结果为编译报错,因为B::fun(int)隐藏了A::fun(),b.fun()无法直接调用基类无参版本。

cpp 复制代码
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(int)
    // b.fun();  // 编译报错,A::fun()被隐藏
    return 0;
}

2)题目2

cpp 复制代码
#include <iostream>
using namespace std;

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;

	// 1. 加类名限定,调用父类的 fun()
	b.A::fun();

	// 2. 调用子类的 fun(int)
	b.fun(1);

	return 0;
}

输出结果:

cpp 复制代码
func()
func(int i)1

核心考点:继承中的 隐藏 / 重定义

  1. 只要函数名相同,就构成隐藏

父类:void fun();子类:void fun(int i);名字相同 = 隐藏;跟参数、返回值都没关系。

  1. 这不是重载! 重载:同一个作用域,同名不同参

隐藏:不同作用域(父类/子类),只要同名就隐藏

所以:b.fun(); // 会报错! 因为子类 fun(int) 把父类无参 fun() 隐藏了。

  1. 想调用父类被隐藏的函数

必须加:b.父类名::函数名(); b.A::fun();

总结:继承中,函数名相同就构成隐藏。子类会遮住父类,想调用父类必须加 父类名::。

4. 派生类的默认成员函数

4.1 4个常见默认成员函数的生成规则

派生类的6个默认成员函数生成时,必须先处理基类部分:

  1. 构造函数:派生类构造函数必须调用基类构造函数初始化基类成员。若基类无默认构造函数,需在派生类初始化列表显式调用。

  2. 拷贝构造函数:派生类拷贝构造必须调用基类拷贝构造完成基类拷贝初始化。

  3. operator=:派生类赋值运算符必须调用基类operator=完成基类复制,且需显式指定基类作用域(因派生类operator=隐藏了基类版本)。

  4. 析构函数:派生类析构函数执行完成后,会自动调用基类析构函数清理基类成员,保证"先清理派生类,再清理基类"的顺序。

  5. 构造与析构顺序:

◦ 对象初始化:先调用基类构造,再调用派生类构造。

◦ 对象析构:先调用派生类析构,再调用基类析构。

  1. 基类析构函数未加virtual时,派生类与基类析构函数构成隐藏关系。

代码1

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 父类 Person
class Person
{
public:
    // 1. 构造函数
    Person(const char* name = "xxx")
        : _name(name)
    {
        cout << "Person()" << endl;
    }

    // 2. 拷贝构造函数
    Person(const Person& p)
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }

    // 3. 赋值重载 operator=
    Person& operator=(const Person& p)
    {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;
        return *this;
    }

    // 4. 析构函数
    ~Person()
    {
        cout << "~Person()" << endl;
    }

protected:
    string _name; // 姓名
};

// 子类 Student 公有继承 Person
class Student : public Person
{
public:
    // 子类构造函数
    // 必须先在初始化列表调用 父类构造函数 Person(name)
    Student(const char* name, int num, const char* addrss)
        : Person(name)        // 初始化父类部分
        , _num(num)          // 初始化子类成员
        , _addrss(addrss)
    {
        // 如果父类没有默认构造,这里必须显式调用父类构造,否则编译报错
    }

    // 子类拷贝构造
    // 必须调用 父类拷贝构造 Person(s)
    Student(const Student& s)
        : Person(s)          // 切片,调用父类拷贝构造
        , _num(s._num)
        , _addrss(s._addrss)
    {
        // 如果有动态资源,这里写深拷贝
    }

    // 子类赋值重载
    Student& operator=(const Student& s)
    {
        cout << "Student& operator=(const Student& s)" << endl;

        if (this != &s)
        {
            // 关键:
            // 子类和父类的 operator= 构成隐藏关系
            // 必须显式指定 父类::operator= 才能调用父类赋值
            Person::operator=(s);

            // 拷贝子类自己的成员
            _num = s._num;
            _addrss = s._addrss;
        }

        return *this;
    }

    // 子类析构函数
    ~Student()
    {
        cout << "~Student()" << endl;

        // 重点规则:
        // 1. 子类析构函数 会 自动调用 父类析构 ~Person()
        // 2. 不需要我们手动写 Person::~Person();
        // 3. 析构顺序:先析构子类,再自动析构父类(保证安全)

        // 如果有动态申请的内存,在这里释放
        // delete[] _ptr;
    }

protected:
    int _num = 1;         // 学号
    string _addrss = "西安市高新区";
    int* _ptr = new int[10]; // 模拟动态资源
};

// 测试
int main()
{
    // 调用:构造
    Student s1("张三", 1, "西安市");

    // 调用:拷贝构造
    Student s2(s1);

    // 调用:赋值重载
    Student s3("李四", 2, "咸阳市");
    s1 = s3;

    return 0;
}

输出结果:

cpp 复制代码
Person()            // s1 构造
Person(const Person& p)  // s2 拷贝构造
Person()            // s3 构造
Person operator=(const Person& p)  // 赋值时先赋值父类
Student& operator=(const Student& s) // 再赋值子类
~Student()          // s3 析构
~Person()
~Student()          // s2 析构
~Person()
~Student()          // s1 析构
~Person()
  1. 子类构造:必须先调用父类构造;父类没有默认构造时,必须在初始化列表显式调用

  2. 子类拷贝构造:必须调用父类拷贝构造,写法:Person(s)

  3. 子类赋值重载:子类与父类的 operator= 构成隐藏,必须写:Person::operator=(s);

  4. 子类析构:不用手动调用父类析构,编译器会自动调父类析构,顺序:先析构子类→自动析构父类

解释 Person(s)

Person(s) 的意思就是:把子对象 s 里的"父类那一部分",拿去初始化父类。

  1. 先搞懂:子类对象里长什么样
    class Person { ... }; // 父类
    class Student : public Person { ... }; // 子类

一个 Student 对象里面,其实是两部分拼起来的:

cpp 复制代码
Student 对象 s
├─ Person 部分(_name) ← 父类的成员
└─ 自己部分 (_num, _addrss) ← 子类的成员
  1. 拷贝构造要做什么?

拷贝构造:用一个现成的 s 对象,拷贝出一个一模一样的新对象。

那就要拷贝两部分:1. 父类那一半 2. 子类自己那一半

  1. 为什么要写 Person(s):父类的成员(_name)在子类里不能直接赋值初始化,必须调用父类的拷贝构造函数来初始化。
cpp 复制代码
Student(const Student& s)
    : Person(s)  // 👈 就是这一句
    , _num(s._num)
    , _addrss(s._addrss)
{}

Person(s) 到底干了什么?把 s 对象里的父类部分,拿去初始化我这个新对象的父类部分。

1) s 是子类对象

2) 传给 Person 的拷贝构造时,会自动切片,只把里面 父类那一部分 拿出来

3) 调用:Person(const Person& p)

4) 把父类部分拷贝完成

  1. 不写会怎样?如果你写成:
cpp 复制代码
Student(const Student& s)
    : _num(s._num)
    , _addrss(s._addrss)
{}

编译器会自动调用父类的默认构造,而不是拷贝构造!结果就是:子类成员拷贝对了, 父类成员没有被拷贝,是随机值/默认值,这就是错的。

  1. 总结:子类拷贝构造必须写:: 父类名(子类对象) 你这里就是: : Person(s)

意思:拷贝父类部分 → 必须调用父类拷贝构造,子类部分 → 自己正常初始化

代码2

cpp 复制代码
// 子类 Student 继承 Person
class Student : public Person
{
public:
	// 默认生成的构造函数行为:
	// 1、内置类型->不确定
	// 2、自定义类型->调用默认构造
	// 3、继承的父类成员看做一个整体对象,会调用父类的默认构造
protected:
	int _num = 1;
	string _addrss = "西安市高新区";
};

int main()
{
	Student s;
	return 0;
}

继承的父类成员,会被看成一个整体对象,调用父类默认构造。

可以把 子类对象 理解成:

cpp 复制代码
Student 对象 s
├─ 【一块整体:Person 父类部分】
├─ int _num
└─ string _addrss

重点:父类那一部分,在子类眼里,就是一个整体对象。

编译器默认生成的构造函数做什么?当你没写构造函数时,编译器自动生成的构造会干 3 件事:

  1. 父类部分:看成一个整体对象 → 调用父类的默认构造 Person()

  2. 自定义类型(如 string):→ 调用它自己的默认构造

  3. 内置类型(int、char)*:→ 不处理,值是随机的(除非你给了缺省值 =1)

结论:子类默认构造 → 自动调用父类默认构造!

Student s; 这句代码执行时:1. 先调用:Person() 2. 再初始化子类成员

什么时候会报错?如果父类 没有默认构造函数,比如:

cpp 复制代码
class Person
{
public:
    // 只有带参构造,没有无参构造
    Person(const char* name)
    { ... }
};

那写:Student s; 就会 编译报错! 因为:子类默认构造要调用父类默认构造,但父类没有!

口诀:子类构造,必先调父类构造。没写就调默认构造,没有默认构造就报错。

4.2 实现一个不能被继承的类

• 方法1(C++98):将基类构造函数私有化,派生类无法调用基类构造,从而无法实例化。

cpp 复制代码
#include<iostream>
using namespace std;

class Base
{
public:
    void func5() { cout << "Base::func5" << endl; }
protected:
    int a = 1;
private:
    // 核心:构造函数私有化
    Base() {}
};

// 【编译报错】无法从 Base 继承
// class Derive : public Base {}; 

int main()
{
    // 【编译报错】外部也无法创建 Base 对象
    // Base b;

    return 0;
}

缺点:这种方式不仅禁止了继承,也禁止了在类外部创建 Base 的对象(相当于一个不可实例化的类)。

• 方法2(C++11):使用final关键字修饰基类,直接禁止继承。

cpp 复制代码
#include<iostream>
using namespace std;

// 核心:使用 final 关键字修饰类
class Base final
{
public:
    // 构造函数公开,允许创建对象
    Base() {}
    
    void func5() { cout << "Base::func5" << endl; }
protected:
    int a = 1;
};

// 【编译报错】error: cannot derive from 'final' type 'Base'
// class Derive : public Base {}; 

int main()
{
    // 正常:Base 自身可以创建对象
    Base b;
    b.func5();

    return 0;
}

优点:

  1. 语义明确,一眼就能看出是为了禁止继承。

  2. 基类的构造函数可以公开,允许创建 Base 类型的对象。

  3. 补充知识点:final 也可以修饰成员函数

除了修饰类,final 还可以修饰虚函数,禁止派生类重写该函数:

cpp 复制代码
class Base
{
public:
    // 禁止派生类重写 show 函数
    virtual void show() final {
        cout << "Base show" << endl;
    }
};

class Derive : public Base
{
public:
    // 【编译报错】无法重写 final 函数
    // void show() override {}
};

总结:

• C++11 及以上:请直接使用 class Base final,这是最优雅的方案。

• C++98:只能通过将基类构造函数设为 private 来间接实现,但会导致基类无法实例化。

5. 继承与友元

友元关系不能继承,基类友元无法访问派生类的私有和保护成员。

友元关系不能被继承;友元关系不传递、不继承、不自动共享。

cpp 复制代码
class Person
{
    friend void f();
};

class Student : public Person
{};

f() 是 Person 的友元;但 f() 不是 Student 的友元;子类不会自动获得父类的友元关系。

这就叫:友元关系不能被继承。

代码示例:

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;   // 正确,可访问基类protected成员
    // cout << s._stuNum << endl; // 编译报错,无法访问派生类protected成员
}

// 解决方案:将Display声明为Student的友元
// class Student : public Person {
//     friend void Display(const Person& p, const Student& s);
// protected:
//     int _stuNum;
// };

• 访问 p._name 需要 Person 的友元; 访问 s._stuNum 需要 Student 的友元

两个都要给,函数才能同时访问两个类的保护/私有成员。

最终可运行完整版:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

class Student;

class Person
{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name = "张三";
};

class Student : public Person
{
    friend void Display(const Person& p, const Student& s);
protected:
       int _stuNum = 10086;
};

void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}

int main()
{
    Person p;
    Student s;
    Display(p, s);

    return 0;
}

总结:友元 不能继承; 友元 不能传递; 想访问谁的私有/保护,就必须成为谁的友元。

6. 继承与静态成员

基类定义的static静态成员,在整个继承体系中只有一个实例,所有派生类共享该成员。

• 普通成员变量:子类会继承一份,父子各有各的

• 静态成员变量:父子共用同一份,属于整个类家族

代码示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

class Person
{
public:
    string _name;         // 非静态成员变量:属于每个对象
    static int _count;    // 静态成员变量:属于整个类,所有对象共享
};

// 静态成员变量必须在类外初始化
int Person::_count = 0;

// 子类公有继承 Person
class Student : public Person
{
protected:
    int _stuNum; // 子类自己的成员变量
};

int main()
{
    Person p;    // 父类对象
    Student s;   // 子类对象

    // 非静态成员:每个对象都有一份,地址不同
    cout << "&p._name = " << &p._name << endl;
    cout << "&s._name = " << &s._name << endl;
    cout << endl;

    // 静态成员:所有对象共享同一份,地址相同
    cout << "&p._count = " << &p._count << endl;
    cout << "&s._count = " << &s._count << endl;
    cout << endl;

    // 静态成员可以通过 类名:: 访问
    cout << "Person::_count = " << Person::_count << endl;
    cout << "Student::_count = " << Student::_count << endl;
    cout << endl;

    // 修改静态成员,所有对象都会变
    Person::_count++;

    // 父类对象查看
    cout << "p._count = " << p._count << endl;
    // 子类对象查看(看到的是同一个值)
    cout << "s._count = " << s._count << endl;

    return 0;
}
  1. 普通成员(非 static) string _name;

父类一个对象,子类一个对象;内存地址 不同;各自独立,互不影响。

  1. 静态成员(static) static int _count;

静态成员属于类,不属于某个对象; 继承后,子类和父类共享同一块内存;地址 完全相同;无论 p._count++ 还是 s._count++,改的是同一个变量。

  1. 访问方式都合法
    Person::_count = 10;
    Student::_count = 20;
    p._count++;
    s._count++;

这些写法全都可以,因为: 静态成员可以通过 类名 访问; 也可以通过 对象 访问; 子类继承后,也能看到父类的静态成员

  1. 运行结果规律

1) &p._name != &s._name → 地址不同

2) &p._count == &s._count → 地址相同

3) 只要改一次 _count,父类、子类、对象、类名访问 值全部一起变

  1. 最精炼总结

1) 非静态成员:每个对象一份,继承后子类也有一份,独立存储。

2) 静态成员:属于类,不属于对象,继承后父子共用一份。

3) 友元不能继承,但静态成员可以继承且共享

7. 多继承及其菱形继承问题

7.1 继承模型

• 单继承:一个派生类只有一个直接基类。

• 多继承:一个派生类有两个或以上直接基类,内存模型为"先继承的基类在前,后继承的在后,派生类成员在最后"。

• 菱形继承:多继承的特殊情况,存在数据冗余和二义性问题(如Assistant继承Student和Teacher,两者又继承Person,导致Person成员存在两份)。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 公共基类
class Person {
public:
    string _name; // 姓名
};

// Student 继承 Person
class Student : public Person {
protected:
    int _num; // 学号
};

// Teacher 继承 Person
class Teacher : public Person {
protected:
    int _id;  // 职工号
};

// 菱形继承:Assistant 同时继承 Student 和 Teacher
class Assistant : public Student, public Teacher {
protected:
    string _majorCourse; // 主修课程
};

int main() {
    Assistant a;

    // 错误写法:编译报错
    // error: 对"_name"的访问不明确
    // 因为 a 里面有两份 _name:一份来自 Student,一份来自 Teacher
    // 编译器不知道你要访问哪一个
    // a._name = "peter";

    // 正确写法:显式指定类域,指明访问哪个父类的 _name
    a.Student::_name = "xxx";  // 访问 Student 继承下来的 _name
    a.Teacher::_name = "yyy";  // 访问 Teacher 继承下来的 _name

    // 验证:两个 _name 是独立的,互不干扰
    cout << a.Student::_name << endl;  // 输出 xxx
    cout << a.Teacher::_name << endl;  // 输出 yyy

    return 0;
}

核心原理

  1. 为什么报错?这是菱形继承(钻石继承):
cpp 复制代码
    Person
   /      \
Student  Teacher
   \      /
  Assistant

Student 继承了一份 Person;Teacher 继承了一份 Person;Assistant 里就有两份 _name

直接写 a._name,编译器二义性,不知道选哪份

  1. 两个问题:1)访问二义性:直接 a._name 报错 ;2)数据冗余:存了两份 Person 成员,浪费空间

  2. 上述解法,显式指定类域:a.Student::_name a.Teacher::_name

只能解决二义性, 解决不了数据冗余

  1. 真正的解决方案 -> 虚继承:
cpp 复制代码
class Student : virtual public Person {};
class Teacher : virtual public Person {};

作用:让 Assistant 中只保留一份 Person; 既解决二义性,又解决数据冗余。

  1. 总结:

普通菱形继承:两份基类 → 二义性 + 冗余;指定类域:只能解决二义性;虚继承:真正解决两个问题

7.2 虚继承

使用virtual关键字修饰继承,可解决菱形继承的数据冗余和二义性问题。

代码1

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 公共基类
class Person {
public:
    string _name;
};

// 虚继承 Person
// 告诉编译器:后续如果有多继承,共享这一份 Person
class Student : virtual public Person {
protected:
    int _num;
};

// 虚继承 Person
class Teacher : virtual public Person {
protected:
    int _id;
};

// 多继承:Assistant 同时继承 Student + Teacher
class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

int main() {
    Assistant a;

    // 虚继承后:
    // Person 部分 只存在一份
    // 不再有二义性,可以直接访问
    a._name = "peter";

    cout << a._name << endl;

    return 0;
}

注意:虚继承底层实现复杂,会带来性能损失,实际开发中应避免设计菱形继承。

核心知识点

  1. 不加 virtual(普通菱形继承):Assistant 里有 两份 Person; a._name 报错:访问不明确

  2. 加了 virtual(虚继承):最终子类里 只有一份 Person;a._name 可以直接用,无歧义;同时解决:二义性 + 数据冗余

  3. 虚继承作用: 让多个中间子类(Student、Teacher)共享同一份祖先类(Person)

代码2

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

// 共同基类
class Person
{
public:
    // 有参构造函数
    Person(const char* name)
        :_name(name)
    {}

    string _name; // 姓名
    /*int _tel;
    int _age;
    string _gender;
    string _address;*/
    // ... 如果成员很多,普通菱形继承会造成大量数据冗余
};

// 虚继承 Person
class Student : virtual public Person
{
public:
    Student(const char* name, int num = 0)
        :Person(name)  // 虚继承下,这里的 Person(name) 不会执行
        ,_num(num)
    {}
protected:
    int _num; //学号
};

// 虚继承 Person
class Teacher : virtual public Person
{
public:
    Teacher(const char* name, int id = 1)
        :Person(name)  // 虚继承下,这里的 Person(name) 不会执行
        , _id(id)
    {}
protected:
    int _id; // 职工编号
};

// 菱形继承:Assistant 同时继承 Student + Teacher
// 实际开发中:不要去玩菱形继承
class Assistant : public Student, public Teacher
{
public:
    // 虚继承后,必须由【最终派生类】直接初始化 共同基类 Person
    Assistant(const char* name1, const char* name2, const char* name3)
        :Student(name1)   // 这里不会初始化 Person
        ,Teacher(name2)   // 这里不会初始化 Person
        ,Person(name3)    // 只有这里会真正初始化 Person(虚继承规定)
    {
        // a._name 最终 = name3("王五")
    }
protected:
    string _majorCourse; // 主修课程
};

int main()
{
    Assistant a("张三", "李四", "王五");

    // 没有虚继承前:
    // 编译报错:error C2385: 对"_name"的访问不明确
    // 因为 Student 和 Teacher 各有一份 _name

    // 有了虚继承后:
    // 只有一份 _name,由 Assistant 直接初始化 Person
    a._name = "peter";       // 不再二义性
    cout << a._name << endl; // 正常访问

    // 虚继承解决了两个问题:
    // 1. 访问二义性
    // 2. 数据冗余(只存一份Person成员)

    return 0;
}

代码解释

  1. 共同基类 Person
cpp 复制代码
class Person
{
public:
    // 有参构造
    Person(const char* name)
        :_name(name)
    {}

    string _name; // 姓名
};

这是最顶层的基类, 只有一个成员:_name, 只有有参构造,没有无参构造

  1. 虚继承:Student / Teacher
cpp 复制代码
class Student : virtual public Person
{
public:
    Student(const char* name, int num = 0)
        :Person(name)  // 虚继承下,这里不会执行!
        ,_num(num)
    {}
protected:
    int _num;
};

class Teacher : virtual public Person
{
public:
    Teacher(const char* name, int id = 1)
        :Person(name)  // 虚继承下,这里不会执行!
        ,_id(id)
    {}
protected:
    int _id;
};

关键点:virtual public Person = 虚继承

意义:告诉编译器,如果以后有多继承,大家共用这一份 Person

重点规则:虚继承时,中间子类(Student/Teacher)的构造函数,不会初始化 Person!

只会由 最终最底层子类 来初始化。

  1. 最终子类:Assistant(菱形结构)
cpp 复制代码
class Assistant : public Student, public Teacher
{
public:
    Assistant(const char* name1, const char* name2, const char* name3)
        :Student(name1)   // 不初始化 Person
        ,Teacher(name2)   // 不初始化 Person
        ,Person(name3)    // 只有这里真正初始化 Person!
    {}

protected:
    string _majorCourse;
};

虚继承的强制规则:

1) 虚继承的顶层基类(Person),必须由最终子类直接初始化

2) Student(name1)、Teacher(name2) 里的 Person(name) 都不会执行

3) 只有你写的 Person(name3) 会真正构造基类

这就是为什么你要传三个名字,但只有最后一个名字生效。

  1. main 函数里发生了什么
cpp 复制代码
int main()
{
    Assistant a("张三", "李四", "王五");

    a._name = "peter";
    cout << a._name << endl;

    return 0;
}

没有虚继承会怎样?Assistant 里面会有 两份 Person,一份来自 Student,一份来自 Teacher,访问 a._name → 编译报错:二义性

有了虚继承:内存中 只有一份 Person ,a._name 可以直接访问,不报错,解决:二义性、数据冗余

• name1:传给 Student → 没用,不会构造 Person

• name2:传给 Teacher → 没用,不会构造 Person

• name3:传给 Person → 只有它真正生效

逐行拆开看 Assistant a("张三", "李四", "王五");

对应构造函数:

cpp 复制代码
Assistant(const char* name1, const char* name2, const char* name3)
    :Student(name1)   // name1 = "张三"
    ,Teacher(name2)   // name2 = "李四"
    ,Person(name3)    // name3 = "王五"

1) name1 = "张三": 传给 Student,但因为是 虚继承,Student 不能去构造 Person,所以这个名字白传了,没用

2) name2 = "李四": 传给 Teacher,同样因为虚继承,Teacher 也不能构造 Person,这个名字也没用

3) name3 = "王五": 直接传给 Person 构造函数

虚继承规定:最终子类必须亲自初始化最顶层基类,所以 只有 name3 真正初始化 _name

最终结果: a._name 最终 = 王五 ;张三、李四都没起作用

虚继承菱形继承中:中间类(Student、Teacher)不能构造公共基类,只有最底下的孙子类(Assistant)才能构造 Person

所以:name1、name2 是摆设;name3 才是真正给 Person 的名字

核心考点

  1. 普通菱形继承的问题

二义性:a._name 不知道是 Student 的还是 Teacher 的; 数据冗余:Person 成员存了两份

  1. 虚继承 virtual public 做了什么:让共同基类 Person 只存在一份;解决二义性 + 数据冗余

  2. 虚继承构造函数规则(重点):中间类(Student、Teacher)的 Person(name) 不执行; 必须由 最终派生类(Assistant)直接初始化 Person

  3. 菱形继承:Person 被继承两次 → 两份数据 → 二义性 + 冗余

  4. 虚继承构造规则(最重要): 中间子类(Student/Teacher)不构造顶层基类, 必须由最终子类直接构造顶层基类

总结:虚继承就是为了解决菱形继承的二义性和数据冗余,且虚继承时,最顶层基类必须由最终子类直接构造。

7.3 多继承 + 指针切片 + 地址偏移

cpp 复制代码
#include<iostream>
using namespace std;

class Base1 {
public:
    int _b1 = 1;  // 4字节
};

class Base2 {
public:
    int _b2 = 2;  // 4字节
};

// 多继承:先继承 Base2,再继承 Base1
class Derive : public Base2, public Base1 {
public:
    int _d = 3;   // 4字节
    int _e = 4;   // 4字节
};

int main()
{
    Derive d;

    // 这三个指针 指向同一个对象d,但类型不同,地址可能不同!
    Base1* p1 = &d;    // 指向对象中 Base1 部分
    Base2* p2 = &d;    // 指向对象中 Base2 部分
    Derive* p3 = &d;   // 指向整个对象起始位置

    // 打印地址观察
    cout << "Derive* p3:  " << p3 << endl;
    cout << "Base2*  p2:  " << p2 << endl;
    cout << "Base1*  p1:  " << p1 << endl;

    // 看大小
    cout << "sizeof(Base1) = " << sizeof(Base1) << endl;
    cout << "sizeof(Base2) = " << sizeof(Base2) << endl;
    cout << "sizeof(Derive) = " << sizeof(Derive) << endl;

    return 0;
}
  1. 内存布局(重点) 你写的是:class Derive : public Base2, public Base1

继承顺序是:先 Base2,再 Base1,最后自己成员 ; 所以 Derive 内存布局是:

cpp 复制代码
 低地址 ---------------------------> 高地址

+-------------------------+
| Base2::_b2 = 2          |   // 先放第一个父类 Base2
+-------------------------+
| Base1::_b1 = 1          |   // 再放第二个父类 Base1
+-------------------------+
| Derive::_d  = 3         |   // 自己的成员
+-------------------------+
| Derive::_e  = 4         |
+-------------------------+

一共:4 + 4 + 4 + 4 = 16 字节

  1. 三个指针的地址区别
cpp 复制代码
Derive* p3 = &d;   → 指向整个对象**开头**
Base2* p2  = &d;   → 也指向**开头**(因为Base2在最前)
Base1* p1  = &d;   → 指向中间:p3 + 4 字节


p3 (Derive*)
  ↓
+-------------------------+
| Base2::_b2 = 2          |  ← p2 (Base2*) 也指向这里
+-------------------------+
| Base1::_b1 = 1          |  ← p1 (Base1*) 指向这里
+-------------------------+
| Derive::_d  = 3         |
+-------------------------+
| Derive::_e  = 4         |
+-------------------------+

所以你会看到: p3 == p2(地址一样); p1 比 p3 大 4(偏移了一个 int)

这就是多继承下,父类指针会自动偏移到对应子对象位置。

总结:

  1. 多继承时,子类对象会按继承顺序,把所有父类对象依次放在前面

  2. 不同父类指针指向同一个子类对象时,地址可能不一样(会自动偏移)

  3. 但它们都指向同一个对象的不同部分

8. 继承和组合

8.1 继承和组合的区别

|------|--------------------|--------------------|
| 特性 | 继承(public) | 组合 |
| 关系 | is-a(派生类是一个基类) | has-a(类包含其他类对象) |
| 复用方式 | 白箱复用(基类内部细节对派生类可见) | 黑箱复用(被组合对象内部细节不可见) |
| 耦合度 | 高(基类改变影响派生类) | 低(对象间依赖弱) |
| 封装性 | 一定程度破坏封装 | 保持良好封装 |
| 优先选择 | 适合is-a关系或实现多态 | 优先使用,代码维护性好 |

cpp 复制代码
#include<iostream>
#include<string>
#include<vector>
using namespace std;

// 组合示例:Car has-a Tire
// 语义:有一个
class Tire
{
protected:
    string _brand = "Michelin"; // 轮胎品牌
    size_t _size = 17;          // 轮胎尺寸
};

class Car
{
protected:
    string _colour = "白色";    // 颜色
    string _num = "陕ABIT00";   // 车牌号

    // 组合:Car 里面有 4 个 Tire
    // 关系:has-a(有一个)
    Tire _t1;
    Tire _t2;
    Tire _t3;
    Tire _t4;
};

// 继承示例:BMW is-a Car
// 语义:是一个
class BMW : public Car
{
public:
    void Drive() { cout << "好开-操控" << endl; }
};

class Benz : public Car
{
public:
    void Drive() { cout << "好坐-舒适" << endl; }
};

// stack 和 vector 的两种实现方式
// 1. 继承方式(不推荐)
// 关系:is-a
// 问题:stack 不该支持 vector 的所有接口(比如 insert、erase)
template<class T>
class stack : public vector<T>
{
    // ...
};

// 2. 组合方式(实际 STL 标准推荐)
// 关系:has-a
// 优点:只暴露需要的接口,低耦合
template<class T>
class stack
{
public:
    // 栈的接口,底层用 vector 实现
private:
    vector<T> _v; // 组合:栈 有一个 vector
};

核心考点:什么时候用继承 / 组合?

  1. 继承:is-a(是一个), 满足 "是一种" 才用继承

例: BMW is a Car ✅ Student is a Person ✅ Cat is a Animal ✅

  1. 组合:has-a(有一个), 满足 "有一个" 才用组合

例: Car has a Tire ✅ Person has a heart ✅ stack has a vector

为什么 stack 推荐用组合而不是继承?

  1. 继承会把父类所有接口都继承下来

stack 本来只应该:push、pop、top;如果继承 vector,就会拥有 insert、erase、[] 等不该有的接口;破坏封装,不安全

  1. 组合低耦合、更安全

只对外暴露栈的接口;底层用 vector 实现,但对外不可见;高内聚、低耦合

总结:is-a 关系 → 用继承;has-a 关系 → 用组合;能用组合就尽量不用继承(组合耦合更低、更安全)


继承是 C++ 面向对象代码复用的核心,理解好权限、隐藏、多继承与虚继承,分清is-a继承和has-a组合的适用场景,才能写出更健壮、易维护的程序。

相关推荐
算法备案代理2 小时前
深度合成算法备案:生成式AI需要备案吗?
人工智能·算法·算法备案
菜鸡儿齐2 小时前
leetcode-全排列
算法·leetcode·深度优先
Wect2 小时前
LeetCode 102. 二叉树的层序遍历:图文拆解+代码详解
前端·算法·typescript
不想看见4042 小时前
Maximal Square 基本动态规划:二维--力扣101算法题解笔记
算法·leetcode·动态规划
Hag_202 小时前
LeetCode Hot100 53.最大子数组和
数据结构·算法·leetcode
一个人旅程~2 小时前
win10LTSB2016与win10LTSC2019对于老机型哪个更合适?
linux·windows·经验分享·电脑
王老师青少年编程2 小时前
csp信奥赛C++之反素数
数据结构·c++·数学·算法·csp·信奥赛·反素数
Renhao-Wan2 小时前
Java 算法实践(七):动态规划
java·算法·动态规划
峰顶听歌的鲸鱼2 小时前
Zabbix监控系统
linux·运维·笔记·安全·云计算·zabbix·学习方法