类和动态内存分配(第 12 章)
目录
🔴动态内存与类
🔺问题:类成员若包含指针,默认的拷贝/赋值只复制指针值(地址),两个对象共享同一块堆内存,析构时 delete 两次 → 未定义行为(UB)
🔺根本原因:编译器自动生成的特殊成员函数不知道指针背后有动态内存,需要手动接管
🔺典型场景:
class StringBad {
private:
char* str; // 指向堆上字符串
int len;
static int num_strings; // 统计对象数量
public:
StringBad(const char* s);
~StringBad();
};🔴特殊成员函数
🔺C++ 会自动生成以下成员函数(若未显式定义):
| 函数 | 自动生成行为 |
|---|---|
| 默认构造函数 | 无参,什么都不做(不初始化内置类型成员) |
| 默认析构函数 | 什么都不做(不释放指针指向的堆内存) |
| 复制构造函数 | 浅拷贝:逐成员复制(指针只复制地址) |
赋值运算符 = | 浅拷贝:逐成员赋值(指针只复制地址) |
地址运算符 & | 返回对象地址 |
| 移动构造/移动赋值 | C++11,转移资源所有权(右值引用) |
🔺何时必须自定义:类中有指针成员且指向堆内存时,必须自定义复制构造函数、赋值运算符、析构函数(Rule of Three)
🔴深拷贝与浅拷贝
🔺浅拷贝(默认行为):只复制指针的值(地址),两个对象指向同一块内存
obj1.str ──┐
├──→ [ "hello" ] ← 同一块堆内存,析构两次 = UB
obj2.str ──┘
🔺深拷贝:重新分配内存,复制内容,两个对象各自独立
// 深拷贝复制构造函数
String::String(const String& st) {
len = st.len;
str = new char[len + 1]; // 新分配内存
strcpy(str, st.str); // 复制内容
}🔺复制构造函数触发时机:
- 用已有对象初始化新对象:
String s2 = s1;或String s2(s1); - 函数按值传参:
void func(String s) - 函数按值返回对象
🔴赋值运算符重载
🔺赋值 s2 = s1 与初始化 String s2 = s1 不同:赋值时 s2 已存在,需先释放被赋值的类的旧内存
String& String::operator=(const String& st) {//⭐注意这里是"=",赋值的时候要用
if (this == &st) // 1. 自赋值检查
return *this;
delete[] str; // 2. 释放旧内存
len = st.len;
str = new char[len + 1]; // 3. 分配新内存
strcpy(str, st.str); // 4. 复制内容
return *this; // 5. 返回自身引用(支持链式赋值)
}🔺三个要点:
- 自赋值检查(
this == &st),否则delete后数据丢失 - 返回
*this的引用,支持a = b = c - 先
delete[]旧内存,再new新内存
🔴静态类成员
🔺静态数据成员:属于类本身,所有对象共享同一份,不属于任何单个对象
class String {
static int num_strings; // 声明(类内)
};
int String::num_strings = 0; // 定义并初始化(类外,不加 static)🔺静态成员函数:没有 this 指针,只能访问静态成员
class String {
static int num_strings;
public:
static int HowMany() { return num_strings; } // 静态成员函数
};
// 调用:不需要对象
int count = String::HowMany();🔺用途:统计对象数量、单例模式、工厂函数等
🔴定位 new 与类
🔺定位 new:在指定内存地址上构造对象,不分配新内存
char buffer[512];
JustTesting* pc1 = new (buffer) JustTesting; // 在 buffer 上构造
JustTesting* pc2 = new (buffer + sizeof(JustTesting)) JustTesting("Heap2", 20);🔺注意:定位 new 创建的对象不能用 delete 释放(因为内存不是 new 分配的),但必须显式调用析构函数:
pc2->~JustTesting(); // 显式析构(顺序与构造相反)
pc1->~JustTesting();🔺应用:内存池、嵌入式系统中避免频繁 new/delete 的碎片化
🔴队列模拟(综合示例)
🔺本章综合示例:用链表实现队列,展示类中动态内存的完整管理
🔺设计要点:
class Queue {
private:
struct Node { Item item; Node* next; }; // 链表节点
Node* front; // 队头指针
Node* rear; // 队尾指针
int items; // 当前元素数
const int qsize; // 最大容量(const 成员,必须用初始化列表)
// 禁止拷贝和赋值(私有声明,不定义)
Queue(const Queue& q) : qsize(0) {}
Queue& operator=(const Queue& q) { return *this; }
public:
Queue(int qs = 10);
~Queue();
bool enqueue(const Item& item);
bool dequeue(Item& item);
};🔺const 成员初始化:const 数据成员只能在成员初始化列表中初始化,不能在构造函数体内赋值:
Queue::Queue(int qs) : qsize(qs), front(nullptr), rear(nullptr), items(0) {}🔺析构函数:遍历链表逐个 delete 节点:
Queue::~Queue() {
Node* temp;
while (front != nullptr) {
temp = front;
front = front->next;
delete temp;
}
}🔺禁止拷贝:将复制构造函数和赋值运算符声明为 private(C++11 可用 = delete),防止浅拷贝导致双重释放
本章小结
| 特性 | 关键点 |
|---|---|
| 浅拷贝问题 | 默认复制构造/赋值只复制指针地址,多个对象共享内存,析构时 UB |
| Rule of Three | 有指针成员时,必须自定义:析构函数、复制构造函数、赋值运算符 |
| 深拷贝 | 复制构造函数中 new 新内存并 strcpy 内容 |
| 赋值运算符 | 先检查自赋值,再 delete[] 旧内存,再 new 新内存,返回 *this |
| 静态成员 | 类内声明,类外定义;静态函数无 this,只能访问静态成员 |
| const 成员 | 只能在成员初始化列表中初始化 |
| 定位 new | 在指定地址构造对象,不能 delete,需显式调用析构函数 |
| 禁止拷贝 | 将复制构造/赋值声明为 private 或 = delete |