面向对象初步(引用,对象特性)
C++ 核心编程实战:手写动态数组与 Rule of Three
在 C++ 面向对象编程中,手动管理内存是一项基本功。本文将通过一个简单的 MyArray 类实现,深入解析 Rule of Three(三大定律)、深拷贝、内存初始化细节以及常见的编程陷阱。
核心知识点解析
在阅读代码之前,需要掌握以下几个关键概念:
-
Rule of Three (三大定律)
如果一个类需要用户定义的析构函数(通常是因为类中包含动态分配的内存),那么它也必定需要自定义的拷贝构造函数和拷贝赋值运算符。- 析构函数:负责释放内存。
- 拷贝构造:实现深拷贝,防止多个对象指向同一块内存。
- 赋值重载:处理“自赋值”、释放旧内存、分配新内存(先破后立)。
-
内存初始化的效率意识
在使用new申请数组时,括号的使用有细微差别:- 默认初始化 (
new int[cap]): 不加括号。如果后续代码紧接着就是数据覆盖(如在拷贝构造中),使用这种方式效率更高,因为它避免了无意义的零初始化。 - 值初始化 (
new int[cap]()): 加括号。这会将申请到的内存空间全部初始化为 0。在普通构造函数中推荐使用,能保证数据的安全性。
- 默认初始化 (
-
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;),且行为未定义。
代码优化建议
针对部分常见的代码逻辑,这里提出几点优化建议(上述代码已采纳正确做法):
-
删除空指针的冗余检查
- 建议:直接调用
delete[] m_data;。 - 理由:C++ 标准明确规定,
delete一个空指针是安全的(它什么都不做)。添加if (m_data != nullptr)只会增加代码行数,没有任何实际收益。
- 建议:直接调用
-
拒绝人为增加复杂度
- 场景:在获取数组元素时,不要写
while循环去查找。 - 理由:数组的核心优势就是 O(1) 的随机访问能力。强行使用循环会将其降级为 O(N),这违背了使用数组的初衷。
- 场景:在获取数组元素时,不要写
-
遵守索引习惯 (0-based Indexing)
- 场景:不要试图在
get函数中模拟从 1 开始计数,而在内部存储使用从 0 开始。 - 理由:C++ 生态中默认下标从 0 开始。混合使用会导致逻辑混乱,增加维护成本。统一使用下标(Index)作为参数。
- 场景:不要试图在
总结
编写 C++ 类时,请牢记以下四句口诀:
“有 New 必有 Delete”
“初始化列表优先”
“赋值重载防自赋”
“谨记 Const 承诺”

浙公网安备 33010602011771号