C++ Practical-2 day8 动态数组
[TOC]
Overview
- C++不练习coding,就相当于空中楼阁,基础不扎实
- 光吃不干,等于没吃
1.在C++中,除了使用new和delete,还有其他方式可以创建动态数组吗?
在C++中,除了使用new
和delete
操作符来创建和销毁动态数组,还有以下几种常见的方法来创建和管理动态数组:
使用
std::vector
:std::vector
是C++标准库中的一个动态数组容器,它封装了内存管理的细节,提供了动态扩容、元素访问、插入和删除等操作。1 2 3 4 5 6
#include <vector> std::vector<int> vec; vec.push_back(1); vec.push_back(2); // ...
使用
std::array
(固定大小):std::array
是一个固定大小的数组容器,它存储在栈上,但提供了类似于std::vector
的接口。1 2 3
#include <array> std::array<int, 4> arr = {1, 2, 3, 4};
使用
std::unique_ptr
和自定义删除器: 可以使用智能指针std::unique_ptr
与自定义删除器来管理动态数组。1 2 3
#include <memory> std::unique_ptr<int[], void(*)(int*)> uptr(new int[10], [](int* p){ delete[] p; });
使用
std::shared_ptr
和自定义删除器: 类似于std::unique_ptr
,但std::shared_ptr
允许多个指针共享同一个动态数组。1 2 3
#include <memory> std::shared_ptr<int> sptr(new int[10], [](int* p){ delete[] p; });
使用
malloc
和free
: C语言的malloc
和free
函数也可以用来分配和释放动态内存。1 2 3 4 5
int* arr = (int*)malloc(10 * sizeof(int)); if (arr != nullptr) { // 使用数组... } free(arr);
使用
std::allocator
:std::allocator
是C++标准库中的一个内存分配器,它可以与容器一起使用。1 2 3 4 5 6
#include <memory> std::allocator<int> alloc; int* arr = alloc.allocate(10); // ... alloc.deallocate(arr, 10);
使用
std::make_unique
:std::make_unique
是一个辅助函数,用于创建std::unique_ptr
。1 2 3
#include <memory> auto uptr = std::make_unique<int[]>(10);
使用
std::make_shared
:std::make_shared
是一个辅助函数,用于创建std::shared_ptr
。1 2 3
#include <memory> auto sptr = std::make_shared<int[]>(10);
使用
std::vector
的data
成员函数: 如果你需要一个指向std::vector
底层数组的指针,可以使用data
成员函数。1 2 3 4
#include <vector> std::vector<int> vec(10); int* arr = vec.data();
使用C++17的
std::allocator_traits
:std::allocator_traits
提供了一种方式来查询和使用分配器的属性和操作。1 2 3 4 5 6
#include <memory> template <typename Allocator> void deallocate(Allocator& alloc, int* ptr, size_t size) { std::allocator_traits<Allocator>::deallocate(alloc, ptr, size); }
在现代C++编程中,推荐使用标准库提供的容器和智能指针,因为它们提供了更好的类型安全性、异常安全性和资源管理。
2.动态数组类
在C++中,动态数组通常指的是使用new
和delete
操作符动态分配和释放内存的数组。与静态数组(在栈上分配的数组)不同,动态数组的生命周期由程序员控制,可以在运行时确定大小。
以下是如何在C++中实现一个简单的动态数组类:
|
|
在这个例子中,我们定义了一个DynamicArray
类模板,它封装了动态数组的分配、使用和释放。类中包含了:
- 一个类型为
T*
的私有成员data
,它指向动态分配的内存。 - 一个
size
成员,记录数组中元素的数量。 - 构造函数和析构函数,分别用于分配和释放内存。
- 拷贝构造函数和拷贝赋值运算符,用于复制数组内容。
- 移动构造函数和移动赋值运算符,用于支持移动语义。
getSize
成员函数,返回数组的大小。operator[]
,重载了下标运算符以访问数组元素。
这个简单的动态数组类模板展示了如何管理动态内存。在实际应用中,你可能还需要添加更多的功能,比如动态扩容、插入和删除元素等操作。
3.如何实现动态数组的动态扩容功能?
实现动态数组的动态扩容功能通常涉及以下几个步骤:
- 初始分配:创建数组时分配初始容量。
- 检测容量:在添加元素前检查是否有足够的空间。
- 扩容:如果空间不足,分配更大的内存空间。
- 复制元素:将旧数组的元素复制到新数组。
- 释放旧数组:删除旧数组以避免内存泄漏。
以下是如何在C++中实现一个具有动态扩容功能的动态数组类:
|
|
3.1.解释
初始分配:
- 构造函数中分配初始容量(默认为10)。
检测容量:
- 在
pushBack
方法中,检查当前数组的大小是否等于容量。如果是,则需要扩容。
- 在
扩容:
resize
方法分配新的容量(默认为当前容量的两倍),并将旧数组的元素复制到新数组。
复制元素:
- 使用
std::copy
将旧数组的元素复制到新数组。
- 使用
释放旧数组:
- 删除旧数组以避免内存泄漏。
3.2.注意事项
- 初始容量:可以根据需要调整初始容量和扩容策略。
- 异常安全:在实际应用中,需要考虑异常安全,确保在分配新内存或复制元素时不会发生内存泄漏。
- 内存管理:确保正确管理内存,避免内存泄漏和野指针。
通过这种方式,你可以实现一个具有动态扩容功能的动态数组类,使其在需要时自动扩展容量。
4.动态数组在内存管理方面有哪些潜在的风险,我应该如何避免?
动态数组在内存管理方面主要存在以下潜在风险:
内存泄漏:如果分配了内存但没有正确释放,会导致内存泄漏。例如,忘记使用
delete[]
释放动态数组 。双重释放:多次释放同一块内存会导致未定义行为,可能引发程序崩溃。
悬挂指针:释放内存后没有将指针置为
nullptr
,使其仍然指向已被释放的内存区域,后续对这片区域的访问是非法的。越界访问:在动态分配的内存区域内进行不合法的读写操作,例如索引超出数组边界。
内存分配失败:
new
分配内存失败时会抛出std::bad_alloc
异常,而malloc
会返回NULL
指针。如果不正确处理,可能会导致程序异常终止。内存碎片:频繁的内存分配和释放可能导致内存碎片,降低内存使用效率。
为了避免这些风险,可以采取以下措施:
使用智能指针:如
std::unique_ptr
和std::shared_ptr
,它们可以自动管理内存的释放,避免内存泄漏和悬挂指针 。确保异常安全:在动态内存分配时使用异常处理机制,确保即使在异常发生时也能正确释放内存。
使用 RAII 技术:确保资源在对象生命周期结束时自动释放,这是一种常用的避免资源泄漏的方法。
避免越界访问:在访问数组元素时,始终检查索引是否在有效范围内。
检查内存分配结果:每次使用
new
或malloc
后,检查返回值是否为空,确保内存分配成功。内存池:对于频繁的内存分配和释放,可以使用内存池来减少内存碎片和提高内存分配效率。
使用标准库容器:如
std::vector
,它内部管理内存,并提供了自动扩容和缩容的功能,减少了内存管理的复杂性。
通过这些方法,可以有效地管理动态数组的内存,避免常见的内存管理问题,提高程序的稳定性和性能。
5.如何使用’std:vector’进行高效的动态数组管理?
std::vector
是 C++ 标准库中的一个动态数组容器,它封装了动态内存管理的细节,提供了动态扩容、元素访问、插入和删除等操作。以下是如何使用 std::vector
进行高效的动态数组管理的一些关键点:
自动内存管理:
std::vector
自动管理内存的分配和释放,无需手动使用new
和delete
。动态扩容: 当添加元素超出当前容量时,
std::vector
会自动分配更大的内存空间,并移动现有元素到新内存。提供异常安全性: 如果内存分配失败,
std::vector
会抛出异常,而不会泄漏内存。随机访问:
std::vector
提供了随机访问迭代器,允许快速访问任何元素。范围检查: 使用
at()
成员函数可以提供边界检查,当访问超出范围的元素时会抛出std::out_of_range
异常。迭代器: 提供迭代器支持,可以用于循环遍历
std::vector
。容量和大小:
size()
返回当前元素数量,capacity()
返回当前分配的内存可以容纳的元素数量。插入和删除操作: 提供
push_back()
,pop_back()
,insert()
,erase()
等成员函数进行元素的插入和删除。内存优化:
shrink_to_fit()
可以请求减小内存使用量,reserve()
可以预先分配足够的内存。数据访问: 使用
operator[]
或at()
访问元素,front()
和back()
获取首尾元素。构造和复制: 提供拷贝构造函数和拷贝赋值运算符,支持移动语义,使用
std::move
进行高效复制。空值检查: 使用
empty()
检查std::vector
是否为空。析构: 当
std::vector
被销毁时,会自动释放其管理的内存。
以下是一些使用 std::vector
的示例代码:
|
|
使用 std::vector
可以大大简化动态数组的管理,减少内存泄漏和悬挂指针的风险,并提高代码的可读性和维护性。
6.在C++中,‘std:unique_ptr’和’std:shared_ptr’在动态数组管理上有什么不同?
在 C++ 中,std::unique_ptr
和 std::shared_ptr
都是智能指针,用于自动管理动态分配的内存,但它们在管理动态数组时有一些关键的区别:
std::unique_ptr
与动态数组:std::unique_ptr
是独占所有权的智能指针,意味着同一时间只能有一个std::unique_ptr
指向特定资源。- 它不能被复制,但可以被移动,这允许在不同对象之间传递所有权。
std::unique_ptr
可以被用来管理动态数组,但需要使用特殊的数组删除器std::default_delete
,因为数组的删除需要使用delete[]
而不是delete
。- 使用
std::unique_ptr
管理动态数组时,可以使用std::make_unique
(C++14 引入)来创建实例。
示例:
1
std::unique_ptr<int[]> uptr(new int[10]);
std::shared_ptr
与动态数组:std::shared_ptr
是共享所有权的智能指针,允许多个std::shared_ptr
实例共享同一资源。- 它使用引用计数机制来跟踪有多少个
std::shared_ptr
实例共享同一个资源。 - 当最后一个引用被销毁时,资源会被释放。
std::shared_ptr
也可以管理动态数组,但同样需要使用数组删除器,因为它也需要使用delete[]
。std::shared_ptr
管理动态数组时,通常需要提供一个自定义删除器。
示例:
1
std::shared_ptr<int> sptr(new int[10], [](int* p){ delete[] p; });
6.1.主要区别
所有权:
std::unique_ptr
强调独占所有权,适合于那些不需要共享资源的场景。std::shared_ptr
允许多个指针共享同一个资源,适合于资源需要在多个对象间共享的情况。
删除器:
std::unique_ptr
和std::shared_ptr
在管理动态数组时都需要指定数组删除器,因为默认的删除器是delete
,而数组需要delete[]
。
性能:
std::unique_ptr
通常比std::shared_ptr
更轻量级,因为它不涉及引用计数的开销。std::shared_ptr
由于需要维护引用计数,所以在性能上可能稍微逊色,尤其是在多线程环境中。
线程安全性:
std::shared_ptr
的引用计数操作是原子的,适合于多线程环境。std::unique_ptr
在多线程环境中使用时,通常不需要考虑线程安全问题,因为它的设计就是用于单线程或所有权明确转移的场景。
使用场景:
- 当你不需要共享资源时,推荐使用
std::unique_ptr
,因为它更高效。 - 当资源需要在多个对象间共享,或者所有权不明确时,使用
std::shared_ptr
。
- 当你不需要共享资源时,推荐使用
在选择使用 std::unique_ptr
还是 std::shared_ptr
时,应该根据实际的资源管理和所有权需求来决定。
7.C++17标准中有哪些新特性可以优化动态数组的内存管理?
在 C++17 标准中,有几个新特性可以优化动态数组的内存管理:
std::shared_ptr 对动态数组的支持: C++17 之前,
std::shared_ptr
不支持动态数组类型。在 C++17 中,std::shared_ptr
可以模板化动态数组,并且可以直接使用数组类型的模板参数。这意味着可以直接创建一个std::shared_ptr<int[]>
来管理动态数组,并且可以使用std::shared_ptr
的内置函数,如operator[]
来访问数组元素。std::unique_ptr 的改进:
std::unique_ptr
已经支持动态数组,并且在 C++17 中继续提供对动态数组的优化管理。它确保同一时间只能有一个std::unique_ptr
指向特定资源,从而避免内存泄漏。std::pmr 内存资源库: C++17 引入了 polymorphic memory resources (PMR),它是一个多态内存分配器的库,允许开发者自定义内存分配策略,这可以用于优化动态数组的内存管理。
结构化绑定: C++17 引入了结构化绑定,它允许从数组或元组中提取多个成员到不同变量中,这可以简化数组元素的访问和操作。
if 常量表达式:
if constexpr
允许在编译时根据条件编译代码块,这可以用来优化模板代码中的条件逻辑,包括动态数组的处理。编译时的类型推导: C++17 允许在函数模板中使用
auto
进行类型推导,这简化了模板代码的编写,尤其是在处理动态数组时。constexpr lambda 表达式: C++17 允许在编译时构建 lambda 表达式,这可以用来创建复杂的编译时计算,包括数组操作。
std::apply:
std::apply
函数允许调用一个可调用对象,并将一个元组或数组作为参数传递,这可以简化数组或元组的函数调用。
通过这些特性,C++17 提供了更灵活和高效的方式来管理动态数组的内存,同时减少了内存泄漏的风险,并提高了代码的可读性和可维护性。
关于作者
- 微信公众号:WeSiGJ
- GitHub:https://github.com/wesigj/cplusplusboys
- CSDN:https://blog.csdn.net/wesigj
- 微博:
- 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
