目录
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