类和动态内存分配(第 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. 返回自身引用(支持链式赋值)
}

🔺三个要点

  1. 自赋值检查(this == &st),否则 delete 后数据丢失
  2. 返回 *this 的引用,支持 a = b = c
  3. 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