一、构造函数 & 析构函数 基础
1. 构造函数
语法规则
- 名字和类名完全一样
- 无返回值、不用写
void/int - 对象创建瞬间自动调用
- 作用:初始化成员、申请资源、配置初始化
示例
cpp
#include <iostream>
using namespace std;
class Person {
public:
// 构造函数:和类名同名
Person() {
cout << "构造函数:对象创建了,做初始化\n";
}
};
int main() {
Person p; // 自动调用构造函数
return 0;
}
2. 析构函数
语法规则
- 固定标准写法:
~类名() ~是语法规定,代表「构造的反向操作」- 无返回值、无参数、不能重载
- 对象销毁 /
delete时自动调用 - 作用:释放内存、关文件、清理资源
示例
cpp
class Person {
public:
Person() {
cout << "构造:对象出生\n";
}
// 标准析构函数写法
~Person() {
cout << "析构:对象销毁,清理资源\n";
}
};
int main() {
Person p; // 构造执行
return 0; // 函数结束,p 销毁,自动执行析构
}
3. 为什么 C++ 要有构造/析构?对比 C / Java / Python
- C 语言
没有类、没有对象生命周期,只能手动写init()、free(),没有语法级别的构造析构。
c
// C 只能手动初始化、手动释放
typedef struct { int age; } Person;
void PersonInit(Person* p) { p->age = 18; }
void PersonFree(Person* p) { /* 手动清理 */ }
-
Java
有构造方法,没有析构 ;靠 GC 垃圾回收 自动清理内存,不用程序员手动释放。
-
Python
构造
__init__,析构__del__几乎不用;靠引用计数+GC自动管内存。 -
C++ 独有原因
手动
new/delete管内存,没有自动GC;必须用构造初始化、析构自动清理,绑定对象生命周期,防内存泄漏。
二、虚函数 核心概念 + 示例
1. 什么是虚函数
成员函数前面加 virtual → 虚函数
虚析构本质也是虚函数,共用同一套虚表机制。
2. 虚表机制 通俗理解
- 有虚函数的类,编译器自动生成一张虚表 vtable(存所有虚函数地址)
- 每个对象暗藏一个 vptr 虚表指针,指向自己真实类型的虚表
- 非虚函数:编译静态绑定,只看指针类型
- 虚函数:运行动态绑定,看对象真实类型
3. 虚函数经典示例(多态+重写)
父类形状,子类圆形、方形:
cpp
class Shape {
public:
// 虚函数
virtual void draw() {
cout << "绘制通用形状\n";
}
};
// 子类:圆形
class Circle : public Shape {
public:
// 重写虚函数
void draw() override {
cout << "绘制圆形\n";
}
};
// 子类:方形
class Square : public Shape {
public:
void draw() override {
cout << "绘制方形\n";
}
};
测试 1:父类指针指向子类对象
cpp
int main() {
// 父类指针 装 子类对象
Shape* s1 = new Circle();
Shape* s2 = new Square();
s1->draw(); // 绘制圆形
s2->draw(); // 绘制方形
delete s1;
delete s2;
return 0;
}
👉 虚函数作用:父类指针调用,自动执行子类重写的逻辑。
测试 2:去掉 virtual 会怎样?
把 virtual 删掉,输出变成:
绘制通用形状
绘制通用形状
只认父类指针类型,不认对象真身。
4 为什么不用 Circle* 非要用 Shape*?
单个子类:直接用子类指针没问题
cpp
Circle* c = new Circle();
c->draw();
多个子类:父类指针统一批量管理(核心价值)
cpp
int main() {
// 一个父类数组,装所有子类
Shape* arr[] = {
new Circle(),
new Square()
};
// 统一循环调用,不用逐个写逻辑
for (auto s : arr) {
s->draw();
}
return 0;
}
好处:新增三角形、星星,不用改循环调度代码,符合开闭原则。
5. 虚函数两大内部关联功能
- 允许子类重写父类虚函数,实现自己业务
- 配合父类指针,实现多态统一调度
二者靠同一张虚表机制实现,不是两个独立功能。
三、虚析构函数 原理 + 完整示例
1. 不加虚析构的坑(内存泄漏演示)
cpp
class Base {
public:
~Base() { // 普通析构,不是虚析构
cout << "父类析构\n";
}
};
class Son : public Base {
public:
~Son() {
cout << "子类析构\n";
}
};
int main() {
Base* p = new Son();
delete p; // 只执行父类析构,子类析构没执行!
return 0;
}
输出:
父类析构
👉 子类资源完全没清理,内存泄漏。
2. 改成虚析构,问题解决
cpp
class Base {
public:
// 虚析构函数
virtual ~Base() {
cout << "父类析构\n";
}
};
class Son : public Base {
public:
~Son() {
cout << "子类析构\n";
}
};
int main() {
Base* p = new Son();
delete p;
return 0;
}
输出:
子类析构
父类析构
👉 先走子类析构,再走父类析构,清理干净。
3. 底层原理总结
- 析构加
virtual→ 进入虚表 delete 父类指针时,走虚表动态绑定- 找到真实子类析构执行,再自动向上执行父类析构
4. 最佳实践铁律 + 规范写法
- 只要类会被继承当基类,析构必须写 virtual
- 不被继承的工具类,不用虚析构,省虚表开销
- 现代 C++ 标准写法:
cpp
virtual ~Base() = default;
四、全篇终极总结
- 构造函数 :和类名同名,对象出生初始化;不能是虚函数。
- 析构函数 :固定
~类名()写法,对象销毁自动清理;~代表构造逆操作。 - C++ 独有构造析构:因为手动管内存无GC,C/Java/Python 不需要这套机制。
- 虚函数 :依靠虚表+虚表指针 ,实现子类重写 + 父类指针多态调用。
- 父类指针装子类:为了批量统一管理多子类,不用重复写调度代码。
- 虚析构 :本质就是虚函数,解决父类指针delete子类对象时内存泄漏。
- 口诀 :
非虚看指针,虚函数看真身;
可继承基类,析构必虚。