C++中的构造函数、析构函数、虚析构函数详与继承详解
在 C++ 中,构造函数和析构函数是类中非常重要的两个特殊函数。它们分别负责对象的创建时初始化与销毁时清理,是理解面向对象编程的关键。
1. 为什么需要构造函数?
来看一个例子:
#include <iostream>
class Entity
{
public:
float X, Y;
void Print()
{
std::cout << X << ", " << Y << std::endl;
}
};
int main()
{
Entity e;
e.Print();
std::cin.get();
}
输出结果可能是:
-1.07374e+08, -1.07374e+08
这是因为 X 和 Y 没有被初始化,访问了未定义的内存内容,结果自然是随机值。
如果想让对象初始化时就有默认值,可以写一个 Init() 方法:
class Entity
{
public:
float X, Y;
void Init()
{
X = 0.0f;
Y = 0.0f;
}
void Print()
{
std::cout << X << ", " << Y << std::endl;
}
};
然后:
Entity e;
e.Init(); // 每次都要手动初始化
但这样太麻烦了,于是就有了 构造函数。
2. 构造函数(Constructor)
构造函数是一个 在对象被创建时自动调用 的特殊函数:
-
名字必须与类名相同;
-
没有返回类型(连
void都不能写); -
可以有参数,也可以没有。
示例:
class Entity
{
public:
float X, Y;
// 构造函数
Entity()
{
X = 0.0f;
Y = 0.0f;
}
void Print()
{
std::cout << X << ", " << Y << std::endl;
}
};
int main()
{
Entity e; // 自动调用构造函数
e.Print(); // 输出 0, 0
}
如果用户没有写构造函数,编译器会自动生成一个默认构造函数:
Entity() { }
但这个默认构造函数不会初始化成员变量,所以 int、float 等内置类型依旧是随机值。
3. 构造函数重载
构造函数可以有多个不同的参数版本,这就是函数重载(在同一个作用域中,允许有多个 同名函数,只要它们的 参数列表不同。编译器会根据你调用函数时传入的参数,自动选择匹配的那个版本。)。
class Entity
{
public:
float X, Y;
Entity() // 默认构造函数
{
X = 0.0f;
Y = 0.0f;
}
Entity(float x, float y) // 带参数的构造函数
{
X = x;
Y = y;
}
};
int main()
{
Entity e1; // 调用默认构造函数
Entity e2(10, 5); // 调用带参数的构造函数
}
这样可以方便地初始化不同状态的对象。
4. 禁止实例化
有些类(例如日志类 Log),我们只想用静态方法,不希望别人去创建实例。
可以通过 删除构造函数 实现:
class Log
{
private:
Log() = delete; // 禁止构造函数
public:
static void Write(const std::string& msg)
{
std::cout << msg << std::endl;
}
};
int main()
{
Log::Write("Hello Log!"); // 正确
// Log log; 报错:构造函数被禁用
}
补充:构造函数初始化列表
在 C++ 中,构造函数初始化列表是一种在构造函数中初始化类成员变量的推荐方式。相比于在构造函数体内赋值,它通常更高效,尤其是对于对象类型(如 std::string、std::vector 等)或 const/mutable 成员。
常规构造函数初始化方式
class Entity {
private:
std::string m_Name;
public:
Entity() {
m_Name = "Unknown"; // 在构造函数体内赋值
}
Entity(const std::string& name) {
m_Name = name;
}
};
-
成员变量
m_Name先调用默认构造函数初始化(空字符串),然后在构造函数体内再赋值"Unknown"或name。 -
对于复杂类型或非 POD 类型,这会产生额外的构造和赋值开销。
构造函数初始化列表
使用冒号 : 后跟成员变量和初始值,可以直接在构造对象时初始化:
class Entity {
private:
std::string m_Name;
int m_Score;
public:
Entity()
: m_Name("Unknown"), m_Score(0) // 初始化列表
{
}
Entity(const std::string& name)
: m_Name(name), m_Score(0) // 初始化列表
{
}
};
-
初始化列表中按成员变量声明顺序执行初始化,而不是写的顺序。
-
对象类型直接调用构造函数,而不是先默认构造再赋值,效率更高。
-
const或reference类型成员必须使用初始化列表初始化,否则无法赋值。
5. 析构函数(Destructor)
和构造函数相对,析构函数是在对象销毁时自动调用的函数:
-
名字必须与类名相同,但前面要加
~; -
没有参数,没有返回值;
-
用来清理资源、释放内存。
示例:
class Entity
{
public:
float X, Y;
Entity()
{
X = 0.0f;
Y = 0.0f;
std::cout << "Created Entity!" << std::endl;
}
~Entity()
{
std::cout << "Destroyed Entity!" << std::endl;
}
};
栈对象(自动存储)
void Function()
{
Entity e; // 构造函数被调用
} // 作用域结束,析构函数被调用
堆对象(动态分配)
int main()
{
Entity* e = new Entity(); // 构造函数被调用
delete e; // 析构函数被调用
}
栈对象在作用域结束时会自动销毁,而堆对象必须用 delete 手动销毁,否则会导致内存泄漏。
6. 析构函数的作用
为什么要写析构函数?
如果类中使用了 new 分配内存或打开了文件,需要在对象销毁时释放这些资源。否则会造成内存或资源泄漏。
例如:
class Buffer
{
private:
char* m_Buffer;
public:
Buffer()
{
m_Buffer = new char[1024]; // 分配内存
}
~Buffer()
{
delete[] m_Buffer; // 析构时释放内存
}
};
7. 小结
-
构造函数:对象创建时调用,用于初始化。可以有多个版本(重载)。
-
析构函数:对象销毁时调用,用于清理资源。名字是
~类名()。 -
栈对象的生命周期由作用域控制,堆对象必须手动
delete。 -
可以删除构造函数来禁止类被实例化。
-
析构函数非常重要,尤其在类中涉及 动态内存管理 的时候。
虚析构函数(Virtual destructor)
在 C++ 多态编程中,基类析构函数必须声明为虚函数,否则会导致派生类资源无法释放。
问题示例
class Base {
public:
Base() { std::cout << "Base Constructor\n"; }
~Base() { std::cout << "Base Destructor\n"; }
};
class Derived : public Base {
public:
Derived() { m_Array = new int[5]; std::cout << "Derived Constructor\n"; }
~Derived() { delete[] m_Array; std::cout << "Derived Destructor\n"; }
private:
int* m_Array;
};
int main() {
Base* poly = new Derived();
delete poly; // 只调用 Base 析构函数
}
输出:
Base Constructor
Derived Constructor
Base Destructor
问题:Derived 的析构函数没有被调用,m_Array 没有释放 → 内存泄漏。
原因
-
静态类型决定析构函数调用:
poly的类型是Base*,编译器只调用Base::~Base()。 -
C++ 的严格别名规则:如果基类析构非虚函数,无法动态识别派生类。
解决方案:虚析构函数
class Base {
public:
virtual ~Base() { std::cout << "Base Destructor\n"; }
};
再次执行:
Base* poly = new Derived();
delete poly;
输出:
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor
派生类资源得到正确释放。
原理总结
-
编译器会在 Base 类中生成一个 虚表(vtable),存储虚函数的地址。
-
派生类 Derived 的析构函数会覆盖 Base 虚析构在 vtable 中的条目。
-
当执行
delete poly;时,编译器会查找 vtable → 动态调用 派生类的析构函数。 -
派生类析构函数执行完毕后,自动再调用基类析构函数 → 顺序安全释放所有资源。
多态示例
class Animal {
public:
virtual void speak() = 0;
virtual ~Animal() {}
};
class Dog : public Animal { void speak() override { std::cout << "Woof!\n"; } };
class Cat : public Animal { void speak() override { std::cout << "Meow!\n"; } };
int main() {
Animal* animals[] = {new Dog(), new Cat()};
for (auto* a : animals) a->speak(); // 多态调用
for (auto* a : animals) delete a; // 虚析构安全释放
}
核心结论
只要类会被继承并通过基类指针使用,基类析构函数就必须是虚函数。
这样才能保证运行时多态下派生类资源正确释放,避免内存泄漏。
继承(Inheritance)
在 C++ 中,继承是面向对象编程(OOP)的三大特性之一(封装、继承、多态)。
它的作用是:让多个 相互关联的类 形成一个层级结构,从而 避免重复代码。
1. 基类与派生类
假设我们在做一个游戏,里面的每个实体(比如玩家、怪物、NPC)都有自己的位置。
我们可以先写一个 基类(父类):
class Entity
{
public:
float X, Y;
void Move(float xa, float ya)
{
X += xa;
Y += ya;
}
};
这里的 Entity 类代表“所有实体共有的功能”——位置和移动。
然后我们写一个 派生类(子类),继承自 Entity:
class Player : public Entity
{
public:
const char* Name;
void PrintName()
{
std::cout << Name << std::endl;
}
};
现在 Player 类就继承了 Entity 的 X、Y 以及 Move() 方法,同时又增加了 Name 和 PrintName()。
2. 使用继承的效果
int main()
{
Player player;
player.X = 10;
player.Y = 20;
player.Move(5, -3);
player.Name = "Alice";
std::cout << player.X << ", " << player.Y << std::endl; // 15, 17
player.PrintName(); // Alice
}
你会发现:
-
Player自动拥有了 Entity 的全部非私有成员; -
我们不用重复写
X, Y, Move(),避免了冗余。
3. is-a 关系
继承代表 is-a(“是一个”)的关系:
-
Player是一个Entity; -
这意味着,在需要
Entity的地方,可以直接使用Player。
void PrintEntity(const Entity& e)
{
std::cout << e.X << ", " << e.Y << std::endl;
}
int main()
{
Player player;
player.X = 1;
player.Y = 2;
PrintEntity(player); // Player 也是 Entity
}
4. 方法重写(Override)
子类不仅能继承父类的功能,还能 修改父类的行为,比如重写(覆盖)一个方法。
(这一点涉及 多态,我们暂时只做引子,后面会详细讲。)
class Entity
{
public:
void Print()
{
std::cout << "Entity" << std::endl;
}
};
class Player : public Entity
{
public:
void Print()
{
std::cout << "Player" << std::endl;
}
};
5. 小结
-
继承让多个类形成层级结构,减少重复代码;
-
子类可以使用父类的非私有成员;
-
子类 is-a 父类,能在需要父类的地方使用;
-
子类还能通过 方法重写 来修改父类的行为(这就是多态的基础)。
更多推荐


所有评论(0)