目录
[1. 最基本的构造函数](#1. 最基本的构造函数)
[2. 带参数的构造函数(重载)](#2. 带参数的构造函数(重载))
[3. 初始化列表:更高效的初始化方式](#3. 初始化列表:更高效的初始化方式)
[1. 构造函数里抛出异常](#1. 构造函数里抛出异常)
[2. 析构函数里再抛异常](#2. 析构函数里再抛异常)
[3. 忘记写析构函数导致内存泄露](#3. 忘记写析构函数导致内存泄露)
一、一个让人头疼的问题
继续用上一讲的Book类:
cpp
Book b;
b.setPrice(128.5); // 必须记得手动初始化
b.print(); // 如果忘了setPrice,price是未定义的垃圾值
C的结构体好歹可以用{0}初始化,但C++的类不行------因为类可能有复杂的内部逻辑,简单的内存置零可能破坏状态。
更麻烦的是,如果你的类需要分配动态内存 (比如用new申请了一块空间),用完必须手动delete。一旦忘记,内存就泄露了。
构造函数和析构函数就是用来解决这些问题的。
二、构造函数:对象出生时的"第一声啼哭"
构造函数是一种特殊的成员函数:
-
名字和类名完全相同
-
没有返回值 (连
void都不写) -
在创建对象时自动调用
-
可以重载(多个构造函数,参数不同)
1. 最基本的构造函数
cpp
class Book {
private:
string title;
string author;
double price;
public:
// 构造函数——名字和类一样,没返回值
Book() {
title = "未知";
author = "未知";
price = 0.0;
cout << "构造函数被调用了" << endl;
}
void print() {
cout << title << ", " << author << ", " << price << endl;
}
};
int main() {
Book b; // 自动调用构造函数,输出"构造函数被调用了"
b.print(); // 输出:未知, 未知, 0
}
注意最后一行的Book b;------我们没有写任何"初始化"的代码,但对象已经有了合理的默认值。
2. 带参数的构造函数(重载)
一个类可以有多个构造函数,只要参数不同:
cpp
class Book {
private:
string title;
string author;
double price;
public:
// 默认构造函数
Book() {
title = "未知";
author = "未知";
price = 0.0;
}
// 带三个参数的构造函数
Book(string t, string a, double p) {
title = t;
author = a;
price = p;
}
// 只传书名的构造函数
Book(string t) {
title = t;
author = "佚名";
price = 0.0;
}
void print() {
cout << "《" << title << "》 " << author << " ¥" << price << endl;
}
};
int main() {
Book b1; // 调用默认构造函数
Book b2("C++ Primer", "Lippman", 128.5); // 调用3参数构造
Book b3("三体"); // 调用1参数构造
b1.print(); // 《未知》 未知 ¥0
b2.print(); // 《C++ Primer》 Lippman ¥128.5
b3.print(); // 《三体》 佚名 ¥0
}
3. 初始化列表:更高效的初始化方式
上面的构造函数在函数体里赋值,但还有更正统的写法------初始化列表:
cpp
Book(string t, string a, double p) : title(t), author(a), price(p) {
// 函数体可以为空,或者写其他逻辑
}
:后面的title(t)意思是"用参数t来初始化title成员变量"。
为什么要用初始化列表?
-
对于
string、vector等有构造函数的类型,初始化列表直接构造一次;在函数体里赋值会先默认构造再赋值,多一步开销 -
const成员变量和引用类型必须用初始化列表 -
成员变量的初始化顺序按它们在类中声明的顺序,不是按初始化列表的顺序
cpp
class Demo {
private:
const int id; // const成员必须用初始化列表
int& ref; // 引用必须用初始化列表
string name;
public:
Demo(int i, int& r, string n) : id(i), ref(r), name(n) {
// 这里id和ref已经初始化好了
}
};
三、默认构造函数:那个"看不见"的函数
如果你写一个类,没有定义任何构造函数 ,编译器会悄悄给你生成一个默认构造函数(什么也不做)。
cpp
class Simple {
int x;
string s;
// 编译器会生成一个 Simple() {}
};
Simple obj; // 可以编译,但x是垃圾值,s是空字符串
但有一个陷阱:一旦你自己写了任何一个构造函数,编译器就不再生成默认构造函数。
cpp
class Book {
public:
Book(string t) { title = t; } // 写了构造函数
private:
string title;
};
Book b1("C++"); // ✅ 没问题
Book b2; // ❌ 编译错误!没有默认构造函数
如果你既想要自己的构造函数,又想要无参的版本,必须显式写出来:
cpp
Book() {} // 或者 = default (C++11)
C++11提供了更简洁的写法:
cpp
Book() = default; // 让编译器生成默认版本
四、析构函数:对象临终前的"遗言"
析构函数是构造函数的"镜像":
-
名字是
~类名 -
没有参数,不能重载(一个类只有一个析构函数)
-
没有返回值
-
对象销毁时自动调用
cpp
class Book {
private:
string* pComment; // 指向动态分配的评论
public:
Book(string comment) {
pComment = new string(comment);
cout << "构造:分配内存" << endl;
}
~Book() {
delete pComment; // 释放内存
cout << "析构:释放内存" << endl;
}
};
int main() {
Book b("很好看的一本书");
// 函数结束时,b被销毁,析构函数自动调用
}
// 输出:
// 构造:分配内存
// 析构:释放内存
什么时候调用析构函数?
| 对象类型 | 析构时机 |
|---|---|
| 局部对象(栈上) | 离开作用域时(如函数结束) |
| 静态局部对象 | 程序结束时 |
| 全局对象 | 程序结束时 |
new创建的对象 |
显式调用delete时 |
五、完整的例子:动态字符串数组类
把构造和析构结合起来,写一个管理动态数组的类StringArray:
cpp
#include <iostream>
#include <string>
using namespace std;
class StringArray {
private:
string* data; // 指向动态数组的指针
int size; // 数组大小
public:
// 构造函数:分配内存并初始化
StringArray(int n) : size(n) {
data = new string[n];
cout << "分配了" << n << "个字符串的空间" << endl;
}
// 析构函数:释放内存
~StringArray() {
delete[] data;
cout << "释放了" << size << "个字符串的空间" << endl;
}
// 设置元素
void set(int index, const string& s) {
if (index >= 0 && index < size) {
data[index] = s;
}
}
// 获取元素
string get(int index) {
if (index >= 0 && index < size) {
return data[index];
}
return "";
}
// 打印所有
void print() {
for (int i = 0; i < size; i++) {
cout << "[" << i << "] = " << data[i] << endl;
}
}
};
int main() {
StringArray arr(3); // 构造:分配内存
arr.set(0, "Hello");
arr.set(1, "World");
arr.set(2, "C++");
arr.print();
// 函数结束,arr被销毁 → 析构函数自动调用,释放内存
return 0;
}
运行结果:
text
分配了3个字符串的空间
[0] = Hello
[1] = World
[2] = C++
释放了3个字符串的空间
注意:我们没有手动调用任何"释放"函数,析构函数自动帮我们做了。
六、三个容易踩的坑
1. 构造函数里抛出异常
如果在构造函数里抛异常,对象被认为"没有构造完成",析构函数不会被调用。这意味着构造函数里已经分配的资源需要自己处理。
cpp
Book() {
p = new int[100];
throw "error"; // 如果这里抛异常,~Book()不会被调用,内存泄露
}
解决方案:用智能指针(后面的章节会讲),或者在构造函数里用try-catch。
2. 析构函数里再抛异常
极其危险 !如果析构函数在栈展开期间(处理另一个异常时)又抛出异常,程序会直接调用terminate()崩溃。
cpp
~Book() {
delete p;
throw "析构异常"; // ❌ 千万别这么写
}
规则:析构函数应该吞掉所有异常,只做清理,不抛出任何东西。
3. 忘记写析构函数导致内存泄露
如果你的类里有指针成员,并且你在构造函数里用new分配了内存,必须写析构函数用delete释放,否则内存就泄露了。
cpp
class Leaky {
int* p;
public:
Leaky() { p = new int[1000]; }
// 没有析构函数 → 内存泄露!
};
七、这一篇的收获
你现在应该理解:
-
构造函数在对象创建时自动调用,适合做初始化
-
析构函数在对象销毁时自动调用,适合做清理
-
构造函数可以重载(多个版本)
-
初始化的优先级:初始化列表 > 构造函数体内的赋值
-
如果你管理了动态资源(new),必须提供析构函数去释放
💡 小作业:写一个
IntArray类,构造函数接收大小n并分配n个int的空间,析构函数释放空间。提供set()和get()方法。在main中创建对象,观察构造和析构的调用时机。
下一篇预告:第4篇《类与对象(三):拷贝构造函数与深浅拷贝问题》------当你把一个对象赋值给另一个对象时,背后发生了什么?默认的拷贝行为为什么可能导致程序崩溃?