面向对象初步(引用,对象特性)

C++ 核心编程实战:手写动态数组与 Rule of Three

在 C++ 面向对象编程中,手动管理内存是一项基本功。本文将通过一个简单的 MyArray 类实现,深入解析 Rule of Three(三大定律)、深拷贝、内存初始化细节以及常见的编程陷阱。

核心知识点解析

在阅读代码之前,需要掌握以下几个关键概念:

  1. Rule of Three (三大定律)
    如果一个类需要用户定义的析构函数(通常是因为类中包含动态分配的内存),那么它也必定需要自定义的拷贝构造函数拷贝赋值运算符

    • 析构函数:负责释放内存。
    • 拷贝构造:实现深拷贝,防止多个对象指向同一块内存。
    • 赋值重载:处理“自赋值”、释放旧内存、分配新内存(先破后立)。
  2. 内存初始化的效率意识
    在使用 new 申请数组时,括号的使用有细微差别:

    • 默认初始化 (new int[cap]): 不加括号。如果后续代码紧接着就是数据覆盖(如在拷贝构造中),使用这种方式效率更高,因为它避免了无意义的零初始化。
    • 值初始化 (new int[cap]()): 加括号。这会将申请到的内存空间全部初始化为 0。在普通构造函数中推荐使用,能保证数据的安全性。
  3. Const 正确性
    对于不修改成员变量的成员函数(如 get_size, traverse),务必加上 const 修饰。这不仅是良好的编程习惯,还能保护调用该函数的对象不被意外修改。

完整实现代码

以下是一个符合 C++98/03 风格的动态数组类实现,包含了构造、析构、深拷贝、赋值重载及基本的增查功能。

#include <iostream>
#include <string>

using std::cin;
using std::cout;
using std::endl;
using std::string;

class MyArray {
   private:
    int* m_data;
    size_t m_size;
    size_t m_capacity;

   public:
    // 普通构造函数:使用值初始化(加括号),确保内存清零
    MyArray(size_t capacity) : m_capacity(capacity), m_size(0) { 
        m_data = new int[m_capacity](); 
    }
    
    // 拷贝构造函数:实现深拷贝
    // 效率优化:这里 new 时不加括号(默认初始化),因为紧接着就会被赋值覆盖
    MyArray(const MyArray& arr) : m_capacity(arr.m_capacity), m_size(arr.m_size) {
        m_data = new int[m_capacity];
        for (int i = 0; i < m_size; ++i) {
            m_data[i] = arr.m_data[i];
        }
    }

    // 赋值运算符重载:防自赋值、释放旧内存、深拷贝
    MyArray& operator=(const MyArray& arr) {
        if (this == &arr) return *this; // 1. 检查自赋值
        
        delete[] m_data;                // 2. 释放旧内存
        m_data = nullptr;
        
        m_capacity = arr.m_capacity;
        m_size = arr.m_size;
        
        m_data = new int[m_capacity];   // 3. 分配新内存
        for (int i = 0; i < m_size; ++i) {
            m_data[i] = arr.m_data[i];  // 4. 拷贝数据
        }
        return *this;                   // 5. 返回引用
    }

    // 析构函数
    ~MyArray() {
        delete[] m_data;
        m_data = nullptr;
    }

    void push_back(int val) {
        if (m_size >= m_capacity) {
            cout << "Error,Array is full" << endl;
            return;
        }
        m_data[m_size] = val;
        ++m_size;
    }

    // const 修饰,保证函数内不可修改成员变量
    bool get_data(size_t index, int& val) const {
        if (index >= m_size) {
            cout << "index is larger than size" << endl;
            return false;
        }
        val = m_data[index];
        return true;
    }

    size_t get_size() const { return m_size; }

    void traverse() const {
        for (int i = 0; i < m_size; ++i) {
            cout << m_data[i] << " ";
        }
        cout << "\n";
    }
};

void test01() {
   MyArray arr1(5);
    arr1.push_back(10);
    arr1.push_back(20);

    MyArray arr2(arr1);  // 调用拷贝构造
    MyArray arr3(100);
    arr3 = arr1;  // 调用赋值重载
    cout << "arr1 地址:" << &arr1 << endl;
    cout << "arr2 地址:" << &arr2 << endl;
    cout << "arr3 地址:" << &arr3 << endl;

    cout << "修改 arr1 之前 的arr2: ";
    arr2.traverse();

    arr1.push_back(999);  // 修改 arr1
    cout << "修改 arr1:" << " ";
    arr1.traverse();

    cout << "修改 arr1 之后 的arr2(应该保持不变): ";
    cout << "arr2 " << &arr2 << " :";
    arr2.traverse();

    cout << "arr3 " << &arr3 << " :";
    arr3.traverse();
}

int main() { test01(); }

常见错误与避坑指南

在实现此类数据结构时,初学者容易犯以下错误,请务必注意:

1. 内存分配的误区

  • 错误写法m_data = new int(*arr.m_data);
  • 分析:这行代码并没有申请数组,而是解引用了原数组的首元素作为大小,或者只申请了一个 int 的空间。
  • 正确写法:必须使用 new int[size] 语法申请连续的内存块。

2. 赋值运算符重载的“大坑”

赋值重载是最容易出错的地方,常见的两个致命错误:

  • 内存泄漏:直接将指针指向新地址,而没有先 delete[] m_data;。原有的堆内存将成为“孤儿”,导致内存泄漏。
  • 返回值缺失:忘记写 return *this;。这会导致无法支持链式赋值(如 a = b = c;),且行为未定义。

代码优化建议

针对部分常见的代码逻辑,这里提出几点优化建议(上述代码已采纳正确做法):

  1. 删除空指针的冗余检查

    • 建议:直接调用 delete[] m_data;
    • 理由:C++ 标准明确规定,delete 一个空指针是安全的(它什么都不做)。添加 if (m_data != nullptr) 只会增加代码行数,没有任何实际收益。
  2. 拒绝人为增加复杂度

    • 场景:在获取数组元素时,不要写 while 循环去查找。
    • 理由:数组的核心优势就是 O(1) 的随机访问能力。强行使用循环会将其降级为 O(N),这违背了使用数组的初衷。
  3. 遵守索引习惯 (0-based Indexing)

    • 场景:不要试图在 get 函数中模拟从 1 开始计数,而在内部存储使用从 0 开始。
    • 理由:C++ 生态中默认下标从 0 开始。混合使用会导致逻辑混乱,增加维护成本。统一使用下标(Index)作为参数。

总结

编写 C++ 类时,请牢记以下四句口诀:

“有 New 必有 Delete”
“初始化列表优先”
“赋值重载防自赋”
“谨记 Const 承诺”

posted @ 2026-01-20 02:37  iewiq  阅读(0)  评论(0)    收藏  举报