【C/C++】标准库相关题型(一)

文章目录

    • 1. vector底层实现原理
      • 1.1 类构成
      • 1.2 构造函数
      • 1.3 插入元素
      • 1.4 删除元素
      • 1.5 读取元素
      • 1.6 修改元素
      • 1.7 释放空间
    • 2. vector内存增长机制
      • 2.1 特点
      • 2.2 内存增长特性
      • 2.3 内存增长过程
      • 2.4 内存清理
      • 2.5 注意事项
    • 3. vector中reserve和resize的区别
      • 3.1 共同点
      • 3.2 区别
      • 3.3 应用场景
    • 4. vector的元素类型为什么不能是引用?
      • 4.1 引用有什么特征?
      • 4.2 Vector的元素类型不能是引用
    • 5. list底层实现原理
      • 5.1 原理图
      • 5.2 类构成
      • 5.3 构造函数
      • 5.4 迭代器
      • 5.5 访问元素
      • 5.6 插入和删除元素

1. vector底层实现原理

一句话概括:底层实现了一个动态数组

1.1 类构成

  • class vector : protected Vector base
    • protected继承:基类的 public 在子类中将变为 protected,其他权限不改变
    • 它实现了一个动态的数组,内部包含指向动态分配内存的指针以及记录数组大小和容量的成员变量
  • _Vector_base类的数据成员
    • _M_start:指向容器开始的位置
    • _M_finish:指向容器的下一个可插入位置
    • _M_end_of_storage:指向分配的动态内存的尾部

1.2 构造函数

  • 无参构造
    • 不立即分配内存,而是在添加元素时按需分配,避免无谓的内存浪费。
    • STL容器总是性能优先
  • 初始化元素个数构造
    • 根据传入的元素数量分配内存,避免后续可能的重新分配
    • 避免多次申请动态内存,从而影响性能
    • 如果知道需要多大的空间,使用这种方法能大大减少动态分配内存的开销

1.3 插入元素

  • 插入最后
    • 如果有足够的内存空间,则直接插入。如果内存空间不足,则将现有元素复制到新的更大的内存区域,释放原来的内存,然后插入新元素
  • 插入不是最后
    • 对于非尾部插入,除了要分配可能的新内存之外,还需要将插入点之后的所有元素向后移动一位

1.4 删除元素

  • 删除最后一个元素
    • 简单地将_M_finish向前移动一位,不立即释放内存,这样可以为后续的插入操作保留空间
  • 删除不是最后一个元素
    • 待删位置之后元素向前平移一位,_M_finish向前移动一位
    • 删除元素同样不会释放现有已经申请的内存

1.5 读取元素

  • 操作符 []:不检查下标的合法性,所以它的效率比 at()
  • at() 函数:在返回元素之前检查下标的合法性,如果下标越界,会抛出 std::out_of_range 异常
  • 它们都是返回具体元素的引用

1.6 修改元素

  • vector 不支持直接修改某个位置的元素
  • 可以通过获取元素的引用,然后直接修改引用的值,来实现对元素的修改

1.7 释放空间

  • std::vector::shrink_to_fit() 函数:尝试减小容器的容量以适应其大小,以释放未使用的内存(C++11新特性)
  • 使用交换技巧:创建一个临时的空vector,并与原vector进行交换,从而释放其占用的全部内存

2. vector内存增长机制

2.1 特点

  • 内存空间只会增加不会减少:在进行插入操作时,如果当前内存不足以容纳更多的元素,std::vector将申请更大的内存空间,但是在删除元素时,它并不会释放已经申请的内存空间
  • vector的内存是连续的:这意味着可以通过指针算术来遍历vector的元素
  • 不同平台或库实现,内存增长方式可能不同:GCC的实现通常会选择翻倍的方式进行增长,而Visual Studio的实现可能选择1.5倍的方式进行增长

2.2 内存增长特性

  • 无参构造,连续插入一个元素,内存增长方式:1、2、4、8、16、32、…,即每次都是上一次的2倍
  • 有参构造,连续插入一个元素,内存增长方式:n、2n、4n、…,其中n是初始时分配的元素数量

2.3 内存增长过程

  • 申请新的更大的内存空间,大小通常是当前内存空间大小的两倍(具体取决于实现)
  • 将原有内存空间中的数据移动(或复制)到新的内存空间中
  • 释放原有内存空间
  • 在新内存空间的尾部插入新的元素

2.4 内存清理

  • 交换一个空的vector:通过创建一个临时的空vector,并与原vector进行交换,可以释放原vector占用的全部内存:

    std::vector<int> v;
    // ... 后续操作
    std::vector<int>().swap(v);  // 释放内存
    
  • 使用 std::vector::shrink_to_fit 方法:尝试减小容器的容量以适应其大小,以释放未使用的内存:

    std::vector<int> v;
    // ... 后续操作
    v.shrink_to_fit();  // 释放未使用的内存
    

2.5 注意事项

  • std::vector中的元素是指针时,std::vector在销毁时不会调用指针指向的对象的析构函数。所以如果std::vector存储的是动态分配的对象,你需要在std::vector被销毁前,自己手动删除这些对象,以防止内存泄漏:

    std::vector<int*> v;
    for (int i = 0; i < 10; ++i) {
        v.push_back(new int(i));
    }
    
    // 手动释放内存
    for (auto ptr : v) {
        delete ptr;
    }
    

3. vector中reserve和resize的区别

3.1 共同点

  • 对容器内原有的元素不产生影响:无论是reserve还是resize,它们都不会影响容器中已经存在的元素
  • 只能增加容器的容量:如果指定的值小于当前的容量,reserveresize都不会减少容器的容量

3.2 区别

  • reserve:只会改变std::vector的容量(capacity),并不会改变其大小(size)。也就是说,它只会预分配内存,但并不会创建新的元素。这意味着,在调用reserve之后,std::vectorsize成员函数返回的值是不会改变的:

    std::vector<int> v;
    v.reserve(100);  // v的容量变为100,但size仍然为0
    
  • resize:会改变std::vector的大小,同时也可能改变其容量。resize会创建新的元素,所以在调用resize之后,std::vectorsize成员函数返回的值可能会改变:

    std::vector<int> v;
    v.resize(100);  // v的容量和size都变为100
    

3.3 应用场景

  • reserve:在已知需要存储大量元素的情况下使用,可以通过一次性分配足够的内存来避免频繁的内存重新分配,从而提高性能
  • resize:确保容器中有足够的元素,这对于使用下标访问元素的操作是必要的,可以避免越界的问题

4. vector的元素类型为什么不能是引用?

std::vector的模板参数表示容器中存储的元素类型,例如std::vector表示一个整数的动态数组。当尝试定义std::vector(T为任意类型)时,编译器会报错。原因如下:

4.1 引用有什么特征?

  • 引用必须在定义时进行初始化,不能初始化为空对象,且初始化后不能改变引用的指向。

    int a = 10;
    int& ref = a;  // 正确
    int& ref2;  // 错误,引用必须在定义时初始化
    ref = 20;  // 正确,改变的是a的值,而非ref的指向
    int b = 30;
    ref = b;  // 错误,不能改变ref的指向
    
  • 引用是别名,不是对象,没有实际的地址,不能定义引用的指针,也不能定义引用的引用。

    int a = 10;
    int& ref = a;
    int&* p = &ref;  // 错误,不能定义引用的指针
    int&& ref2 = ref;  // 错误,不能定义引用的引用
    

4.2 Vector的元素类型不能是引用

  • std::vector 在内部使用内存分配操作为元素分配存储空间,但引用不是对象,没有实际的地址,因此不能为其分配存储空间。
  • std::vector::push_backstd::vector::emplace_back 都会尝试复制或移动其参数,以创建新的元素。然而,引用不能被赋值,只能在定义时进行初始化。
  • std::vector 的有参构造函数会尝试初始化一定数量的元素,但引用必须在定义时初始化,因此不能用于初始化 std::vector 的元素。
  • 基于操作符 []at,将会获取引用的引用,这在 C++ 中是不合法的,从而产生矛盾。

5. list底层实现原理

一句话概括:list底层实现了一个双向循环链表

5.1 原理图

【C/C++】标准库相关题型(一)_第1张图片

5.2 类构成

  • class list : protected _List_base
    • liststd::list 的主体,提供接口和基本功能
  • _list_base._list_impl._list_node
    • _list_node 是链表中的节点,包含了节点中存储的数据以及指向其他节点的指针。
      • _M_storage:存储具体的值。
      • _M_next:指向下一个元素。
      • _M_prev:指向上一个元素。

5.3 构造函数

  • 无论如何构造,std::list 都会初始化一个空节点,这个空节点用来标记链表的终点,它没有存储任何用户数据

5.4 迭代器

std::list 提供了双向迭代器,通过这些迭代器,可以对链表进行前向和后向的遍历

  • ++:将迭代器向后移动到下一个节点
  • --:将迭代器向前移动到前一个节点

5.5 访问元素

  • 获取第一个元素:空节点的下一个节点存储了链表的第一个元素,可以通过 begin() 方法获取
  • 获取最后一个元素:空节点的上一个节点存储了链表的最后一个元素,可以通过 --end() 获取

5.6 插入和删除元素

  • 插入元素:std::list 对于每个新插入的元素,都会创建一个新的节点并动态为其分配内存。插入操作包括 push_back, push_front, insert 等方法
  • 删除元素:std::list 删除元素时会释放对应节点的内存。删除操作包括 pop_back, pop_front, erase 等方法

你可能感兴趣的:(面试,c++,面试,c语言)