C++ Primer 第10到12章非常详细讲解
第10章 泛型算法
10.1 概述
10.1.1 什么是泛型算法
泛型算法(Generic Algorithms)是C++标准库提供的一系列算法,它们可以用于不同类型的容器(如vector、list、deque等)和数组。这些算法通过迭代器来访问容器中的元素,而不是直接操作容器本身,从而实现了对多种数据结构的通用操作。
关键特点:
-
泛型性:算法不依赖于特定的容器类型,通过迭代器实现对不同容器的操作。
-
不修改容器结构:大多数泛型算法不直接改变容器的结构(如不添加或删除元素),而是对元素进行操作。
-
基于迭代器:通过迭代器访问和操作元素,使得算法具有高度的灵活性和通用性。
-
标准库提供:包含在
<algorithm>和<numeric>头文件中。
10.1.2 如何使用泛型算法
要使用泛型算法,首先需要包含相应的头文件,然后通过迭代器将算法应用于容器或数组。
常用头文件:
-
<algorithm>:包含大多数泛型算法。 -
<numeric>:包含一些数值算法,如累加、求积等。
基本用法示例:
#include <iostream>
#include <vector>
#include <algorithm> // 包含泛型算法
#include <numeric> // 包含数值算法
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 使用std::sort对vector进行排序
std::sort(vec.begin(), vec.end());
// 输出排序后的vector
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
// 使用std::accumulate计算总和
int sum = std::accumulate(vec.begin(), vec.end(), 0);
std::cout << "Sum: " << sum << std::endl;
return 0;
}
10.2 初识泛型算法
10.2.1 只读算法
只读算法是指那些只读取容器中的元素而不修改它们的算法。这类算法通常用于查找、计算总和等操作。
常见只读算法:
-
std::find:查找指定值。 -
std::count:计算指定值的出现次数。 -
std::accumulate:计算元素的累积值(如总和、乘积等)。 -
std::equal:比较两个序列是否相等。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用std::find查找元素3
auto it = std::find(vec.begin(), vec.end(), 3);
if (it != vec.end()) {
std::cout << "Found 3 at position: " << std::distance(vec.begin(), it) << std::endl;
} else {
std::cout << "3 not found" << std::endl;
}
// 使用std::count计算元素5的出现次数
int count = std::count(vec.begin(), vec.end(), 5);
std::cout << "Number of 5s: " << count << std::endl;
// 使用std::accumulate计算总和
int sum = std::accumulate(vec.begin(), vec.end(), 0);
std::cout << "Sum: " << sum << std::endl;
return 0;
}
10.2.2 写容器元素的算法
写容器元素的算法会修改容器中的元素值,但不改变容器的结构(如不添加或删除元素)。
常见写容器元素的算法:
-
std::fill:将指定值填充到指定范围。 -
std::generate:使用生成器函数填充指定范围。 -
std::transform:对指定范围的元素应用操作并存储结果。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec(5); // 5个元素,值未初始化
// 使用std::fill将所有元素填充为10
std::fill(vec.begin(), vec.end(), 10);
// 输出填充后的vector
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
// 使用std::transform将每个元素乘以2
std::transform(vec.begin(), vec.end(), vec.begin(), [](int i) { return i * 2; });
// 输出变换后的vector
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
10.2.3 重排容器元素的算法
重排容器元素的算法会改变容器中元素的顺序,但不改变元素的值。
常见重排容器元素的算法:
-
std::sort:对元素进行排序。 -
std::reverse:反转元素的顺序。 -
std::unique:移除相邻的重复元素(需先排序)。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 使用std::sort进行排序
std::sort(vec.begin(), vec.end());
// 输出排序后的vector
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
// 使用std::reverse反转顺序
std::reverse(vec.begin(), vec.end());
// 输出反转后的vector
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
// 使用std::unique移除相邻的重复元素(需要先排序)
auto last = std::unique(vec.begin(), vec.end());
vec.erase(last, vec.end()); // 擦除重复元素后的多余位置
// 输出去重后的vector
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
10.3 定制操作
10.3.1 向算法传递函数
定制操作允许我们通过传递函数(如函数指针、函数对象或Lambda表达式)来定义算法的具体行为,从而实现更灵活的操作。
常见使用场景:
-
使用比较函数自定义排序规则。
-
使用谓词函数进行条件查找或过滤。
示例:使用Lambda表达式自定义排序规则:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 使用Lambda表达式按降序排序
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b;
});
// 输出降序排序后的vector
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
10.3.2 Lambda表达式
Lambda表达式是C++11引入的一种匿名函数,可以方便地在需要的地方定义短小的函数,尤其适用于作为算法的参数。
Lambda表达式的基本语法:
[capture](parameters) -> return_type {
// 函数体
}
-
capture:捕获列表,指定Lambda表达式可以访问的外部变量。
-
parameters:参数列表,类似于普通函数的参数。
-
return_type:返回类型,可以省略,编译器会自动推导。
-
函数体:Lambda表达式的具体实现。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用Lambda表达式打印每个元素
std::for_each(vec.begin(), vec.end(), [](int n) {
std::cout << n << " ";
});
std::cout << std::endl;
return 0;
}
捕获列表说明:
-
值捕获:
[a],捕获变量a的值。 -
引用捕获:
[&a],捕获变量a的引用。 -
隐式值捕获:
[=],捕获所有外部变量,按值捕获。 -
隐式引用捕获:
[&],捕获所有外部变量,按引用捕获。 -
混合捕获:
[a, &b],按值捕获a,按引用捕获b。
示例:使用引用捕获修改外部变量:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
int sum = 0;
// 使用Lambda表达式按引用捕获sum并累加元素
std::for_each(vec.begin(), vec.end(), [&sum](int n) {
sum += n;
});
std::cout << "Sum: " << sum << std::endl;
return 0;
}
10.3.3 使用标准库函数对象
函数对象(Function Objects)是重载了函数调用运算符operator()的类对象,可以像函数一样被调用。
标准库提供的常用函数对象(定义在<functional>头文件中):
-
算术运算:
plus<>,minus<>,multiplies<>,divides<>,modulus<> -
关系运算:
equal_to<>,not_equal_to<>,greater<>,less<>,greater_equal<>,less_equal<> -
逻辑运算:
logical_and<>,logical_or<>,logical_not<>
示例:使用std::greater进行降序排序:
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional> // 包含标准库函数对象
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 使用std::greater进行降序排序
std::sort(vec.begin(), vec.end(), std::greater<int>());
// 输出降序排序后的vector
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
使用函数对象与Lambda表达式的比较:
-
函数对象:适用于需要多次复用或复杂逻辑的情况,具有更好的封装性和可重用性。
-
Lambda表达式:适用于一次性、简短的操作,代码更简洁直观。
10.4 再探迭代器
10.4.1 插入迭代器
插入迭代器(Insert Iterators)是一种特殊的迭代器,用于在容器的特定位置插入元素,而不是覆盖现有元素。它们常用于需要在不覆盖元素的情况下向容器添加元素的算法。
三种插入迭代器:
-
back_inserter:创建一个使用push_back的迭代器,适用于支持push_back的容器(如vector、deque、list)。 -
front_inserter:创建一个使用push_front的迭代器,适用于支持push_front的容器(如list、deque)。 -
inserter:创建一个使用insert的迭代器,可以在任意位置插入元素,适用于所有标准容器。
示例:使用back_inserter向vector添加元素:
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator> // 包含插入迭代器
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dest;
// 使用back_inserter将src的元素添加到dest的末尾
std::copy(src.begin(), src.end(), std::back_inserter(dest));
// 输出dest
for (const auto &v : dest) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
示例:使用inserter在指定位置插入元素:
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin() + 2; // 指向第三个元素
// 使用inserter在it位置插入元素
std::fill_n(std::inserter(vec, it), 3, 99);
// 输出vec
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
10.4.2 iostream迭代器
iostream迭代器允许算法与输入/输出流交互,将流视为序列进行处理。
类型:
-
istream_iterator:用于从输入流(如std::cin、文件流)读取元素。 -
ostream_iterator:用于向输出流(如std::cout、文件流)写入元素。
示例:使用istream_iterator读取输入并存储到vector:
#include <iostream>
#include <vector>
#include <iterator> // 包含iostream迭代器
#include <algorithm>
int main() {
std::cout << "请输入一系列整数,以非数字结束: ";
// 创建istream_iterator,默认从std::cin读取int
std::istream_iterator<int> input_begin(std::cin);
std::istream_iterator<int> input_end; // 默认构造表示流结束
// 将输入的整数存储到vector中
std::vector<int> vec(input_begin, input_end);
// 输出读取到的整数
std::cout << "你输入的整数是: ";
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
示例:使用ostream_iterator将vector内容输出到标准输出:
#include <iostream>
#include <vector>
#include <iterator> // 包含iostream迭代器
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 创建ostream_iterator,默认向std::cout输出int,以空格分隔
std::ostream_iterator<int> output(std::cout, " ");
// 使用copy算法将vec内容输出到std::cout
std::copy(vec.begin(), vec.end(), output);
std::cout << std::endl;
return 0;
}
10.4.3 反向迭代器
反向迭代器(Reverse Iterators)允许算法从后向前遍历容器。它们是适配器,将正常的迭代器转换为反向移动的迭代器。
常用操作:
-
rbegin():返回指向容器最后一个元素的反向迭代器。 -
rend():返回指向容器第一个元素之前位置的反向迭代器。
示例:使用反向迭代器逆序输出vector:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用反向迭代器逆序输出vector
std::cout << "逆序输出: ";
for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
std::cout << *rit << " ";
}
std::cout << std::endl;
return 0;
}
与泛型算法结合使用:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用std::reverse算法反转向量
std::reverse(vec.begin(), vec.end());
// 输出反转后的vector
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
10.5 泛型算法结构
10.5.1 算法的分类
泛型算法可以根据其功能进行分类,常见的分类包括:
-
只读算法:如
std::find、std::count、std::accumulate。 -
写算法:如
std::fill、std::generate、std::transform。 -
排序和重排算法:如
std::sort、std::stable_sort、std::unique。 -
数值算法:如
std::accumulate(在<numeric>中)、std::inner_product、std::partial_sum。
10.5.2 算法的通用性
泛型算法的通用性体现在它们通过迭代器与容器交互,而不关心容器的具体类型。这使得相同的算法可以应用于不同类型的容器,如vector、list、deque等。
关键点:
-
迭代器抽象:算法通过迭代器访问元素,而不直接操作容器。
-
参数化行为:通过传递函数对象或Lambda表达式,算法的行为可以灵活定制。
10.6 特定容器算法
虽然大多数泛型算法适用于所有标准容器,但某些容器(特别是list和forward_list)提供了专属的成员函数,这些函数通常比通用算法更高效。
示例:list的专属排序函数:
#include <iostream>
#include <list>
#include <algorithm>
int main() {
std::list<int> lst = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 使用list的成员函数sort进行排序
lst.sort();
// 输出排序后的list
for (const auto &l : lst) {
std::cout << l << " ";
}
std::cout << std::endl;
return 0;
}
注意:
-
list和forward_list:这些容器不支持随机访问迭代器,因此某些泛型算法(如std::sort)不能直接使用。它们提供了自己的成员函数来实现类似功能,通常更高效。 -
选择合适的方法:在可能的情况下,优先使用容器专属的成员函数,以获得更好的性能和更简洁的代码。
第11章 关联容器
11.1 概述
11.1.1 什么是关联容器
关联容器(Associative Containers)是C++标准库提供的另一种类型的容器,它们存储键值对(key-value pairs),并根据键(key)进行排序和快速查找。与顺序容器(如vector、list)不同,关联容器中的元素是按照键的顺序自动排序的,这使得查找操作更加高效。
主要特点:
-
基于键的排序:元素根据键自动排序,通常使用红黑树(Red-Black Tree)实现,保证有序性。
-
快速查找:通过键快速查找对应的值,时间复杂度通常为O(log n)。
-
唯一性或多重性:键可以是唯一的(每个键只能对应一个值)或允许多个元素拥有相同的键。
常见关联容器类型:
-
有序关联容器:
-
map:键值对集合,键唯一,按键排序。 -
set:键的集合,键唯一,按键排序。 -
multimap:键值对集合,键可以重复,按键排序。 -
multiset:键的集合,键可以重复,按键排序。
-
-
无序关联容器(C++11引入):
-
unordered_map:基于哈希表的键值对集合,键唯一,无序。 -
unordered_set:基于哈希表的键集合,键唯一,无序。 -
unordered_multimap:基于哈希表的键值对集合,键可以重复,无序。 -
unordered_multiset:基于哈希表的键集合,键可以重复,无序。
-
本章主要介绍有序关联容器,无序关联容器将在后续章节(如有)中讨论。
11.2 关联容器概述
11.2.1 定义关联容器
常用有序关联容器:
-
map<Key, Value>:存储键值对,键唯一,按键排序。 -
set<Key>:存储键,键唯一,按键排序。 -
multimap<Key, Value>:存储键值对,键可以重复,按键排序。 -
multiset<Key>:存储键,键可以重复,按键排序。
定义关联容器:
#include <iostream>
#include <map>
#include <set>
int main() {
// 定义一个map,键为int,值为string
std::map<int, std::string> myMap;
// 定义一个set,键为int
std::set<int> mySet;
return 0;
}
初始化关联容器:
-
可以使用初始化列表、范围初始化或其他构造函数进行初始化。
示例:
#include <iostream>
#include <map>
#include <set>
int main() {
// 使用初始化列表初始化map
std::map<int, std::string> myMap = {
{1, "one"},
{2, "two"},
{3, "three"}
};
// 使用初始化列表初始化set
std::set<int> mySet = {3, 1, 4, 1, 5}; // 重复元素会被忽略,结果为{1, 3, 4, 5}
// 输出map内容
for (const auto &pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
// 输出set内容
for (const auto &elem : mySet) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
11.2.2 关联容器概述
关联容器的主要类别:
-
map:-
描述:存储键值对(key-value pairs),每个键在容器中是唯一的,按键排序。
-
用途:用于快速查找与特定键相关联的值,如字典。
-
-
set:-
描述:存储唯一的键,按键排序。
-
用途:用于存储唯一的元素集合,快速查找元素是否存在。
-
-
multimap:-
描述:存储键值对,键可以重复,按键排序。
-
用途:用于一个键对应多个值的情况,如一个作者对应多本书。
-
-
multiset:-
描述:存储键,键可以重复,按键排序。
-
用途:用于存储可以重复的元素集合,保持排序。
-
关联容器与顺序容器的对比:
-
顺序容器(如
vector、list):元素按插入顺序存储,不自动排序,查找效率较低(通常为O(n))。 -
关联容器:元素按键的顺序自动排序,查找效率高(通常为O(log n))。
11.3 关联容器操作
11.3.1 关联容器迭代器
关联容器的迭代器允许遍历容器中的元素。对于map和multimap,迭代器解引用后得到的是一个键值对(通常是一个pair<const Key, Value>);对于set和multiset,迭代器解引用后得到的是键。
示例:遍历map:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap = {
{1, "one"},
{2, "two"},
{3, "three"}
};
// 使用迭代器遍历map
for (auto it = myMap.begin(); it != myMap.end(); ++it) {
std::cout << it->first << ": " << it->second << std::endl;
}
return 0;
}
示例:遍历set:
#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {3, 1, 4, 1, 5};
// 使用迭代器遍历set
for (auto it = mySet.begin(); it != mySet.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
11.3.2 添加元素
关联容器添加元素的方法:
-
insert:插入一个元素或一组元素。 -
使用
emplace(C++11):在容器中直接构造元素,避免不必要的拷贝或移动。
示例:向map插入元素:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap;
// 使用insert插入单个元素
myMap.insert(std::pair<int, std::string>(1, "one"));
myMap.insert(std::make_pair(2, "two"));
myMap.insert({3, "three"}); // C++11统一初始化
// 使用emplace插入单个元素
myMap.emplace(4, "four");
// 输出map内容
for (const auto &pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
示例:向set插入元素:
#include <iostream>
#include <set>
int main() {
std::set<int> mySet;
// 使用insert插入元素
mySet.insert(3);
mySet.insert(1);
mySet.insert(4);
mySet.insert(1); // 重复元素,不会被插入
// 输出set内容
for (const auto &elem : mySet) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
注意:
-
对于
map和set,键是唯一的,重复插入相同的键不会改变容器。 -
对于
multimap和multiset,键可以重复,插入相同的键会保留多个元素。
11.3.3 访问元素
访问关联容器中的元素:
-
通过键查找对应的值(对于
map和multimap)。 -
使用
find函数查找元素。 -
使用
[]运算符访问map中的元素(不适用于set、multimap、multiset)。
示例:使用find查找元素:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap = {
{1, "one"},
{2, "two"},
{3, "three"}
};
// 查找键为2的元素
auto it = myMap.find(2);
if (it != myMap.end()) {
std::cout << "Found: " << it->first << " -> " << it->second << std::endl;
} else {
std::cout << "Key 2 not found" << std::endl;
}
// 查找键为4的元素
it = myMap.find(4);
if (it != myMap.end()) {
std::cout << "Found: " << it->first << " -> " << it->second << std::endl;
} else {
std::cout << "Key 4 not found" << std::endl;
}
return 0;
}
示例:使用[]运算符访问map元素:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap = {
{1, "one"},
{2, "two"},
{3, "three"}
};
// 使用[]访问键为2的元素
std::cout << "Key 2: " << myMap[2] << std::endl;
// 使用[]访问键为4的元素,如果不存在则插入默认值
std::cout << "Key 4: " << myMap[4] << std::endl; // 插入键4,值为空字符串
// 输出map内容
for (const auto &pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
注意:
-
使用
[]运算符时,如果键不存在,会自动插入一个具有默认值的键值对(对于map和multimap,值的类型需要有默认构造函数)。 -
对于
set和multiset,不能使用[]运算符,因为它们只存储键。
11.3.4 删除元素
删除关联容器中的元素:
-
erase:通过迭代器、键或范围删除元素。
示例:使用erase通过迭代器删除元素:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap = {
{1, "one"},
{2, "two"},
{3, "three"}
};
// 找到键为2的元素
auto it = myMap.find(2);
if (it != myMap.end()) {
myMap.erase(it); // 通过迭代器删除
}
// 输出map内容
for (const auto &pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
示例:使用erase通过键删除元素:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap = {
{1, "one"},
{2, "two"},
{3, "three"}
};
// 通过键删除元素
size_t count = myMap.erase(2); // 返回删除的元素数量
std::cout << "Deleted " << count << " elements with key 2" << std::endl;
// 输出map内容
for (const auto &pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
示例:使用erase通过范围删除元素:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap = {
{1, "one"},
{2, "two"},
{3, "three"},
{4, "four"}
};
// 定义要删除的范围,例如删除键为2和3的元素
auto first = myMap.find(2);
auto last = myMap.find(4);
if (first != myMap.end() && last != myMap.end()) {
myMap.erase(first, last); // 删除从first到last之前的元素
}
// 输出map内容
for (const auto &pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
11.4 无序关联容器(简介)
无序关联容器(Unordered Associative Containers)基于哈希表实现,提供平均常数时间复杂度的查找、插入和删除操作。与有序关联容器不同,无序关联容器中的元素不按任何特定顺序存储。
常见的无序关联容器(C++11引入):
-
unordered_map<Key, Value>:键值对集合,键唯一,无序。 -
unordered_set<Key>:键的集合,键唯一,无序。 -
unordered_multimap<Key, Value>:键值对集合,键可以重复,无序。 -
unordered_multiset<Key>:键的集合,键可以重复,无序。
特点:
-
哈希表实现:使用哈希函数将键映射到桶(buckets),以实现快速查找。
-
无序性:元素不按任何特定顺序存储,访问顺序不确定。
-
性能:在平均情况下,查找、插入和删除操作的时间复杂度为O(1),但在最坏情况下可能为O(n)。
注意:无序关联容器的使用与有序关联容器类似,但由于其基于哈希表,需要提供哈希函数和相等比较函数(通常通过模板参数指定)。
示例(简要介绍,详细内容可参考C++ Primer或其他资源):
#include <iostream>
#include <unordered_map>
int main() {
std::unordered_map<int, std::string> myUnorderedMap = {
{1, "one"},
{2, "two"},
{3, "three"}
};
// 访问元素
std::cout << "Key 2: " << myUnorderedMap[2] << std::endl;
// 插入元素
myUnorderedMap[4] = "four";
// 遍历元素(顺序不确定)
for (const auto &pair : myUnorderedMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
总结:
-
有序关联容器(如
map、set):元素按键排序,查找效率高(O(log n)),适用于需要有序访问的场景。 -
无序关联容器(如
unordered_map、unordered_set):元素无序,查找效率高(平均O(1)),适用于不需要有序访问且追求更高性能的场景。
C++ Primer 第12章 动态内存(详细讲解)
12.1 动态内存与智能指针
12.1.1 为什么需要动态内存
动态内存(Dynamic Memory)是指在程序运行时根据需要分配和释放的内存空间。与静态内存(如全局变量、静态变量)和栈内存(如局部变量)不同,动态内存的生命周期由程序员显式控制,或者在现代C++中通过智能指针自动管理。
使用动态内存的常见场景:
-
运行时确定大小的数据结构:如动态数组、链表、树等,其大小在编译时无法确定,需要在运行时根据需求分配内存。
-
共享数据:多个对象或函数需要访问同一块内存区域,动态内存可以提供这种共享机制。
-
长时间存在的数据:需要在函数调用结束后仍然存在的数据,动态内存可以确保这些数据的生命周期超出函数的作用域。
传统动态内存管理的问题:
-
手动管理:使用
new和delete进行内存的分配和释放,容易导致内存泄漏(忘记释放内存)、悬挂指针(访问已释放的内存)和重复释放(多次释放同一块内存)等问题。 -
复杂性:手动管理动态内存增加了代码的复杂性和出错的可能性,特别是在大型项目中。
现代C++的解决方案:
-
智能指针:自动管理动态内存的生命周期,减少手动管理带来的风险,提高代码的安全性和可维护性。
12.1.2 智能指针概述
智能指针是C++标准库提供的类模板,用于自动管理动态分配的内存。它们通过重载指针操作符(如*和->)来模拟普通指针的行为,同时负责在适当的时候自动释放所管理的内存,防止内存泄漏。
C++11引入的智能指针类型:
-
std::unique_ptr:独占所有权的智能指针,确保同一时间只有一个unique_ptr可以指向特定的对象,防止多个指针管理同一块内存。 -
std::shared_ptr:共享所有权的智能指针,多个shared_ptr可以指向同一个对象,通过引用计数来管理对象的生命周期,当最后一个shared_ptr被销毁时,对象才会被释放。 -
std::weak_ptr:弱引用的智能指针,不增加引用计数,用于解决shared_ptr之间的循环引用问题。
C++14及以后引入的智能指针:
-
std::make_unique(C++14):用于创建std::unique_ptr的便捷函数。 -
std::make_shared(C++11):用于创建std::shared_ptr的便捷函数,通常比直接使用new更高效。
12.1.3 std::unique_ptr
std::unique_ptr 是一种独占所有权的智能指针,它确保同一时间只有一个unique_ptr可以指向特定的对象。当unique_ptr被销毁(例如离开作用域)时,它所管理的对象也会被自动删除。
主要特点:
-
独占所有权:同一时间只能有一个
unique_ptr指向特定的对象。 -
不可复制:
unique_ptr不能被复制,只能被移动(通过std::move)。 -
自动释放:当
unique_ptr被销毁时,它所管理的对象会被自动删除,防止内存泄漏。
使用示例:
#include <iostream>
#include <memory> // 包含智能指针的头文件
int main() {
// 创建一个unique_ptr,管理一个动态分配的int
std::unique_ptr<int> uptr(new int(10));
// 访问unique_ptr管理的对象
std::cout << "Value: " << *uptr << std::endl;
// unique_ptr不能被复制,只能被移动
// std::unique_ptr<int> uptr2 = uptr; // 错误:不能复制
std::unique_ptr<int> uptr2 = std::move(uptr); // 正确:通过移动语义转移所有权
if (!uptr) {
std::cout << "uptr is nullptr after move." << std::endl;
}
std::cout << "Value managed by uptr2: " << *uptr2 << std::endl;
// 当uptr2离开作用域时,它所管理的int会被自动删除
return 0;
}
创建unique_ptr的推荐方式(C++14及以上):
使用std::make_unique函数可以更安全、更高效地创建unique_ptr,避免直接使用new。
#include <iostream>
#include <memory>
int main() {
// 使用std::make_unique创建unique_ptr
std::unique_ptr<int> uptr = std::make_unique<int>(20);
std::cout << "Value: " << *uptr << std::endl;
// 当uptr离开作用域时,它所管理的int会被自动删除
return 0;
}
unique_ptr与数组:
std::unique_ptr也可以用来管理动态分配的数组,需要指定删除器为delete[],或者使用std::unique_ptr<T[]>的特化版本(C++11起支持)。
#include <iostream>
#include <memory>
int main() {
// 创建一个管理动态数组的unique_ptr
std::unique_ptr<int[]> uptrArray(new int[5]{1, 2, 3, 4, 5});
// 访问数组元素
for (int i = 0; i < 5; ++i) {
std::cout << uptrArray[i] << " "; // 使用[]操作符访问数组元素
}
std::cout << std::endl;
// 当uptrArray离开作用域时,它所管理的数组会被自动删除
return 0;
}
注意:
-
使用
std::unique_ptr管理数组时,C++11起支持std::unique_ptr<T[]>,可以直接使用[]操作符访问数组元素。 -
在C++14及以上,可以使用
std::make_unique<T[]>(n)来创建管理数组的unique_ptr。
#include <iostream>
#include <memory>
int main() {
// 使用std::make_unique创建管理数组的unique_ptr(C++14及以上)
std::unique_ptr<int[]> uptrArray = std::make_unique<int[]>(5);
for (int i = 0; i < 5; ++i) {
uptrArray[i] = i + 1;
}
for (int i = 0; i < 5; ++i) {
std::cout << uptrArray[i] << " ";
}
std::cout << std::endl;
return 0;
}
12.1.4 std::shared_ptr
std::shared_ptr 是一种共享所有权的智能指针,多个shared_ptr可以指向同一个对象,通过引用计数来管理对象的生命周期。当最后一个shared_ptr被销毁时,对象才会被自动删除。
主要特点:
-
共享所有权:多个
shared_ptr可以共享同一个对象的所有权。 -
引用计数:内部维护一个引用计数器,记录有多少个
shared_ptr指向同一个对象。 -
自动释放:当最后一个
shared_ptr被销毁时,引用计数降为0,对象会被自动删除。
使用示例:
#include <iostream>
#include <memory>
int main() {
// 创建一个shared_ptr,管理一个动态分配的int
std::shared_ptr<int> sptr1 = std::make_shared<int>(30);
{
// 创建另一个shared_ptr,指向同一个对象
std::shared_ptr<int> sptr2 = sptr1;
std::cout << "sptr1 use_count: " << sptr1.use_count() << std::endl; // 输出2
std::cout << "sptr2 use_count: " << sptr2.use_count() << std::endl; // 输出2
std::cout << "Value: " << *sptr1 << std::endl;
} // sptr2离开作用域,引用计数减1
std::cout << "sptr1 use_count: " << sptr1.use_count() << std::endl; // 输出1
// 当sptr1离开作用域时,引用计数降为0,对象会被自动删除
return 0;
}
创建shared_ptr的推荐方式:
使用std::make_shared函数可以更安全、更高效地创建shared_ptr,并且通常比直接使用new更高效,因为它一次性分配内存用于对象和控制块。
#include <iostream>
#include <memory>
int main() {
// 使用std::make_shared创建shared_ptr
std::shared_ptr<int> sptr = std::make_shared<int>(40);
std::cout << "Value: " << *sptr << std::endl;
std::cout << "Use count: " << sptr.use_count() << std::endl; // 输出1
// 当sptr离开作用域时,它所管理的int会被自动删除
return 0;
}
shared_ptr与数组:
std::shared_ptr也可以用来管理动态分配的数组,但需要指定删除器为delete[],因为std::shared_ptr<T>默认使用delete作为删除器。
#include <iostream>
#include <memory>
int main() {
// 创建一个管理动态数组的shared_ptr,指定删除器为delete[]
std::shared_ptr<int> sptrArray(new int[5]{1, 2, 3, 4, 5}, std::default_delete<int[]>());
// 访问数组元素
for (int i = 0; i < 5; ++i) {
std::cout << sptrArray.get()[i] << " "; // 使用get()获取原始指针,然后使用[]访问
}
std::cout << std::endl;
// 当sptrArray离开作用域时,它所管理的数组会被自动删除
return 0;
}
注意:
-
在C++17及以上,
std::shared_ptr<T[]>得到了更好的支持,可以更直接地管理动态数组。 -
使用
std::make_shared不支持管理数组,因此需要使用new并指定删除器。
12.1.5 std::weak_ptr
std::weak_ptr 是一种弱引用的智能指针,它不增加引用计数,用于解决shared_ptr之间的循环引用问题。weak_ptr不能直接访问所管理的对象,需要通过lock函数转换为shared_ptr来访问。
主要特点:
-
弱引用:不增加引用计数,不影响对象的生命周期。
-
解决循环引用:用于打破
shared_ptr之间的循环引用,防止内存泄漏。 -
不能直接访问对象:需要通过
lock函数获取一个shared_ptr来访问对象。
使用示例:
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
// 创建相互引用的shared_ptr,导致循环引用
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// 此时a和b的引用计数都为2,当main函数结束时,a和b的引用计数减为1,不会降为0,导致内存泄漏
// 使用weak_ptr解决循环引用
std::shared_ptr<A> a2 = std::make_shared<A>();
std::shared_ptr<B> b2 = std::make_shared<B>();
a2->b_ptr = b2;
b2->a_ptr = a2;
// 将其中一个shared_ptr改为weak_ptr
std::weak_ptr<A> weak_a = a2;
std::weak_ptr<B> weak_b = b2;
// 访问weak_ptr需要通过lock()函数
if (auto shared_a = weak_a.lock()) {
std::cout << "A is still alive" << std::endl;
} else {
std::cout << "A has been destroyed" << std::endl;
}
// 当a2和b2离开作用域时,引用计数降为0,对象会被自动删除
return 0;
}
注意:
-
在上述示例中,原始的
A和B类会导致循环引用,使得shared_ptr的引用计数无法降为0,导致内存泄漏。 -
通过引入
weak_ptr,可以打破循环引用,确保对象在不再需要时被正确释放。
更实际的循环引用解决方案:
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 使用weak_ptr代替shared_ptr
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // b_ptr是weak_ptr,不会增加引用计数
// 访问weak_ptr需要通过lock()函数
if (auto shared_a = b->a_ptr.lock()) {
std::cout << "A is still alive" << std::endl;
} else {
std::cout << "A has been destroyed" << std::endl;
}
// 当a和b离开作用域时,引用计数降为0,对象会被自动删除
return 0;
}
解释:
-
在类
B中,将a_ptr从std::shared_ptr<A>改为std::weak_ptr<A>,这样B对A的引用不会增加引用计数。 -
通过
weak_ptr的lock函数,可以安全地访问A对象,同时不会影响其生命周期。
12.1.6 选择合适的智能指针
选择智能指针的指导原则:
-
优先使用
std::unique_ptr:-
当资源的独占所有权是所需时,使用
std::unique_ptr。 -
unique_ptr更轻量,没有引用计数的开销,性能更高。
-
-
需要共享所有权时使用
std::shared_ptr:-
当多个对象需要共享同一资源的所有权时,使用
std::shared_ptr。 -
注意潜在的循环引用问题,必要时使用
std::weak_ptr来打破循环。
-
-
使用
std::weak_ptr解决循环引用:-
当
shared_ptr之间形成循环引用,导致对象无法被正确释放时,引入std::weak_ptr来打破循环。
-
-
优先使用
std::make_unique和std::make_shared:-
这些函数不仅更安全(例如,避免显式使用
new),而且在某些情况下更高效(例如,std::make_shared可以一次性分配内存用于对象和控制块)。
-
示例:使用std::make_unique和std::make_shared
#include <iostream>
#include <memory>
int main() {
// 使用std::make_unique创建unique_ptr
std::unique_ptr<int> uptr = std::make_unique<int>(50);
std::cout << "unique_ptr value: " << *uptr << std::endl;
// 使用std::make_shared创建shared_ptr
std::shared_ptr<int> sptr = std::make_shared<int>(60);
std::cout << "shared_ptr value: " << *sptr << std::endl;
std::cout << "Use count: " << sptr.use_count() << std::endl; // 输出1
{
std::shared_ptr<int> sptr2 = sptr;
std::cout << "Use count inside inner scope: " << sptr.use_count() << std::endl; // 输出2
} // sptr2离开作用域,引用计数减1
std::cout << "Use count outside inner scope: " << sptr.use_count() << std::endl; // 输出1
return 0;
}
12.2 动态数组
除了单个对象的动态内存管理,C++还支持动态数组的管理。动态数组的大小可以在运行时确定,并且可以使用智能指针或标准库容器(如std::vector)来管理。
12.2.1 使用new和delete管理动态数组
传统方法:
使用new[]分配动态数组,使用delete[]释放动态数组。
示例:
#include <iostream>
int main() {
// 动态分配一个包含5个int的数组
int* arr = new int[5]{1, 2, 3, 4, 5};
// 访问数组元素
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// 释放动态数组
delete[] arr;
return 0;
}
注意事项:
-
必须使用
delete[]来释放通过new[]分配的数组,使用delete会导致未定义行为。 -
忘记释放动态数组会导致内存泄漏。
-
手动管理动态数组容易出错,建议使用智能指针或标准库容器。
12.2.2 使用智能指针管理动态数组
C++11及以上,std::unique_ptr和std::shared_ptr可以用来管理动态数组,但需要指定正确的删除器或使用特化版本。
12.2.2.1 使用std::unique_ptr管理动态数组
std::unique_ptr<T[]>:
C++11起,std::unique_ptr提供了对动态数组的特化版本,可以直接管理动态数组,使用delete[]作为删除器。
示例:
#include <iostream>
#include <memory>
int main() {
// 使用std::unique_ptr管理动态数组
std::unique_ptr<int[]> uptrArray(new int[5]{10, 20, 30, 40, 50});
// 访问数组元素
for (int i = 0; i < 5; ++i) {
std::cout << uptrArray[i] << " "; // 使用[]操作符访问数组元素
}
std::cout << std::endl;
// 当uptrArray离开作用域时,它所管理的数组会被自动删除
return 0;
}
使用std::make_unique管理动态数组(C++14及以上):
std::make_unique不直接支持管理数组,但C++14起,std::make_unique<T[]>可用于创建管理动态数组的unique_ptr。
#include <iostream>
#include <memory>
int main() {
// 使用std::make_unique创建管理动态数组的unique_ptr(C++14及以上)
std::unique_ptr<int[]> uptrArray = std::make_unique<int[]>(5);
for (int i = 0; i < 5; ++i) {
uptrArray[i] = i + 1;
}
for (int i = 0; i < 5; ++i) {
std::cout << uptrArray[i] << " ";
}
std::cout << std::endl;
return 0;
}
12.2.2.2 使用std::shared_ptr管理动态数组
std::shared_ptr 默认使用delete作为删除器,不支持直接管理动态数组。要管理动态数组,需要指定删除器为delete[]。
示例:
#include <iostream>
#include <memory>
int main() {
// 使用std::shared_ptr管理动态数组,指定删除器为delete[]
std::shared_ptr<int> sptrArray(new int[5]{100, 200, 300, 400, 500}, std::default_delete<int[]>());
// 访问数组元素,需要使用get()获取原始指针
for (int i = 0; i < 5; ++i) {
std::cout << sptrArray.get()[i] << " ";
}
std::cout << std::endl;
// 当sptrArray离开作用域时,它所管理的数组会被自动删除
return 0;
}
注意:
-
使用
std::shared_ptr管理动态数组较为繁琐,需要手动指定删除器。 -
推荐使用
std::unique_ptr<T[]>来管理动态数组,因其更简洁和安全。
12.2.3 使用标准库容器std::vector
推荐方法:
尽管C++提供了动态数组的管理方式,但在大多数情况下,推荐使用标准库容器std::vector来管理动态数组。std::vector提供了动态大小的数组功能,并且自动管理内存,避免了手动使用new和delete带来的风险。
std::vector的主要优点:
-
自动内存管理:无需手动分配和释放内存,减少内存泄漏的风险。
-
动态大小:可以根据需要动态调整大小。
-
丰富的接口:提供了大量的成员函数,便于操作和管理元素。
-
安全性:相比原始指针和动态数组,
std::vector更安全,不易出错。
示例:
#include <iostream>
#include <vector>
int main() {
// 创建一个包含5个int的vector
std::vector<int> vec(5, 0); // 5个元素,初始化为0
// 访问和修改元素
for (int i = 0; i < 5; ++i) {
vec[i] = i + 1;
}
// 输出vector内容
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
// 动态添加元素
vec.push_back(6);
// 输出添加后的vector内容
for (const auto &v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
std::vector vs 动态数组:
-
std::vector:更安全、更方便,自动管理内存,推荐在大多数情况下使用。 -
动态数组(new/delete 或智能指针):在需要更底层控制或特定优化时使用,但需谨慎管理内存。
12.3 使用动态内存的陷阱与最佳实践
12.3.1 常见陷阱
-
内存泄漏:
-
原因:分配了内存但未释放,导致内存无法被再利用。
-
解决方法:使用智能指针或标准库容器,确保内存自动释放。
-
-
悬挂指针(Dangling Pointers):
-
原因:访问已释放的内存。
-
解决方法:释放内存后将指针置为
nullptr,避免访问已释放的内存。
-
-
重复释放:
-
原因:多次释放同一块内存。
-
解决方法:确保每块动态内存只被释放一次,使用智能指针自动管理。
-
-
未初始化的指针:
-
原因:使用未初始化的指针进行操作。
-
解决方法:始终初始化指针,使用智能指针或确保指针指向有效的内存。
-
-
内存碎片:
-
原因:频繁分配和释放不同大小的内存块,导致内存利用率下降。
-
解决方法:合理管理内存分配,使用内存池或标准库容器减少频繁分配。
-
12.3.2 最佳实践
-
优先使用智能指针:
-
使用
std::unique_ptr和std::shared_ptr自动管理动态内存,减少手动管理带来的风险。
-
-
优先使用标准库容器:
-
如
std::vector、std::list等,自动管理内存,提供丰富的功能和更高的安全性。
-
-
避免裸指针的滥用:
-
尽量减少使用裸指针(raw pointers),特别是在管理动态内存时。如果必须使用,确保正确管理其生命周期。
-
-
使用
std::make_unique和std::make_shared:-
这些函数不仅更安全,而且在某些情况下更高效,推荐在创建智能指针时使用。
-
-
明确所有权:
-
理解并明确每个动态内存资源的所有权,避免多个部分尝试管理同一块内存。
-
-
小心循环引用:
-
当使用
std::shared_ptr时,注意潜在的循环引用问题,必要时使用std::weak_ptr来打破循环。
-
-
初始化动态内存:
-
动态分配的内存应进行适当的初始化,避免未定义行为。
-
-
使用RAII原则:
-
资源获取即初始化(Resource Acquisition Is Initialization),通过对象的生命周期管理资源,如使用智能指针。
-
12.4 动态内存管理的高级话题
12.4.1 自定义删除器
智能指针允许指定自定义删除器,用于在释放资源时执行特定的清理操作。这在管理非内存资源(如文件句柄、网络连接等)时特别有用。
示例:使用自定义删除器管理文件指针
#include <iostream>
#include <memory>
#include <cstdio>
// 自定义删除器,用于关闭文件
void file_deleter(FILE* fp) {
if (fp) {
std::cout << "Closing file." << std::endl;
fclose(fp);
}
}
int main() {
// 使用unique_ptr管理文件指针,指定自定义删除器
std::unique_ptr<FILE, decltype(&file_deleter)> filePtr(fopen("example.txt", "r"), file_deleter);
if (filePtr) {
char buffer[100];
while (fgets(buffer, sizeof(buffer), filePtr.get())) {
std::cout << buffer;
}
} else {
std::cerr << "Failed to open file." << std::endl;
}
// 文件会在filePtr离开作用域时自动关闭
return 0;
}
解释:
-
std::unique_ptr的第二个模板参数指定了自定义删除器的类型。 -
decltype(&file_deleter)获取了删除器函数的类型。 -
当
filePtr离开作用域时,自定义删除器file_deleter会被调用,关闭文件。
12.4.2 使用std::allocator
std::allocator 是C++标准库提供的模板类,用于分配和释放内存,但不初始化或销毁对象。它提供了更低级别的内存管理控制,适用于需要精细控制内存分配和对象构造的场景。
示例:使用std::allocator分配和构造对象
#include <iostream>
#include <memory> // 包含std::allocator
int main() {
// 创建一个allocator,用于分配int
std::allocator<int> alloc;
// 分配可以存储5个int的内存
int* p = alloc.allocate(5);
// 构造对象
for (int i = 0; i < 5; ++i) {
alloc.construct(p + i, i + 1); // 在分配的内存上构造int对象
}
// 访问对象
for (int i = 0; i < 5; ++i) {
std::cout << p[i] << " ";
}
std::cout << std::endl;
// 销毁对象
for (int i = 0; i < 5; ++i) {
alloc.destroy(p + i);
}
// 释放内存
alloc.deallocate(p, 5);
return 0;
}
解释:
-
std::allocator<int> alloc;创建一个用于分配int类型内存的分配器。 -
alloc.allocate(5);分配可以存储5个int的内存,返回指向该内存的指针。 -
alloc.construct(p + i, i + 1);在分配的内存位置上构造int对象,并初始化为i + 1。 -
alloc.destroy(p + i);调用对象的析构函数(对于int类型,实际上不执行任何操作)。 -
alloc.deallocate(p, 5);释放之前分配的内存。
注意:
-
使用
std::allocator需要手动管理对象的构造和析构,适用于需要更灵活内存管理的场景。 -
在大多数情况下,推荐使用智能指针或标准库容器,它们自动管理内存和对象的生命周期。
总结
第12章《动态内存》主要介绍了C++中动态内存的管理,包括传统的手动管理方式(使用new和delete)和现代C++中推荐的智能指针(如std::unique_ptr、std::shared_ptr和std::weak_ptr)。以下是本章的关键要点:
-
动态内存:
-
动态内存是在程序运行时根据需要分配和释放的内存,与静态内存和栈内存不同。
-
传统上使用
new和delete进行动态内存管理,但容易导致内存泄漏、悬挂指针和重复释放等问题。
-
-
智能指针:
-
std::unique_ptr:独占所有权的智能指针,确保同一时间只有一个unique_ptr指向特定的对象,自动释放内存,防止内存泄漏。推荐在需要独占所有权时使用。 -
std::shared_ptr:共享所有权的智能指针,多个shared_ptr可以共享同一个对象的所有权,通过引用计数管理对象的生命周期。适用于需要共享所有权的场景,但需注意循环引用问题。 -
std::weak_ptr:弱引用的智能指针,不增加引用计数,用于解决shared_ptr之间的循环引用问题。
-
-
智能指针的使用:
-
推荐使用
std::make_unique和std::make_shared来创建智能指针,这些函数更安全、更高效。 -
std::unique_ptr可以管理动态数组(使用std::unique_ptr<T[]>或std::make_unique<T[]>(n))。 -
std::shared_ptr管理动态数组需要指定删除器为delete[],或者使用std::shared_ptr<T[]>(C++17及以上支持更好)。
-
-
动态数组:
-
除了单个对象,C++还支持动态数组的管理,可以使用
new[]和delete[],或通过智能指针(如std::unique_ptr<T[]>)管理。 -
推荐使用标准库容器
std::vector来管理动态数组,因其自动管理内存,提供丰富的功能,且更安全、更方便。
-
-
陷阱与最佳实践:
-
常见陷阱包括内存泄漏、悬挂指针、重复释放和未初始化的指针。
-
最佳实践包括优先使用智能指针和标准库容器,明确所有权,避免裸指针的滥用,使用RAII原则,以及小心循环引用。
-
-
高级话题:
-
自定义删除器:允许智能指针在释放资源时执行特定的清理操作,适用于管理非内存资源。
-
std::allocator:提供更低级别的内存管理控制,适用于需要精细控制内存分配和对象构造的场景。
-
通过理解和正确使用动态内存管理技术,特别是智能指针,可以显著提高C++程序的安全性、可维护性和性能。在现代C++编程中,推荐优先使用智能指针和标准库容器,以减少手动管理动态内存带来的风险和复杂性。
4万+

被折叠的 条评论
为什么被折叠?



