【C++ STL部分】总结篇

点我–>C++语言基础
点我–>C++内存
点我–>面向对象
点我–>新特性

【操作系统】常见问题汇总及解答
【数据库】常见问题汇总及解答
【计算机网络】常见问题汇总及解答

1. STL 的基本组成部分

  • STL(Standard Template Library)是C++标准库的一部分,它提供了一组通用的、高效的算法、数据结构和函数对象,可以用来处理各种类型的数据。
  • 广义上讲,STL分为3类:容器(Container)、算法(Algorithm)和迭代器(Iterator),容器和算法通过迭代器可以进行无缝地连接。
  • 详细的说,STL由6部分组成:容器(Container)、算法(Algorithm)、 迭代器(Iterator)、仿函数(Function Object)、适配器(Adaptor)、空间配制器(Allocator)。
  1. 容器(Container):容器是一种数据结构,它用于存储不同类型的数据。STL中提供了一些标准容器,如vector、list、deque、set等,它们都有不同的特点和用途。这些容器类提供了一些基本操作(如添加、删除、查找),并且支持迭代器(iterator)的概念,允许用户对容器中的元素进行遍历和访问。
  2. 算法(Algorithm):算法是STL中最重要的部分之一,它提供了各种算法,例如排序、查找、二分查找等等。STL中的标准算法都是通用的,它们可以对各种容器对象(如vector、list、set等)执行操作,从而使得代码更具有可重用性。
  3. 迭代器(Iterator):迭代器是一种对象,它允许用户使用通用的算法来操作容器中的元素。迭代器让算法代码独立于容器类型,从而增加了代码的可重用性。STL中提供了多种迭代器类型,如前向迭代器、双向迭代器、随机访问迭代器等。
  4. 仿函数(Function Object):仿函数又称之为函数对象,函数对象是类对象,可以像函数一样执行特定任务。STL中定义了很多函数对象,例如unary_function、binary_function、less、greater等等。函数对象可以与STL中的算法一起使用,实现更加灵活和高效的操作。
  5. 适配器(Adaptor):简单的说就是一种接口类,专门用来修改现有类的接口,提供一种新的接口;或调用现有的函数来实现所需要的功能。主要包括3中适配器:Container Adaptor、Iterator Adaptor、Function Adaptor。
  6. 空间配制器(Allocator):用来管理内存分配和释放。在STL中,容器类和算法函数默认使用系统默认的分配器,但是STL也提供了自定义的分配器接口,可以进行内存池管理等更细粒度的内存管理。

2. 说说 STL 中常见的容器,并介绍一下实现原理

顺序容器:容器并非排序的,元素的插入位置同元素的值无关。

  • vector(动态数组):vector是一种动态数组,它可以自动调整大小并且支持快速的随机访问。在vector中,元素存储在一段连续的内存空间中,可以通过下标来访问。
    vector的实现原理基于动态内存分配和内存重分配。vector存储元素时,首先分配一块内存空间用于存储元素,当元素数量增加时,vector会重新分配更大的内存空间,并将原有数据复制到新的空间中。这种内存分配和内存重分配的方式可以保证vector的性能。
  • list(链表):list是一种双向链表,它支持快速的插入和删除操作。在list中,元素存储在不同的内存空间中,相邻的元素通过指针连接起来。
    list的实现原理基于指针。在list中,每个元素都有指向前一个元素和后一个元素的指针,这样就可以快速地插入或删除一个元素。同时,list还支持快速的前向或后向遍历操作。
  • deque(双端队列):deque是一种双端队列,它支持在队列头部和尾部进行快速插入和删除操作。在deque中,元素存储在不同的内存块中,每个内存块中存储多个元素。
    deque的实现原理基于分块结构。在deque中,每个内存块存储一个固定数量的元素,每个内存块有一个指向前一个内存块和后一个内存块的指针,这样就可以实现快速的插入和删除操作。

关联式容器:元素是排序的。插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现。

  • set(集合):set是一种基于红黑树(Red-Black Tree)实现的集合,它支持快速的插入、查找和删除操作。在set中,元素按照一定的顺序进行排列,且每个元素只出现一次。
    set的实现原理基于红黑树。红黑树是一种自平衡二叉搜索树,它能够保证插入、查找和删除操作都可以在O(logN)的时间内完成。
  • map(映射):map是一种基于红黑树(Red-Black Tree)实现的映射,它支持快速的插入、查找和删除操作。在map中,元素由键值对组成,按照键的大小进行排序,且每个键只出现一次。
    map的实现原理也基于红黑树。通过红黑树的自平衡特性,map能够保证查找、插入和删除的时间复杂度都为O(logN)。

容器适配器:封装了一些基本的容器,使之具备了新的函数功能,比如把deque封装一下变为一个具有stack功能的数据结构。这新得到的数据结构就叫适配器。

  • stack(栈):栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项)。后进先出。
  • queue(队列):插入只可以在尾部进行,删除、检索和修改只允许从头部进行。先进先出。
  • priority_queue(优先级队列):内部维持某种有序,然后确保优先级最高的元素总是位于头部。最高优先级元素总是第一个出列。

3. STL中map hashtable deque list的实现原理

map、hashtable、deque、list 实现原理分别为红黑树、函数映射、双向队列、双向链表,他们的特性分别如下:

  • map实现原理
    map 内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map 中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
  • hashtable(也称散列表,直译作哈希表)实现原理
    hashtable 采用了函数映射的思想,记录的存储位置与记录的关键字关联起来,从而能够很快速地进行查找。这决定了哈希表特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,它能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。
    哈希表的实现代码:
#include <iostream>
#include <vector>
#include <list>
#include <random>
#include <ctime>
using namespace std;

const int hashsize = 12;

//定一个节点的结构体
template <typename T, typename U>
struct HashNode 
{
    
    
    T _key;
    U _value;
};

//使用拉链法实现哈希表类
template <typename T, typename U>
class HashTable
{
    
    
public:
    HashTable() : vec(hashsize) {
    
    }//类中的容器需要通过构造函数来指定大小
    ~HashTable() {
    
    }
    bool insert_data(const T &key, const U &value);
    int hash(const T &key);
    bool hash_find(const T &key);
private:
    vector<list<HashNode<T, U>>> vec;//将节点存储到容器中
};

//哈希函数(除留取余)
template <typename T, typename U>
int HashTable<T, U>::hash(const T &key)
{
    
    
    return key % 13;
}

//哈希查找
template <typename T, typename U>
bool HashTable<T, U>::hash_find(const T &key)
{
    
    
    int index = hash(key);//计算哈希值
    for (auto it = vec[index].begin(); it != vec[index].end(); ++it)
    {
    
    
        if (key == it -> _key)//如果找到则打印其关联值
        {
    
    
            cout << it->_value << endl;//输出数据前应该确认是否包含相应类型
            return true;
        }
    }
    return false;
}

//插入数据
template <typename T, typename U>
bool HashTable<T, U>::insert_data(const T &key, const U &value)
{
    
    
    //初始化数据
    HashNode<T, U> node;
    node._key = key;
    node._value = value;
    for (int i = 0; i < hashsize; ++i)
    {
    
    
        if (i == hash(key))//如果溢出则把相应的键值添加进链表
        {
    
    
            vec[i].push_back(node);
            return true;
        }
    }
}

int main(int argc, char const *argv[])
{
    
    
    HashTable<int, int> ht;
    static default_random_engine e;
    static uniform_int_distribution<unsigned> u(0, 100);
    long long int a = 10000000;
    for (long long int i = 0; i < a; ++i)
        ht.insert_data(i, u(e));
    clock_t start_time = clock();
    ht.hash_find(114);
    clock_t end_time = clock();
    cout << "Running time is: " << static_cast<double>(end_time - start_time) / CLOCKS_PER_SEC * 1000 <<
        "ms" << endl;//输出运行时间。
    system("pause");
    system("pause");
    return 0;
}
  • deque实现原理
    deque 内部实现的是一个双向队列。元素在内存连续存放。随机存取任何元素都在常数时间完成(仅次于vector)。所有适用于 vector 的操作都适用于 deque。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
  • list实现原理
    list 内部实现的是一个双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。无成员函数,给定一个下标 i,访问第 i 个元素的内容,只能从头部挨个遍历到第 i 个元素。

4. 说说 STL 的空间配置器(allocator)

一般情况下,一个程序包括数据结构和相应的算法,而数据结构作为存储数据的组织形式,与内存空间有着密切的联系。在C++ STL中,空间配置器便是用来实现内存空间(一般是内存,也可以是硬盘等空间)分配的工具,他与容器联系紧密,每一种容器的空间分配都是通过空间分配器 allocator 实现的。它为STL中的容器和算法提供内存管理功能,使得它们可以动态地分配和释放内存,而不需要手动编写内存分配和释放的代码

STL的空间配置器是一个模板类,定义在头文件<allocator>中。它的实现通常基于一个底层分配器,例如操作系统提供的分配器或者C++语言内置的分配器。空间配置器通常包括两个主要的函数:allocate和deallocate。

allocate函数用于分配一段指定大小的内存空间。它接受一个整数参数n,表示要分配的字节数,返回一个指向已经分配的内存空间的指针。

deallocate函数用于释放先前分配的内存。它接受一个指向已分配内存空间的指针,并将该内存空间释放。

空间配置器还可以提供构造函数和析构函数,用于初始化和销毁已分配的内存中的对象。

STL的空间配置器还可以被用户定义和扩展,允许用户根据自己的需求实现自定义的内存管理策略。通过定义自己的空间配置器,用户可以实现更高效或更安全的内存管理方式,并为自己的应用程序提供更好的性能和稳定性。

5. 说说用过 STL 的哪些容器,查找的时间复杂度是多少,为什么

STL中常用的容器有 vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set 等。容器底层实现方式及时间复杂度分别如下:

  • vector:采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为:
    插入: O(N)
    查看: O(1)
    删除: O(N)
  • deque:采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为:
    插入: O(N)
    查看: O(1)
    删除: O(N)
  • list:采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为:
    插入: O(1)
    查看: O(N)
    删除: O(1)
  • map、set、multimap、multiset:这四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为:
    插入: O(logN)
    查看: O(logN)
    删除: O(logN)
  • unordered_map、unordered_set、unordered_multimap、unordered_multiset:这四种容器采用哈希表实现,不同操作的时间复杂度为:
    插入: O(1),最坏情况O(N)
    查看: O(1),最坏情况O(N)
    删除: O(1),最坏情况O(N)

注意:容器的时间复杂度取决于其底层实现方式。

6. 迭代器什么时候会失效

  • 对于序列容器 vector、deque 来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
  • 对于关联容器 map、set 来说,使用了erase后,当前元素的迭代器失效,但其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
  • 对于 list 来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。

7. 说一下STL中迭代器的作用,有指针为何还要迭代器

  1. 迭代器的作用:
    1)用于指向顺序容器和关联容器中的元素;
    2)通过迭代器可以读取它指向的元素;
    3)通过非const迭代器还可以修改其指向的元素。
  2. 迭代器和指针的区别:
    迭代器不是指针,是类模板,表现地像指针。它只是模拟了指针的一些功能,重载了指针的一些操作符,–>、++、- -等。迭代器封装了指针,是一个”可遍历STL容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,它可以根据不同类型的数据结构来实现不同的++,- -等操作。
    迭代器返回的是对象引用而不是对象的值,所以 cout 只能输出迭代器使用取值后的值而不能直接输出其自身。
  3. 迭代器产生的原因:
    Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

答案解析:

  • 迭代器:Iterator(迭代器)模式又称游标(Cursor)模式,用于提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。 或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。 由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展Iterator。
  • 迭代器示例:
#include <iostream>
#include<vector>
using namespace std;

int main() 
{
    
    
	vector<int> vec; // 一个存放int元素的数组,一开始里面没有元素
	vec.push_back(1);
	vec.push_back(2);
	vec.push_back(3);
	vec.push_back(4);
	vec.push_back(5);

	vector<int>::const_iterator cit; // 常量迭代器

	// vec.begin()表示vec第一个元素迭代器指针,++cit指向下一个元素
	for (cit = vec.begin(); cit != vec.end(); ++cit)
	{
    
    
		cout << *cit << " "; // *cit表示迭代器指向的元素
		// 1 2 3 4 5
	}
	cout << endl;

	vector<int>::reverse_iterator rit; // 反向迭代器

	for (rit = vec.rbegin(); rit != vec.rend(); ++rit)
	{
    
    
		cout << *rit << " ";
		// 5 4 3 2 1
	}
	cout << endl;

	vector<int>::iterator it; // 非常量迭代器

	for (it = vec.begin(); it != vec.end(); ++it)
	{
    
    
		*it = 100;
	}
	for (it = vec.begin(); it != vec.end(); ++it)
	{
    
    
		cout << *it << " ";
		// 100 100 100 100 100
	}
	cout << endl;
	
	return 0;
}

8. STL 迭代器是怎么删除元素的

对于不同类型的容器,删除元素的方式也有所不同。下面我们以向量为例来说明迭代器删除元素的具体操作。
假设我们有一个 vector<int> v,其中包含了若干个整数。我们可以通过迭代器来删除其中的某一个元素,具体操作如下:

vector<int>::iterator iter = v.begin(); // 获取迭代器

while (iter != v.end()) {
    
     // 遍历容器

    if (*iter == 5) {
    
     // 如果找到需要删除的元素

        iter = v.erase(iter); // 调用 erase() 函数删除该元素,此时迭代器会指向删除后的下一个元素。
    }
    else {
    
    
        ++iter; // 继续遍历下一个元素
    }
}

在以上代码中,我们首先使用 begin() 函数获取向量 v 的起始迭代器,然后使用 while 循环遍历 v 中的所有元素。当找到需要删除的元素时,我们调用 erase() 函数来删除该元素。需要注意的是,在 erase() 函数中,我们传入的是一个迭代器,这个迭代器指向的元素将被删除,并返回一个指向删除元素后的下一个元素的迭代器。因此,在代码中,我们将迭代器 iter 指向 erase() 函数返回的迭代器,以便继续遍历容器。如果找到的不是需要删除的元素,则我们仅仅是将迭代器指向下一个元素,继续遍历即可。
需要注意的是,在使用迭代器删除元素时,我们不能直接使用 v[i] 的方式来删除元素,因为这样会破坏迭代器的有效性,导致未定义的行为。因此,使用 erase() 函数是一个更加安全、有效的方式。

容器 容器的迭代器类型
vector 随机访问
deque 随机访问
list 双向
set/multiset 双向
map/multimap 双向
stack 不支持迭代器
queue 不支持迭代器
priority_queue 不支持迭代器

9. STL 中 resize 和 reserve 的区别

  • 首先必须弄清楚两个概念:
    • capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。
    • size:指的是此时容器中实际的元素个数。可以通过下标访问0~(size-1)范围内的对象。
  • resize和reserve区别主要有以下几点:
    • resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正地创建对象,需要通过 insert() 或 push_back() 等创建对象。
    • resize既修改capacity大小,也修改size大小;reserve只修改capacity大小,不修改size大小。
    • 两者的形参个数不一样。 resize带两个参数,一个表示容器大小,一个表示初始值(默认为0);reserve只带一个参数,表示容器预留的大小。

详细说明:
1、resize():resize()函数用于改变容器的大小。其将更改容器中元素的数目,并在适当的情况下添加或删除元素。此函数接受一个参数n,表示改变容器大小后元素的数量。有以下几种情况:

  • 如果n小于当前元素数,则会丢弃多余的元素。
  • 如果n大于当前元素数,容器将调用构造函数来创建额外的元素。
  • 如果n等于当前元素数,则该函数不做任何事情。

resize()函数会返回void。

示例代码:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    
    
    vector<int> vec{
    
    1, 2, 3, 4, 5}; // vec现在包含5个元素
    vec.resize(8);                  // 在末尾添加3个元素,vec包含8个元素
    vec.resize(2);                  // 删除最后6个元素,vec包含2个元素
    for (int& i : vec)
    {
    
    
        cout << i << " ";
    }
    return 0;
}

2、reserve():reserve()函数用于预分配内存,但实际上并不改变容器的大小。其接受一个参数n,表示分配的存储空间大小。当reserve()函数被调用时,容器的存储空间将被预分配到n个元素的大小,但不会调用构造函数来创建元素,也不会更改容器的元素数。

当向预分配的容器中添加新元素时,如果元素数量已经达到预分配的最大数量,容器将重新分配内存,并向新的内存中复制所有已有的元素,这些可能导致效率降低。

示例代码:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    
    
    vector<int> vec1;
    vec1.reserve(5);      // 预分配5个元素的存储空间

    vector<int> vec2(5);  // 直接初始化五个值为零的元素
    vec2.reserve(10);     // 预分配10个元素的存储空间

	// 5
    cout << "vec1的容量为:" << vec1.capacity() << endl;  // 输出预分配后的容器大小
    // 10
    cout << "vec2的容量为:" << vec2.capacity() << endl;
    return 0;
}

因此,这两种函数在分配内存时的操作不同。resize()会根据需要更改元素的数量(并且添加或删除元素),而reserve()仅预分配容纳未来元素的内存空间,而不更改当前容器的大小。

10. STL 容器动态链接可能产生的问题

  • 可能产生的问题:容器是一种动态分配内存空间的一个变量集合类型变量。在一般的程序函数里,局部容器,参数传递容器,参数传递容器的引用,参数传递容器指针都是可以正常运行的,而在动态链接库函数内部使用容器也是没有问题的,但是给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。
  • 产生问题的原因:容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。

11. map 和 unordered_map 的区别、底层实现

C++的unordered_map容器简单介绍及用法
map 和 unordered_map 的区别在于它们的实现基理不同:

  • map实现机理:map 内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡的二叉搜索树),红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map 中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
  • unordered_map实现机理:unordered_map 内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。

12. vector 和 list 的区别,分别适用于什么场景

  • vector(一维数组)
    特点:元素在内存连续存放,动态数组,在堆中分配内存,有保留内存,如果减少大小后内存也不会释放。
    优点:和数组类似,开辟一段连续的空间,并且支持随机访问,所以它的查找效率高,其时间复杂度为O(1)。
    缺点:由于开辟一段连续的空间,所以插入删除会需要对数据进行移动,比较麻烦,时间复杂度为O(n),另外当空间不足时还需要进行扩容。
    扩容方式:(1)当 vector 空间不足时,vector 会重新分配一块更大的连续内存空间,新空间大小通常是原来空间大小的两倍或者更多。(2)然后,vector 将原来的元素逐个拷贝到新的空间中。(3)对于内置类型和简单类类型,vector 将使用 memcpy 函数来拷贝原来的元素,对于自定义类类型,vector 将调用该类的拷贝构造函数来拷贝原来的元素。(4)最后,vector 释放原来的空间,并将新空间的地址保存在 vector 类中。
  • list(双向链表)
    特点:元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问。
    优点:底层实现是循环双链表,当对大量数据进行插入删除时,其时间复杂度为O(1)。
    缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历,其时间复杂度为O(n),没有提供[]操作符的重载。
  • 应用场景
    vector 拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随机访问,而不在乎插入和删除的效率,使用 vector。
    list 拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用 list。

13. C++的 vector 和 list 中,如果删除末尾的元素,其指针和迭代器如何变化,若删除的是中间的元素呢

  • vector 和 list 特性
    vector 特性:动态数组。元素在内存连续存放。随机存取任何元素都在常数时间完成。在尾端增删元素具有较大的性能(大部分情况下是常数时间)。
    list 特性:双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
  • vector 增删元素:对于 vector 而言,删除某个元素以后,该元素后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
  • list 增删元素:对于 list 而言,删除某个元素,只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。

14. map 和 set 有什么区别,分别又是怎么实现的

set 是一种关联式容器,其特性如下:

  • set以红黑树作为底层容器。
  • 所得元素的只有key没有value,value就是key。
  • 不允许出现键值重复。
  • 所有的元素都会被自动排序。
  • 不能通过迭代器来改变 set 的值,因为 set 的值就是键,set 的迭代器是 const 的。

map 和 set 一样是关联式容器,其特性如下:

  • map以红黑树作为底层容器。
  • 所有元素都是键+值存在。
  • 不允许键重复。
  • 所有元素是通过键进行自动排序的。
  • map的键是不能修改的,但是其键对应的值是可以修改的。

综上所述,map 和 set 底层实现都是红黑树;map 和 set 的区别在于 map 的值不作为键,键和值是分开的。

15. push_back 和 emplace_back 的区别

如果要将一个临时变量添加到容器的末尾,push_back() 需要先构造临时对象,再将这个对象拷贝到容器的末尾,可以理解为调用了拷贝构造函数。而 emplace_back() 则直接在容器的末尾构造对象,这样就省去了拷贝的过程,可以理解为调用了相应的构造函数。由于直接构造对象的方式避免了对象的拷贝和移动,因此 emplace_back() 在性能上比 push_back() 效率更高。

示例:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class A {
    
    
public:
	A(int i) {
    
    
		str = to_string(i);
		cout << "构造函数" << endl;
	} 
	~A() {
    
    }
	A(const A& other) : str(other.str) {
    
    
		cout << "拷贝构造" << endl;
	}
public:
	string str;
};

int main()
{
    
    
	vector<A> vec;
	vec.reserve(10);
	for (int i = 0; i < 10; i++) {
    
    
		vec.push_back(A(i)); // 调用了10次构造函数和10次拷贝构造函数
		// vec.emplace_back(i); //调用了10次构造函数,一次拷贝构造函数都没有调用过
	}
}

16. map中[ ]与find的区别

  • map的下标运算符[]的作用是:将关键码作为下标去执行查找,并返回对应的值。如果不存在这个关键码,就将一个具有该关键码和值类型的默认值的项插入这个map。
  • map的find函数:用关键码执行查找,找到了返回该位置的迭代器。如果不存在这个关键码,就返回尾迭代器。

17. map 插入方式有哪几种

  • 用 insert 函数插入 pair 数据:
mapStudent.insert(pair<int, string>(1, "student_one")); 
  • 用 insert 函数插入 value_type 数据:
mapStudent.insert(map<int, string>::value_type (1, "student_one"));
  • 在 insert 函数中使用 make_pair() 函数:
mapStudent.insert(make_pair(1, "student_one"));
  • 用数组方式插入数据:
mapStudent[1] = "student_one"; 

18. 迭代器的底层机制和失效的问题(了解就可以)

  1. 迭代器的底层原理

迭代器是连接容器和算法的一种重要桥梁,通过迭代器可以在不了解容器内部原理的情况下遍历容器。它的底层实现包含两个重要的部分:萃取技术和模板偏特化。

萃取技术(traits)可以进行类型推导,根据不同类型可以执行不同的处理流程,比如容器是vector,那么traits必须推导出其迭代器类型为随机访问迭代器,而list则为双向迭代器。

例如STL算法库中的distance函数,distance函数接受两个迭代器参数,然后计算他们两者之间的距离。显然对于不同的迭代器计算效率差别很 大。比如对于vector容器来说,由于内存是连续分配的,因此指针直接相减即可获得两者的距离;而list容器是链式表,内存一般都不是连续分 配,因此只能通过一级一级调用next()或其他函数,每调用一次再判断迭代器是否相等来计算距离。vector迭代器计算distance的效率为O(1),而 list则为O(n),n为距离的大小。

使用萃取技术(traits)进行类型推导的过程中会使用到模板偏特化。模板偏特化可以用来推导参数,如果我们自定义了多个类型,除非我们把这 些自定义类型的特化版本写出来,否则我们只能判断他们是内置类型,并不能判断他们具体属于是个类型。

template <typename T>
struct TraitsHelper {
    
    
     static const bool isPointer = false;
};
template <typename T>
struct TraitsHelper<T*> {
    
    
     static const bool isPointer = true;
};

if (TraitsHelper<T>::isPointer)
     ...... // 可以得出当前类型int*为指针类型
else
     ...... // 可以得出当前类型int非指针类型

  1. 一个理解traits的例子
// 需要在T为int类型时,Compute方法的参数为int,返回类型也为int,
// 当T为float时,Compute方法的参数为float,返回类型为int
template <typename T>
class Test {
    
    
public:
     TraitsHelper<T>::ret_type Compute(TraitsHelper<T>::par_type d);
private:
     T mData;
};

template <typename T>
struct TraitsHelper {
    
    
     typedef T ret_type;
     typedef T par_type;
};

// 模板偏特化,处理int类型
template <>
struct TraitsHelper<int> {
    
    
     typedef int ret_type;
     typedef int par_type;
};

// 模板偏特化,处理float类型
template <>
struct TraitsHelper<float> {
    
    
     typedef float ret_type;
     typedef int par_type;
};

当函数,类或者一些封装的通用算法中的某些部分会因为数据类型不同而导致处理或逻辑不同时,traits会是一种很好的解决方案。

  1. 迭代器的种类

输入迭代器:是只读迭代器,在每个被遍历的位置上只能读取一次。例如上面find函数参数就是输入迭代器。

输出迭代器:是只写迭代器,在每个被遍历的位置上只能被写一次。

前向迭代器:兼具输入和输出迭代器的能力,但是它可以对同一个位置重复进行读和写。但它不支持operator–,所以只能向前移动。

双向迭代器:很像前向迭代器,只是它向后移动和向前移动同样容易。

随机访问迭代器:有双向迭代器的所有功能。而且,它还提供了“迭代器算术”,即在一步内可以向前或向后跳跃任意位置, 包含指针的所有操作,可进行随机访问,随意移动指定的步数。支持前面四种Iterator的所有操作,并另外支持it + nit – nit += nit -= nit1 – it2it[n]等 操作。

  1. 迭代器失效的问题

(1)插入操作

对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入 点之后的iterator失效;

对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代 器失效,但reference和pointers有效;

对于list和forward_list,所有的iterator,pointer和refercnce有效。

(2)删除操作

对于vector和string,删除点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的。

对于deque,如果删除点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失 效,其他的iterators,pointers,references有效。

对于list和forward_list,所有的iterator,pointer和refercnce有效。

对于关联容器map来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。

猜你喜欢

转载自blog.csdn.net/m0_51913750/article/details/130321192
今日推荐