C++ 动态数组
动态数组简介
在C++编程中,数组是一种常用的数据结构,用于存储同类型的多个数据元素。我们可以将数组分为两种主要类型:静态数组和动态数组。静态数组在编译时分配内存,而动态数组则在程序运行时分配内存。
静态数组的声明方式如下:
int numbers[10]; // 声明一个包含10个整数的静态数组
然而,静态数组有一个显著的局限性:其大小必须在编译时确定,这意味着我们不能根据运行时的需求来调整数组的大小。这就是动态数组派上用场的地方。
动态数组的创建与释放
使用new和delete操作符
在C++中,我们可以使用new
操作符来动态分配内存,并使用delete
操作符来释放内存。对于动态数组,我们使用new[]
和delete[]
。
// 动态分配包含5个整数的数组
int* dynamicArray = new int[5];
// 使用数组
dynamicArray[0] = 10;
dynamicArray[1] = 20;
// ...
// 释放数组内存
delete[] dynamicArray;
使用new[]
分配的内存必须使用delete[]
释放,而不是delete
。使用错误的释放方式可能导致内存泄漏或不可预测的行为。
分配内存并初始化
从C++11开始,我们可以在分配动态数组的同时进行初始化:
// 分配并初始化
int* dynamicArray = new int[5]{10, 20, 30, 40, 50};
// 使用数组
for (int i = 0; i < 5; i++) {
std::cout << dynamicArray[i] << " ";
}
// 输出: 10 20 30 40 50
// 释放数组内存
delete[] dynamicArray;
动态数组大小管理
使用原生C++动态数组时,需要手动跟踪数组大小,因为数组本身不存储其大小信息:
// 分配动态数组
int size = 5; // 我们需要单独记住数组大小
int* dynamicArray = new int[size]{1, 2, 3, 4, 5};
// 使用数组时必须知道大小
for (int i = 0; i < size; i++) {
std::cout << dynamicArray[i] << " ";
}
std::cout << std::endl;
// 释放内存
delete[] dynamicArray;
动态二维数组
创建多维动态数组需要一些额外的技巧。下面是创建二维动态数组的几种方法:
方法1:使用指针的指针
int rows = 3;
int cols = 4;
// 分配行指针数组
int** matrix = new int*[rows];
// 为每行分配列数组
for (int i = 0; i < rows; i++) {
matrix[i] = new int[cols];
}
// 使用数组
matrix[1][2] = 42;
// 释放内存时需要逐行释放
for (int i = 0; i < rows; i++) {
delete[] matrix[i];
}
delete[] matrix;
方法2:使用单个一维数组模拟二维数组
int rows = 3;
int cols = 4;
// 分配单个连续内存块
int* matrix = new int[rows * cols];
// 访问元素 (i,j)
matrix[i * cols + j] = 42; // 例如: 设置第1行第2列的元素为42
// 在数学上表示为matrix[1][2]
// 释放内存
delete[] matrix;
第二种方法通常性能更好,因为它只需要一次内存分配和释放,并且内存是连续的,这有助于缓存性能。
自动调整大小的动态数组
管理原生动态数组的一个主要挑战是无法轻松地调整其大小。下面是一个简单的实现,展示如何手动增加数组大小:
#include <iostream>
int* resizeArray(int* oldArray, int oldSize, int newSize) {
// 分配新数组
int* newArray = new int[newSize];
// 复制旧数据
int copySize = (oldSize < newSize) ? oldSize : newSize;
for (int i = 0; i < copySize; i++) {
newArray[i] = oldArray[i];
}
// 释放旧数组
delete[] oldArray;
return newArray;
}
int main() {
int size = 3;
int* array = new int[size]{1, 2, 3};
// 显示原始数组
std::cout << "原始数组: ";
for (int i = 0; i < size; i++) {
std::cout << array[i] << " ";
}
std::cout << std::endl;
// 增加数组大小
int newSize = 5;
array = resizeArray(array, size, newSize);
size = newSize;
// 添加新元素
array[3] = 4;
array[4] = 5;
// 显示新数组
std::cout << "调整大小后的数组: ";
for (int i = 0; i < size; i++) {
std::cout << array[i] << " ";
}
std::cout << std::endl;
// 释放内存
delete[] array;
return 0;
}
/*
输出:
原始数组: 1 2 3
调整大小后的数组: 1 2 3 4 5
*/
使用std::vector代替原生动态数组
虽然了解原生动态数组的工作原理很重要,但在现代C++中,我们通常更推荐使用std::vector
容器,它是标准库提供的动态数组实现:
#include <iostream>
#include <vector>
int main() {
// 创建vector
std::vector<int> numbers = {1, 2, 3};
// 添加元素
numbers.push_back(4);
numbers.push_back(5);
// 访问元素
std::cout << "第三个元素: " << numbers[2] << std::endl;
// 打印所有元素
std::cout << "所有元素: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
// 获取大小
std::cout << "数组大小: " << numbers.size() << std::endl;
// 调整大小
numbers.resize(10, 0); // 调整为10个元素,新元素用0填充
std::cout << "调整后大小: " << numbers.size() << std::endl;
return 0;
}
/*
输出:
第三个元素: 3
所有元素: 1 2 3 4 5
数组大小: 5
调整后大小: 10
*/
std::vector
提供了以下优势:
- 自动内存管理:不需要手动调用
delete[]
- 可以轻松调整大小
- 提供大量实用功能(如
push_back
,insert
,erase
等) - 安全:提供边界检查(通过
at()
方法)
原生动态数组的常见陷阱
使用原生动态数组时需要小心以下几个常见问题:
1. 内存泄漏
void leakyFunction() {
int* numbers = new int[100];
// 使用数组...
// 忘记调用 delete[] numbers;
} // 内存泄漏!
每次使用new[]
分配内存,都必须确保在不再需要内存时调用delete[]
。
2. 数组越界
int* array = new int[5];
array[5] = 10; // 越界!数组只有索引0-4
C++不会自动检查数组边界,越界访问会导致未定义行为,可能会破坏程序的其他部分或导致崩溃。
3. 释放后使用(悬挂指针)
int* array = new int[5];
delete[] array;
// array现在是悬挂指针
array[0] = 10; // 危险!访问已释放的内存
4. 使用错误的删除操作符
int* array = new int[5];
delete array; // 错误!应该使用delete[]
实际应用案例:图像处理
以下是一个使用动态数组处理简单灰度图像的例子,展示了动态数组在实际应用中的用途:
#include <iostream>
#include <fstream>
// 灰度图像类
class GrayscaleImage {
private:
unsigned char* pixels;
int width;
int height;
public:
// 构造函数
GrayscaleImage(int w, int h) : width(w), height(h) {
pixels = new unsigned char[width * height](); // 初始化为0
}
// 析构函数
~GrayscaleImage() {
delete[] pixels;
}
// 获取像素值
unsigned char getPixel(int x, int y) const {
if (x >= 0 && x < width && y >= 0 && y < height) {
return pixels[y * width + x];
}
return 0; // 边界外返回黑色
}
// 设置像素值
void setPixel(int x, int y, unsigned char value) {
if (x >= 0 && x < width && y >= 0 && y < height) {
pixels[y * width + x] = value;
}
}
// 应用简单的模糊滤镜
void applyBlur() {
// 创建临时图像以存储结果
unsigned char* newPixels = new unsigned char[width * height];
// 应用3x3平均模糊
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int sum = 0;
int count = 0;
// 3x3邻域
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int nx = x + dx;
int ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
sum += getPixel(nx, ny);
count++;
}
}
}
// 计算平均值
newPixels[y * width + x] = sum / count;
}
}
// 用新像素替换旧像素
delete[] pixels;
pixels = newPixels;
}
// 保存为PGM格式
void saveToPGM(const std::string& filename) {
std::ofstream file(filename, std::ios::binary);
if (file) {
// 写入PGM头部
file << "P5\n" << width << " " << height << "\n255\n";
// 写入像素数据
file.write(reinterpret_cast<char*>(pixels), width * height);
std::cout << "图像保存至: " << filename << std::endl;
} else {
std::cerr << "无法打开文件: " << filename << std::endl;
}
}
};
int main() {
// 创建100x100的图像
GrayscaleImage image(100, 100);
// 绘制简单图案
for (int y = 0; y < 100; y++) {
for (int x = 0; x < 100; x++) {
// 创建一个梯度图案
image.setPixel(x, y, (x + y) % 256);
}
}
// 保存原始图像
image.saveToPGM("original.pgm");
// 应用模糊
image.applyBlur();
// 保存模糊后的图像
image.saveToPGM("blurred.pgm");
return 0;
}
这个例子展示了动态数组在图像处理中的应用,我们使用一维数组存储二维图像数据,并实现了一个简单的模糊滤镜。
总结
C++动态数组是一个强大的工具,允许在运行时根据需要分配内存。主要要点包括:
- 使用
new[]
分配动态数组,delete[]
释放内存 - 动态数组允许在运行时确定数组大小
- 手动管理动态数组需要谨慎处理内存泄漏、数组越界等问题
- 对于多维数组,可以使用指针的指针或单个一维数组模拟
- 在现代C++中,通常推荐使用
std::vector
代替原生动态数组
练习
- 创建一个动态整数数组,让用户指定大小并填充数据,然后计算所有元素的平均值。
- 实现一个简单的动态字符串类,使用动态字符数组存储字符数据,并提供连接、复制等基本功能。
- 实现一个函数,可以安全地调整动态数组大小并保留原始数据。
- 使用动态二维数组实现矩阵乘法。
- 将一个使用原生动态数组的程序改写为使用
std::vector
,比较两者的代码复杂度和安全性。
附加资源
- C++ 参考手册
- 《C++ Primer》- 动态内存章节
- 《Effective C++》- Scott Meyers