【经典书籍】C++ Primer 第10到12章精华讲解
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());// 输出排序后的vectorfor (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查找元素3auto 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将所有元素填充为10std::fill(vec.begin(), vec.end(), 10);// 输出填充后的vectorfor (const auto &v : vec) {std::cout << v << " ";}std::cout << std::endl;// 使用std::transform将每个元素乘以2std::transform(vec.begin(), vec.end(), vec.begin(), [](int i) { return i * 2; });// 输出变换后的vectorfor (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());// 输出排序后的vectorfor (const auto &v : vec) {std::cout << v << " ";}std::cout << std::endl;// 使用std::reverse反转顺序std::reverse(vec.begin(), vec.end());// 输出反转后的vectorfor (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()); // 擦除重复元素后的多余位置// 输出去重后的vectorfor (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;});// 输出降序排序后的vectorfor (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>());// 输出降序排序后的vectorfor (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));// 输出destfor (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);// 输出vecfor (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读取intstd::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::coutstd::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};// 使用反向迭代器逆序输出vectorstd::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());// 输出反转后的vectorfor (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();// 输出排序后的listfor (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,值为stringstd::map<int, std::string> myMap;// 定义一个set,键为intstd::set<int> mySet;return 0;
}
初始化关联容器:
可以使用初始化列表、范围初始化或其他构造函数进行初始化。
示例:
#include <iostream>
#include <map>
#include <set>int main() {// 使用初始化列表初始化mapstd::map<int, std::string> myMap = {{1, "one"},{2, "two"},{3, "three"}};// 使用初始化列表初始化setstd::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"}};// 使用迭代器遍历mapfor (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};// 使用迭代器遍历setfor (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,管理一个动态分配的intstd::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_ptrstd::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_ptrstd::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,管理一个动态分配的intstd::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; // 输出2std::cout << "sptr2 use_count: " << sptr2.use_count() << std::endl; // 输出2std::cout << "Value: " << *sptr1 << std::endl;} // sptr2离开作用域,引用计数减1std::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_ptrstd::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_ptrstd::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_ptrstd::unique_ptr<int> uptr = std::make_unique<int>(50);std::cout << "unique_ptr value: " << *uptr << std::endl;// 使用std::make_shared创建shared_ptrstd::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离开作用域,引用计数减1std::cout << "Use count outside inner scope: " << sptr.use_count() << std::endl; // 输出1return 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的vectorstd::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::allocatorint main() {// 创建一个allocator,用于分配intstd::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++编程中,推荐优先使用智能指针和标准库容器,以减少手动管理动态内存带来的风险和复杂性。