【C++】继承

前言

C++有三大特性------封装、继承、多态,是面向对象的基石。此前模拟实现string、vector、list等容器时,我们也就体会到封装的价值,迭代器本身属于三大特性中的封装,所有会感到string、vector、list的结构很相似,但底层天差地别,这就在于把底层复杂的细节全部屏蔽掉,然后用相似的迭代器来访问,这就是封装带来的便利之处。

前面我们模拟实现过string、vector、list、stack、queue的底层结构,那这篇博客就来细讲C++三大特性之一的继承。

继承

  • 一、继承的概念及定义
      • [1.1 无继承的痛点:代码冗余](#1.1 无继承的痛点:代码冗余)
      • [1.2 继承的解决方案:抽离公共部分](#1.2 继承的解决方案:抽离公共部分)
  • 二、继承的基础语法
    • [2.1 继承的定义格式](#2.1 继承的定义格式)
    • [2.2 继承方式与成员访问权限](#2.2 继承方式与成员访问权限)
    • [2.3 类模板的继承](#2.3 类模板的继承)
  • 三、基类与派生类的类型转换
  • 四、继承的核心坑点:作用域与隐藏
  • [4.1 类域的隐藏规则](#4.1 类域的隐藏规则)
    • [4.2 经典面试题:函数隐藏 vs 重载](#4.2 经典面试题:函数隐藏 vs 重载)
  • 五、派生类的默认成员函数
    • [5.1 构造函数](#5.1 构造函数)
    • [5.2 析构函数](#5.2 析构函数)
    • [5.3 拷贝构造 / 赋值重载](#5.3 拷贝构造 / 赋值重载)
    • [5.4 总代码](#5.4 总代码)
  • 六、继承的特殊场景
    • [6.1 不能被继承的类](#6.1 不能被继承的类)
    • [6.2 继承与友元](#6.2 继承与友元)
    • [6.3 继承与静态成员](#6.3 继承与静态成员)
  • [七、多继承与菱形继承(C++ 的坑)](#七、多继承与菱形继承(C++ 的坑))
    • [7.1 多继承的基本概念](#7.1 多继承的基本概念)
    • [7.2 菱形继承的问题](#7.2 菱形继承的问题)
    • [7.3 虚继承解决菱形继承(不推荐)](#7.3 虚继承解决菱形继承(不推荐))
    • [7.4 多继承中指针偏移问题](#7.4 多继承中指针偏移问题)
    • [7.5 IO库中的菱形虚拟继承](#7.5 IO库中的菱形虚拟继承)
  • [八、继承 vs 组合(设计原则)](#八、继承 vs 组合(设计原则))

一、继承的概念及定义

C 语言的复用停留在函数层级 ,而 C++ 的继承实现了类层级的复用 ------ 在保留原有类(基类)成员的基础上,扩展新成员生成派生类,贴合 "从简单到复杂" 的认知逻辑。

1.1 无继承的痛点:代码冗余

StudentTeacher类为例,二者包含大量重复的成员(姓名、地址、身份验证等),仅少数成员不同:

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

// 学生类
class Student
{
public:
	void identity() { /* 身份验证逻辑 */ } // 重复
	void study() { /* 学习逻辑 */ }        // 独有
protected:
	string _name; string _address; string _tel; int _age; // 重复
	int _stuid; // 独有
};

// 教师类
class Teacher
{
public:
	void identity() { /* 身份验证逻辑 */ } // 重复
	void teaching() { /* 授课逻辑 */ }     // 独有
protected:
	string _name; string _address; string _tel; int _age; // 重复
	string _title; // 独有
};

重复代码不仅增加开发量,还会导致后续维护成本翻倍(比如修改身份验证逻辑需改两处)。

1.2 继承的解决方案:抽离公共部分

将重复成员抽离为Person基类,StudentTeacher通过继承复用这些成员,仅需定义独有部分:

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

// 基类:封装学生/教师的公共成员
class Person
{
public:
    void identity() {
        cout << "身份验证:" << _name << endl;
    }
protected:
    string _name = "yuuki";
    string _address;
    string _tel;
    int _age = 18;
};

// 派生类:学生(public继承Person)
class Student : public Person
{
public:
    void study() { /* 学习逻辑 */ }
protected:
    int _stuid; // 独有成员
};

// 派生类:教师(public继承Person)
class Teacher : public Person
{
public:
    void teaching() { /* 授课逻辑 */ }
protected:
    string _title; // 独有成员
};

int main()
{
    Student s;
    Teacher t;
    s.identity(); // 复用基类的identity方法
    t.identity(); // 复用基类的identity方法
    return 0;
}

输出结果

复制代码
身份验证:yuuki 
身份验证:yuuki

二、继承的基础语法

2.1 继承的定义格式

plaintext 复制代码
class 派生类名 : 继承方式 基类名 { 
// 派生类独有成员 
};
  • 基类(父类) :被继承的类(如Person);
  • 派生类(子类) :基于基类扩展的类(如Student/Teacher);
  • 继承方式public/protected/private(实际开发优先用public)。

2.2 继承方式与成员访问权限

基类成员有public/protected/private三种访问权限,不同继承方式会改变派生类中基类成员的访问权限,核心规则如下(记重点即可):

核心规则 说明
1 基类private成员:无论哪种继承方式,派生类中不可访问(仅基类自身可访问);
2 基类protected成员:派生类可访问,外部不可访问;
3 class默认继承方式为privatestruct默认为public
4 实际开发仅用public继承(protected/private继承扩展性差)。
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Person
{
public:
	void Print() { cout << _name << endl; } // public成员
protected:
	string _name; // protected成员
private:
	int _age; // private成员
};

// public继承(推荐)
class Student : public Person
{
public:
	void Test() {
		Print(); // 可访问(基类public→派生类public)
		_name = "Tom"; // 可访问(基类protected→派生类protected)
		// _age = 20; // 不可访问(基类private)
	}
protected:
	int _stuid;
};

2.3 类模板的继承

继承模板类时,需通过类模板名<类型>::指定基类域(编译器无法自动推导):

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

namespace yuuki
{
    // 继承std::vector模板类实现栈
    template<class T>
    class stack : public std::vector<T>
    {
    public:
        void push(const T& x) {
            vector<T>::push_back(x); // 必须指定vector<T>域
        }
        void pop() {
            vector<T>::pop_back();
        }
        const T& top() {
            return vector<T>::back();
        }
        bool empty() {
            return vector<T>::empty();
        }
    };
}

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

三、基类与派生类的类型转换

public继承下的类型转换是面试高频考点,核心规则:

  1. 派生类对象 → 基类指针 / 引用:直接支持(称为 "切片"------ 切出派生类中的基类部分);
  2. 基类对象 → 派生类对象:不支持(基类不含派生类的独有成员);
  3. 基类指针 → 派生类指针:需强制类型转换(仅当基类指针指向派生类对象时安全)。
cpp 复制代码
#include<iostream>
using namespace std;

class Person // 基类
{
    virtual void func() {} // 虚函数(为dynamic_cast做准备)
protected:
    string _name;
    int _age;
};

class Student : public Person // 派生类
{
public:
    int _stuid;
};

int main()
{
    Student sobj;
    // 1. 派生类对象 → 基类指针/引用(切片)
    Person* pp = &sobj;
    Person& rp = sobj;
    Person pobj = sobj; // 切片赋值

    // 2. 基类对象 → 派生类对象(报错)
    // sobj = pobj; 

    // 3. 基类指针 → 派生类指针(安全场景)
    Student* ps1 = dynamic_cast<Student*>(pp); // pp指向sobj,转换成功
    cout << ps1 << endl; // 非空地址

    // 3. 基类指针 → 派生类指针(不安全场景)
    Person pobj2;
    pp = &pobj2;
    Student* ps2 = dynamic_cast<Student*>(pp); // pp指向基类对象,转换失败
    cout << ps2 << endl; // 空地址

    return 0;
}

四、继承的核心坑点:作用域与隐藏

4.1 类域的隐藏规则

继承体系中,基类和派生类有独立作用域,若出现同名成员,派生类成员会 "隐藏" 基类成员:

  1. 同名成员变量:优先访问派生类的;
  2. 同名成员函数:仅函数名相同就隐藏(无需参数 / 返回值一致);
  3. 访问被隐藏的基类成员:需加基类名::
cpp 复制代码
#include<iostream>
using namespace std;

class Person // 基类
{
protected:
	string _name = "yuuki";
	int _num = 18; // 身份证号
};

class Student : public Person // 派生类
{
public:
	void Print() {
		cout << "姓名:" << _name << endl; // 复用基类_name
		cout << "学生编号:" << _num << endl; // 访问派生类_num(隐藏基类)
		cout << "身份证号:" << Person::_num << endl; // 访问被隐藏的基类_num
	}
protected:
	int _num = 999; // 学生编号(与基类_num同名)
};

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

输出结果

复制代码
姓名:yuuki
学生编号:999
身份证号:18

4.2 经典面试题:函数隐藏 vs 重载

cpp 复制代码
class A
{
public:
	void func() { cout << "func()" << endl; }
};

class B : public A
{
public:
	void func(int i) { cout << "func(int i): " << i << endl; }
};

int main()
{
	B b;
	b.func(10); // 正常调用B::func(int)
	// b.func(); // 报错!A::func()被B::func(int)隐藏,无法直接访问
	b.A::func(); // 正确:显式访问基类被隐藏的函数
	return 0;
}

结论 :A 和 B 的func隐藏关系(而非重载)------ 重载要求函数在同一作用域,而隐藏是不同作用域的同名函数。

五、派生类的默认成员函数

派生类的 6 个默认成员函数(构造、拷贝构造、赋值重载、析构等),需遵循 "先基类、后派生类" 的规则:

5.1 构造函数

  • 派生类构造必须先调用基类构造,初始化基类成员;
  • 若基类无默认构造(无参 / 全缺省),派生类需在初始化列表显式调用基类构造。
cpp 复制代码
class Person
{
public:
    // 基类无默认构造(必须传参)
    Person(const char* name) : _name(name) {
        cout << "Person构造" << endl;
    }
protected:
    string _name;
};

class Student : public Person
{
public:
    // 派生类构造:先调用Person(name),再初始化_stuid
    Student(const char* name, int stuid)
        : Person(name) // 显式调用基类构造(必须)
        , _stuid(stuid)
    {
        cout << "Student构造" << endl;
    }
protected:
    int _stuid;
};

int main()
{
    Student s("Tom", 1001); // 输出:Person构造 → Student构造
    return 0;
}

5.2 析构函数

  • 派生类析构执行完毕后,编译器自动调用基类析构(保证 "先析构派生、后析构基类");
  • 析构函数名会被编译器统一处理为destructor(),因此基类析构不加virtual时,派生类析构会隐藏基类析构。
cpp 复制代码
class Person
{
public:
	~Person() { cout << "Person析构" << endl; }
};

class Student : public Person
{
public:
	~Student() { cout << "Student析构" << endl; }
};

int main()
{
	Student s; // 析构顺序:Student析构 → Person析构
	return 0;
}

5.3 拷贝构造 / 赋值重载

  • 拷贝构造:派生类需先拷贝基类部分,再拷贝自身成员;
  • 赋值重载:派生类需先调用基类的operator=,再赋值自身成员。
cpp 复制代码
class Person
{
public:
    Person(const char* name = "yuuki") : _name(name) {}
    // 基类拷贝构造
    Person(const Person& p) : _name(p._name) {
        cout << "Person拷贝构造" << endl;
    }
    // 基类赋值重载
    Person& operator=(const Person& p) {
        if (this != &p) _name = p._name;
        cout << "Person赋值重载" << endl;
        return *this;
    }
protected:
    string _name;
};

class Student : public Person
{
public:
    // 派生类拷贝构造
    Student(const Student& s)
        : Person(s) // 拷贝基类部分
        , _stuid(s._stuid)
    {
        cout << "Student拷贝构造" << endl;
    }
    // 派生类赋值重载
    Student& operator=(const Student& s) {
        if (this != &s) {
            Person::operator=(s); // 调用基类赋值重载
            _stuid = s._stuid;
        }
        cout << "Student赋值重载" << endl;
        return *this;
    }
protected:
    int _stuid = 1001;
};

int main()
{
    Student s1;
    Student s2 = s1; // 拷贝构造:Person拷贝构造 → Student拷贝构造
    Student s3;
    s3 = s1; // 赋值重载:Person赋值重载 → Student赋值重载
    return 0;
}

5.4 总代码

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

class Person
{
public:

	/*默认构造第一种情况*/

	//// 默认构造(有初始化)
	//Person(const char* name = "YUUKI")
	//	:_name(name)
	//{
	//	cout << "Person()" << endl;
	//}


	/*默认构造第二种情况,需要子类帮助*/

	// 默认构造(无初始化)
	Person(const char* name)
		:_name(name)
	{
		cout << "Person()" << endl;
	}

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

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

	// 析构函数
	~Person()
	{
		cout << "~Person" << endl;
	}
protected:
	string _name;
};

class Student : public Person
{
public:
	// 默认生成的构造函数的行为
	// 1、内置类型->不确定
	// 2、自定义类型->调用默认构造
	// 3、继承父类成员看做一个整体对象,调用父类的默认构造

	// 子类默认构造函数,
	Student(const char* name, int num, const char* addrss)
		// 将Person看做一个整体
		:Person(name)// 错误写法  ->  :_name(name)
		,_num(num)
		,_addrss(addrss)
	{}

	// 报错:因为父类和子类改成隐藏,因重复调用子类,导致栈溢出
	/*Student& operator=(const Student& s)
	{
		operator=(s);
	}*/

	~Student()
	{
		// 错误写法: ~Person();
		// 原因:
		// 1. 语法定义,先子类后父类。如果写成上面,就成了先父类再子类
		// 2. 子类析构完后,编译器会自动掉用父类的析构
	}
protected:
	// 无缺省值
	/*int _num;
	string _addrss;*/

	// 有缺省值
	int _num = 18;
	string _addrss = "广东佛山市"; // 自定义类型会调用自动生成的构造
};

int main()
{
	Student s1("yuuki", 18, "广东佛山市");
	Student s2(s1);

	Student s3("YUUKI", 28, "广东深圳市");
	s1 = s3; // 不需要在子类写赋值运算符,只需要父类里写即可

	return 0;
}

方法:将父类看成一个类型,与其他类型一起编写,可更好理解

六、继承的特殊场景

6.1 不能被继承的类

  • 方法 1(C++98):将基类构造函数设为私有(派生类无法调用构造,无法实例化);
  • 方法 2(C++11) :用final关键字修饰基类(直接禁止继承)。
cpp 复制代码
// 方法2:final修饰(推荐)
class Base final
{
public:
	void func() { cout << "Base::func()" << endl; }
};

// class Derive : public Base {}; // 报错!Base被final修饰,不能继承

6.2 继承与友元

友元关系不能继承------ 基类的友元无法直接访问派生类的私有 / 保护成员,需在派生类中重新声明友元:

cpp 复制代码
class Student; // 前向声明

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

class Student : public Person
{
public:
	friend void Display(const Person& p, const Student& s); // 重新声明友元
protected:
	int _stuid = 1001;
};

// 友元函数:可访问Person和Student的保护成员
void Display(const Person& p, const Student& s) {
	cout << p._name << endl;
	cout << s._stuid << endl;
}

int main()
{
	Person p;
	Student s;
	Display(p, s); // 输出:yuuki → 1001
	return 0;
}

6.3 继承与静态成员

基类的静态成员在整个继承体系中只有一份(所有派生类共享):

cpp 复制代码
class Person
{
public:
	static int _count; // 静态成员:统计对象数量
};
int Person::_count = 0; // 静态成员类外初始化

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

int main()
{
	Person::_count++;
	Student::_count++;
	Teacher::_count++;
	cout << Person::_count << endl; // 输出:3(三者共享_count)
	return 0;
}

七、多继承与菱形继承(C++ 的坑)

7.1 多继承的基本概念

  • 单继承:一个派生类只有一个基类(推荐使用);
  • 多继承:一个派生类有多个基类(易出问题,尽量避免);
  • 菱形继承 :多继承的特殊情况(A→B、A→C、B+C→D),会导致数据冗余二义性(D 对象中有两份 A 的成员)。

7.2 菱形继承的问题

cpp 复制代码
class Person { public: string _name; }; // 基类
					 class Student : public Person { public: int _stuid; }; // 派生类1
					 class Teacher : public Person { public: string _title; }; // 派生类2
					 class Assistant : public Student, public Teacher { public: int _id; }; // 菱形顶点

					 int main()
					 {
						 Assistant a;
						 // a._name = "Tom"; // 报错!二义性:_name来自Student还是Teacher?
						 a.Student::_name = "Tom"; // 显式指定,解决二义性(但数据冗余仍存在)
						 a.Teacher::_name = "Jerry";
						 return 0;
					 }

7.3 虚继承解决菱形继承(不推荐)

通过virtual关键字实现虚继承,可消除数据冗余和二义性,但底层实现复杂、性能损耗大,实战中建议避免设计菱形继承

cpp 复制代码
class Person { public: string _name; };
					 class Student : virtual public Person { public: int _stuid; }; // 虚继承
					 class Teacher : virtual public Person { public: string _title; }; // 虚继承
					 class Assistant : public Student, public Teacher { public: int _id; };

					 int main()
					 {
						 Assistant a;
						 a._name = "Tom"; // 正常访问(仅一份_name)
						 return 0;
					 }

7.4 多继承中指针偏移问题

复制代码
选择以下选项:()
A: p1 == p2 == p3  B: p1 < p2 < p3  C: p1 == p3 != p2  D: p1 != p2 != p3
cpp 复制代码
class Base1 { public: int _b1;};
class Base2 { public: int _b2;};
class Derive : public Base1, public Base2 { public: int _d;};

int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	
	return 0;
}

7.5 IO库中的菱形虚拟继承

八、继承 vs 组合(设计原则)

特性 继承(is-a 关系) 组合(has-a 关系)
关系 派生类是一个基类(如 Student 是 Person) 类包含另一个类(如 Car 包含 Engine)
封装性 破坏基类封装(派生类可访问基类保护成员) 高封装(被组合类的细节不可见)
耦合度 高(基类修改会影响派生类) 低(被组合类修改不影响组合类)
复用方式 白箱复用(基类细节可见) 黑箱复用(仅通过接口访问)

设计原则:优先使用组合

  • 若类之间是 "is-a" 关系(如 Student 是 Person),用继承;
  • 若类之间是 "has-a" 关系(如 Car 有 Engine),用组合;
  • 若两者皆可,优先选组合(降低耦合,提升代码可维护性)。

总结

  1. 继承的核心是代码复用 ,实战中优先用public继承;
  2. 继承的核心坑点是同名成员隐藏 ,需通过基类::访问被隐藏成员;
  3. 派生类默认成员函数需遵循 "先基类、后派生类" 的规则;
  4. 多继承(尤其是菱形继承)易出问题,尽量避免;
  5. 设计类时,优先用组合而非继承(降低耦合)。

继承是 C++ 多态的基础,但滥用会导致代码臃肿、难以维护 ------ 理解继承的规则,更要理解 "何时不用继承",才是面向对象设计的关键。

相关推荐
222you5 小时前
Redis的主从复制和哨兵机制
java·开发语言
牛奔5 小时前
如何理解 Go 的调度模型,以及 G / M / P 各自的职责
开发语言·后端·golang
梵刹古音5 小时前
【C++】 析构函数
开发语言·c++
wangjialelele5 小时前
Linux下的IO操作以及ext系列文件系统
linux·运维·服务器·c语言·c++·个人开发
非凡ghost5 小时前
PowerDirector安卓版(威力导演安卓版)
android·windows·学习·软件需求
Sylvia-girl5 小时前
IO流~~
java·开发语言
打工哪有不疯的5 小时前
使用 MSYS2 为 Qt (MinGW 32/64位) 完美配置 OpenSSL
c++·qt
Re.不晚5 小时前
JAVA进阶之路——无奖问答挑战3
java·开发语言
代码游侠5 小时前
C语言核心概念复习——C语言基础阶段
linux·开发语言·c++·学习