C++ 类的继承与派生 学习文档

C++ 类的继承与派生 学习文档
本文章由尚方咸鱼编写,后经是天创呀修改
本文档系统整理 C++ 面向对象编程中基类与派生类、派生类实现、访问控制、构造析构、成员标识、虚基类等核心知识点,包含定义、语法、代码示例与核心规则,适合学习与复习使用。
1、基类与派生类
1.1 类的继承与派生
类的继承:面向对象的核心机制,在保持原有类特性的基础上,进行更具体、更详细的定义。
核心用途:代码重用性(继承) + 可扩充性(派生)。
示例:植物大战僵尸中,普通僵尸为基类,路障僵尸、铁门僵尸为派生类。
继承:新类从已有类获取原有特性(站在已有类角度)。
派生:从已有类创建新类,并新增自身特性(站在新类角度)。
关系:继承与派生是同一过程,仅观察角度不同。
目的:
继承:实现代码重用;
派生:改造原有程序,解决新问题。
1.2 核心概念
基类 / 父类:被继承的已有类;
派生类 / 子类:通过派生得到的新类;
一个基类可以派生多个派生类;
一个派生类可以继承多个基类。
1.3 基类分类
直接基类:直接参与派生当前类的基类;
间接基类:基类的基类及更高层级的基类。
1.4 继承分类
单继承:派生类仅有1 个基类(多继承的特例);
多继承:派生类拥有多个基类(多个单继承的组合)。
2、派生类
2.1 派生类定义语法
// 多继承语法
class 派生类名: 继承方式 基类名1, 继承方式 基类名2,... //默认私有继承
{
派生类成员声明;
};
示例:
// c1公有继承c2,私有继承c3,保护继承c4
class c1:public c2,private c3,protected c4
{
private:
int c1_mem=0; // 新增数据成员
public:
int fun(); // 新增函数成员
};
2.2 代码示例:Rectangle 公有继承 Point
#include <iostream>
using namespace std;
// 基类:点
class Point
{
public:
Point(float xx = 0, float yy = 0) : x(xx), y(yy) {}
void initPoint(float x = 0, float y = 0)
{
this->x = x;
this->y = y;
}
// 移动坐标
void move(float offX, float offY)
{
x += offX;
y += offY;
}
void show() { cout << "this is Point's Show:" << x << y << endl; }
float getX() const { return x; }
float getY() const { return y; }
private:
float x, y;
};
// 派生类:矩形,公有继承Point
class Rectangle : public Point
{
public:
void initRectangle(float x = 0, float y = 0, float w = 0, float h = 0)
{
initPoint(x, y); // 调用基类成员函数
this->w = w;
this->h = h;
}
void show() { cout << "this is Rectangle's Show:" << w << h << endl; }
float getW() const { return w; }
float getH() const { return h; }
private:
float w, h; // 派生类新增成员
};
int main()
{
Rectangle r1;
r1.initRectangle(1, 1, 3.0, 4.0);
r1.move(2, 1); // 调用基类成员函数
cout << r1.getX() << ',' << r1.getY() << ',' << r1.getW() << ',' << r1.getH() << endl;
r1.show();
return 0;
}
规则:派生类会继承基类所有非静态成员(构造函数、析构函数除外)。
2.3 访问控制
2.3.1 继承访问规则总表
| 基类成员 | 公有继承 (public) | 私有继承 (private) | 保护继承 (protected) |
|---|---|---|---|
| public | public | private | protected |
| protected | protected | private | protected |
| private | 不可访问 | 不可访问 | 不可访问 |
详细说明:
基类public成员:
公有继承:子类内 / 外均可访问;
私有 / 保护继承:子类内可访问,类外不可访问。
基类protected成员:三种继承方式下,子类内可访问,类外不可访问。
基类private成员:所有继承方式下,子类均不可直接访问。
简易编程建议: 测试程序中,成员声明为public/protected,使用公有继承,简化访问逻辑。
2.3.2 记忆技巧
基类private成员 → 派生类永远无法直接访问;
基类非private成员 → 访问权限向保守方向变化(public→protected→private);
仅基类 public 成员 + 公有继承,可通过子类对象在类外访问。
2.3.3 成员访问权限基础
| 成员类型 | 类内访问 | 类外访问(通过对象) |
|---|---|---|
| public | 允许 | 允许 |
| protected | 允许 | 禁止 |
| private | 允许 | 禁止 |
2.4 类型兼容性规则
核心定义
在需要基类对象的任何位置,都可以使用公有派生类对象替代,且派生类仅表现出基类的功能。
2.4.1 三大规则
派生类对象 → 隐式转换为基类对象
A a1;
B b1;
a1 = b1;
派生类对象 → 初始化基类引用
B b1;
A &rf = b1;
派生类对象地址 → 转换为基类指针
A *p;
B b;
p = &b;
2.4.2 核心特性:仅发挥基类作用
示例:基类指针指向派生类对象,调用的是基类成员函数。
#include <iostream>
using namespace std;
class Base1{
public:
void display() const{ cout << "Base1::display()" << endl; }
};
class Base2 : public Base1{
public:
void display() const{ cout << "Base2::display()" << endl; }
};
class Derived : public Base2{
public:
void display() const{ cout << "Derived::display()" << endl; }
};
// 函数参数为基类指针
void fun(Base1 *p){ p->display(); }
int main()
{
Base1 b1; Base2 b2; Derived d1;
fun(&b1); fun(&b2); fun(&d1);
return 0;
}
// 运行结果:全部输出 Base1::display()
3、派生类的构造函数和析构函数
3.1 派生类构造函数语法
// 派生类构造函数完整语法(可主动调用基类有参构造)
派生类名::派生类名(形参表):
基类名1(基类1实参), 基类名2(基类2实参), // 主动调用基类有参构造
成员对象名(对象实参), // 初始化内嵌对象有参构造
普通成员(初始值) // 初始化普通数据成员
{
其他初始化操作;
}
// 核心写法示例:基类含带参构造,派生类主动调用
class Base{
public:
// 仅定义带参构造,无默认无参构造
Base(int a){ cout << "基类有参构造调用" << endl; }
};
class Derived : public Base{
public:
// 必须在初始化列表主动调用基类有参构造,否则编译报错
Derived(int x) : Base(x) {
cout << "派生类构造调用" << endl;
}
};
默认构造函数使用条件:
无需调用基类带参构造函数;
无需调用内嵌对象带参构造函数。
系统自动生成默认构造函数,调用基类 / 对象的默认构造函数(无参构造函数)。
核心补充规则(高频易错):
默认调用规则:派生类默认构造函数,会自动调用所有直接基类、内嵌成员对象的无参构造函数,无需手动书写。
报错场景:若基类只定义了带参构造函数、未定义无参构造函数(系统不会自动生成默认无参构造),派生类默认构造时找不到可用的无参构造,直接编译报错。
解决方案1:基类手动补充无参构造函数。
解决方案2:派生类显式定义构造函数,在初始化列表中主动调用基类的有参构造函数,强制覆盖默认调用逻辑。
3.2 构造函数调用顺序 & 核心调用规则
基类构造函数:严格按照类继承声明顺序调用,与构造函数初始化列表书写顺序无关;未手动指定则默认调用无参构造,需传参则必须在初始化列表显式调用。
内嵌对象构造函数:按照类内成员声明先后顺序调用。
派生类自身构造函数:最后执行派生类构造函数函数体内部逻辑。
3.3 代码示例:多继承 + 内嵌对象构造
#include <iostream>
using namespace std;
class Base1{public: Base1(int i) { cout << "Constructing Base1 " << i << endl; }};
class Base2{public: Base2(int i) { cout << "Constructing Base2 " << i << endl; }};
class Base3{public: Base3() { cout << "Constructing Base3 *" << endl; }};
// 继承顺序:Base2 → Base1 → Base3
class Derived : public Base2, public Base1, public Base3
{
public:
Derived(int a,int b,int c,int d):Base1(a),m2(d),m1(c),Base2(b) {}
private:
Base1 m1;
Base2 m2;
Base3 m3;
};
int main(){ Derived obj(1,2,3,4); return 0; }
运行结果:
Constructing Base2 2
Constructing Base1 1
Constructing Base3 *
Constructing Base1 3
Constructing Base2 4
Constructing Base3 *
3.4 派生类复制构造函数
// 语法
Derived::Derived(const Derived &v):Base(v){}
系统默认生成复制构造函数,自动调用基类复制构造函数;
基类delete复制构造函数,派生类也无法拷贝。
3.5 派生类析构函数
语法:Derived::~Derived(){},与普通类写法一致;
自动调用基类和内嵌对象的析构函数;
执行顺序:与构造函数严格相反。
析构顺序:派生类析构 → 内嵌对象析构 → 基类析构。
3.6 析构函数示例
对 3.3 代码添加析构函数,运行结果中析构顺序与构造完全相反。
4、派生类成员的标识和访问
4.1 作用域分辨符 ::
用于区分同名成员,格式:类名::成员名
4.2 同名成员隐藏规则
派生类定义与基类同名函数(无论参数是否相同),基类所有重载函数被隐藏;
同名隐藏≠函数重载(重载要求同一作用域);
调用基类同名函数:必须加基类名::限定。
示例:
class Base{
public:
void print();
void print(int i);
};
class Derived:public Base{
public:
void print(int i,int j,int k); // 隐藏基类所有print
};
// 错误用法:d.print() / d.print(1)
// 正确用法:d.Base::print()
4.3 多继承同名二义性
多个基类有同名成员,派生类未定义 → 访问产生二义性;
解决方案:
方式 1:基类名::限定;
方式 2:派生类定义同名成员,隐藏基类成员。
4.4 多层继承同名问题
多个派生类继承同一个基类,会导致成员冗余、赋值不一致 → 虚基类解决。
5、虚基类
5.1 解决问题
消除多层多继承中共同基类带来的成员冗余、数据不一致问题。
直白来说:在多层 + 多重继承场景下,当多个父类又继承了同一个公共父类时,普通继承会让最终子类里重复保留多份公共父类的成员,造成数据冗余、数据错乱。
5.2 声明语法
class 派生类: virtual 继承方式 基类名{};
关键:第一级继承就必须声明为虚基类。
5.3 代码示例
#include <iostream>
using namespace std;
// 虚基类
class Base0
{
public:
int var0;
void fun0() { cout << "Base0 var0=" << var0 << endl; }
};
// 虚继承
class Base1 : virtual public Base0 {};
class Base2 : virtual public Base0 {};
// 最终派生类
class Derived : public Base1, public Base2 {};
int main()
{
Derived d;
d.var0 = 2; // 直接访问,无冗余、无二义性
d.fun0();
return 0;
}
5.4 核心规则
虚基类成员由最远派生类的构造函数初始化;
仅最远派生类调用虚基类构造函数,其他类调用被忽略;
虚基类有带参构造函数时,所有派生类必须在初始化列表中初始化。
6、虚函数、多态与纯虚函数
承接前文类型兼容性,解决的核心问题,是继承体系的终极用途。
6.1 虚函数 virtual
6.1.1 核心作用
实现:基类指针/引用指向派生类对象时,优先调用派生类的重写方法,而非基类方法。
6.1.2 语法规则
基类中声明:virtual 返回值 函数名(参数) const;
核心:虚函数支持子类重写,实现不同子类的差异化逻辑。
6.2 重写与 override 关键字
6.2.1 重写(覆盖)条件
1、公有继承:class 子类 : public 基类(默认私有继承无法重写);
2、函数签名必须完全一致:返回值、函数名、参数列表、const 常量修饰,四项缺一不可;
3、基类函数必须是 virtual 虚函数。
6.2.2 override 核心作用
强制编译器校验:当前函数是否正确重写匹配基类的虚函数(含const匹配),不满足条件直接报错,杜绝隐性错误。
常见报错原因(实战高频):
① 缺少 public 公有继承;② 子类函数少写/多写 const;③ 函数参数、返回值不匹配;④ 基类非虚函数。
6.2.3 正确重写示例
class Shape {
public:
// 基类虚函数,带const
virtual double area() const = 0;
virtual void print() const = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
public:
double r;
Circle(double r):r(r) {}
// 必须带const + override,完全匹配基类签名
void print() const override {
cout << "Circle(" << r << ")";
}
double area() const override {
return 3.14 * r * r;
}
};
6.3 纯虚函数
6.3.1 定义语法
virtual 返回值 函数名(参数) const = 0;
6.3.2 核心特性
1、无函数体,仅做接口声明;
2、包含纯虚函数的类为抽象类;
3、抽象类,只能作为基类被继承;
4、派生类必须全部重写所有纯虚函数,且严格匹配 const 修饰,否则依然是抽象类,无法创建对象。
6.4 示例(图形计算面积)
结合继承、虚函数、重写、override,实现统一接口调用不同子类逻辑
#include <iostream>
using namespace std;
class Shape {
public:
virtual double area() const = 0; // 纯虚函数:计算面积
virtual void print() const = 0; // 纯虚函数:打印图形信息
virtual ~Shape() {} // 虚析构:避免内存泄漏
};
const double PI = 3.14;
class Circle:public Shape {
public:
double r;
Circle(double r):r(r) {}
void print() const override {
cout << "Circle(" << r << ")";
}
double area() const override {
return PI * r * r;
}
};
class Rectangle:public Shape {
public:
double l, w;
Rectangle(double len, double wid):l(len), w(wid) {}
void print() const override {
cout << "Rectangle(" << l << "," << w << ")";
}
double area() const override {
return l * w;
}
};
int main() {
double r, l, w;
cin >> r >> l >> w;
Circle c(r);
Rectangle rect(l, w);
Shape* shapes[2] = { &c, &rect };
for (int i = 0; i < 2; ++i) {
shapes[i]->print();
cout << " area=" << shapes[i]->area() << endl;
}
return 0;
}
6.5 虚析构函数
6.5.1 作用
基类指针删除派生类对象时,保证先调用派生类析构,再调用基类析构,彻底避免内存泄漏。若不使用虚析构,仅会调用基类析构,派生类资源无法释放。
6.5.2 代码对比示例(有无虚析构区别)
错误示例:普通析构函数(内存泄漏)
#include <iostream>
using namespace std;
class Base {
public:
// 普通析构,非虚析构
~Base() {
cout << "基类析构函数调用" << endl;
}
virtual void test() {}
};
class Derived : public Base {
public:
~Derived() {
cout << "派生类析构函数调用" << endl;
}
};
int main() {
// 基类指针指向派生类对象
Base* p = new Derived();
delete p;
// 仅调用基类析构,派生类析构不执行,内存泄漏
return 0;
}
错误运行结果:仅输出「基类析构函数调用」,派生类析构失效。
正确示例:virtual 虚析构函数(完全析构)
#include <iostream>
using namespace std;
class Base {
public:
// 虚析构函数
virtual ~Base() {
cout << "基类析构函数调用" << endl;
}
virtual void test() {}
};
class Derived : public Base {
public:
~Derived() {
cout << "派生类析构函数调用" << endl;
}
};
int main() {
Base* p = new Derived();
delete p;
// 先析构派生类、再析构基类,资源完全释放
return 0;
}
正确运行结果:
派生类析构函数调用
基类析构函数调用
6.5.3 核心规则(必考)
1、只要类中声明了任意虚函数,该类析构函数必须加 virtual;
2、多态场景下(基类指针/引用操作派生类对象),必须使用虚析构;
3、虚析构不影响构造函数、不影响多态逻辑,只负责安全释放内存。
6.5.2 规则
只要类存在虚函数,必须将析构函数声明为 virtual。
总结
继承实现代码重用,派生实现功能扩展,公有继承是最常用方式;
派生类构造顺序:基类→内嵌对象→自身,析构顺序相反;
同名成员用::区分,多继承二义性通过虚基类解决;
类型兼容性规则:公有派生类对象可替代基类对象,仅表现基类功能;
虚函数实现多态,override 强制校验重写合法性,纯虚函数定义抽象类接口,是面向对象核心考点。





