c++ 面试题(2)--STL ,新特性

news2024/11/25 2:57:58

目录

1 STL

1 请说说 STL 的基本组成部分

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

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

4 请你来介绍一下 STL 的空间配置器(allocator)

6 迭代器用过吗?什么时候会失效?

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

9 说说 STL 中 resize 和 reserve 的区别

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

11 说说 map 和 unordered_map 的区别?底层实现

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

13 简述 vector 的实现原理

14 简述 STL 中的 map 的实现原理

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

16 请你来说一下 map 和 set 有什么区别,分别又是怎么实现的?

18 说说 push_back 和 emplace_back 的区别

19 STL 中 vector 与 list 具体是怎么实现的?常见操作的时间复杂度是多少?

2.新特性

1 说说 C++11 的新特性有哪些

2 说说 C++ 中智能指针和指针的区别是什么?

3 说说 C++中的智能指针有哪些?分别解决的问题以及区别?

4 简述 C++ 右值引用与转移语义

5 简述 C++ 中智能指针的特点

6 weak_ptr 能不能知道对象计数为 0,为什么?

7 weak_ptr 如何解决 shared_ptr 的循环引用问题?

8说说智能指针及其实现,shared_ptr 线程安全性,原理

9 请你回答一下智能指针有没有内存泄露的情况

11 简述一下 C++ 11 中 auto 的具体用法

12简述一下 C++11 中的可变参数模板新特性

13 简述一下 C++11 中 Lambda 新特性


1 STL

        

1 请说说 STL 的基本组成部分

参考回答
标准模板库( Standard Template Library, 简称 STL )简单说,就是一些常用数据结构和算法的模板的集合。
广义上讲 STL 分为 3 类: Algorithm (算法)、 Container (容器)和 Iterator (迭代器) ,容器和算法通过迭代器可以进行无缝地连接。
详细的说 STL 6 部分组成:容器 (Container) 、算法( Algorithm )、 迭代器( Iterator )、仿函数
Function object )、适配器( Adaptor )、空间配制器( Allocator )。
答案解析
标准模板库 STL 主要由 6 大组成部分:容器 (Container)
1. 是一种数据结构, 如 list, vector, deques ,以模板类的方法提供 。为了访问容器中的数据,可以使用由容器类输出的迭代器。
2. 算法( Algorithm
是用来 操作容器中的数据的模板函数 。例如, STL sort() 来对一 个 vector 中的数据进行排序,用
find() 来搜索一个 list 中的对象, 函数本身与他们操作的数据的结构和类型无关,因此他们可以用于
从简单数组到高度复杂容器的任何数据结构上。
3. 迭代器( Iterator
提供了访问容器中对象的方法。例如,可以使用一对迭代器指定 list vector 中的一定范围的对象。
迭代器就如同一个指针。事实上, C++ 的指针也是一种迭代器。 但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象 ;
4. 仿函数( Function object
仿函数又称之为函数对象, 其实就是重载了操作符的 struct, 没有什么特别的地方。
5. 适配器( Adaptor
简单的说就是一种接口类,专门用来修改现有类的接口,提供一中新的接口 ;或调用现有的函数来
实现所需要的功能。主要包括 3 中适配器 Container Adaptor Iterator Adaptor Function
Adaptor
6. 空间配制器( Allocator
STL 提供空间配置的系统 。其中主要工作包括两部分:
(1)对象的创建与销毁;(2)内存的获取与释放

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

参考回答
容器可以用于存放各种类型的数据(基本类型的变量,对象等)的数据结构,都是模板类,分为顺序容 器、关联式容器、容器适配器三种类型,三种类型容器特性分别如下:
1. 顺序容器
容器并非排序的,元素的插入位置同元素的值无关。包含 vector deque list ,具体实现原理如
下:
(1) vector 头文件
动态数组。元素在内存 连续存放 随机存取 任何元素都能在 常数时间完成 。在 尾端增删元素具有较
佳的性能
(2) deque 头文件
双向队列。元素在内存连续存放 随机存取任何元素都能在常数时间完成 (仅次于 vector )。在
端增删元素具有较佳的性能 (大部分情况下是常数时间)。
(3) list 头文件
双向链表。元素在 内存不连续存放 。在 任何位置增删元素都能在常数时间完成。不支持随机存取
2. 关联式容器
元素是排序的 ;插入任何元素,都按相应的排序规则来确定其位置;在 查找时具有非常好的性能
通常以 平衡二叉树的方式实现。包含 set multiset map multimap 具体实现原理如下:
(1) set/multiset 头文件
set 即集合。 set 中不允许相同元素, multiset 中允许存在相同元素。
(2) map/multimap 头文件
map set 的不同在于 map 中存放的元素有且仅有两个成员变,一个名为 first, 另一个名为 second,
map 根据 first 值对元素从小到大排序 ,并可快速地根据 first 来检索元素。
注意: map multimap 的不同在于是否允许相同 first 值的元素。
3. 容器适配器
封装了一些基本的容器,使之具备了新的函数功能 ,比如把 deque 封装一下变为一个具有 stack
能的数据结构。这新得到的数据结构就叫适配器。包含 stack,queue,priority_queue ,具体实现原
理如下:
(1) stack 头文件
栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的
项)。后进先出。
(2) queue 头文件
队列。插入只可以在尾部进行,删除、检索和修改只允许从头部进行。先进先出。
(3) priority_queue 头文件
优先级队列。内部维持某种有序,然后确保优先级最高的元素总是位于头部。最高优先级元素总是
第一个出列。

3 说说 STL map hashtable deque list 的实现原理

参考回答
map hashtable deque list 实现机理分别为红黑树、函数映射、双向队列、双向链表,他们的特性
分别如下:
1. map 实现原理
map 内部实现了一个 红黑树 (红黑树是非严格平衡的二叉搜索树,而 AVL 是严格平衡二叉搜索
树),红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表
map 的一个元素。因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树
进行的操作。 map 中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特 点就是左子
树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序
遍历可将键值按照从小到大遍历出来。
2. hashtable (也称散列表,直译作哈希表)实现原理
hashtable 采用了 函数映射的思想 记录的存储位置与记录的关键字关联起来,从而能够很快速地进
行查找。这决定了哈希表特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区
别,它能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。
3. deque 实现原理
deque 内部实现的是一个 双向队列 元素在内存连续存放 。随机存取任何元素都在常数时间完成
(仅次于 vector )。所有适用于 vector 的操作都适用于 deque 。在两端增删元素具有较佳的性能
(大部分情况下是常数时间)。
4. list 实现原理
list 内部实现的是一个 双向链表 。元素在内存不连续存放。在任何位置增删元素都能在常数时间完
成。不支持随机存取。无成员函数,给定一个下标 i ,访问第 i 个元素的内容,只能从头部挨个遍历
到第 i 个元素。

4 请你来介绍一下 STL 的空间配置器(allocator

参考回答
一般情况下 , 一个程序包括数据结构和相应的算法,而数据结构作为存储数据的组织形式,与内存空间有着密切的联系。在C++ STL 中, 空间配置器便是用来实现内存空间 ( 一般是内存,也可以是硬盘等空间 ) 分配的工具 ,他与容器联系紧密,每一种容器的空间分配都是通过空间分配器 alloctor 实现的。
答案解析
1. 两种 C++ 类对象实例化方式的异同
c++ 中,创建类对象一般分为两种方式:一种是 直接利用构造函数 , 直接构造类对象 ,如 Test
test() ;另一种是 通过 new 来实例化一个类对象 ,如 Test *pTest = new Test ;那么,这两种方式有
什么异同点呢?
我们知道,内存分配主要有三种方式:
(1) 静态存储区分配 :内存在程序 编译的时候已经分配好 ,这块内存在程序的 整个运行空间内都
存在 。如全局变量 , 静态变量等。
(2) 栈空间分配: 程序在 运行期间,函数内的局部变量通过栈空间来分配存储 (函数调用栈),
函数执行完毕返回时,相对应的栈空间被立即回收。主要是局部变量。
(3) 堆空间分配: 程序在运行期间,通过在堆空间上为数据分配存储空间,通过 malloc new
建的对象都是从堆空间分配内存,这类空间需要程序员自己来管理,必须通过 free() 或者是 delete()
函数对堆空间进行释放,否则会造成内存溢出。
那么,从 内存空间分配的角度 来对这两种方式的区别,就比较容易区分 :
(1)对于第一种方式来说,是直接通过调用 Test 类的构造函数来实例化 Test 类对象的 , 如果该实例
化对象是一个局部变量,则其是在栈空间分配相应的存储空间。
(2)对于第二种方式来说 , 就显得比较复杂。这里主要以 new 类对象来说明一下。 new 一个类对象 ,
其实是执行了两步操作:首先 , 调用 new 在堆空间分配内存 , 然后调用类的构造函数构造对象的内
容;同样,使用 delete 释放时,也是经历了两个步骤:首先调用类的析构函数释放类对象,然后调
delete 释放堆空间。
2. C++ STL 空间配置器实现
很容易想象,为了实现空间配置器,完全可以利用 new delete 函数并对其进行封装实现 STL 的空
间配置器,的确可以这样。但是,为了最大化提升效率, SGI STL 版本并没有简单的这样做,而是
采取了一定的措施,实现了更加高效复杂的空间分配策略。由于以上的构造都分为两部分,所以,
SGI STL 中,将对象的构造切分开来,分成空间配置和对象构造两部分。
内存配置操作 : 通过 alloc::allocate() 实现
内存释放操作 : 通过 alloc::deallocate() 实现
对象构造操作 : 通过 ::construct() 实现
对象释放操作 : 通过 ::destroy() 实现
关于内存空间的配置与释放, SGI STL 采用了两级配置器:一级配置器主要是考虑大块内存空间,
利用 malloc free 实现;二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片
问题,进而提升效率),采用链表 free_list 来维护内存池( memory pool ), free_list 通过 union
构实现,空闲的内存块互相挂接在一块,内存块一旦被使用,则被从链表中剔除,易于维护。
5 STL 容器用过哪些,查找的时间复杂度是多少,为什么?
参考回答
STL 中常用的容器有 vector deque list map set multimap multiset unordered_map
unordered_set 等。容器底层实现方式及时间复杂度分别如下:
1. vector
采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为:
插入 : O(N)
查看 : O(1)
删除 : O(N)
2. deque
采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为:
插入 : O(N)
查看 : O(1)
删除 : O(N)
3. list
采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为:
插入 : O(1)
查看 : O(N)
删除 : O(1)
4. map set multimap multiset
上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为 :
插入 : O(logN)
查看 : O(logN)
删除 : O(logN)
5. unordered_map unordered_set unordered_multimap unordered_multiset
上述四种容器采用哈希表实现,不同操作的时间复杂度为:
插入 : O(1) ,最坏情况 O(N)
查看 : O(1) ,最坏情况 O(N)
删除 : O(1) ,最坏情况 O(N)
注意: 容器的时间复杂度取决于其底层实现方式。

6 迭代器用过吗?什么时候会失效?

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

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

参考回答
1. 迭代器的作用
(1)用于指向顺序容器和关联容器中的元素
(2)通过迭代器可以读取它指向的元素
(3)通过非 const 迭代器还可以修改其指向的元素
2. 迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。 他只是模拟了指针的一些功能,重载了指针的一些操
作符, --> ++ -- 等。迭代器封装了指针,是一个 可遍历 STL Standard Template Library )容
器内全部或部分元素 的对象, 本质 是封装了原生指针,是指针概念的一种提升,提供了比指针更
高级的行为,相当于一种智能指针 ,他可以根据不同类型的数据结构来实现不同的 ++ -- 等操作。
迭代器返回的是对象引用而不是对象的值 ,所以 cout 只能输出迭代器使用取值后的值而不能直接输
出其自身。
3. 迭代器产生的原因
Iterator 类的访问方式就是把不同集合类的访问逻辑抽象出来, 使得不用暴露集合内部的结构而达
到循环遍历集合的效果。
答案解析
1. 迭代器
Iterator (迭代器)模式又称游标( Cursor )模式,用于提供一种方法 顺序访问一个聚合对象中各
个元素 , 而又不需暴露该对象的内部表示 。 或者这样说可能更容易理解: Iterator 模式是运用于聚
合对象的一种模式,通过运用该模式,使得我们 可以在不知道对象内部表示的情况下,按照一定顺
序(由 iterator 提供的方法)访问聚合对象中的各个元素 。 由于 Iterator 模式的以上特性:与聚合
对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如 STL list
vector stack 等容器类及 ostream_iterator 等扩展 Iterator
2. 迭代器示例:
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> v; //一个存放int元素的数组,一开始里面没有元素
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::const_iterator i; //常量迭代器
for (i = v.begin(); i != v.end(); ++i) //v.begin()表示v第一个元素迭代器指针,++i
指向下一个元素
cout << *i << ","; //*i表示迭代器指向的元素
cout << endl;
vector<int>::reverse_iterator r; //反向迭代器
for (r = v.rbegin(); r != v.rend(); r++)
cout << *r << ",";
cout << endl;
vector<int>::iterator j; //非常量迭代器
for (j = v.begin();j != v.end();j++)
*j = 100;
for (i = v.begin();i != v.end();i++)
cout << *i << ",";
return 0;
}
/* 运行结果:
1,2,3,4,
4,3,2,1,
100,100,100,100,
*/
8 说说 STL 迭代器是怎么删除元素的
参考回答
这是主要考察迭代器失效的问题。
1. 对于序列容器 vector deque 来说,使用 erase 后,后边的每个元素的迭代器都会失效,后边每个
元素都往前移动一位, erase 返回下一个有效的迭代器;
2. 对于关联容器 map set 来说,使用了 erase 后,当前元素的迭代器失效,但是其结构是红黑树,删 除当前元素,不会影响下一个元素的迭代器,所以在调用erase 之前,记录下一个元素的迭代器即 可;
3. 对于 list 来说,它使用了不连续分配的内存,并且它的 erase 方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。
答案解析
容器上迭代器分类如下表(详细实现过程请翻阅相关资料详细了解):

9 说说 STL resize reserve 的区别

参考回答
1. 首先必须弄清楚两个概念:
(1) capacity :该值在容器初始化时赋值,指的是 容器能够容纳的最大的元素的个数 。还不能通
过下标等访问,因为此时容器中还没有创建任何对象。
(2) size :指的是 此时容器中实际的元素个数。可以通过下标访问 0-(size-1) 范围内的对象
2. resize reserve 区别主要有以下几点:
(1) resize 既分配了空间,也创建了对象; reserve 表示容器预留空间,但并不是真正的创建对
象,需要通过 insert ()或 push_back ()等创建对象。
(2) resize 既修改 capacity 大小,也修改 size 大小; reserve 只修改 capacity 大小,不修改 size
小。
(3)两者的形参个数不一样。 resize 带两个参数,一个表示容器大小,一个表示初始值(默认为
0 ); reserve 只带一个参数,表示容器预留的大小。
答案解析
问题延伸:
resize reserve 既有差别,也有共同点。两个接口的 共同点 它们都保证了 vector 的空间大小
(capacity) 最少达到它的参数所指定的大小。 下面就他们的细节进行分析。
为实现 resize 的语义, resize 接口做了两个保证:
(1)保证区间 [0, new_size) 范围内数据有效,如果下标 index 在此区间内, vector[indext] 是合法的;
(2)保证区间 [0, new_size) 范围以外数据无效,如果下标 index 在区间外, vector[indext] 是非法的。 reserve只是保证 vector 的空间大小 (capacity) 最少达到它的参数所指定的大小 n 。在区间 [0, n) 范围内,
如果下标是 index vector[index] 这种访问有可能是合法的,也有可能是非法的,视具体情况而定。
以下是两个接口的源代码:

void resize(size_type new_size)
{
resize(new_size, T());
}
void resize(size_type new_size, const T& x)
{
if (new_size < size())
erase(begin() + new_size, end()); // erase区间范围以外的数据,确保区间以
外的数据无效
else
insert(end(), new_size - size(), x); // 填补区间范围内空缺的数据,确保区
间内的数据有效
}
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int> a;
cout<<"initial capacity:"<<a.capacity()<<endl;
cout<<"initial size:"<<a.size()<<endl;
/*resize改变capacity和size*/
a.resize(20);
cout<<"resize capacity:"<<a.capacity()<<endl;
cout<<"resize size:"<<a.size()<<endl;
vector<int> b;
/*reserve改变capacity,不改变resize*/
b.reserve(100);
cout<<"reserve capacity:"<<b.capacity()<<endl;
cout<<"reserve size:"<<b.size()<<endl;
return 0;
}
/* 运行结果:
initial capacity:0
initial size:0
resize capacity:20
resize size:20
reserve capacity:100
reserve size:0
*/
注意: 如果 n 大于当前的 vector 的容量 ( 是容量,并非 vector size) ,将会引起自动内存分配。所以现有的pointer,references,iterators 将会失效。而内存的重新配置会很耗时间。

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

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

11 说说 map unordered_map 的区别?底层实现

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

12 说说 vector list 的区别,分别适用于什么场景?

参考回答
vector list 区别在于 底层实现机理不同 ,因而特性和适用场景也有所不同。
vector :一维数组
特点:元素在内存连续存放,动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小 后内存也不会释放。
优点:和数组类似开辟一段连续的空间,并且支持随机访问,所以它的查找效率高其时间复杂度 O(1)
缺点:由于开辟一段连续的空间,所以插入删除会需要对数据进行移动比较麻烦,时间复杂度 O
(n),另外当空间不足时还需要进行扩容。
list :双向链表
特点:元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问。 优点:底层实现是循环双链表,当对大量数据进行插入删除时,其时间复杂度O(1) 。 缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历其时间复杂度O (n),没 有提供[] 操作符的重载。
应用场景
vector 拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除 的效率,使用vector 。list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用 list

13 简述 vector 的实现原理

参考回答
vector 底层实现原理为 一维数组 (元素在空间连续存放)。
1. 新增元素
Vector 通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内
存,将原来的数据复制过来,释放之前的内存,在插入新增的元素。插入新的数据分在最后插入
push_back 和通过迭代器在任何位置插入,这里说一下通过迭代器插入,通过迭代器与第一个元素
的距离知道要插入的位置,即 int index=iter-begin() 。这个元素后面的所有元素都向后移动一个位
置,在空出来的位置上存入新增的元素。
//新增元素
void insert(const_iterator iter,const T& t )
{
int index=iter-begin();
if (index<size_)
{
if (size_==capacity_)
{
int capa=calculateCapacity();
newCapacity(capa);
}
memmove(buf+index+1,buf+index,(size_-index)*sizeof(T));
buf[index]=t;
size_++;
}
}
2. 删除元素
删除和新增差不多,也分两种,删除最后一个元素 pop_back 和通过迭代器删除任意一个元素
erase(iter) 。通过迭代器删除还是先找到要删除元素的位置,即 int index=iter-begin(); 这个位置后
面的每个元素都想前移动一个元素的位置。同时我们知道 erase 不释放内存只初始化成默认值。
删除全部元素 clear :只是循环调用了 erase ,所以删除全部元素的时候,不释放内存。内存是在析
构函数中释放的。
//删除元素
iterator erase(const_iterator iter)
{
int index=iter-begin();
if (index<size_ && size_>0)
{
memmove(buf+index ,buf+index+1,(size_-index)*sizeof(T));
buf[--size_]=T();
}
return iterator(iter);
}
3. 迭代器 iteraotr
迭代器 iteraotr STL 的一个重要组成部分 , 通过 iterator 可以很方便的存储集合中的元素 .STL 为每个
集合都写了一个迭代器 , 迭代器其实是对一个指针的包装 , 实现一些常用的方法 , ++,--,!=,==,*,-> ,
通过这些方法可以找到当前元素或是别的元素 . vector STL 集合中比较特殊的一个 , 因为 vector 中的
每个元素都是连续的 , 所以在自己实现 vector 的时候可以用指针代替。
//迭代器的实现
template<class _Category,
class _Ty,
class _Diff = ptrdiff_t,
class _Pointer = _Ty *,
class _Reference = _Ty&>
struct iterator
{ // base type for all iterator classes
typedef _Category iterator_category;
typedef _Ty value_type;
typedef _Diff difference_type;
typedef _Diff distance_type; // retained
typedef _Pointer pointer;
typedef _Reference reference;
};

14 简述 STL 中的 map 的实现原理

参考回答
map 是关联式容器,它们的底层容器都是 红黑树 map 的所有元素都是 pair ,同时拥有实值( value
和键值( key )。 pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
1. map 的特性如下
(1) map RBTree 作为底层容器;
(2)所有元素都是键 + 值存在;
(3)不允许键重复;
(4)所有元素是通过键进行自动排序的;
(5) map 的键是不能修改的,但是其键对应的值是可以修改的。

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

参考回答
1. 迭代器和指针之间的区别
迭代器不是指针,是类模板,表现的像指针。 他只是模拟了指针的一些功能,重载了指针的一些操
作符, --> ++ -- 等。迭代器封装了指针,是一个 可遍历 STL Standard Template Library )容
器内全部或部分元素 的对象, 本质 是封装了原生指针,是指针概念的一种提升,提供了比指针更
高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的 ++ -- 等操作。
迭代器返回的是对象引用而不是对象的值 ,所以 cout 只能输出迭代器使用取值后的值而不能直接输
出其自身。
2. vector list 特性
vector 特性 动态数组。元素在内存连续存放。随机存取任何元素都在常数时间完成。在尾端增删
元素具有较大的性能(大部分情况下是常数时间)。
list 特性 双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随
机存取。
3. vector 增删元素
对于 vector 而言,删除某个元素以后,该元素后边的每个元素的迭代器都会失效,后边每个元素都
往前移动一位, erase 返回下一个有效的迭代器。
4. list 增删元素
对于 list 而言,删除某个元素,只有 指向被删除元素 的那个迭代器失效,其它迭代器不受任何影
响。

16 请你来说一下 map set 有什么区别,分别又是怎么实现的?

参考回答
1. set 是一种关联式容器,其特性如下:
(1) set RBTree 作为底层容器
(2)所得元素的只有 key 没有 value value 就是 key
(3)不允许出现键值重复
(4)所有的元素都会被自动排序
(5)不能通过迭代器来改变 set 的值,因为 set 的值就是键, set 的迭代器是 const
2. map set 一样是关联式容器,其特性如下:
(1) map RBTree 作为底层容器
(2)所有元素都是键 + 值存在
(3)不允许键重复
(4)所有元素是通过键进行自动排序的
(5) map 的键是不能修改的,但是其键对应的值是可以修改的
综上所述, map set 底层实现 都是红黑树; map set 区别 在于 map 的值不作为键,键和值是分
开的。

18 说说 push_back emplace_back 的区别

参考回答
如果要将一个临时变量 push 到容器的末尾, push_back() 需要先构造临时对象,再将这个对象拷贝到容器的末尾,而emplace_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次构造函数一次拷贝构造函数都没有调用过
}

19 STL vector list 具体是怎么实现的?常见操作的时间复杂度是多少?

参考回答
1. vector 一维数组(元素在内存连续存放)
是动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放;
如果新增大小当前大小时才会重新分配内存。
扩容方式: a. 倍放开辟三倍的内存
b. 旧的数据开辟到新的内存
c. 释放旧的内存
d. 指向新内存
2. list 双向链表(元素存放在堆中)
元素存放在堆中,每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行
数据的访问,这个特点,使得它的随机存取变得非常没有效率,因此它没有提供 [ ] 操作符的重载。
但是由于链表的特点,它可以很有效的支持任意地方的删除和插入操作。
特点: a. 随机访问不方便
b. 删除插入操作方便
3. 常见时间复杂度
(1) vector 插入、查找、删除时间复杂度分别为: O(n) O(1) O(n)
(2) list 插入、查找、删除时间复杂度分别为: O(1) O(n) O(1)

2.新特性

        

1 说说 C++11 的新特性有哪些

参考回答
C++ 新特性主要包括包含语法改进和标准库扩充两个方面,主要包括以下 11 点:
1. 语法的改进
(1)统一的初始化方法
(2)成员变量默认初始化
(3) auto 关键字 用于定义变量,编译器可以自动判断的类型(前提:定义一个变量时对其进行初
始化)
(4) decltype 求表达式的类型
(5) 智能指针 shared_ptr
(6) 空指针 nullptr (原来 NULL
(7) 基于范围的 for 循环
(8) 右值引用和 move 语义 让程序员有意识减少进行深拷贝操作,节约内存空间。
2. 标准库扩充(往 STL 里新加进一些模板类,比较好用)
(9) 无序容器(哈希表 ) 用法和功能同 map 一模一样,区别在于哈希表的效率更高
10 )正则表达式 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字
符串
11 Lambda 表达式
答案解析
1. 统一的初始化方法
C++98/03 可以使用初始化列表( initializer list )进行初始化:
        
int i_arr[3] = { 1, 2, 3 };
long l_arr[] = { 1, 3, 2, 4 };
struct A
{
int x;
int y;
} a = { 1, 2 };
但是 这种初始化方式的 适用性非常狭窄 ,只有上面提到的这两种数据类型可以使用初始化列表。在
C++11 中,初始化列表的适用性被大大增加了。它现在可以用于任何类型对象的初始化,实例如
下。
class Foo
{
public:
Foo(int) {}
private:
Foo(const Foo &);
};
int main(void)
{
Foo a1(123);
Foo a2 = 123; //error: 'Foo::Foo(const Foo &)' is private
Foo a3 = { 123 };
Foo a4 { 123 };
int a5 = { 3 };
int a6 { 3 };
return 0;
}
在上例中, a3 a4 使用了新的初始化方式来初始化对象,效果如同 a1 的直接初始化。 a5 a6
是基本数据类型的列表初始化方式。可以看到,它们的形式都是统一的。这里需要注意的是, a3
虽然使用了等于号,但它仍然是列表初始化,因此,私有的拷贝构造并不会影响到它。 a4 a6
写法,是 C++98/03 所不具备的。在 C++11 中,可以直接在变量名后面跟上初始化列表,来进行
对象的初始化。
        
2. 成员变量默认初始化
好处:构建一个类的对象不需要用构造函数初始化成员变量
//程序实例
#include<iostream>
using namespace std;
class B
{
public:
int m = 1234; //成员变量有一个初始值
int n;
};
int main()
{
B b;
cout << b.m << endl;
return 0;
}
3. auto 关键字
用于定义变量,编译器可以自动判断的类型 (前提:定义一个变量时对其进行初始化)。
//程序实例
#include <vector>
using namespace std;
int main(){
vector< vector<int> > v;
vector< vector<int> >::iterator i = v.begin();
return 0;
}
可以看出来,定义迭代器 i 的时候,类型书写比较冗长,容易出错。然而有了 auto 类型推导,我
们大可不必这样,只写一个 auto 即可。
5. decltype 求表达式的类型
decltype C++ 11 新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推
导。
(1) 为什么要有 decltype
因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚
至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。
auto decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:
auto varname = value ;
decltype ( exp ) varname = value ;
其中, varname 表示变量名, value 表示赋给变量的值, exp 表示一个表达式。
auto 根据 "=" 右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的
类型,跟 "=" 右边的 value 没有关系。
另外, auto 要求变量必须初始化,而 decltype 不要求。这很容易理解, auto 是根据变量的初始
值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。 decltype 可以写成下面的形
式:
decltype ( exp ) varname ;

 (2)代码示例

        

// decltype 用法举例
nt a = 0;
decltype(a) b = 1; //b 被推导成了 int
decltype(10.8) x = 5.5; //x 被推导成了 double
decltype(x + 100) y; //y 被推导成了 double
6. 智能指针 shared_ptr
unique_ptr weak_ptr 不同之处在于, 多个 shared_ptr 智能指针可以共同使用同一块堆内
。并且,由于该类型智能指针在 实现上采用的是引用计数机制 ,即便有一个 shared_ptr 指针放
弃了堆内存的 使用权 (引用计数减 1 ),也不会影响其他指向同一堆内存的 shared_ptr 指针(只
有引用计数为 0 时,堆内存才会被自动释放)。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
//构建 2 个智能指针
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2(p1);
//输出 p2 指向的数据
cout << *p2 << endl;
p1.reset();//引用计数减 1,p1为空指针
if (p1) {
cout << "p1 不为空" << endl;
}
else {
cout << "p1 为空" << endl;
}
//以上操作,并不会影响 p2
cout << *p2 << endl;
//判断当前和 p2 同指向的智能指针有多少个
cout << p2.use_count() << endl;
return 0;
}
/* 程序运行结果:
10
p1 为空
10
1
*/
7. 空指针 nullptr (原来 NULL
nullptr nullptr_t 类型的右值常量,专用于初始化空类型指针。 nullptr_t C++11 新增加的数
据类型,可称为 指针空值类型 。也就是说, nullpter 仅是该类型的一个实例对象(已经定义好,
可以直接使用),如果需要我们完全定义出多个同 nullptr 完全一样的实例对象。值得一提的是,
nullptr 可以被隐式转换成任意的指针类型。例如:
int * a1 = nullptr ;
char * a2 = nullptr ;
double * a3 = nullptr ;
显然,不同类型的指针变量都可以使用 nullptr 来初始化,编译器分别将 nullptr 隐式转换成 int
char 以及 double* 指针类型。另外,通过将指针初始化为 nullptr ,可以很好地解决 NULL 遗留的
问题,比如:
#include <iostream>
using namespace std;
void isnull(void *c){
cout << "void*c" << endl;
}
void isnull(int n){
cout << "int n" << endl;
}
int main() {
isnull(NULL);
isnull(nullptr);
return 0;
}
/* 程序运行结果:
int n
void*c
*/
8. 基于范围的 for 循环
如果要用 for 循环语句遍历一个数组或者容器,只能套用如下结构:
for ( 表达式 1 ; 表达式 2 ; 表达式 3 ){
// 循环体
}
 
//程序实例
#include <iostream>
#include <vector>
#include <string.h>
using namespace std;
int main() {
char arc[] = "www.123.com";
int i;
//for循环遍历普通数组
for (i = 0; i < strlen(arc); i++) {
cout << arc[i];
}
cout << endl;
vector<char>myvector(arc,arc+3);
vector<char>::iterator iter;
//for循环遍历 vector 容器
for (iter = myvector.begin(); iter != myvector.end(); ++iter) {
cout << *iter;
}
return 0;
}
/* 程序运行结果:
www.123.com
www
*/
9. 右值引用和 move 语义
1. 右值引用
C++98/03 标准中就有引用, 使用 "&" 表示。但此种引用方式有一个缺陷,即正常情况下只能
操作 C++ 中的左值,无法对右值添加引用 。举个例子:
int num = 10 ;
int & b = num ; // 正确
int & c = 10 ; // 错误
如上所示,编译器允许我们为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。
因此, C++98/03 标准中的引用又称为左值引用。
注意,虽然 C++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作
右值。也就是说,常量左值引用既可以操作左值,也可以操作右值,例如:
int num = 10 ;
const int & b = num ;
const int & c = 10 ;

我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问
题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方
式是行不通的。为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 "&&" 表示。
需要注意的,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值
进行初始化,比如:
int num = 10 ;
//int && a = num; // 右值引用不能初始化为左值
int && a = 10 ;

 和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:

int && a = 10;
a = 100;
cout << a << endl;
/* 程序运行结果:
100
*/
另外值得一提的是, C++ 语法上是支持定义常量右值引用的,例如:
         const int && a = 10 ; // 编译器不会报错
但这种定义出来的右值引用并无实际用处。一方面,右值引用主要用于移动语义和完美转
发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右
值,这项工作完全可以交给常量左值引用完成。
2. move 语义
move 本意为 " 移动 " ,但该函数并不能移动任何数据,它的功能很简单, 就是将某个左值强制
转化为右值。基于 move() 函数特殊的功能 ,其常用于实现移动语义。 move() 函数的用法也
很简单,其语法格式如下:
move ( arg ) // 其中, arg 表示指定的左值对象。该函数会返回 arg 对象的右值形式。
//程序实例
#include <iostream>
using namespace std;
class first {
public:
first() :num(new int(0)) {
cout << "construct!" << endl;
}
//移动构造函数
first(first &&d) :num(d.num) {
d.num = NULL;
cout << "first move construct!" << endl;
}
public: //这里应该是 private,使用 public 是为了更方便说明问题
int *num;
};
class second {
public:
second() :fir() {}
//用 first 类的移动构造函数初始化 fir
second(second && sec) :fir(move(sec.fir)) {
cout << "second move construct" << endl;
}
public: //这里也应该是 private,使用 public 是为了更方便说明问题
first fir;
};
int main() {
second oth;
second oth2 = move(oth);
//cout << *oth.fir.num << endl; //程序报运行时错误
return 0;
}
/* 程序运行结果:
construct!
first move construct!
second move construct
*/
10. 无序容器(哈希表)
用法和功能同 map 一模一样,区别在于哈希表的效率更高。
(1) 无序容器具有以下 2 个特点:
a. 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键,
b. 和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1) );但对
于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器。
(2) 和关联式容器一样,无序容器只是一类容器的统称,其包含有 4 个具体容器,分别为
unordered_map unordered_multimap unordered_set 以及 unordered_multiset 。功能如下
表:
        

 (3) 程序实例(以 unordered_map 容器为例)

        

#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
//创建并初始化一个 unordered_map 容器,其存储的 <string,string> 类型的键值对
std::unordered_map<std::string, std::string> my_uMap{
{"教程1","www.123.com"},
{"教程2","www.234.com"},
{"教程3","www.345.com"} };
//查找指定键对应的值,效率比关联式容器高
string str = my_uMap.at("C语言教程");
cout << "str = " << str << endl;
//使用迭代器遍历哈希容器,效率不如关联式容器
for (auto iter = my_uMap.begin(); iter != my_uMap.end(); ++iter)
{
//pair 类型键值对分为 2 部分
cout << iter->first << " " << iter->second << endl;
}
return 0;
}
/* 程序运行结果:
教程1 www.123.com
教程2 www.234.com
教程3 www.345.com
*/
11. 正则表达式
可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串。常用符号的意
义如下:

12. Lambda 匿名函数
所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式。
1 )定义
lambda 匿名函数很简单,可以套用如下的语法格式:
[ 外部变量访问方式说明符 ] ( 参数 ) mutable noexcept/throw() -> 返回值类型
{
函数体 ;
};
其中各部分的含义分别为:
a. [ 外部变量方位方式说明符 ]
[ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当
lambda 函数的函数体中可以使用哪些 外部变量
所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。
b. ( 参数 )
和普通函数的定义一样, lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果 不需要传递参数,可以连同 () 小括号一起省略;

 c. mutable

此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0 )。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。
注意 : 对于以值传递方式引入的外部变量, lambda 表达式修改的是拷贝的那一份,并不会修改真
正的外部变量;

 d. noexcept/throw()

可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0 )。默认情况下, lambda函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异 常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。

e. -> 返回值类型

指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void ,则编译器可以自行推断出返回值类型,此情况下可以直接省略 "-> 返回值类型"

 f. 函数体

        

和普通函数一样, lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传
递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。
(2)程序实例

#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int num[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行排序
sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : num){
cout << n << " ";
}
return 0;
}
/* 程序运行结果:
1 2 3 4
*/

2 说说 C++ 中智能指针和指针的区别是什么?

参考回答
1. 智能指针
如果在程序中 使用 new 从堆(自由存储区)分配内存,等到不需要时,应使用 delete 将其释放 。 C++引用了 智能指针 auto_ptr ,以帮助自动完成这个过程 。随后的编程体验(尤其是使用 STL )表
明,需要有更精致的机制。基于程序员的编程体验和 BOOST 库提供的解决方案, C++11 摒弃了 auto_ptr,并新增了三种智能指针: unique_ptr shared_ptr weak_ptr 。所有新增的智能指针都能与STL 容器和移动语义协同工作。
2. 指针
C 语言规定所有变量在使用前必须先定义,指定其类型,并按此分配内存单元。指针变量不同于整
型变量和其他类型的变量,它是专门用来存放地址的,所以必须将它定义为 指针类型
3. 智能指针和普通指针的区别
智能指针和普通指针的区别 在于智能指针实际上是 对普通指针加了一层封装机制,区别是它负责自动释放所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。

3 说说 C++中的智能指针有哪些?分别解决的问题以及区别?

参考回答
1. C++ 中的智能指针有 4 种,分别为: shared_ptr unique_ptr weak_ptr auto_ptr ,其中
auto_ptr C++11 弃用。
2. 使用智能指针的原因
申请的空间(即 new 出来的空间),在使用结束时,需要 delete 掉,否则会形成内存碎片。在程序
运行期间, new 出来的对象,在析构函数中 delete ,但是这种方法不能解决所有问题,因为有时
new 发生在某个全局函数里面,该方法会给程序员造成精神负担。 此时,智能指针就派上了用
场。 使用 智能指针可以很大程度上避免这个问题,因为智能指针就是一个类,当超出了类的作用域
时,类会自动调用析构函数,析构函数会自动释放资源。所以,智能指针的作用原理就是在函数结
束时自动释放内存空间,避免了手动释放内存空间。
3. 四种指针分别解决的问题以及各自特性如下:
(1) auto_ptr C++98 的方案, C++11 已经弃用)
采用所有权模式。
auto_ptr < string > p1 ( new string ( "I reigned loney as a cloud." ));
auto_ptr < string > p2 ;
p2 = p1 ; //auto_ptr 不会报错
此时不会报错, p2 剥夺了 p1 的所有权,但是当程序运行时访问 p1 将会报错。所以 auto_ptr 的缺点
是:存在潜在的内存崩溃问题。
(2) unique_ptr (替换 auto_ptr
unique_ptr 实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对
象。它对于避免资源泄露 ,例如,以 new 创建对象后因为发生异常而忘记调用 delete 时的情形特别
有用。采用所有权模式,和上面例子一样。
auto_ptr < string > p3 ( new string ( "I reigned loney as a cloud." ));
auto_ptr < string > p4 ;
p4 = p3 ; // 此时不会报错
编译器认为 P4=P3 非法,避免了 p3 不再指向有效数据的问题。因此, unique_ptr auto_ptr 更安
全。 另外 unique_ptr 还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源
unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁
止这么做,比如:
unique_ptr < string > pu1 ( new string ( "hello world" ));
unique_ptr < string > pu2 ;
pu2 = pu1 ; // #1 not allowed
unique_ptr < string > pu3 ;
pu3 = unique_ptr < string > ( new string ( "You" )); // #2 allowed
其中 #1 留下悬挂的 unique_ptr(pu1) ,这可能导致危害。而 #2 不会留下悬挂的 unique_ptr ,因为它
调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这
种随情况而已的行为表明, unique_ptr 优于允许两种赋值的 auto_ptr
注意: 如果确实想执行类似与 #1 的操作,要安全的重用这种指针,可给它赋新值。 C++ 有一个标准
库函数 std::move() ,让你能够将一个 unique_ptr 赋给另一个。例如:

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
(3) shared_ptr (非常好使)
shared_ptr 实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在
后一个引用被销毁 时候释放 。从名字 share 就可以看出了资源可以被多个指针共享, 它使用计数机
制来表明资源被几个指针共享。可以通过成员函数 use_count() 来查看资源的所有者个数。除了可
以通过 new 来构造,还可以通过传入 auto_ptr, unique_ptr,weak_ptr 来构造。当我们调用 release()
时,当前指针会释放资源所有权,计数减一。当计数等于 0 时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的 ), 在使用引用计数
的机制上提供了可以共享所有权的智能指针。
成员函数:
use_count 返回引用计数的个数
unique 返回是否是独占所有权 ( use_count 1)
swap 交换两个 shared_ptr 对象 ( 即交换所拥有的对象 )
reset 放弃内部对象的所有权或拥有对象的变更 , 会引起原有对象的引用计数的减少
get 返回内部对象 ( 指针 ), 由于已经重载了 () 方法 , 因此和直接使用对象是一样的 . shared_ptr
sp(new int(1)); sp sp.get() 是等价的
(4) weak_ptr
weak_ptr 是一种 不控制 对象生命周期的智能指针 , 它指向一个 shared_ptr 管理的对象 。进行该对
象的内存管理的是那个强引用的 shared_ptr weak_ptr 只是提供了对管理对象的一个访问手段。
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只
可以从一个 shared_ptr 或另一个 weak_ptr 对象构造 , 它的构造和析构 不会 引起引用记数的增加或
减少 weak_ptr 是用来解决 shared_ptr 相互引用时的死锁问题, 如果说两个 shared_ptr相互引
用,那么这两个指针的引用计数永远不可能下降为 0, 资源永远不会释放。它是对对象的一种弱引
用,不会增加对象的引用计数,和 shared_ptr 之间可以相互转化, shared_ptr 可以直接赋值给它,
它可以通过调用 lock 函数来获得 shared_ptr
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}
可以看到 fun 函数中 pa pb 之间互相引用,两个资源的引用计数为 2 ,当要跳出函数时,智能指针
pa pb 析构时两个资源引用计数会减一,但是两者引用计数还是为 1 ,导致跳出函数时资源没有被
释放( A B 的析构函数没有被调用),如果把其中一个改为 weak_ptr 就可以了,我们把类 A 里面的
shared_ptr pb_; 改为 weak_ptr pb; 运行结果如下,这样的话,资源 B 的引用开始就只有 1 ,当 pb
构时, B 的计数变为 0 B 得到释放, B 释放的同时也会使 A 的计数减一,同时 pa 析构时使 A 的计数减
一,那么 A 的计数为 0 A 得到释放。
注意 :我们不能通过 weak_ptr 直接访问对象的方法,比如 B 对象中有一个方法 print(), 我们不能这样
访问, pa->pb ->print(); 英文 pb 是一个 weak_ptr ,应该先把它转化为 shared_ptr ,如: shared_ptr
p = pa->pb_.lock(); p->print();

4 简述 C++ 右值引用与转移语义

参考回答
1. 右值引用
一般来说, 不能取地址的表达式,就是右值引用,能取地址的,就是左值
class A { };
A & r = A (); //error,A() 是无名变量,是右值
A && r = A (); //ok,r 是右值引用
1. 转移语义
m ove 本意为 " 移动 " ,但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化
为右值。 基于 move() 函数特殊的功能,其常用于实现移动语义。
答案解析
1. 右值引用
C++98/03 标准中就有引用,使用 "&" 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子:
int num = 10 ;
int & b = num ; // 正确
int & c = 10 ; // 错误

如上所示,编译器允许我们为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。因此,C++98/03 标准中的引用又称为左值引用。
注意: 虽然 C++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。也 就是说,常量左值引用既可以操作左值,也可以操作右值,例如:
int num = 10 ;
const int & b = num ;
const int & c = 10 ;
我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的。为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 "&&" 表示。 注意: 和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:
int num = 10 ;
//int && a = num; // 右值引用不能初始化为左值
int && a = 10 ;

和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:  

int && a = 10 ;
a = 100 ;
cout << a << endl ;
/* 程序运行结果:
100
*/

另外值得一提的是,C++ 语法上是支持定义常量右值引用的,例如

const int&& a = 10;//编译器不会报错 

但这种定义出来的右值引用并无实际用处。一方面,右值引用主要用于移动语义和完美转发,其中前者 需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以 交给常量左值引用完成。
1. move 语义
//程序实例
#include <iostream>
using namespace std;
class first {
public:
first() :num(new int(0)) {
cout << "construct!" << endl;
}
//移动构造函数
first(first &&d) :num(d.num) {
d.num = NULL;
cout << "first move construct!" << endl;
}
public: //这里应该是 private,使用 public 是为了更方便说明问题
int *num;
};
class second {
public:
second() :fir() {}
//用 first 类的移动构造函数初始化 fir
second(second && sec) :fir(move(sec.fir)) {
cout << "second move construct" << endl;
}
public: //这里也应该是 private,使用 public 是为了更方便说明问题
first fir;
};
int main() {
second oth;
second oth2 = move(oth);
//cout << *oth.fir.num << endl; //程序报运行时错误
return 0;
}
/* 程序运行结果:
construct!
first move construct!
second move construct
*/

5 简述 C++ 中智能指针的特点

参考回答
1. C++ 中的智能指针有 4 种,分别为: shared_ptr unique_ptr weak_ptr auto_ptr ,其中
auto_ptr C++11 弃用。
2. 为什么要使用智能指针 智能指针的作用是管理一个指针,因为存在申请的空间在函数结束时忘记释放,造成内存泄漏的情况。使用智能指针可以很大程度上避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,自动释放资源
3. 四种指针各自特性
1 auto_ptr
auto 指针存在的问题是, 两个智能指针同时指向一块内存,就会两次释放同一块资源,自然报错。
2 unique_ptr
unique 指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。
实现原理: 将拷贝构造函数和赋值拷贝构造函数申明为 private delete 。不允许拷贝构造函数和
赋值操作符,但是支持移动构造函数,通过 std:move 把一个对象指针变成右值之后可以移动给另一
unique_ptr
3 shared_ptr
共享指针可以实现多个智能指针指向相同对象,该对象和其相关资源会在引用为 0 时被销毁释放。
实现原理: 有一个引用计数的指针类型变量,专门用于引用计数,使用拷贝构造函数和赋值拷贝构
造函数时,引用计数加 1 ,当引用计数为 0 时,释放资源。
注意: weak_ptr shared_ptr 存在一个问题,当两个 shared_ptr 指针相互引用时,那么这两个指针的引 用计数不会下降为0 ,资源得不到释放。因此引入 weak_ptr weak_ptr 是弱引用, weak_ptr 的构造和析 构不会引起引用计数的增加或减少。
答案解析

6 weak_ptr 能不能知道对象计数为 0,为什么?

参考回答
不能
weak_ptr 是一种 不控制对象生命周期的智能指针 ,它 指向一个 shared_ptr 管理的对象 。进行该对象管 理的是那个引用的shared_ptr weak_ptr 只是提供了对管理 对象的一个访问手段 weak_ptr 设计的目 的只是为了配合shared_ptr 而引入的一种智能指针,配合 shared_ptr 工作,它只可以从一个 shared_ptr 或者另一个weak_ptr 对象构造, 它的构造和析构不会引起计数的增加或减少

7 weak_ptr 如何解决 shared_ptr 的循环引用问题?

参考回答
为了解决循环引用导致的内存泄漏 ,引入了弱指针 weak_ptr w eak_ptr 的构造函数 不会修改引用计数 的值 ,从而 不会对对象的内存进行管理 ,其类似一个普通指针,但是 不会指向引用计数的共享内存, 但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。

8说说智能指针及其实现,shared_ptr 线程安全性,原理

同上3

9 请你回答一下智能指针有没有内存泄露的情况

参考回答
智能指针有内存泄露的情况发生。
1. 智能指针发生内存泄露的情况
当两个对象同时使用一个 shared_ptr 成员变量指向对方,会造成循环引用,使引用计数失效,从而
导致内存泄露
2. 智能指针的内存泄漏如何解决?
为了解决循环引用导致的内存泄漏,引入了弱指针 weak_ptr weak_ptr 的构造函数不会修改引用
计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享
内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。
//程序实例
#include <memory>
#include <iostream>
using namespace std;
class Child;
class Parent{
private:
std::shared_ptr<Child> ChildPtr;
public:
void setChild(std::shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
if (this->ChildPtr.use_count()) {
}
}
~Parent() {
}
};
class Child{
private:
std::shared_ptr<Parent> ParentPtr;
public:
void setPartent(std::shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
std::weak_ptr<Parent> wpp;
std::weak_ptr<Child> wpc;
{
std::shared_ptr<Parent> p(new Parent);
std::shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
std::cout << p.use_count() << std::endl;
std::cout << c.use_count() << std::endl;
}
std::cout << wpp.use_count() << std::endl;
std::cout << wpc.use_count() << std::endl;
return 0;
}
/* 程序运行结果:
2
2
1
1
*/
上述代码中, parent 有一个 shared_ptr 类型的成员指向孩子,而 child 也有一个 shared_ptr 类型的成员 指向父亲。然后在创建孩子和父亲对象时也使用了智能指针c p ,随后将 c p 分别又赋值给 child 的智能 指针成员parent parent 的智能指针成员 child 。从而形成了一个循环引用。
10 简述一下 C++11 中四种类型转换
参考回答
C++ 中四种类型转换分别为 const_cast static_cast dynamic_cast reinterpret_cast ,四种转换
功能分别如下:
1. const_cast
const 变量转为非 const
2. static_cast
最常用,可以用于各种隐式转换,比如非 const const static_cast 可以用于类向上转换,但向下
转换能成功但是不安全。
3. dynamic_cast
只能用于含有虚函数的类转换,用于类向上和向下转换
向上转换: 指子类向基类转换。
向下转换: 指基类向子类转换。
这两种转换,子类包含父类,当父类转换成子类时可能出现非法内存访问的问题。
dynamic_cast 通过判断变量运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
dynamic_cast 可以做类之间上下转换,转换的时候会进行类型检查,类型相等成功转换,类型不等
转换失败。运用 RTTI 技术, RTTI ”Runtime Type Information” 的缩写,意思是运行时类型信息,
它提供了运行时确定对象类型的方法。在 c++ 层面主要体现在 dynamic_cast typeid vs 中虚函数
表的 -1 位置存放了指向 type_info 的指针,对于存在虚函数的类型, dynamic_cast typeid 都会去
查询 type_info
4. reinterpret_cast
        低级位级别的它用于进行底层的重新解释类型转换。 reinterpret_cast可以将一个指针或引用转换为其他类型的指针或引用,而不需要进行类型检查或转换。这种转换通常用于需要直接操作底层内存的情况,但它是一种非常危险的操作,需要谨慎使用
注意: 为什么不用 C 的强制转换: C 的强制转换表面上看起来功能强大什么都能转,但是转换不够明确, 不能进行错误检查,容易出错。

11 简述一下 C++ 11 auto 的具体用法

同上1

12简述一下 C++11 中的可变参数模板新特性

参考回答
可变参数模板 (variadic template) 使得编程者能够创建这样的模板函数和模板类,即可接受可变数量的参数。例如要编写一个函数,它可接受任意数量的参数,参数的类型只需是cout 能显示的即可,并将参 数显示为用逗号分隔的列表。
int n = 14;
double x = 2.71828;
std::string mr = "Mr.String objects!";
show_list(n, x);
show_list(x*x, '!', 7, mr); //这里的目标是定义show_list()
/* 运行结果:
14, 2.71828
7.38905, !, 7, Mr.String objects!
*/
要创建可变参数模板,需要理解几个要点:
(1)模板参数包( parameter pack );
(2)函数参数包;
(3)展开( unpack )参数包;
(4)递归。

13 简述一下 C++11 Lambda 新特性

同上1

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/949654.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

医院常见的HIS、CIS、LIS、EMR、PACS、RIS医疗信息化中的介绍

医院常见的HIS、CIS、LIS、EMR、PACS、RIS分别是&#xff1a; HIS&#xff08;Hospital Information System&#xff09;&#xff1a;医院信息系统&#xff0c;是医院管理信息化的核心系统&#xff0c;包括病人管理、医生管理、药品管理、医疗设备管理、财务管理等多个方面&am…

拿来即用修改密码功能

<template><div><!-- 重置密码 --><el-dialogtitle"修改密码"v-model"state.resetPwdDialogVisible":showClose"state.firstLogin ! 1"width"550px"close"onCancel":close-on-click-modal"false&…

环球新材国际公布2023年中期业绩:上半年业绩符合预期 公司经营稳健可持续

8月25日&#xff0c;环球新材国际&#xff08;6616.HK&#xff09;公布2023年中期业绩&#xff0c;上半年公司营收4.64亿元&#xff0c;毛利润为2.25亿元。毛利率为48.4%&#xff0c;较2022年同期略降2.8个百分点。净利润为9470万元&#xff0c;较2022年同期下降14.5%&#xff…

工业状态监测如何选择合适的无线技术?

工业领域的状态监测在提高生产效率和产品质量方面起着关键作用。过去依赖于预防性维护和例行检查的方式已经不再能满足日益复杂的生产需求&#xff0c;随着工业物联网&#xff08;IIoT&#xff09;的兴起&#xff0c;设备状态监测逐渐成为一种关键策略&#xff0c;催生了预测性…

机器学习---决策树的划分依据(熵、信息增益、信息增益率、基尼值和基尼指数)

1. 熵 物理学上&#xff0c;熵 Entropy 是“混乱”程度的量度。 系统越有序&#xff0c;熵值越低&#xff1b;系统越混乱或者分散&#xff0c;熵值越⾼。 1948年⾹农提出了信息熵&#xff08;Entropy&#xff09;的概念。 从信息的完整性上进⾏的描述&#xff1a;当系统的有序…

纵行科技与山鹰绿能达成合作,提供物联网资产管理数据服务

近日&#xff0c;纵行科技与山鹰绿能宣布双方达成深度合作关系&#xff0c;纵行科技将为山鹰绿能提供专业的物联网技术服务&#xff0c;使用物联网技术帮助山鹰绿能对循环包装载具等资产进行在线管理和数字化运营。 据悉&#xff0c;山鹰绿能是一家由山鹰国际控股的全资子公司…

Meta发布AIGC生成式人工智能模型来生成音乐与音效——AudioCraft

近年来,包括语言模型在内的生成式人工智能模型取得了巨大进步,特别是ChatGPT的发布,让大家看到了大语言模型的魅力。无论是计算机视觉,还是nlp领域的文本描述生成各种图像和视频,到执行机器翻译,文本生成等等大模型上,其都取得了令人意想不到的发展。但音乐与音频上似乎…

LLM推理部署(一):LLM七种推理服务框架总结

自从ChatGPT发布以来&#xff0c;国内外的开源大模型如雨后春笋般成长&#xff0c;但是对于很多企业和个人从头训练预训练模型不太现实&#xff0c;即使微调开源大模型也捉襟见肘&#xff0c;那么直接部署这些开源大模型服务于企业业务将会有很大的前景&#xff0c;本文将介绍七…

多张图片转为pdf怎么弄?

多张图片转为pdf怎么弄&#xff1f;在网络传输过程中&#xff0c;为了避免图片格式文件出现差错&#xff0c;并确保图片的清晰度和色彩不因不同设备而有所改变&#xff0c;常见的做法是将图片转换为PDF格式。然而&#xff0c;当涉及到多张图片时&#xff0c;逐一转换将会变得相…

如何使用Puppeteer进行新闻网站数据抓取和聚合

导语 Puppeteer是一个基于Node.js的库&#xff0c;它提供了一个高级的API来控制Chrome或Chromium浏览器。通过Puppeteer&#xff0c;我们可以实现各种自动化任务&#xff0c;如网页截图、PDF生成、表单填写、网络监控等。本文将介绍如何使用Puppeteer进行新闻网站数据抓取和聚…

SpringBoot的自动装配源码分析

文章目录 一&#xff1a;什么是自动装配二、springboot的启动流程1.调用SpringApplication&#xff08;&#xff09;的构造方法2.执行核心run方法&#xff08;&#xff09;3.执行核心prepareContext&#xff08;&#xff09;4.执行核心refreshContext&#xff08;&#xff09;5…

信创软件测试质量的四个特性是什么?

对于信创软件而言&#xff0c;需结合其自身的特点、用户单位的实际使用需求&#xff0c;选择合适的质量特性范围&#xff0c;制定恰当的测试方案&#xff0c;以最大效率发现适配问题、尽快地完成适配质量的提升。那么&#xff0c;信创软件测试质量的四个特性是什么?下面&#…

【状压+概率DP】CF678 E

Problem - E - Codeforces 题意&#xff1a; 思路&#xff1a; 首先&#xff0c;n < 18&#xff0c;应当想到状压 很明显&#xff0c;这里可以使用状压DP 设 dp[s][i] 表示&#xff0c;现在选的方案为 s &#xff0c;且我是 i 的最终胜利的概率是多少 重要的是转移 这是…

1688API技术解析,实现获得店铺的所有商品

获得店铺的所有商品可以通过1688的开放API实现。以下是对1688API的技术解析&#xff1a; 1. 注册成为开发者&#xff1a;首先&#xff0c;你需要在1688开放平台上注册成为开发者&#xff0c;并创建一个应用来获取API授权。 2. 获取授权访问令牌&#xff1a;通过使用OAuth 2.0…

Centos7 + Apache Ranger 2.4.0 部署

一、Ranger简介 Apache Ranger提供一个集中式安全管理框架, 并解决授权和审计。它可以对Hadoop生态的组件如HDFS、Yarn、Hive、Hbase等进行细粒度的数据访问控制。通过操作Ranger控制台,管理员可以轻松的通过配置策略来控制用户访问权限。 1、组件列表 # Service Name Liste…

nil、空接口和空结构体联系与区别

nil&#xff1a; nil是空&#xff0c;并不一定是空指针&#xff0c;nil是一个变量&#xff0c;类型是Type 可能是一下6中类型&#xff0c;以下6种类型的初始值 空结构体

简易虚拟培训系统-UI控件的应用4

目录 Slider组件的常用参数 示例-使用Slider控制主轴 示例-Slider控制溜板箱的移动 本文以操作面板为例&#xff0c;介绍使用Slider控件控制开关和速度。 Slider组件的常用参数 Slider组件下面包含了3个子节点&#xff0c;都是Image组件&#xff0c;负责Slider的背景、填充区…

linux————ELK(日志收集系统集群)

一、概述 一、为什么要使用ELK 日志对于分析系统、应用的状态十分重要&#xff0c;但一般日志的量会比较大&#xff0c;并且比较分散。 如果管理的服务器或者程序比较少的情况我们还可以逐一登录到各个服务器去查看、分析。但如果服务器或者程序的数量比较多了之后这种方法就显…

C++ ASIO 实现异步套接字管理

Boost ASIO&#xff08;Asynchronous I/O&#xff09;是一个用于异步I/O操作的C库&#xff0c;该框架提供了一种方便的方式来处理网络通信、多线程编程和异步操作。特别适用于网络应用程序的开发&#xff0c;从基本的网络通信到复杂的异步操作&#xff0c;如远程控制程序、高并…