【C++】继承篇

1. 继承的概念及定义

1.1 继承的概念

继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有 类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的 复⽤,继承是类设计层次的复⽤。

下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/ 电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们 也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣ 的独有成员函数是学习,⽼师的独有成员函数是授课。

继承的核心目的:代码复用、建立类之间的层次关系、为多态打基础 一句话:派生类(子类)继承基类(父类),获得基类的成员

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;          
};
int main()
{
return 0;
}

下⾯我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复⽤这些成员,就 不需要重复定义了,省去了很多⿇烦。

cpp 复制代码
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 派生类名 : 继承方式 基类名
{
    // 派生类自己新增的成员
};

三种继承方式:public / protected / private

继承方式 基类 public 成员 基类 protected 成员 基类 private 成员 说明
public 公有继承 public protected 不可见 最常用,访问权限不变
protected 保护继承 protected protected 不可见 对外全部隐藏,只给后代类访问
private 私有继承 private private 不可见 基类成员到子类全变私有,后续再派生就无法访问

重点:基类的 private 成员,任何继承方式下,派生类内部都不能直接访问!

如果想让子类可以访问、但是对外隐藏:把成员设为 protected

默认继承:

  • class:默认 private 继承
  • struct:默认 public 继承
  1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。

  2. 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

  3. 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员 在派⽣类的访问⽅式==Min(成员在基类的访问限定符,继承⽅式),public >protected>private。

  4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。

  5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤ protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实 际中扩展维护性不强。

1.3 继承类模板

三种继承组合形式

  1. 普通类 继承 类模板

基类是模板,继承时必须指定模板参数,让基类实例化

cpp 复制代码
template<class T>
class Base{};
class Derive : public Base<int>{}; // ✅
// class Derive : public Base;      // ❌ Base只是模板,不是类

特点:基类已经确定实例化,没有依赖名字查找问题,成员可以直接访问。

  1. 模板类 继承 普通类
cpp 复制代码
class Base{};
template<class T>
class Derive : public Base{};

特点:基类和 T 无关,不属于依赖基类,直接调用成员,无查找报错。

  1. 模板类 继承 类模板
cpp 复制代码
namespace cl
{
	template<class T>
	class vector
	{};
	// stack和vector的关系,既符合is - a,也符合has - a
	template<class T>
	class stack : public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域,
			// 否则编译报错: error C3861 : "push_back":找不到标识符
			// 因为stack<int>实例化时,也实例化vector<int>了
			// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
			// 模板继承,访问依赖基类成员,加 `this->` 或者 `基类<T>::`
			std::vector<T>::push_back(x);
			//this->push_back();
		}

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

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

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

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

核心原理:两阶段名字查找(two-phase lookup)

模板解析分两步:

  1. 第一阶段(模板定义阶段,未实例化) 编译器看到 Base<T>,它依赖模板参数 T,属于依赖基类 。 规则:编译器不会进入依赖基类内部查找名字,防止歧义。
  2. 第二阶段(实例化阶段,如 Derive<int>) T 确定,才实例化基类 Base<int>,此时才知道基类里面的成员。

一句话口诀:模板子类继承模板基类,不能直接写基类成员名 两种修复写法:this->成员名 / 基类<T>::成员名

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

通常情况下我们把⼀个类型的对象赋值给另⼀个类型的指针或者引⽤时,存在类型转换,中间会产 ⽣临时对象,所以需要加const,如: int a = 1; const double& d = a; public继承 中,就是⼀个特殊处理的例外,派⽣类对象可以赋值给基类的指针/基类的引⽤,⽽不需要加 const,这⾥的指针和引⽤绑定是派⽣类对象中的基类部分,如下图所⽰。也就意味着⼀个基类的指针或者引⽤,可能指向基类对象,也可能指向派⽣类对象。

派⽣类对象赋值给基类对象是通过基类的拷⻉构造函数或者赋值重载函数完成的,这个过程就像派⽣类⾃⼰定义部分成员切掉了⼀样,所以也被叫做切割或者切⽚,如下图中所⽰。

基类对象不能赋值给派⽣类对象。

基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针 是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-TimeType Information)的dynamic_cast 来进⾏识别后进⾏安全转换。

核心概念:向上转型(赋值兼容规则),是 public 继承独有的特殊转换规则

1. 向上转型(public 继承专属,重点)

派生类对象 → 基类指针 / 基类引用(前面可以赋值给后面)

cpp 复制代码
class Base {};
class Derive : public Base {};

Derive d;
Base* p = &d;    // ✅ 向上转型,不需要const
Base& r = d;     // ✅ 向上转型,不需要const

原理:指针 / 引用绑定的是派生类对象里面的基类子对象,没有产生临时对象!

对比普通隐式转换(会产生临时对象,引用必须 const):

cpp 复制代码
int a = 1;
const double& d = a; 
// 这里生成临时double,临时对象具有常性,所以引用必须加const

普通类型转换:产生临时对象 → 引用必须const public 继承向上转型:直接绑定派生里的基类部分,不产生临时对象 → 不用 const

一句话:public 继承的向上转型是 C++ 的赋值兼容特例

2. 对象切片(切片 / 切割)

派生类对象 → 基类对象(不是指针、引用!是对象赋值)

cpp 复制代码
Derive d;
Base b = d; // ✅ 切片,调用Base的拷贝构造

原理:只会拷贝派生类里面基类部分的成员 ,派生类自己新增的成员直接丢掉,像被切掉一样。 ⚠️ 注意:切片是对象之间赋值,指针 / 引用向上转型不会切片!

3. 反向规则(向下)

  1. 基类对象 不能直接赋值给派生类对象
cpp 复制代码
Base b;
Derive d = b; // ❌ 编译报错,不允许

基类里面没有派生类新增的成员,信息不全,无法构造派生对象。

2.基类指针 / 引用 → 派生类指针 / 引用(向下转型)

不能直接隐式转换,必须强制转换,分两种:

  • static_cast / C 风格强制转换:不安全,编译器不检查 如果基类指针本身指向的就是基类对象,强行转成派生指针,访问派生独有成员会越界崩溃
  • dynamic_cast(多态场景专用)
  • ✅ 要求:基类必须包含虚函数(基类是多态类型)
  • ✅ 作用:运行时 RTTI 检查
    • 如果基类指针确实指向派生类对象 → 转换成功,返回派生指针
    • 如果指向的就是基类本身 → 返回nullptr(指针版本)/ 抛异常(引用版本)
转换方向 是否隐式 是否切片 备注
派生类对象 → 基类指针 / 引用(向上转型) ✅ 允许(public 继承) ❌ 不切片 绑定基类子对象,无临时,不用 const
派生类对象 → 基类对象 ✅ 允许 ✅ 切片 调用基类拷贝构造 / 赋值重载,丢掉派生独有成员
基类对象 → 派生类对象 ❌ 禁止 - 信息不足
基类指针 / 引用 → 派生类指针 / 引用(向下转型) ❌ 不能隐式,必须强转 - dynamic_cast 需要基类多态,运行时安全检查
  1. 向上转型只有public 继承才支持;protected/private 继承不支持赋值兼容
  2. 指针引用向上转型不会切片;只有对象赋值才会切片(超级容易考判断题)
  3. 普通内置类型隐式转换产生临时对象,引用需要 const;向上转型是特例,不用 const
  4. dynamic_cast 依赖 RTTI,基类必须有虚函数,否则编译失败
  5. 向下转型风险很高:只有原本指向派生对象时,转换才有意义

3. 继承中的作⽤域

3.1 隐藏规则:

  1. 在继承体系中基类和派⽣类都有独⽴的作⽤域。

  2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派⽣类成员函数中,可以使⽤基类::基类成员显⽰访问)

  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

  4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。

5.成员函数隐藏判断重点:只要函数名相同,就构成隐藏,不看参数、返回值!

和重载区分开:重载要求同一个作用域 ,函数名相同、参数不同; 隐藏是不同作用域(基类 / 派生类),只要名字一样就隐藏,参数不一样也照样隐藏。

cpp 复制代码
class Base
{
public:
    void func()
    {
        cout << "Base::func()" << endl;
    }
    int _a = 1;
};

class Derive : public Base
{
public:
    void func(int x)  // 名字和基类func一样 → 构成隐藏!不是重载
    {
        cout << "Derive::func(int)" << endl;
    }
    int _a = 100;
    void test()
    {
        // func();    // ❌ 报错!直接找派生类的func,派生类func要参数
        func(10);     // ✅ 调用派生类自己的func
        Base::func(); // ✅ 显式指定,访问基类被隐藏的func

        cout << _a << endl;        // 100 派生类的_a
        cout << Base::_a << endl;  // 1   基类被隐藏的_a
    }
};

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

C++ 一共 6 个默认成员函数:构造、拷贝构造、赋值重载operator=、析构、取地址、const 取地址;这里重点考察前 4 个。 核心逻辑:派生类里面包含基类子对象,基类部分必须交给基类自己的成员函数去初始化 / 清理

规则逐条梳理

  1. 派生类构造函数

派生类构造,必须调用基类构造函数初始化基类成员

  • 如果基类有无参默认构造:编译器自动在派生类初始化列表调用基类默认构造
  • 如果基类没有默认构造 (只写了带参构造):派生类构造必须在初始化列表显式调用基类带参构造
cpp 复制代码
class Base
{
public:
    Base(int a)
    :_a(a)
    {} // 没有默认构造
    int _a;
};
class Derive : public Base
{
public:
    // 必须初始化列表显式调用Base构造,否则编译报错
    Derive(int x)
    :Base(x)
    {}
};

执行顺序:先基类构造 → 再派生类构造

2.派生类拷贝构造函数

编译器自动生成的派生拷贝构造,会自动调用基类的拷贝构造函数 ,拷贝基类子对象部分。 如果自己手写拷贝构造,必须手动在初始化列表调用基类拷贝构造,否则基类部分走默认构造,不是拷贝!

cpp 复制代码
Derive(const Derive& d)
:Base(d) 
// Base(d) 调用基类拷贝构造
{
    // 拷贝派生自己的成员
}
  1. 派生类赋值重载 operator=

编译器自动生成的赋值重载,会自动调用**基类的 operator=**赋值基类部分。

⚠️ 重点坑:派生类的operator=会隐藏基类的 operator= ! 如果手写赋值重载,必须手动显式调用基类赋值重载,带上类域:

cpp 复制代码
Derive& operator=(const Derive& d)
{
    if(this == &d) 
    return *this;
    Base::operator=(d); // 显式调用基类赋值重载,不能直接写operator=(d)
    // 赋值派生自己新增成员
    return *this;
}
  1. 派生类析构函数
  • 不需要手动调用基类析构!派生类析构函数执行完毕之后,编译器自动调用基类析构
  • 执行顺序:先派生类析构(清理派生新增成员) → 再基类析构(清理基类成员)

为什么这个顺序?构造是先老后新;析构是先新后老,栈的销毁逻辑。

  1. 析构函数隐藏的特殊考点(非常高频) 编译器会把所有析构函数名字统一处理成 destructor() 👉 所以:基类析构、派生析构,函数名本质是一样的
  • 如果基类析构没有加virtual:派生析构和基类析构构成隐藏
  • 如果基类析构加virtual:满足条件就构成重写(override),用于多态

执行顺序速记

构造:基先派后

析构:派先基后

对比表格

成员函数 编译器自动生成时行为 手写时要求
构造 自动调用基类默认构造 基类无默认构造 → 初始化列表手动调用基类构造
拷贝构造 自动调用基类拷贝构造 初始化列表手动写 Base(d)
operator= 自动调用基类赋值重载 函数体内写 Base::operator=(d),不能省略类域
析构 派生析构结束后自动调用基类析构 完全不用手动调用基类析构

易错点汇总

  1. ❌ 手写派生构造,忘记在初始化列表调用带参基类构造 → 编译报错
  2. ❌ 手写赋值重载,直接写operator=(d),不加Base:: → 递归调用自己,死循环
  3. ❌ 手写拷贝构造,不写Base(d),基类部分走默认构造,不是拷贝
  4. ❌ 手动在派生析构里面写 Base::~Base(),重复释放(灾难)
  5. ✅ 析构名字特殊处理,不加 virtual 就是隐藏,不是重写

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

⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以 后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。

⽅法2:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。

cpp 复制代码
// C++11的⽅法
class Base final
{
public:
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;



private:
	//// C++98的方法
	//Base()
	//{}
};

class Derive :public Base
{
    void func4() { cout << "Derive::func4" << endl; }
protected:
    int b = 2;
};
int main()
{
    Base b;
    Derive d;
    return 0;
}

5. 继承与友元

友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员 。

cpp 复制代码
// 前置声明
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; // 学号

};
void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}
int main()
{
    Person p;
    Student s;
    //  编译报错:error C2248 : "Student::_stuNum" :⽆法访问protected成员
    // 解决⽅案:Display也变成Student的友元即可

    Display(p, s);
    return 0;
}
//
//1. * *友元关系不能继承 * *:父类的友元,不会自动成为子类的友元
//2. 友元关系单向、不能传递、不能继承
//3. protected 成员:本类 + 子类内部可以访问;外部普通函数不能访问,除非声明为友元

6. 继承与静态成员

基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例。

静态成员属于类本身,不属于某个对象;继承之后,基类的静态成员,在整个继承体系里只有一份!

cpp 复制代码
class Base
{
public:
    static int _staticNum;
};
int Base::_staticNum = 0;

class Derive : public Base
{};

关键要点

  1. 内存只有一份 Base::_staticNum 只有一份,不管派生类继承多少层、创建多少个 Base/Derive 对象,共用同一个静态成员。
  2. 访问方式(三种都可以)
cpp 复制代码
Base::_staticNum = 10;    // 基类名::静态成员
Derive::_staticNum = 20;  // ✅ 派生类名也能访问到基类静态成员(继承可见)
Base b;
Derive d;
b._staticNum = 30;
d._staticNum = 40;

上面所有写法操作的都是同一个变量。

3.作用域、同名隐藏

如果派生类定义了和基类同名的静态成员:构成隐藏

cpp 复制代码
class Base
{
public:
    static int a;
};
int Base::a = 1;
class Derive : public Base
{
public:
    static int a; // 和基类静态成员同名 → 隐藏
};
int Derive::a = 100;
cpp 复制代码
cout << Derive::a;        // 100 派生自己的静态a
cout << Derive::Base::a;  // 1   基类的静态a,显式指定类域访问

静态成员的隐藏规则和普通成员一模一样:同名就隐藏,静态只是存储在全局静态区,不属于对象。

项目 普通成员 静态成员
归属 属于对象 属于类,一份
继承后 每个对象独立一份 整个继承体系共用一份
同名 隐藏 隐藏
virtual 可以虚函数重写 不能 virtual,不能重写
调用 对象. / 指针 -> 类名::优先,也可以对象.

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

7.1 继承模型

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承

多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。

菱形继承:菱形继承是多继承的⼀种特殊情况。

菱形继承的问题,从下⾯的对象成员模型构造,可以 看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就 ⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议 设计出菱形继承这样的模型的。

7.2 虚继承

很多⼈说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有 菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可 以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java。

8. 继承和组合

8.1 继承和组合

• public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。

• 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。

• 继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤ (white-box reuse)。术语"⽩箱"是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可 ⻅。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依 赖关系很强,耦合度⾼。

• 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-boxreuse), 因为对象的内部细节是不可⻅的。对象只以"⿊箱"的形式出现。组合类之间没有很强的依赖关 系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。

• 优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太 那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的 关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。

相关推荐
SomeOtherTime15 分钟前
第 1 讲:内存布局与所有权解剖——栈帧、胖指针与“移动/拷贝”在汇编层面的真实面目
java·开发语言·汇编
青少儿编程课堂18 分钟前
蓝桥杯青少组图形化 + 全国青少年信息素养大赛 + 电子学会图形化考级:校园大富翁经营闯关解析
c++·python·算法·bfs·信息学竞赛
another heaven19 分钟前
【C/C++ 第一次跨越(上):用 C++11/14 停止制造新的旧代码】
c语言·c++·制造
qq_4069810320 分钟前
c++ lamda(三)Lambda表达式
java·前端·c++
头发还在的女程序员20 分钟前
Java在线租房系统源码解析:Spring Boot+Vue 3+UniApp房源管理与预约看房方案
java·vue.js·spring boot·租房小程序·租房系统·租房软件
FYKJ_201027 分钟前
springboot同人周边订单管理系统48955-计算机毕业设计、课程设计
java·javascript·vue.js·spring boot·django·php·课程设计
总要学点什么38 分钟前
第106篇:三维特效-流动线效果——让一颗光点沿整条线一路滚到头
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
Flynn Liang1 小时前
LLM 应用安全:Prompt 注入、越权与输入输出护栏
java·人工智能·大模型·langchain4j·llm 安全·prompt 注入·guardrail
特立独行的猫A7 小时前
用仓颉语言 cj-tauri 框架,五分钟给娃做了个鸿蒙形近字游戏
java·后端·python