C++STL面试题

1. C++vector原理?resize和reserve的区别是什么?size和capacity的区别?

C++中的 std::vector 是一个动态数组,能够存储可变大小的元素集合。它提供了许多方便的操作,允许开发者以类似数组的方式访问元素,同时能够自动管理内存。

  1. std::vector 的基本原理
  • 动态数组:std::vector 在内部维护一个可动态调整大小的数组。
  • 内存管理:当元素数量超出当前容量(capacity)时,vector 会重新分配内存,通常会扩展到原来的两倍,并将现有元素移动到新的内存地址中。
  • 随机访问:vector 提供随机访问特性,可以用下标直接访问任意元素。
  1. 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; // 有可能增加  
  1. 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 允许在两端进行快速操作,适用于需要频繁进行插入和删除的场景。

  1. std::deque 的基本原理
  • 双端队列:std::deque 允许在两端(头部和尾部)插入和删除元素,同时支持随机访问。
  • 动态存储:deque 在内部使用多个动态数组(或块)来存储元素。这允许它在不同的块中动态分配内存并使得插入和删除操作更高效。
  1. 内部实现
  • 分段存储:deque 的实现通常使用一个指针数组(或块数组),每个指针指向存储一块元素的内存。这种结构使得在 deque 的两端进行插入和删除变得高效,因为可以在块的边缘操作而不需要移动整个数组。

  • 容量管理:deque 维护一个前哨块和一个后哨块,这两个块分别用于存储队列的头部和尾部,这样可以在两端进行有效的添加和删除,同时保持对中间元素的访问能力。

  • 内存分配:deque 会预分配一些块,一旦块中元素达到一定数量,就会分配新的块。其具体实现可能依赖于编译器和库的实现,但基本思路是通过维护一个块池来节省内存和缩短操作时间。

  1. 性能特点
  • 插入和删除操作:在头部或尾部插入和删除元素的复杂度为 O(1)。因为只需改变指针而不需要移动元素。
  • 随机访问:虽然 deque 支持随机访问,复杂度为 O(1),但由于它是分段存储的,连续性并不如 vector 高,因此,随机访问可能相对较慢。
  • 内存使用:相较于 vector,deque 的内存使用更加灵活,但可能会导致较多的内存分配和碎片。
  1. 总结
  • std::deque 是一个双端队列,允许在两端进行高效的插入与删除操作。
  • 它的内部实现基于多个动态数组(或块),使用指针数组管理存储块。
  • 插入和删除操作是 O(1),但由于分段存储,随机访问性能不如 std::vector。

通过这些特性,std::deque 在适用场景上提供了比 vector 和其他容器更加灵活的选择。

3. C++中map和unordered_map的区别?分别在什么场景下使用?

在 C++ 中,std::map 和 std::unordered_map 是两种主要的关联容器。它们都用于存储键值对,但在实现和性能特征上有一定的区别。

  1. 基本区别

存储结构:

std::map:

  • 基于红黑树(自平衡的二叉搜索树)。
  • 自动按照键的顺序保存元素。

std::unordered_map:

  • 基于哈希表。
  • 插入顺序不确定,元素没有特定的顺序。

访问时间复杂度:

std::map:

  • 查找、插入和删除操作的复杂度为 O(log n)。

std::unordered_map:

  • 查找、插入和删除操作的平均复杂度为 O(1),最坏情况下为 O(n)(当哈希冲突严重或哈希表需要扩展时)。

键的要求:

std::map:

  • 键必须支持 < 运算符(或者提供自定义比较器)。

std::unordered_map:

  • 键必须支持哈希函数(默认使用 std::hash)和相等比较。
  1. 使用场景

使用 std::map 的场景:

  • 需要维护一个键的有序集合,方便按顺序遍历。
  • 需要频繁进行范围查询,即查找某一范围内的键(例如,查找键大于某个值的所有键)。
  • 键的顺序非常重要或与其他逻辑相关,例如实现一个优先队列或经典的范围查询。

使用 std::unordered_map 的场景:

  • 在查找、插入或删除操作的性能要求非常高时(尤其是在大量数据时)。
  • 对键的顺序没有要求。
  • 需要快速访问数据,例如常量时间的查找、更新。
  1. 示例代码
#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;  
}  

  1. 总结

选择 std::map 还是 std::unordered_map 取决于具体的需求:

  • 如果需要有序存储并支持范围查询,使用 std::map。
  • 如果对性能和无序性有更高的需求,选择 std::unordered_map。

通过了解这两者的区别,能够更合理地选择合适的数据结构,以满足不同的应用场景需求。

4. C++中list的使用场景

在 C++ 中,std::list 是一个双向链表容器,提供高效的插入和删除操作。以下是 std::list 的一些使用场景及其优势和劣势:

使用场景

  1. 频繁的插入和删除操作:
  • 当你需要在序列的中间位置频繁地插入或删除元素时,std::list 比其他容器(如 std::vector 或 std::deque)更高效,因为它只需要更新指针,而不需要移动其余元素。
  1. 不需要随机访问:
  • 如果你不需要通过索引访问元素(例如,使用 operator[]),那么使用 std::list 是一个不错的选择。访问元素的效率是 O(n),所以如果访问次数很少而插入和删除很频繁,使用 std::list 是合适的。
  1. 需要稳定的迭代器:
  • 在 std::list 中,迭代器在插入和删除操作后不会失效。这意味着在遍历时可以安全地插入或删除节点,而不会影响其他迭代器。
  1. 实现队列和堆栈:
  • 由于 std::list 提供了高效的前端和后端插入/删除功能,因此可以用作队列(FIFO)和堆栈(LIFO)的实现。

优势

  1. 高效的插入和删除:
  • 在任意位置进行插入和删除操作的时间复杂度为 O(1),只需修改指针。
  1. 不需要移动元素:
  • 插入和删除操作不会影响其他元素的存储位置,因此在数据频繁变动的情况下性能较好。

劣势

  1. 内存开销:
  • 由于每个元素需要存储两个指针(前驱和后驱),std::list 的内存开销相比 std::vector 更大。
  1. 随机访问性能差:
  • 访问元素的时间复杂度为 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风格数组更具优势:

  1. 类型安全:std::array 是一个模板类,它能够确保数组元素的类型得到正确管理,避免类型不匹配的问题。

  2. 固定大小:与 C 风格的数组相比,std::array 的大小是在编译时确定的,确保数组大小的一致性和可控性。

  3. 封装:std::array 提供了类的方法来处理数组,例如大小获取 (size())、访问元素 (at() 和 operator[]),提升了代码的可读性和可维护性。

  4. 与 STL 兼容性:std::array 兼容 C++ 标准模板库的算法,能够直接与 STL 算法一起使用,而 C 风格数组则不具备这种灵活性。

  5. 更好的功能:std::array 支持常用的 STL 功能,例如复制、比较、排序等,使其在管理数组时更为方便。

  6. 重载的运算符:std::array 支持常用的运算符重载,允许数组对象之间的比较和赋值操作。

  7. 避免内存管理问题:使用 std::array 可以减少内存泄露和溢出的风险,因为它是在栈上分配的,不需要手动管理内存。

综上所述,虽然 C 风格的数组在某些性能敏感的场合仍然会被使用,但 std::array 提供的优越性使其在日常开发中成为了更为推荐的选择。

6. C++中的vector的push_back和emplace_back有什么区别?

std::vector 中的 push_back 和 emplace_back 是用于向容器中添加元素的方法,但它们之间有一些重要的区别:

  1. 参数传递方式:

push_back:

  • 接受一个已经构造好的对象,并将其拷贝(或移动)到 vector 中。
  • 例如,如果你有一个对象 obj,你可以通过 vector.push_back(obj); 来添加这个对象。

emplace_back:

  • 直接在 vector 的内部构造对象,接受参数并以传递的参数在 vector 中建造新对象。
  • 例如,如果类有一个构造函数,你可以直接调用 vector.emplace_back(arg1, arg2,…);,传递构造新对象所需的参数。
  1. 性能:
  • push_back 可能会涉及到对象的拷贝构造或移动构造,尤其是在对象较大或重载了拷贝构造函数时。
  • 与 push_back 相比,emplace_back 通常更高效,因为它避免了这些额外的拷贝或移动开销。
  1. 使用场景:
  • 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++ 中的迭代器和指针有许多相似之处,因为它们都可以用于遍历容器中的元素,但它们之间有一些重要区别:

  1. 定义和用途:

指针:

  • 指向内存中的某个地址,可以直接进行算术运算(例如加减)来访问不同的内存位置。
  • 通常用于动态内存分配和管理。

迭代器:

  • 是一个抽象层,用于访问容器(如 std::vector, std::list, std::map 等)中的元素。
  • 提供一种统一的方式来遍历不同的数据结构,而不需要对底层容器的实现细节进行关注。
  1. 类型和灵活性:

指针:

  • 指针的类型是固定的,指向特定的数据类型(例如 int*, char*)。
  • 使用时需要关注指针的所有权、生命周期和内存管理。

迭代器:

  • 类型可以根据使用的容器而变化,迭代器可以是随机访问迭代器、双向迭代器、前向迭代器等。
  • 使用迭代器提供了更高的灵活性,可以轻松地在不同类型的容器之间切换而不需要修改代码。
  1. 操作和语义:

指针:

  • 支持指针算术运算,例如 ptr + 1 直接跳到下一个元素的地址。
  • 需要手动进行内存管理,如申请和释放内存。

迭代器:

  • 支持与指针类似的操作,但通常采用成员函数和运算符,如 ++iter 以移动到下一个元素。
  • 迭代器通常隐藏具体的实现细节,用户无需考虑底层存储结构。
  1. 安全性:

指针:

  • 因为是低级别的内存操作,容易受到悬空指针、内存泄露等问题的影响。

迭代器:

  • 通常提供更高的安全性,尤其是在使用标准库容器时,管理内存和生命周期的问题会被更好地抽象化。

示例:

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

总结:

指针是一种低级别的内存地址引用,使用起来灵活但需谨慎管理。迭代器则是一种高级抽象,提供统一的接口来访问不同容器的元素,带来更强的安全性和代码的可读性。通常建议在容器操作中优先使用迭代器。

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值