1. C++vector原理?resize和reserve的区别是什么?size和capacity的区别?
C++中的 std::vector 是一个动态数组,能够存储可变大小的元素集合。它提供了许多方便的操作,允许开发者以类似数组的方式访问元素,同时能够自动管理内存。
- std::vector 的基本原理
- 动态数组:std::vector 在内部维护一个可动态调整大小的数组。
- 内存管理:当元素数量超出当前容量(capacity)时,vector 会重新分配内存,通常会扩展到原来的两倍,并将现有元素移动到新的内存地址中。
- 随机访问:vector 提供随机访问特性,可以用下标直接访问任意元素。
- size 和 capacity 的区别
- size:返回当前存储元素的数量。可以通过 vector.size() 获取。
- capacity:返回当前分配的内存容量,也就是在不再进行重新分配的情况下,vector 可以容纳的元素数量。可以通过 vector.capacity() 获取。
std::vector<int> vec = {1, 2, 3};
std::cout << "Size: " << vec.size() << std::endl; // 输出 3
std::cout << "Capacity: " << vec.capacity() << std::endl; // 输出 3 或更高的值
vec.push_back(4); // 可能导致重新分配
std::cout << "Size: " << vec.size() << std::endl; // 输出 4
std::cout << "Capacity: " << vec.capacity() << std::endl; // 有可能增加
- resize 和 reserve 的区别
resize(size_type count):
- 作用:改变 vector 的大小。若 count 增加,新的元素将使用默认构造(如果有)或者需提供的值初始化。若 count 减少,则会去除尾部元素。
- 例子:
std::vector<int> vec = {1, 2, 3};
vec.resize(5, 0); // vec 变为 {1, 2, 3, 0, 0}
vec.resize(2); // vec 变为 {1, 2}
reserve(size_type new_cap):
- 作用:更改 vector 的容量,但不改变其大小。仅会分配内存,以允许在未来的操作中容纳更多的元素,避免频繁的内存重新分配。
- 例子:
std::vector<int> vec;
vec.reserve(10); // 现在 capacity 至少为 10,但 size 仍然是 0
总结
- size 表示 vector 中当前的元素数量。
- capacity 是实际可容纳的元素数量。
- resize 用于更改 vector 的大小并初始化新元素。
- reserve 用于增加 vector 的容量以提高性能,但不改变逻辑大小。
通过合理利用这两个函数,可以有效地管理动态数组的内存使用,优化性能。
2. C++deque的原理?它的内部是如何实现的?
C++中的 std::deque(双端队列)是一个能够在两端高效插入和删除的序列容器。与 std::vector 不同,deque 允许在两端进行快速操作,适用于需要频繁进行插入和删除的场景。
- std::deque 的基本原理
- 双端队列:std::deque 允许在两端(头部和尾部)插入和删除元素,同时支持随机访问。
- 动态存储:deque 在内部使用多个动态数组(或块)来存储元素。这允许它在不同的块中动态分配内存并使得插入和删除操作更高效。
- 内部实现
-
分段存储:deque 的实现通常使用一个指针数组(或块数组),每个指针指向存储一块元素的内存。这种结构使得在 deque 的两端进行插入和删除变得高效,因为可以在块的边缘操作而不需要移动整个数组。
-
容量管理:deque 维护一个前哨块和一个后哨块,这两个块分别用于存储队列的头部和尾部,这样可以在两端进行有效的添加和删除,同时保持对中间元素的访问能力。
-
内存分配:deque 会预分配一些块,一旦块中元素达到一定数量,就会分配新的块。其具体实现可能依赖于编译器和库的实现,但基本思路是通过维护一个块池来节省内存和缩短操作时间。
- 性能特点
- 插入和删除操作:在头部或尾部插入和删除元素的复杂度为 O(1)。因为只需改变指针而不需要移动元素。
- 随机访问:虽然 deque 支持随机访问,复杂度为 O(1),但由于它是分段存储的,连续性并不如 vector 高,因此,随机访问可能相对较慢。
- 内存使用:相较于 vector,deque 的内存使用更加灵活,但可能会导致较多的内存分配和碎片。
- 总结
- std::deque 是一个双端队列,允许在两端进行高效的插入与删除操作。
- 它的内部实现基于多个动态数组(或块),使用指针数组管理存储块。
- 插入和删除操作是 O(1),但由于分段存储,随机访问性能不如 std::vector。
通过这些特性,std::deque 在适用场景上提供了比 vector 和其他容器更加灵活的选择。
3. C++中map和unordered_map的区别?分别在什么场景下使用?
在 C++ 中,std::map 和 std::unordered_map 是两种主要的关联容器。它们都用于存储键值对,但在实现和性能特征上有一定的区别。
- 基本区别
存储结构:
std::map:
- 基于红黑树(自平衡的二叉搜索树)。
- 自动按照键的顺序保存元素。
std::unordered_map:
- 基于哈希表。
- 插入顺序不确定,元素没有特定的顺序。
访问时间复杂度:
std::map:
- 查找、插入和删除操作的复杂度为 O(log n)。
std::unordered_map:
- 查找、插入和删除操作的平均复杂度为 O(1),最坏情况下为 O(n)(当哈希冲突严重或哈希表需要扩展时)。
键的要求:
std::map:
- 键必须支持 < 运算符(或者提供自定义比较器)。
std::unordered_map:
- 键必须支持哈希函数(默认使用 std::hash)和相等比较。
- 使用场景
使用 std::map 的场景:
- 需要维护一个键的有序集合,方便按顺序遍历。
- 需要频繁进行范围查询,即查找某一范围内的键(例如,查找键大于某个值的所有键)。
- 键的顺序非常重要或与其他逻辑相关,例如实现一个优先队列或经典的范围查询。
使用 std::unordered_map 的场景:
- 在查找、插入或删除操作的性能要求非常高时(尤其是在大量数据时)。
- 对键的顺序没有要求。
- 需要快速访问数据,例如常量时间的查找、更新。
- 示例代码
#include <iostream>
#include <map>
#include <unordered_map> int main() { // 使用 std::map std::map<int, std::string> ordered_map; ordered_map[3] = "three"; ordered_map[1] = "one"; ordered_map[2] = "two"; // 按照键的顺序输出 for (const auto& pair : ordered_map) { std::cout << pair.first << ": " << pair.second << std::endl; } // 使用 std::unordered_map std::unordered_map<int, std::string> unordered_map; unordered_map[3] = "three"; unordered_map[1] = "one"; unordered_map[2] = "two"; // 不确保顺序 for (const auto& pair : unordered_map) { std::cout << pair.first << ": " << pair.second << std::endl; } return 0;
}
- 总结
选择 std::map 还是 std::unordered_map 取决于具体的需求:
- 如果需要有序存储并支持范围查询,使用 std::map。
- 如果对性能和无序性有更高的需求,选择 std::unordered_map。
通过了解这两者的区别,能够更合理地选择合适的数据结构,以满足不同的应用场景需求。
4. C++中list的使用场景
在 C++ 中,std::list 是一个双向链表容器,提供高效的插入和删除操作。以下是 std::list 的一些使用场景及其优势和劣势:
使用场景
- 频繁的插入和删除操作:
- 当你需要在序列的中间位置频繁地插入或删除元素时,std::list 比其他容器(如 std::vector 或 std::deque)更高效,因为它只需要更新指针,而不需要移动其余元素。
- 不需要随机访问:
- 如果你不需要通过索引访问元素(例如,使用 operator[]),那么使用 std::list 是一个不错的选择。访问元素的效率是 O(n),所以如果访问次数很少而插入和删除很频繁,使用 std::list 是合适的。
- 需要稳定的迭代器:
- 在 std::list 中,迭代器在插入和删除操作后不会失效。这意味着在遍历时可以安全地插入或删除节点,而不会影响其他迭代器。
- 实现队列和堆栈:
- 由于 std::list 提供了高效的前端和后端插入/删除功能,因此可以用作队列(FIFO)和堆栈(LIFO)的实现。
优势
- 高效的插入和删除:
- 在任意位置进行插入和删除操作的时间复杂度为 O(1),只需修改指针。
- 不需要移动元素:
- 插入和删除操作不会影响其他元素的存储位置,因此在数据频繁变动的情况下性能较好。
劣势
- 内存开销:
- 由于每个元素需要存储两个指针(前驱和后驱),std::list 的内存开销相比 std::vector 更大。
- 随机访问性能差:
- 访问元素的时间复杂度为 O(n),因此不适合需要频繁随机访问的应用场景。
示例代码
以下是一个简单的使用 std::list 的示例:
#include <iostream>
#include <list> int main() { std::list<int> myList; // 插入元素 myList.push_back(1); // 在末尾插入 myList.push_front(0); // 在开头插入 myList.insert(++myList.begin(), 2); // 在指定位置插入 // 遍历输出 std::cout << "List elements: "; for (const auto& elem : myList) { std::cout << elem << " "; } std::cout << std::endl; // 删除元素 myList.remove(1); // 删除值为 1 的元素 // 再次遍历输出 std::cout << "After removal: "; for (const auto& elem : myList) { std::cout << elem << " "; } std::cout << std::endl; return 0;
}
总结
std::list 适合于频繁进行插入和删除的场合,但不适合频繁需要随机访问的场合。选择合适的容器可以提高程序的性能和效率。
5.C++中为什么要使用std::array?它有什么优点?
std::array 是 C++ 标准库中提供的一种固定大小的数组封装,具有若干优点,使它在某些情况下比传统的C风格数组更具优势:
-
类型安全:std::array 是一个模板类,它能够确保数组元素的类型得到正确管理,避免类型不匹配的问题。
-
固定大小:与 C 风格的数组相比,std::array 的大小是在编译时确定的,确保数组大小的一致性和可控性。
-
封装:std::array 提供了类的方法来处理数组,例如大小获取 (size())、访问元素 (at() 和 operator[]),提升了代码的可读性和可维护性。
-
与 STL 兼容性:std::array 兼容 C++ 标准模板库的算法,能够直接与 STL 算法一起使用,而 C 风格数组则不具备这种灵活性。
-
更好的功能:std::array 支持常用的 STL 功能,例如复制、比较、排序等,使其在管理数组时更为方便。
-
重载的运算符:std::array 支持常用的运算符重载,允许数组对象之间的比较和赋值操作。
-
避免内存管理问题:使用 std::array 可以减少内存泄露和溢出的风险,因为它是在栈上分配的,不需要手动管理内存。
综上所述,虽然 C 风格的数组在某些性能敏感的场合仍然会被使用,但 std::array 提供的优越性使其在日常开发中成为了更为推荐的选择。
6. C++中的vector的push_back和emplace_back有什么区别?
std::vector 中的 push_back 和 emplace_back 是用于向容器中添加元素的方法,但它们之间有一些重要的区别:
- 参数传递方式:
push_back:
- 接受一个已经构造好的对象,并将其拷贝(或移动)到 vector 中。
- 例如,如果你有一个对象 obj,你可以通过 vector.push_back(obj); 来添加这个对象。
emplace_back:
- 直接在 vector 的内部构造对象,接受参数并以传递的参数在 vector 中建造新对象。
- 例如,如果类有一个构造函数,你可以直接调用 vector.emplace_back(arg1, arg2,…);,传递构造新对象所需的参数。
- 性能:
- push_back 可能会涉及到对象的拷贝构造或移动构造,尤其是在对象较大或重载了拷贝构造函数时。
- 与 push_back 相比,emplace_back 通常更高效,因为它避免了这些额外的拷贝或移动开销。
- 使用场景:
- push_back 更适合已经存在的对象,或者当你希望以特定方式处理对象时。
- emplace_back 更适合于直接用构造函数构造对象时,尤其是在操作复杂对象时。
示例:
#include <vector>
#include <string> struct MyStruct { MyStruct(int x, const std::string& str) : a(x), b(str) {} int a; std::string b;
}; int main() { std::vector<MyStruct> vec; MyStruct obj(1, "example"); // 使用 push_back:创建 obj 的拷贝 vec.push_back(obj); // 使用 emplace_back:直接在 vec 中构造 MyStruct vec.emplace_back(2, "example2"); return 0;
}
总结:
- push_back 是用于添加已存在对象,而 emplace_back 则是在容器内部直接构造对象。
- 在性能敏感的情景下,尤其是涉及到大型对象或复杂构造时,优先考虑使用 emplace_back。
7.C++迭代器和指针有什么区别?
C++ 中的迭代器和指针有许多相似之处,因为它们都可以用于遍历容器中的元素,但它们之间有一些重要区别:
- 定义和用途:
指针:
- 指向内存中的某个地址,可以直接进行算术运算(例如加减)来访问不同的内存位置。
- 通常用于动态内存分配和管理。
迭代器:
- 是一个抽象层,用于访问容器(如 std::vector, std::list, std::map 等)中的元素。
- 提供一种统一的方式来遍历不同的数据结构,而不需要对底层容器的实现细节进行关注。
- 类型和灵活性:
指针:
- 指针的类型是固定的,指向特定的数据类型(例如 int*, char*)。
- 使用时需要关注指针的所有权、生命周期和内存管理。
迭代器:
- 类型可以根据使用的容器而变化,迭代器可以是随机访问迭代器、双向迭代器、前向迭代器等。
- 使用迭代器提供了更高的灵活性,可以轻松地在不同类型的容器之间切换而不需要修改代码。
- 操作和语义:
指针:
- 支持指针算术运算,例如 ptr + 1 直接跳到下一个元素的地址。
- 需要手动进行内存管理,如申请和释放内存。
迭代器:
- 支持与指针类似的操作,但通常采用成员函数和运算符,如 ++iter 以移动到下一个元素。
- 迭代器通常隐藏具体的实现细节,用户无需考虑底层存储结构。
- 安全性:
指针:
- 因为是低级别的内存操作,容易受到悬空指针、内存泄露等问题的影响。
迭代器:
- 通常提供更高的安全性,尤其是在使用标准库容器时,管理内存和生命周期的问题会被更好地抽象化。
示例:
#include <vector>
#include <iostream> int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; // 使用指针 int* ptr = &vec[0]; for (size_t i = 0; i < vec.size(); ++i) { std::cout << *(ptr + i) << " "; // 通过指针访问 } std::cout << std::endl; // 使用迭代器 for (auto it = vec.begin(); it != vec.end(); ++it) { std::cout << *it << " "; // 通过迭代器访问 } std::cout << std::endl; return 0;
}
总结:
指针是一种低级别的内存地址引用,使用起来灵活但需谨慎管理。迭代器则是一种高级抽象,提供统一的接口来访问不同容器的元素,带来更强的安全性和代码的可读性。通常建议在容器操作中优先使用迭代器。