当前位置: 首页 > news >正文

【经典书籍】C++ Primer 第10到12章精华讲解

C++ Primer 第10到12章非常详细讲解

第10章 泛型算法

10.1 概述

10.1.1 什么是泛型算法

泛型算法(Generic Algorithms)是C++标准库提供的一系列算法,它们可以用于不同类型的容器(如vectorlistdeque等)和数组。这些算法通过迭代器来访问容器中的元素,而不是直接操作容器本身,从而实现了对多种数据结构的通用操作。

关键特点

  • 泛型性:算法不依赖于特定的容器类型,通过迭代器实现对不同容器的操作。

  • 不修改容器结构:大多数泛型算法不直接改变容器的结构(如不添加或删除元素),而是对元素进行操作。

  • 基于迭代器:通过迭代器访问和操作元素,使得算法具有高度的灵活性和通用性。

  • 标准库提供:包含在<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)是一种特殊的迭代器,用于在容器的特定位置插入元素,而不是覆盖现有元素。它们常用于需要在不覆盖元素的情况下向容器添加元素的算法。

三种插入迭代器

  1. back_inserter:创建一个使用push_back的迭代器,适用于支持push_back的容器(如vectordequelist)。

  2. front_inserter:创建一个使用push_front的迭代器,适用于支持push_front的容器(如listdeque)。

  3. 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::findstd::countstd::accumulate

  • 写算法:如std::fillstd::generatestd::transform

  • 排序和重排算法:如std::sortstd::stable_sortstd::unique

  • 数值算法:如std::accumulate(在<numeric>中)、std::inner_productstd::partial_sum

10.5.2 算法的通用性

泛型算法的通用性体现在它们通过迭代器与容器交互,而不关心容器的具体类型。这使得相同的算法可以应用于不同类型的容器,如vectorlistdeque等。

关键点

  • 迭代器抽象:算法通过迭代器访问元素,而不直接操作容器。

  • 参数化行为:通过传递函数对象或Lambda表达式,算法的行为可以灵活定制。

10.6 特定容器算法

虽然大多数泛型算法适用于所有标准容器,但某些容器(特别是listforward_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;
}

注意

  • listforward_list:这些容器不支持随机访问迭代器,因此某些泛型算法(如std::sort)不能直接使用。它们提供了自己的成员函数来实现类似功能,通常更高效。

  • 选择合适的方法:在可能的情况下,优先使用容器专属的成员函数,以获得更好的性能和更简洁的代码。


第11章 关联容器

11.1 概述

11.1.1 什么是关联容器

关联容器(Associative Containers)是C++标准库提供的另一种类型的容器,它们存储键值对(key-value pairs),并根据键(key)进行排序和快速查找。与顺序容器(如vectorlist)不同,关联容器中的元素是按照键的顺序自动排序的,这使得查找操作更加高效。

主要特点

  • 基于键的排序:元素根据键自动排序,通常使用红黑树(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 关联容器概述

关联容器的主要类别

  1. map

    • 描述:存储键值对(key-value pairs),每个键在容器中是唯一的,按键排序。

    • 用途:用于快速查找与特定键相关联的值,如字典。

  2. set

    • 描述:存储唯一的键,按键排序。

    • 用途:用于存储唯一的元素集合,快速查找元素是否存在。

  3. multimap

    • 描述:存储键值对,键可以重复,按键排序。

    • 用途:用于一个键对应多个值的情况,如一个作者对应多本书。

  4. multiset

    • 描述:存储键,键可以重复,按键排序。

    • 用途:用于存储可以重复的元素集合,保持排序。

关联容器与顺序容器的对比

  • 顺序容器(如vectorlist):元素按插入顺序存储,不自动排序,查找效率较低(通常为O(n))。

  • 关联容器:元素按键的顺序自动排序,查找效率高(通常为O(log n))。

11.3 关联容器操作

11.3.1 关联容器迭代器

关联容器的迭代器允许遍历容器中的元素。对于mapmultimap,迭代器解引用后得到的是一个键值对(通常是一个pair<const Key, Value>);对于setmultiset,迭代器解引用后得到的是键。

示例:遍历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;
}

注意

  • 对于mapset,键是唯一的,重复插入相同的键不会改变容器。

  • 对于multimapmultiset,键可以重复,插入相同的键会保留多个元素。

11.3.3 访问元素

访问关联容器中的元素

  • 通过键查找对应的值(对于mapmultimap)。

  • 使用find函数查找元素。

  • 使用[]运算符访问map中的元素(不适用于setmultimapmultiset)。

示例:使用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;
}

注意

  • 使用[]运算符时,如果键不存在,会自动插入一个具有默认值的键值对(对于mapmultimap,值的类型需要有默认构造函数)。

  • 对于setmultiset,不能使用[]运算符,因为它们只存储键。

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;
}

总结

  • 有序关联容器(如mapset):元素按键排序,查找效率高(O(log n)),适用于需要有序访问的场景。

  • 无序关联容器(如unordered_mapunordered_set):元素无序,查找效率高(平均O(1)),适用于不需要有序访问且追求更高性能的场景。


C++ Primer 第12章 动态内存(详细讲解)

12.1 动态内存与智能指针

12.1.1 为什么需要动态内存

动态内存(Dynamic Memory)是指在程序运行时根据需要分配和释放的内存空间。与静态内存(如全局变量、静态变量)和栈内存(如局部变量)不同,动态内存的生命周期由程序员显式控制,或者在现代C++中通过智能指针自动管理。

使用动态内存的常见场景

  1. 运行时确定大小的数据结构:如动态数组、链表、树等,其大小在编译时无法确定,需要在运行时根据需求分配内存。

  2. 共享数据:多个对象或函数需要访问同一块内存区域,动态内存可以提供这种共享机制。

  3. 长时间存在的数据:需要在函数调用结束后仍然存在的数据,动态内存可以确保这些数据的生命周期超出函数的作用域。

传统动态内存管理的问题

  • 手动管理:使用newdelete进行内存的分配和释放,容易导致内存泄漏(忘记释放内存)、悬挂指针(访问已释放的内存)和重复释放(多次释放同一块内存)等问题。

  • 复杂性:手动管理动态内存增加了代码的复杂性和出错的可能性,特别是在大型项目中。

现代C++的解决方案

  • 智能指针:自动管理动态内存的生命周期,减少手动管理带来的风险,提高代码的安全性和可维护性。

12.1.2 智能指针概述

智能指针是C++标准库提供的类模板,用于自动管理动态分配的内存。它们通过重载指针操作符(如*->)来模拟普通指针的行为,同时负责在适当的时候自动释放所管理的内存,防止内存泄漏。

C++11引入的智能指针类型

  1. std::unique_ptr:独占所有权的智能指针,确保同一时间只有一个unique_ptr可以指向特定的对象,防止多个指针管理同一块内存。

  2. std::shared_ptr:共享所有权的智能指针,多个shared_ptr可以指向同一个对象,通过引用计数来管理对象的生命周期,当最后一个shared_ptr被销毁时,对象才会被释放。

  3. 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;
}

注意

  • 在上述示例中,原始的AB类会导致循环引用,使得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_ptrstd::shared_ptr<A>改为std::weak_ptr<A>,这样BA的引用不会增加引用计数。

  • 通过weak_ptrlock函数,可以安全地访问A对象,同时不会影响其生命周期。

12.1.6 选择合适的智能指针

选择智能指针的指导原则

  1. 优先使用std::unique_ptr

    • 当资源的独占所有权是所需时,使用std::unique_ptr

    • unique_ptr更轻量,没有引用计数的开销,性能更高。

  2. 需要共享所有权时使用std::shared_ptr

    • 当多个对象需要共享同一资源的所有权时,使用std::shared_ptr

    • 注意潜在的循环引用问题,必要时使用std::weak_ptr来打破循环。

  3. 使用std::weak_ptr解决循环引用

    • shared_ptr之间形成循环引用,导致对象无法被正确释放时,引入std::weak_ptr来打破循环。

  4. 优先使用std::make_uniquestd::make_shared

    • 这些函数不仅更安全(例如,避免显式使用new),而且在某些情况下更高效(例如,std::make_shared可以一次性分配内存用于对象和控制块)。

示例:使用std::make_uniquestd::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 使用newdelete管理动态数组

传统方法

使用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_ptrstd::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提供了动态大小的数组功能,并且自动管理内存,避免了手动使用newdelete带来的风险。

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 常见陷阱

  1. 内存泄漏

    • 原因:分配了内存但未释放,导致内存无法被再利用。

    • 解决方法:使用智能指针或标准库容器,确保内存自动释放。

  2. 悬挂指针(Dangling Pointers)

    • 原因:访问已释放的内存。

    • 解决方法:释放内存后将指针置为nullptr,避免访问已释放的内存。

  3. 重复释放

    • 原因:多次释放同一块内存。

    • 解决方法:确保每块动态内存只被释放一次,使用智能指针自动管理。

  4. 未初始化的指针

    • 原因:使用未初始化的指针进行操作。

    • 解决方法:始终初始化指针,使用智能指针或确保指针指向有效的内存。

  5. 内存碎片

    • 原因:频繁分配和释放不同大小的内存块,导致内存利用率下降。

    • 解决方法:合理管理内存分配,使用内存池或标准库容器减少频繁分配。

12.3.2 最佳实践

  1. 优先使用智能指针

    • 使用std::unique_ptrstd::shared_ptr自动管理动态内存,减少手动管理带来的风险。

  2. 优先使用标准库容器

    • std::vectorstd::list等,自动管理内存,提供丰富的功能和更高的安全性。

  3. 避免裸指针的滥用

    • 尽量减少使用裸指针(raw pointers),特别是在管理动态内存时。如果必须使用,确保正确管理其生命周期。

  4. 使用std::make_uniquestd::make_shared

    • 这些函数不仅更安全,而且在某些情况下更高效,推荐在创建智能指针时使用。

  5. 明确所有权

    • 理解并明确每个动态内存资源的所有权,避免多个部分尝试管理同一块内存。

  6. 小心循环引用

    • 当使用std::shared_ptr时,注意潜在的循环引用问题,必要时使用std::weak_ptr来打破循环。

  7. 初始化动态内存

    • 动态分配的内存应进行适当的初始化,避免未定义行为。

  8. 使用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++中动态内存的管理,包括传统的手动管理方式(使用newdelete)和现代C++中推荐的智能指针(如std::unique_ptrstd::shared_ptrstd::weak_ptr)。以下是本章的关键要点:

  1. 动态内存

    • 动态内存是在程序运行时根据需要分配和释放的内存,与静态内存和栈内存不同。

    • 传统上使用newdelete进行动态内存管理,但容易导致内存泄漏、悬挂指针和重复释放等问题。

  2. 智能指针

    • std::unique_ptr:独占所有权的智能指针,确保同一时间只有一个unique_ptr指向特定的对象,自动释放内存,防止内存泄漏。推荐在需要独占所有权时使用。

    • std::shared_ptr:共享所有权的智能指针,多个shared_ptr可以共享同一个对象的所有权,通过引用计数管理对象的生命周期。适用于需要共享所有权的场景,但需注意循环引用问题。

    • std::weak_ptr:弱引用的智能指针,不增加引用计数,用于解决shared_ptr之间的循环引用问题。

  3. 智能指针的使用

    • 推荐使用std::make_uniquestd::make_shared来创建智能指针,这些函数更安全、更高效。

    • std::unique_ptr可以管理动态数组(使用std::unique_ptr<T[]>std::make_unique<T[]>(n))。

    • std::shared_ptr管理动态数组需要指定删除器为delete[],或者使用std::shared_ptr<T[]>(C++17及以上支持更好)。

  4. 动态数组

    • 除了单个对象,C++还支持动态数组的管理,可以使用new[]delete[],或通过智能指针(如std::unique_ptr<T[]>)管理。

    • 推荐使用标准库容器std::vector来管理动态数组,因其自动管理内存,提供丰富的功能,且更安全、更方便。

  5. 陷阱与最佳实践

    • 常见陷阱包括内存泄漏、悬挂指针、重复释放和未初始化的指针。

    • 最佳实践包括优先使用智能指针和标准库容器,明确所有权,避免裸指针的滥用,使用RAII原则,以及小心循环引用。

  6. 高级话题

    • 自定义删除器:允许智能指针在释放资源时执行特定的清理操作,适用于管理非内存资源。

    • std::allocator:提供更低级别的内存管理控制,适用于需要精细控制内存分配和对象构造的场景。

通过理解和正确使用动态内存管理技术,特别是智能指针,可以显著提高C++程序的安全性、可维护性和性能。在现代C++编程中,推荐优先使用智能指针和标准库容器,以减少手动管理动态内存带来的风险和复杂性。

 

http://www.dtcms.com/a/503559.html

相关文章:

  • 前端数据存储localStorage、sessionStorage 与 Cookie
  • 电影网站制作有哪些做微信小游戏的网站
  • Git从入门到精通教程
  • 课程视频网站建设的必要性色母图片
  • GEO内容更新与迭代策略:趋势话题的快速响应机制
  • 【Spring】Spring事务和事务传播机制
  • php网站开发源码网站开发部门结构
  • 03-流程控制语句-导读
  • MATLAB基于混合算法改进灰色模型的装备故障预测
  • Next.JS环境搭建,对接Rust的RESTful API
  • 目前流行的网站分辨率做多大自己做网站需要备份么
  • NativeScript-Vue 开发指南:直接使用 Vue构建原生移动应用
  • 珠海市横琴建设局网站html网站自带字体怎么做
  • 看汽车图片的网站可以做壁纸差异基因做聚类分析网站
  • 了解制造过程中的BOM类型
  • 小九源码-springboot088-宾馆客房管理系统
  • 数字芯片的版图和制造--被称为3极管的晶体管却有4极!
  • 专门做优惠劵的网站专业的销售网站
  • 从0到1学习Qt -- 内存管理与乱码问题
  • html`<mark>`
  • C++ stack 和 queue
  • 洛谷 P1035:[NOIP 2002 普及组] 级数求和 ← double
  • C程序中的大规模程序设计:从模块化到抽象数据类型
  • 响应式网站高度如何计算seo自动点击排名
  • 企业网站引导页模板江西门户网站建设
  • Prim 算法和 Kruskal 算法应用场景
  • 雷电模拟器环境配置
  • 南沙移动网站建设中元建设集团网站
  • 公司网站百度推广wordpress没中文插件
  • 手写Spring第7弹:Spring IoC容器深度解析:XML配置的完整指南