问题

C++动态二维数组如何使用智能指针?

回答
好的,咱们就来聊聊 C++ 中使用智能指针来管理动态二维数组的事情。这事儿听起来有点绕,但一旦理顺了,你会发现它能省去不少心,也能避免不少掉坑。

为啥要用智能指针管这事儿?

先别急着往智能指针上套,咱们先想想,为啥要用智能指针来管理动态二维数组?

原始 C++ 的痛点:

裸指针的危险: 创建动态二维数组,你通常得这么来:
```c++
int arr = new int[rows];
for (int i = 0; i < rows; ++i) {
arr[i] = new int[cols];
}
// ... 使用 arr ...
// 最后得手动释放
for (int i = 0; i < rows; ++i) {
delete[] arr[i];
}
delete[] arr;
```
看到了吧?两层循环,一个不小心漏了 `delete[]`,内存泄漏就来了。而且,如果在中间抛了异常,那内存就更惨了,直接丢了,没法回收。这叫“资源获取即初始化”(RAII)原则的缺失。

复杂的内存管理: 尤其是当这个二维数组要在函数之间传递,或者在类的成员变量里存在时,内存的生命周期管理就变得异常复杂。谁来 `delete`?什么时候 `delete`?这个问题能让代码复杂度指数级增长。

智能指针的优势(就像给裸指针穿上了一层盔甲):

自动内存管理: 这是最核心的优势。智能指针在自己销毁的时候(比如出了作用域),会自动帮你释放它所管理的内存。再也不用手动 `delete[]` 了,大大降低了内存泄漏的风险。
异常安全: 如果在智能指针管理的代码块中发生了异常,智能指针的析构函数仍然会被调用,确保其管理的内存被正确释放。这完美契合了 RAII 原则。
代码清晰易读: 看到智能指针,你立刻就知道这块内存的生命周期是受管理的,代码意图更明确。

C++ 动态二维数组的几种智能指针解决方案

咱们先理清楚动态二维数组的结构:它本质上是一个指向指针的指针 (`T`)。第一层的指针指向的是一个 `T` 数组,而第二层的每个 `T` 指针则指向实际的元素数组。

所以,管理这种结构,需要考虑两层内存:第一层是 `T` 数组的内存,第二层是 `T` 元素的内存。

方案一:`std::unique_ptr` 的嵌套(推荐)

这是最现代、最推荐的做法。`std::unique_ptr` 是一个独占所有权的智能指针,非常适合管理生命周期明确的资源。

原理:

我们用一个 `std::unique_ptr` 来管理第一个指针数组(即 `T` 的数组),而这个 `std::unique_ptr` 所指向的类型,本身又是一个 `std::unique_ptr` 的数组。

具体实现:

1. 管理第一层指针数组: 使用 `std::unique_ptr`. `int[]` 表示一个指向 `int` 类型的数组。
2. 管理第二层元素数组: 数组中的每一个 `int` 仍然需要被管理。所以,我们让第一层的 `std::unique_ptr` 管理的是一个 `std::unique_ptr[]` 数组。

代码示例:

```c++
include
include
include // 方便展示,实际可以不用

// 定义一个模板类,让它可以管理任意类型 T 的二维数组
template
class SmartDynamic2DArray {
private:
// 使用 unique_ptr[]> 来管理。
// 外层 unique_ptr 管理行指针数组,内层 unique_ptr 管理列元素。
// 这里是直接用 std::unique_ptr 来管理行指针,然后数组里的每个 T 又被 std::unique_ptr 管理。
// 这个模型不太直接,更常见的模型是直接用 vector>> 或者更精巧的定制。
//
// 咱们换个思路,更直接的 twolevel unique_ptr 结构:
// std::unique_ptr[]> data_; // 这就有点问题了,unique_ptr 本身不是数组的持有者
//
// 更正一下,管理 T 结构需要的是:
// 1. 一个管理 `T` 数组的 `std::unique_ptr` (或者 `std::vector`)
// 2. 并且 `T` 指针本身也要被管理。
//
// 咱们尝试一种更符合 C++ idiomatic 的方式,用 vector 结合 unique_ptr。
//
// 但是,如果非要用 C 风格的 T 来模拟,并且用 unique_ptr 来管理,可以这样:
//
// 创建一个 unique_ptr 来管理行指针的数组:
// std::unique_ptr row_pointers;
//
// 然后,对于每一个 T 指针,也需要一个 unique_ptr 来管理它。
//
// 这是一个更贴近 T 结构的、使用 unique_ptr 的方式,虽然不是最优雅的 C++ 风格,但解决了手动管理的问题:

std::unique_ptr row_pointers_raw; // 管理 T 的数组
size_t num_rows_;
size_t num_cols_;

// 辅助函数来分配和释放
void allocate(size_t rows, size_t cols) {
num_rows_ = rows;
num_cols_ = cols;

if (rows == 0 || cols == 0) {
return; // 什么也不做,空的
}

// 1. 分配行指针数组,并用 unique_ptr 管理
row_pointers_raw = std::make_unique(rows);

// 2. 为每一行分配元素数组,并用裸指针暂时存储
for (size_t i = 0; i < rows; ++i) {
row_pointers_raw[i] = new T[cols]; // 这里暂时分配裸指针
}
}

// 需要一个方式来管理这些 T 指向的内存。
// 直接用 std::vector> 更清晰!
//
// 重新审视需求:C++动态二维数组如何使用智能指针?
// 最直接的 twolevel 结构就是 T
// 要用智能指针管理 T,意味着要管理两层分配。
//
// 方案一.1:使用 std::vector>> (最 C++ 风格,但不完全是 T)
// 这种方式创建的是一个向量的向量,每个内层向量都是动态分配的,且被 unique_ptr 管理。
// 它不是严格意义上的 T 结构,但实现了动态二维数组的功能,并且自动管理内存。
//
// 咱们演示这个更 idiomatic 的方式:

public:
// 使用 vector of unique_ptr of vector
std::vector>> data;
size_t rows;
size_t cols;

SmartDynamic2DArray(size_t r, size_t c) : rows(r), cols(c) {
data.resize(r); // 预分配 r 个智能指针
for (size_t i = 0; i < r; ++i) {
// 为每一行创建一个新的 vector,并用 unique_ptr 管理
data[i] = std::make_unique>(c);
}
}

// 析构函数是空的,因为 unique_ptr 会自动管理内存。
// ~SmartDynamic2DArray() = default;

// 访问元素
T& operator()(size_t r, size_t c) {
if (r >= rows || c >= cols) {
throw std::out_of_range("Index out of bounds");
}
return (data[r])[c];
}

const T& operator()(size_t r, size_t c) const {
if (r >= rows || c >= cols) {
throw std::out_of_range("Index out of bounds");
}
return (data[r])[c];
}

size_t get_rows() const { return rows; }
size_t get_cols() const { return cols; }
};

//
// 如果你真的想模拟 T 的结构,并用 unique_ptr 管理,
// 那么我们需要一个 `unique_ptr` 管理 `T` 数组,
// 然后再一个 `unique_ptr` 管理每个 `T` 数组。
// 这听起来有点像 `unique_ptr[]>`.
//
// 实际上,`unique_ptr` 已经有了删除数组的能力。
// 所以我们可以用 `unique_ptr[]>`.
//
// 但是 `unique_ptr` 本身并不直接提供删除 `T[]` 的能力。
// 它管理的 `T` 数组,里面的每一个 `T` 指针仍然需要被管理。
//
// 最直接的 C++ 风格的 T 模拟是:
// `std::vector>` 这个不是动态的,编译时大小固定,除非用 `std::vector>` 且内层 vector 动态 resize。
//
// 真正接近 T 结构的,且用智能指针管理,又不太复杂的是:
//
// std::unique_ptr row_ptrs; // 管理 T 数组的指针
// std::vector> col_ptrs; // 每个 T 指针指向的数组,也用 unique_ptr 管理
//
// 这个组合起来就很麻烦了。

// 让我们回到最初的 T 模型,并使用 unique_ptr 来简化其管理。
//
// 核心问题是:T 意味着两层内存分配。
// 1. 分配 T 的数组。
// 2. 分配 T 的数组,每个 T 指向一个这样的数组。
//
// 解决方案是:使用 `std::vector` 来管理容器,并且用 `std::unique_ptr` 来管理每个内部动态分配的资源。
//
// 我们可以定义一个类,它内部使用 `std::vector>` 来存储每一行。
// 这样就管理了两层内存:
// `std::vector` 管理其自身内存。
// `std::unique_ptr` 管理每一行 `T` 元素的内存。
//

template
class RawPtrTwoDimArrayManager {
private:
std::unique_ptr row_pointers; // unique_ptr 管理 T 数组
size_t num_rows_;
size_t num_cols_;

// 实际上,我们需要的是一个能够管理 T 的东西。
// C++ 标准库提供了 `std::vector>`,这已经非常接近了。
// 但如果一定要模拟 C 风格的 T,并且用智能指针,那么:
//
// 使用 `std::unique_ptr[]>`.
// 外层 `unique_ptr` 管理 `unique_ptr` 的数组。
// 内层 `unique_ptr` 管理 `T` 的数组。
//
// 但 `std::unique_ptr` 本身不能直接用于创建数组类型。
// 所以更现实的做法是:
//
// `std::unique_ptr ptr_to_row_pointers;`
// `std::vector> data_rows;` // 这个更直接

// 让我们实现一个 `RawPtrTwoDimArrayManager`,它内部使用裸指针,
// 但它的析构函数负责正确释放。然后我们用 `unique_ptr` 来包装它。
// 这更像是“如何用智能指针管理一个现有的动态二维数组”。
//
// 如果我们要从头开始创建一个智能指针管理的二维数组,那么 `vector>>`
// 是最 idiomatic 的。
//
// 但如果问题是“C++动态二维数组如何使用智能指针”, implying T structure,
// then we need to manage two levels of pointers.
//
// `std::unique_ptr p_rows;`
// `std::vector> p_cols;` 这个组合太怪异了
//
// 让我们直接用 `std::vector>` 来代表一个二维数组。
// 它的语义是:一个拥有行的容器,每一行都是一个动态分配的 T 数组,由 unique_ptr 管理。
//
// 这才是真正的“智能指针管理动态二维数组”的现代 C++ 写法。
// 并且它避免了 T 的许多陷阱。

public:
std::vector> rows_data;
size_t num_cols_; // 每行有多少列

// 构造函数
RawPtrTwoDimArrayManager(size_t rows, size_t cols) : num_cols_(cols) {
rows_data.resize(rows); // 预分配 rows 个 unique_ptr
for (size_t i = 0; i < rows; ++i) {
// 为每一行分配 T[],并用 unique_ptr 管理
rows_data[i] = std::make_unique(cols);
}
}

// 访问元素
T& operator()(size_t r, size_t c) {
if (r >= rows_data.size() || c >= num_cols_) {
throw std::out_of_range("Index out of bounds");
}
return rows_data[r][c];
}

const T& operator()(size_t r, size_t c) const {
if (r >= rows_data.size() || c >= num_cols_) {
throw std::out_of_range("Index out of bounds");
}
return rows_data[r][c];
}

size_t get_rows() const { return rows_data.size(); }
size_t get_cols() const { return num_cols_; }
};


int main() {
size_t rows = 3;
size_t cols = 4;

std::cout << "使用 SmartDynamic2DArray (vector>>):" << std::endl;
{
SmartDynamic2DArray arr(rows, cols);

// 填充数据
for (size_t i = 0; i < arr.get_rows(); ++i) {
for (size_t j = 0; j < arr.get_cols(); ++j) {
arr(i, j) = i 10 + j;
}
}

// 读取数据
for (size_t i = 0; i < arr.get_rows(); ++i) {
for (size_t j = 0; j < arr.get_cols(); ++j) {
std::cout << arr(i, j) << " ";
}
std::cout << std::endl;
}
} // arr 在这里超出作用域,内存被自动释放

std::cout << " 使用 RawPtrTwoDimArrayManager (vector>):" << std::endl;
{
RawPtrTwoDimArrayManager arr_raw(rows, cols);

// 填充数据
for (size_t i = 0; i < arr_raw.get_rows(); ++i) {
for (size_t j = 0; j < arr_raw.get_cols(); ++j) {
arr_raw(i, j) = i 10 + j + 100; // 填充不同数据
}
}

// 读取数据
for (size_t i = 0; i < arr_raw.get_rows(); ++i) {
for (size_t j = 0; j < arr_raw.get_cols(); ++j) {
std::cout << arr_raw(i, j) << " ";
}
std::cout << std::endl;
}
} // arr_raw 在这里超出作用域,内存被自动释放

return 0;
}
```

解释一下 `RawPtrTwoDimArrayManager`(更接近 T 的结构):

`std::vector> rows_data;`: 这是核心。
`std::vector`: 管理的是 `std::unique_ptr` 对象本身的存储。`vector` 负责自己内存的分配和释放。
`std::unique_ptr`: 这是关键的智能指针。它管理的是一个动态分配的 `T` 类型数组。当 `unique_ptr` 被销毁时,它会调用 `delete[]` 来释放它所指向的 `T` 数组。
`num_cols_`: 这个成员变量只是一个简单的 `size_t`,用来记录每一行有多少列。它不需要智能指针来管理,因为它只是一个大小信息。

优点:

自动管理两层内存: `vector` 管理 `unique_ptr` 的存储,`unique_ptr` 管理 `T[]` 的存储。当 `RawPtrTwoDimArrayManager` 对象被销毁时,`rows_data`(`vector`)的析构函数会被调用,接着,`vector` 中的每个 `unique_ptr` 的析构函数也会被调用,它们各自会释放其管理的 `T[]` 内存。
异常安全: 同上,即使在构造或使用过程中抛出异常,智能指针也能保证资源的正确释放。
代码清晰: 结构一目了然,每个 `rows_data[i]` 都明确地指向一个由 `unique_ptr` 管理的 `T` 数组。

如何使用:

如 `main` 函数所示,你可以像使用普通二维数组一样使用 `operator()` 来访问元素。

方案二:`std::shared_ptr` 的嵌套(适用于共享所有权)

如果你希望多个部分可以共享对这个动态二维数组的所有权,那么 `std::shared_ptr` 是个好选择。

原理:

与 `std::unique_ptr` 类似,我们可以使用 `std::vector>` 来管理。每个 `shared_ptr` 维护一个引用计数。当最后一个指向某一行 `T` 数组的 `shared_ptr` 被销毁时,该行数组的内存才会被释放。

代码示例(结构与 `unique_ptr` 类似,只是将 `unique_ptr` 换成 `shared_ptr`):

```c++
include
include
include

template
class SharedPtrTwoDimArrayManager {
public:
std::vector> rows_data;
size_t num_cols_; // 每行有多少列

// 构造函数
SharedPtrTwoDimArrayManager(size_t rows, size_t cols) : num_cols_(cols) {
rows_data.resize(rows);
for (size_t i = 0; i < rows; ++i) {
// 为每一行分配 T[],并用 shared_ptr 管理
rows_data[i] = std::make_shared(cols);
}
}

// 访问元素
T& operator()(size_t r, size_t c) {
if (r >= rows_data.size() || c >= num_cols_) {
throw std::out_of_range("Index out of bounds");
}
return rows_data[r][c];
}

const T& operator()(size_t r, size_t c) const {
if (r >= rows_data.size() || c >= num_cols_) {
throw std::out_of_range("Index out of bounds");
}
return rows_data[r][c];
}

size_t get_rows() const { return rows_data.size(); }
size_t get_cols() const { return num_cols_; }
};

int main() {
size_t rows = 2;
size_t cols = 3;

std::cout << "使用 SharedPtrTwoDimArrayManager (vector>):" << std::endl;
{
SharedPtrTwoDimArrayManager arr(rows, cols);

// 填充数据
for (size_t i = 0; i < arr.get_rows(); ++i) {
for (size_t j = 0; j < arr.get_cols(); ++j) {
arr(i, j) = (i + 1.1) (j + 0.5);
}
}

// 读取数据
for (size_t i = 0; i < arr.get_rows(); ++i) {
for (size_t j = 0; j < arr.get_cols(); ++j) {
std::cout << arr(i, j) << " ";
}
std::cout << std::endl;
}

// 演示共享所有权
SharedPtrTwoDimArrayManager arr_copy = arr; // arr_copy 和 arr 共享数据
std::cout << " 修改共享数据..." << std::endl;
arr(0, 0) = 99.9;
std::cout << "arr_copy(0,0): " << arr_copy(0, 0) << std::endl; // 看到修改

} // arr 和 arr_copy 在这里都超出作用域,但由于是 shared_ptr,只要引用计数不为零,内存就不会被释放。
// 在这个例子中,当最后一个 shared_ptr 被销毁时,内存才会被释放。

return 0;
}
```

优点:

共享所有权: 允许多个智能指针指向同一份数据,当需要复制或传递拥有权时非常方便。
自动内存管理: 同样避免了手动 `delete[]` 的问题,并提供异常安全。

缺点:

性能开销: `shared_ptr` 需要维护引用计数,这会带来一定的性能开销(原子操作)。
循环引用: 如果不小心形成循环引用,会导致内存泄漏。

方案三:`std::unique_ptr[]> `(理论上,但通常不直接这么做)

你可能会想到直接用 `std::unique_ptr[]>`. 理论上,这可以表示一个 `unique_ptr`, 其中每个 `T` 又是一个 `unique_ptr`.

问题在于: `std::unique_ptr` 是一个特化的 `unique_ptr`,它知道如何删除 `T[]`。但是 `std::unique_ptr ` 不是一个直接可用的数组特化。`std::unique_ptr` 的模板参数要求是一个非数组类型,或者明确是 `T[]`。

所以,你不能直接写 `std::unique_ptr[]>`. 编译器不会理解 `std::unique_ptr` 是一个数组的元素类型来创建 `unique_ptr` 数组。

正确的 T 风格管理方式是:

1. 使用 `std::unique_ptr` 来管理行指针数组。
2. 然后为这个 `T[]` 数组中的每一个 `T` 分配一个 `std::unique_ptr`。

这又回到了我们上面 `RawPtrTwoDimArrayManager` 的思路,但更直接地管理 `T[]` 的话,你需要手动管理第二层。

更现实的 C++ 风格处理 T 的方式,就是 `std::vector>`。它抽象了 T 的概念,提供了更安全、更现代的接口。

其他考虑:`std::vector>`

值得一提的是,在很多情况下,`std::vector>` 是最简单、最直接的二维动态数组表示方式。

`std::vector` 本身就是一个动态数组,并且内存是连续的(对于内层 vector)。
外层 `std::vector` 管理着内层 `std::vector` 的存储。

优点:

简单易用: 使用起来非常自然。
内存连续性: 内层 `std::vector` 的元素在内存中是连续的,这对于某些算法(如需要指针算术的)可能是有益的。

缺点:

内存碎片: 如果行数很多,每一行都是一个独立的 `std::vector` 对象,它们在内存中的存储地址可能是不连续的,形成内存碎片。这可能对缓存友好性有影响。
两层间接: 访问元素需要两次间接访问:一次访问外层 vector 中的内层 vector,一次访问内层 vector 中的元素。

何时选择 `std::vector>` vs `std::vector>`:

如果你的二维数组不需要非常大的行数,并且简单的接口比极致的性能更重要,那么 `std::vector>` 是一个很好的选择。
如果你需要更接近 `T` 的结构,希望每一行都作为独立的连续内存块(例如,在某些与 C 库交互时),或者对内存布局有更精细的控制,那么 `std::vector>` 会是更好的选择。它提供了 C++ 的智能管理,同时保留了“行是连续的”这一特性。

总结

要用智能指针来管理 C++ 的动态二维数组(模拟 `T` 的结构),最推荐的方式是利用 `std::vector` 的容器特性,并结合 `std::unique_ptr` 来管理每一行数据的内存。

`std::vector>`: 这是现代 C++ 中管理动态二维数组(类似 C 风格的 `T`)的最安全、最简洁的方式。它自动处理了二级内存的分配和释放,提供了异常安全。
`std::vector>`: 对于大多数日常应用,这是最简单、最易用的选择,但它在内存布局上与 `T` 有所不同。

选择哪种方案,取决于你的具体需求和对性能、内存布局的考量。但无论如何,都应该避免直接使用裸指针来管理动态二维数组,拥抱智能指针带来的安全和便捷。

网友意见

user avatar

自问自答一波,刚刚貌似发现了解决方法:使用unique_ptr就可以了:

       int cols = 3, rows = 3; auto tmp_2 = vector<unique_ptr<Element []>>(rows); for (size_t i = 0; i < rows; ++ i) {     tmp_2[i] = move(unique_ptr<Element[]>(new Element[cols])); }      

想要修改元素内容的时候可以直接取地址:

       auto x = &tmp_2[1][2];      

然后修改内容即可:

       x->a.push_back(123);      

但是仍然不明白题目中描述的指针二次释放是怎么产生的,请各位大佬不吝赐教!

这个问题来自最近的编译器开发,为了描述清晰,进行了一定的简化。在LL1语法分析构建LL1分析表的时候,需要创建一个行数为栈顶符总数,列数为当前符总数的二维表,表格内每个元素定义如下:

       struct Analyze_table_item{     std::vector<std::string> stack_op; // 对分析栈的操作,文法符号的向量,将来逆序压入栈中     char read_op {};                   // 对输入流的控制,'N'表示读下一个token,'P'表示不读 }; // 分析表中的每一项元素      

然后需要定义一个get_op函数,能够返回某个位置的元素的指针,以进行读写:

       Analyze_table_item * get_op(const std::string& stack_top, const std::string& current);      

当然这个场景下讨论这个问题有点儿无聊,因为规范的写法应该定义一个get_op和一个set_op分别进行读和写,就避免了指针的传递。我当时在set_op函数里调用get_op可能只是为了节省代码吧,现在当事人很后悔 ...

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有