C++一些新的特性的理解
一、智能指针
为什么需要智能指针?
智能指针主要解决一下问题:
- 内存泄漏:内存手动释放,使用智能指针可以自动释放
- 共享所有权的指针的传播和释放,比如多线程使用同一个对象时析构的问题。
C++里面的四个智能指针,auto_ptr,shared_ptr,unique_ptr,weak_ptr其中后3个是C++11支持,并且第一个已经被C++弃用。
几个指针的特点
- unique_ptr独占对象的所有权,由于没有引用技术,因此性能较好。
- shared_ptr共享对象的所有权,但性能略差。
- weak_ptr配合shared_ptr,解决循环引用的问题。
三者如何选择呢?
1.1 Shared_ptr内存模型
从图中我们可以知道我们主要是保存一个原始的指针对象,还有个控制快,保存了shared_ptr的引用计数等。
1.2智能指针使用场景案例
- 使用智能指针可以自己释放占用的内存
例子:
class A
{
public:
A();
~A();
private:
};
A::A()
{
std::cout << "A Created!" << std::endl;
}
A::~A()
{
std::cout << "A Deleted!" << std::endl;
}
int main()
{
auto Aptr = std::make_shared<A>();
auto Aptr1 = new A{};
return 0;
}
可以看到,智能指针会自动释放内存
2. 使用智能指针可以自己释放占用的内存
1.3 shared_ptr共享的智能指针
std::shared_ptr使用引用计数每一个shared_ptr的拷贝都指向相同的内存,最后一个shared_ptr析构的时候,内存才会被释放。
shared_ptr共享被管理对象,同一时刻可以有多个shared_ptr拥有对象的所有权,当最后一个shared_ptr对象销毁时,被管理对象自动销毁。
简单来说,shared_ptr实现包含了两部分,
-
一个指向堆上创建的对象的裸指针,raw_ptr
-
一个指向内部隐藏的、共享的管理对象.share_count_object
-
user_count,当前这个堆上对象被多少对象引用了,简单来说就是引用计数。
1.3.1 shared_ptr的基本用法和常用函数
s.get(): 返回shared_ptr中保存的裸指针;
s.reset():重置shared_ptr;
- reset()不带参数时,若智能指针s是唯一指向该对象的指针,则释放。并置空。若智能指针P不是唯一指向该对象的指针,则引用技术减少一,同时将P置空。
- reset()带参数时,若智能指针s是唯一指向对象的指针,则释放并指向新的对象。若P不是唯一的指针,则只减少引用技术,并指向新的对象。如:
auto ptr = std::make_shared<int>(303);
ptr.reset(new int(10000));
s.use_count();
返回shared_ptr的强引用计数。
s.unique();
若use_count()为1.则返回true,否则返回false。
1. 初始化make_shared/reset
通过构造函数,std::make_shared_ptr辅助函数和reset方法来初始化shared_ptr,代码如下:
std::shared_ptr<int>p1(new int(1));
std::shared_ptr<int>p2 = p1;
std::shared_ptr<int>p3;
p3.reset(new int(1));
if (p3)
{
std::cout << "p3 is not null!" << std::endl;
}
我们应该优先使用make_shared来构造智能指针,因为它更高效
auto sp1=make_shared<int>(100);
//或
shared_ptr<int>sql=make_shared<int>(100);
//相当于
shared_ptr<int> sp1(new int(100))
不能将一个原始指针直接赋值给一个智能指针,例如,下面这种方式错误的:
std::shared_ptr<int> p = new int(1);
shared_ptr不能通过"直接将原始这种赋值"来初始化,需要通过构造函数和辅助方法来初始化。
- 对于一个未初始化的智能指针,可以通过reset方法来初始化;
- 当智能指针有值的时候调用reset会引起引用计数减1.
另外智能指针可以通过重载的bool类型来操作符来判断。
std::shared_ptr<int> p1;
p1.reset(new int(1));
std::shared_ptr<int>p2 = p1;
//引用次数此时应该是2
std::cout << "p2.use_count() =" << p2.use_count() << std::endl;
p1.reset();
std::cout << "p1.reset()\n";
//引用次数此时应该是1
std::cout << "p2.use_count() =" << p2.use_count() << std::endl;
if (!p1)
{
std::cout << "p1 is empty!\n";
}
if(!p2)
{
std::cout << "p2 is empty!\n";
}
p2.reset();
std::cout << "p2.reset()\n";
std::cout << "p2.use_count() =" << p2.use_count() << std::endl;
if (!p2)
{
std::cout << "p2 is empty!\n";
}
2.获取原始指针get
当需要获取原始指针时,可以通过get方法来返回原始指针,代码如下所示:
std::shared_ptr<int> ptr (new int(1));
int *p=ptr.get();
//不可以 delete p;
谨慎使用p.get()的返回值,如果你不知道其危险性则永远不要调用get()函数。
p.get()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发声方法,遵守一下几个约定:
- 不要保存p.get()的返回值,无论是保存为裸指针还是shared_ptr都是错误的。
- 保存为裸指针不知道什么时候就会变成空悬指针,保存为shared_ptr则产生了独立指针
- 不要delete p.get()的返回值,会导致对一块内存delete两次的错误
3. 指定删除器
如果用shared_pptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。
示例代码如下:
void DeleteIntPtr(int *p)
{
std::cout << "call DeleteIntPtr" << std::endl;
delete p;
}
int main()
{
std::shared_ptr<int> p(new int(1), DeleteIntPtr);
return 0;
}
当p的引用计数为0时,自动调用删除器DeleteIntPtr来释放对象的内存,删除器可以时一个lambda表达式,上面的写法可以改为:
std::shared_ptr<int> p(new int(1), [](int *p) {
cout << "call lambda delete p" << endl;
delete p;});
当我们用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对象,代码如下所示::
std::shared_ptr<int> p(new int[20], [](int* p) {delete[] p; });
1.3.2 使用shared_ptr要注意的问题
1. 不要用一个原始指针初始化多个shared_ptr
例如下面错误范例:
int * ptr=new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); //逻辑错误
2. 不要在函数实参中创建shared_ptr
对于下面的写法:
function(shared_ptr<int>(new int),g());
因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int ,然后调用g(),如果恰好g()发生异常,而shared_ptr还没有创建,则int内存泄漏了,正确的写法应该是先创建智能指针,代码如下:
shared_ptr<int> p(new int);
function(p,g());
3.通过shared_from_this()返回this指针
不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此,这样可能会到重复析构,看下面的例子。
class A
{
public:
std::shared_ptr<A> GetSelf()
{
return std::shared_ptr<A>(this); // 不要这么做
}
~A()
{
std::cout << "Destructor A" << std::endl;
}
};
int main()
{
std::shared_ptr<A> sp1(new A);
std::shared_ptr<A> sp2 = sp1->GetSelf();
return 0;
}
运行后调用了两次析构函数。
在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能各自析构,导致重复析构的错误。
正确返回this的shared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用积累的成员函数shared_from_this()来返回this的shared_ptr,如下所示:
class A : public std::enable_shared_from_this<A>
{
public:
std::shared_ptr<A>GetSelf()
{
return shared_from_this(); //
}
~A()
{
std::cout << "Destructor A" << std::endl;
}
};
int main()
{
std::shared_ptr<A> sp1(new A);
std::shared_ptr<A> sp2 = sp1->GetSelf(); // ok
return 0;
}
4.避免循环引用
循环引用会导致内存泄漏,比如:
class A;
class B;
class A {
public:
std::shared_ptr<B> bptr;
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
cout<< "main leave" << endl; // 循环引用导致ap bp退出了作用域都没有析构
return 0;
}
循环引用导致ap和bp的引用次数为2,在离开作用域以后,ap和BP的引用计数减为1,并不会减为0,导致两个指针都不会被析构,产生内存泄漏。
解决的方法就是把A和B任何一个成员变量改为weak_ptr,具体方法见weak_ptr章节。
1.4unique_ptr独占的智能指针
- unique_ptr是一个独占性的智能指针,不能将其赋值给另一个unique_ptr.
- unique_ptr可以指向一个数组
- unique_ptr需要确定删除器的类型
std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正确
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); // 正确
unique_ptr是一个独占型的智能指针,它不允许其他的智能智能共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr,下面的错误的示例
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制
unique_ptr不允许赋值,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。例如
unique_ptr<T> my_ptr(new T); // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确
unique_ptr<T> ptr = my_ptr; // 报错,不能复制
std::make_shared是C++11的一部分,但std::make_unique不是。它实在c++14里加入标准库的。、
auto upw1(std::make_unique<Widget>()); // with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func
使用new的版本重复了被创建对象的键入,但是make_unique函数则没有。重复类型违背了软件工程的
一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀。
除了unique_ptr的独占性, unique_ptr和shared_ptr还有一些区别,比如
- unique_ptr可以指向一个数组,代码如下所示
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
- unique_ptr指定删除器和shared_ptr有区别
std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正确
std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;}); // 错误
unique_ptr需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器,可以这样写:
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); //
正确
关于shared_ptr和unique_ptr的使用场景是要根据实际应用需求来选择。
如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个
资源就用shared_ptr。
1.5 weak_ptr弱引用的智能指针
什么是weak_ptr
weak_ptr解决了什么问题;
weak_ptr为什么能解决问题。
share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互
使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内
存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从
一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
1.5.1 weak_ptr的基本用法
- 通过use_count()方法获取当前观察资源的引用计数,如下所示:
std::shared_ptr<int> sp(new int(10));
std::weak_ptr<int> wp(sp);
std::cout << wp.use_count() << std::endl; //结果讲输出1
- 通过expired()方法判断所观察资源是否已经释放,如下所示:
std::shared_ptr<int> sp(new int(10));
std::weak_ptr<int> wp(sp);
if (wp.expired())
std::cout << "weak_ptr无效,资源已释放";
else
std::cout << "weak_ptr有效";
- 通过lock方法获取监视的shared_ptr,如下所示:
lock有什么用处?
std::weak_ptr<int> gw;
void f2()
{
std::cout << "lock\n";
auto spt = gw.lock(); // 锁好资源再去判断是否有效
std::this_thread::sleep_for(std::chrono::seconds(2));
if (gw.expired()) {
std::cout << "gw Invalid, resource released\n";
}
else {
std::cout << "gw Valid, *spt = " << *spt << std::endl;
}
}
int main()
{
{
auto sp = std::make_shared<int>(42);
gw = sp;
std::thread([&]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "sp reset\n";
sp.reset();
}).detach();
f2();
}
f2();
return 0;
}
1.5.2 weak_ptr返回this指针
shared_ptr章节中提到不能直接将this指针返回shared_ptr,需要通过派生
std::enable_shared_from_this类,并通过其方法shared_from_this来返回指针,原因是
std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用
shared_from_this()方法是,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回,再看
前面的范例
#include <iostream>
#include <memory>
using namespace std;
class A: public std::enable_shared_from_this<A>
{
public:
shared_ptr<A>GetSelf()
{
return shared_from_this(); //
}
~A()
{
cout << "Destructor A" << endl;
}
};
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf(); // ok
return 0;
}
输出结果如下:
Destructor A
在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内
部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,spy的引用计数减为0,A对象会
被析构,不会出现A对象被析构两次的问题。
需要注意的是,获取自身智能指针的函数尽在shared_ptr的构造函数被调用之后才能使用,因为
enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。
1.5.3 weak_ptr解决循环引用问题
在shared_ptr章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过
weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A {
public:
std::weak_ptr<B> bptr; // 修改为weak_ptr
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
cout<< "main leave" << endl;
return 0;
}
这样在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是weak_ptr,它并不会增加引用计数,所
以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,A指针会被析构,析构后其内
部的bptr的引用计数会被减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发
生内存泄漏。
1.5.4 weak_ptr使用注意事项
1. weak_ptr在使用前需要检查合法性。
weak_ptr<int> wp;
{
shared_ptr<int> sp(new int(1)); //sp.use_count()==1
wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
shared_ptr<int> sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}
shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0
因为上述代码中sp和sp_ok离开了作用域,其容纳的K对象已经被释放了。
得到了一个容纳NULL指针的sp_null对象。在使用wp前需要调用wp.expired()函数判断一下。
因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后
一个weak_ptr对象被析构,这块“堆”存储块才能被回收。否则weak_ptr无法直到自己所容纳的那个指针
资源的当前状态。
如果shared_ptr sp_ok和weak_ptr wp;属于同一个作用域呢?如下所示:
weak_ptr<int> wp;
shared_ptr<int> sp_ok;
{
shared_ptr<int> sp(new int(1)); //sp.use_count()==1
wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}
if(wp.expired()) {
cout << "shared_ptr is destroy" << endl;
} else {
cout << "shared_ptr no destroy" << endl;
}
1.6 智能指针安全性问题
引用计数本身是安全的,至于智能指针是否安全需要结合实际使用分情况讨论:
情况1:多线程代码操作的是同一个shared_ptr的对象,此时是不安全的。
比如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr.
std::thread td([&sp1]()){....});
又或者通过回调函数的参数传入的shared_ptr对象,参数类型引用
void fn(shared_ptr<A>&sp) {
...
}
..
std::thread td(fn, sp1);
这时候必然不是线程安全的。
情况2:多线程代码操作的不是同一个shared_ptr的对象
这里指的是管理的数据是同一份,而shared_ptr不是同一个对象。比如多线程回调的lambda的是按值捕
获的对象。
std::thread td([sp1]()){....});
另个线程传递的shared_ptr是值传递,而非引用:
void fn(shared_ptr<A>sp) {
...
}
..
std::thread td(fn, sp1);
这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的
对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的。
也就是说,如下操作是安全的。
void fn(shared_ptr<A>sp) {
...
if(..){
sp = other_sp;
} else {
sp = other_sp2;
}
}
需要注意:所管理数据的线程安全性问题。显而易见,所管理的对象必然不是线程安全的,必然 sp1、
sp2、sp3智能指针实际都是指向对象A, 三个线程同时操作对象A,那对象的数据安全必然是需要对象
A自己去保证。
2.什么是左值引用、右值引用
引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝。
2.1 左值引用
左值引用:能指向左值,不能指向右值的就是左值引用
但是,const左值引用是可以指向右值的:
const int &ref_a = 5; // 编译通过
const左值引用不会修改指向值,值,因此可以指向右值,这也是为什么要使用 const &
作为函数参数的原因之一,如 std::vector
的 push_back
:
void push_back (const value_type& val);
如果没有const
,vec.push_back(5)
这样的代码就无法编译通过。
2.2 右值引用
再看下右值引用,右值引用的标志是 && ,顾名思义,右值引用专门为右值而生,可以指向右值,不能指
向左值:
int &&ref_a_right = 5; // ok
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
ref_a_right = 6; // 右值引用的用途:可以修改右值
2.3对左右值引用本质的讨论
我们必须先掌握这个概念——值类别。
值类别是 C++11 标准中新引入的概念,具体来说它是表达式的一种属性,该属性
将表达式分为 3 个类别,它们分别是左值(lvalue)、纯右值(prvalue)和将亡值
(xvalue),如图 6-1 所示。从前面的内容中我们知道早在 C++98 的时候,已经有了
一些关于左值和右值的概念了,只不过当时这些概念对于 C++程序编写并不重要。
但是由于 C++11 中右值引用的出现,值类别被赋予了全新的含义。可惜的是,在
C++11 标准中并没能够清晰地定义它们,比如在 C++11 的标准文档中,左值的概
念只有一句话:“指定一个函数或一个对象”,这样的描述显然是不清晰的。这种
糟糕的情况一直延续到 C++17 标准的推出才得到解决。所以现在是时候让我们重
新认识这些概念了。
表达式首先被分为了泛左值(glvalue)和右值(rvalue),其中泛左值被进一步划
分为左值和将亡值,右值又被划分为将亡值和纯右值。理解这些概念的关键在于泛
左值、纯右值和将亡值。
1.所谓泛左值是指一个通过评估能够确定对象、位域或函数的标识的表达式。
简单来说,它确定了对象或者函数的标识(具名对象)。
2.而纯右值是指一个通过评估能够用于初始化对象和位域,或者能够计算运算
符操作数的值的表达式。
3.将亡值属于泛左值的一种,它表示资源可以被重用的对象和位域,通常这是
因为它们接近其生命周期的末尾,另外也可能是经过右值引用的转换产生的。
2.3.1 右值引用有办法指向左值吗?
有办法,使用 std::move
:
nt a = 5; // a是个左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
cout << a; // 打印结果:5
在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值
了?并不是,打印出a的值仍然是5。
std::move
是一个非常有迷惑性的函数:
- 不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量;
- 但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:
static_cast<T&&>(lvalue)
。 所以,单纯的std::move(xxx)
不会有性能提升。
同样的,**右值引用能指向右值****,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move
指向该左值:
int &&ref_a = 5;
ref_a = 6;
等同于以下代码:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;
// 此时temp等于?
2.3.2 左值引用、右值引用本身是左值还是右值?
被声明出来的左、右值引用都是左值.因为被声明出的左右值引用是有地址的,也位于等号左边。仔细
看下边代码:
// 形参是个右值引用
void change(int&& right_value) {
right_value = 8;
}
int main() {
int a = 5; // a是个左值
int &ref_a_left = a; // ref_a_left是个左值引用
int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
change(a); // 编译不过,a是左值,change参数要求右值
change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
change(std::move(a)); // 编译通过
change(std::move(ref_a_right)); // 编译通过
change(std::move(ref_a_left)); // 编译通过
change(5); // 当然可以直接接右值,编译通过
cout << &a << ' ';
cout << &ref_a_left << ' ';
cout << &ref_a_right;
// 打印这三个左值的地址,都是一样的
}
看完后你可能有个问题,std::move会返回一个右值引用int &&
,它是左值还是右值呢? 从表达式int &&ref = std::move(a)
来看,右值引用ref
指向的必须是右值,所以move返回的int &&
是个右值。
所以右值引用既可能是左值,又可能是右值吗? 确实如此:右值引用既可以是左值也可以是右值,如果
有名称则为左值,否则是右值。
或者说:作为函数返回值的&&是右值,直接声明出来的&&是左值.这同样也符合前面章节对左值,
右值的判定方式:其实引用和普通变量是一样的, int &&ref = std::move(a) 和 int a = 5 没有什
么区别,等号左边就是左值,右边就是右值。
最后,从上述分析中我们得到如下结论:
- 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
- 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左
值引用也能指向右值)。 - 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修
改,有一定局限性。
void f(const int& n) {
n += 1; // 编译失败,const左值引用不能修改指向变量
}
void f2(int && n) {
n += 1; // ok
}
int main() {
f(5);
f2(5);
}
3 右值引用和std::move使用场景
std::move
只是类型转换工具,不会对性能有好处;
右值引用在作为函数形参时更具灵活性。他们有什么实际应用场景吗?
3.1 右值引用优化性能,避免深拷贝
浅拷贝重复释放
对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的
重复删除,比如下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 运行报错
}
cout << "main finish" << endl;
return 0;
}
打印
constructor A
constructor A
ready return
destructor A, m_ptr:000002628A9323D0
destructor A, m_ptr:000002628A92A0B0
destructor A, m_ptr:000002628A9323D0
深拷贝构造函数
在上面的代码中,默认构造函数是浅拷贝,main函数的 a 和Get函数的 b 会指向同一个指针 m_ptr,在
析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 正确运行
}
cout << "main finish" << endl;
return 0;
}
constructor A
constructor A
ready return
copy constructor A
destructor A, m_ptr:000002455ECEA0B0
destructor A, m_ptr:000002455ECEA070
destructor A, m_ptr:000002455ECEA0F0
main finish
移动构造函数
这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造
就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对
象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,
带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
}
// 移动构造函数,可以浅拷贝
A(A&& a) :m_ptr(a.m_ptr) {
a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr
cout << "move constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
if(m_ptr)
delete m_ptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 正确运行
}
cout << "main finish" << endl;
return 0;
}
constructor A
constructor A
ready return
move constructor A
destructor A, m_ptr:0000000000000000
destructor A, m_ptr:000002640DF0A030
destructor A, m_ptr:000002640DF0A070
main finish
上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。从移动构造函数的实现
中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了
对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时
值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷
贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义),右值引用的一个重
要目的是用来支持移动语义的。
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少
不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护
(创建和销毁)对性能的影响。
3.2 移动(move )语义
move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要move语
义起作用,核心在于需要对应类型的构造函数支持。
//2-3-2-move
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string.h>
using namespace std;
class MyString {
private:
char* m_data;
size_t m_len;
void copy_data(const char *s) {
m_data = new char[m_len+1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
public:
MyString() {
m_data = NULL;
m_len = 0;
}
MyString(const char* p) {
m_len = strlen (p);
copy_data(p);
}
MyString(const MyString& str) {
m_len = str.m_len;
copy_data(str.m_data);
std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
m_len = str.m_len;
copy_data(str.m_data);
}
std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl;
return *this;
}
// 用c++11的右值引用来定义这两个函数
MyString(MyString&& str) {
std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
MyString& operator=(MyString&& str) {
std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;
if (this != &str) {
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
return *this;
}
virtual ~MyString() {
if (m_data) free(m_data);
}
};
int main()
{
MyString a;
a = MyString("Hello"); // Move Assignment
MyString b = a; // Copy Constructor
MyString c = std::move(a); // Move Constructor is called! 将左值转为右值
std::vector<MyString> vec;
vec.push_back(MyString("World")); // Move Constructor is called!
return 0;
}
有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引
用的拷贝构造函数和赋值函数,以提高应用程序的效率
3.3 forward 完美转发
forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左
值,若是右值,则传递之后仍然是右值。
现存在一个函数
Template<class T>
void func(T &&val);
根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。
但要注意,引用以后,这个val值它本质上是一个左值!
看下面例子
int &&a = 10;
int &&b = a; //错误
注意这里,a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这
是不对的。
因此我们有了std::forward()完美转发,这种T &&val中的val是左值,但如果我们用std::forward (val),
就会按照参数原来的类型转发;
int &&a = 10;
int &&b = std::forward<int>(a);
这样是正确的!
通过范例巩固下知识:
//2-3-3-forward1
#include <iostream>
using namespace std;
template <class T>
void Print(T &t)
{
cout << "L" << t << endl;
}
template <class T>
void Print(T &&t)
{
cout << "R" << t << endl;
}
template <class T>
void func(T &&t)
{
Print(t);
Print(std::move(t));
Print(std::forward<T>(t));
}
int main()
{
cout << "-- func(1)" << endl;
func(1);
int x = 10;
int y = 20;
cout << "-- func(x)" << endl;
func(x); // x本身是左值
cout << "-- func(std::forward<int>(y))" << endl;
func(std::forward<int>(y)); //
return 0;
}
-- func(1)
L1
R1
R1
-- func(x)
L10
R10
L10
-- func(std::forward
L20
R20
R20
解释:
func(1) :由于1是右值,所以未定的引用类型T&&v被一个右值初始化后变成了一个右值引用,但是在
func()函数体内部,调用PrintT(v) 时,v又变成了一个左值(因为在std::forward里它已经变成了一个具
名的变量,所以它是一个左值),因此,示例测试结果第一个PrintT被调用,打印出“L1"
调用PrintT(std::forward(v))时,由于std::forward会按参数原来的类型转发,因此,它还是一个右值
(这里已经发生了类型推导,所以这里的T&&不是一个未定的引用类型,会调用void PrintT(T&&t)函
数打印 “R1”.调用PrintT(std::move(v))是将v变成一个右值(v本身也是右值),因此,它将输出”R1"
func(x)未定的引用类型T&&v被一个左值初始化后变成了一个左值引用,因此,在调用
PrintT(std::forward(v))时它会被转发到void PrintT(T&t).
forward将左值转换为右值:
MyString str1 = "hello";
MyString str2(str1);
MyString str3 = Fun();
MyString str4 = move(str2);
MyString str5(forward<MyString>(str3));
综合示例
//2-3-3-forward2
#include "stdio.h"
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class A
{
public:
A() : m_ptr(NULL), m_nSize(0) {}
A(int *ptr, int nSize)
{
m_nSize = nSize;
m_ptr = new int[nSize];
printf("A(int *ptr, int nSize) m_ptr:%p\n", m_ptr);
if (m_ptr)
{
memcpy(m_ptr, ptr, sizeof(sizeof(int) * nSize));
}
}
A(const A &other) // 拷贝构造函数实现深拷贝
{
m_nSize = other.m_nSize;
if (other.m_ptr)
{
printf("A(const A &other) m_ptr:%p\n", m_ptr);
if(m_ptr)
delete[] m_ptr;
printf("delete[] m_ptr\n");
m_ptr = new int[m_nSize];
memcpy(m_ptr, other.m_ptr, sizeof(sizeof(int) * m_nSize));
}
else
{
if(m_ptr)
delete[] m_ptr;
m_ptr = NULL;
}
cout << "A(const int &i)" << endl;
}
// 右值引用移动构造函数
A(A &&other)
{
m_ptr = NULL;
m_nSize = other.m_nSize;
if (other.m_ptr)
{
m_ptr = move(other.m_ptr); // 移动语义
other.m_ptr = NULL;
}
}
~A()
{
if (m_ptr)
{
delete[] m_ptr;
m_ptr = NULL;
}
}
void deleteptr()
{
if (m_ptr)
{
delete[] m_ptr;
m_ptr = NULL;
}
}
int *m_ptr = NULL; // 增加初始化
int m_nSize = 0;
};
int main()
{
int arr[] = {1, 2, 3};
A a(arr, sizeof(arr) / sizeof(arr[0]));
cout << "m_ptr in a Addr: 0x" << a.m_ptr << endl;
A b(a);
cout << "m_ptr in b Addr: 0x" << b.m_ptr << endl;
b.deleteptr();
A c(std::forward<A>(a)); // 完美转换
cout << "m_ptr in c Addr: 0x" << c.m_ptr << endl;
c.deleteptr();
vector<int> vect{1, 2, 3, 4, 5};
cout << "before move vect size: " << vect.size() << endl;
vector<int> vect1 = move(vect);
cout << "after move vect size: " << vect.size() << endl;
cout << "new vect1 size: " << vect1.size() << endl;
return 0;
}
3.4 emplace_back 减少内存拷贝和移动
对于STL容器,C++11后引入了emplace_back接口。
emplace_back是就地构造,不用构造后再次复制到容器中。因此效率更高。
考虑这样的语句:
vector<string> testVec;
testVec.push_back(string(16, 'a'));
上述语句足够简单易懂,将一个string对象添加到testVec中。底层实现:
- 首先,string(16, ‘a’)会创建一个string类型的临时对象,这涉及到一次string构造过程。
- 其次,vector内会创建一个新的string对象,这是第二次构造。
- 最后在push_back结束时,最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次
string构造和一次析构。
c++11可以用emplace_back代替push_back,emplace_back可以直接在vector中构建一个对象,而非
创建一个临时对象,再放进vector,再销毁。emplace_back可以省略一次构建和一次析构,从而达到优
化的目的。
//2-5-emplace_back
#include <vector>
#include <string>
#include "time_interval.h"
int main() {
std::vector<std::string> v;
int count = 10000000;
v.reserve(count); //预分配十万大小,排除掉分配内存的时间
{
TIME_INTERVAL_SCOPE("push_back string:");
for (int i = 0; i < count; i++)
{
std::string temp("ceshi");
v.push_back(temp);// push_back(const string&),参数是左值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("push_back move(string):");
for (int i = 0; i < count; i++)
{
std::string temp("ceshi");
v.push_back(std::move(temp));// push_back(string &&), 参数是右值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("push_back(string):");
for (int i = 0; i < count; i++)
{
v.push_back(std::string("ceshi"));// push_back(string &&), 参数是右值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("push_back(c string):");
for (int i = 0; i < count; i++)
{
v.push_back("ceshi");// push_back(string &&), 参数是右值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("emplace_back(c string):");
for (int i = 0; i < count; i++)
{
v.emplace_back("ceshi");// 只有一次构造函数,不调用拷贝构造函数,速度最快
}
}
}
结果
push_back string:9439 ms
push_back move(string):8566 ms
push_back(string):7990 ms
push_back(c string):8427 ms
emplace_back(c string):3581 ms
3.5 小结
C++11 在性能上做了很大的改进,最大程度减少了内存移动和复制,通过右值引用、 forward、
emplace 和一些无序容器我们可以大幅度改进程序性能。
- 右值引用仅仅是通过改变资源的所有者来避免内存的拷贝,能大幅度提高性能。
- forward 能根据参数的实际类型转发给正确的函数。
- emplace 系列函数通过直接构造对象的方式避免了内存的拷贝和移动。
四 匿名函数lambda
重点:
- 怎么传递参数
- 传引用还是传值
4.1匿名函数的基本语法
[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}
语法规则:lambda表达式可以看成是一般函数的函数名被略去,返回值使用了一个 -> 的形式表示。唯
一与普通函数不同的是增加了“捕获列表”。
//[捕获列表](参数列表)->返回类型{函数体}
int main()
{
auto Add = [](int a, int b)->int {
return a + b;
};
std::cout << Add(1, 2) << std::endl; //输出3
return 0;
}
一般情况下,编译器可以自动推断出lambda表达式的返回类型,所以我们可以不指定返回类型,即:
//[捕获列表](参数列表){函数体}
int main()
{
auto Add = [](int a, int b) {
return a + b;
};
std::cout << Add(1, 2) << std::endl; //输出3
return 0;
}
但是如果函数体内有多个return语句时,编译器无法自动推断出返回类型,此时必须指定返回类型。
//[捕获列表](参数列表){函数体}
int main()
{
auto Add = [](int a, int b) {
return a + b;
};
std::cout << Add(1, 2) << std::endl; //输出3
return 0;
}
但是如果函数体内有多个return语句时,编译器无法自动推断出返回类型,此时必须指定返回类型。
4.2 捕获列表
有时候,需要在匿名函数内使用外部变量,所以用捕获列表来传递参数。根据传递参数的行为,捕获列
表可分为以下几种:
1 值捕获
与参数传值类似,值捕获的前提是变量可以拷贝,不同之处则在于,被捕获的变量在 lambda表达式被
创建时拷贝,而非调用时才拷贝:
void test3()
{
cout << "test3" << endl;
int c = 12;
int d = 30;
auto Add = [c, d](int a, int b)->int {
cout << "d = " << d << endl;
return c;
};
d = 20;
std::cout << Add(1, 2) << std::endl;
}
2 引用捕获
与引用传参类似,引用捕获保存的是引用,值会发生变化。
如果Add中加入一句:c = a;
void test5()
{
cout << "test5" << endl;
int c = 12;
int d = 30;
auto Add = [&c, &d](int a, int b)->int {
c = a; // 编译对的
cout << "d = " << d << endl;
return c;
};
d = 20;
std::cout << Add(1, 2) << std::endl;
}
3 隐式捕获
手动书写捕获列表有时候是非常复杂的,这种机械性的工作可以交给编译器来处理,这时候可以在捕获
列表中写一个 & 或 = 向编译器声明采用引用捕获或者值捕获。
void test7()
{
cout << "test7" << endl;
int c = 12;
int d = 30;
// 把捕获列表的&改成=再测试
auto Add = [&](int a, int b)->int {
c = a; // 编译对的
cout << "d = " << d << endl;
return c;
};
d = 20;
std::cout << Add(1, 2) << std::endl;
std::cout << "c:" << c<< std::endl;
}
4 空捕获列表
捕获列表'[]'中为空,表示Lambda不能使用所在函数中的变量。
void test8()
{
cout << "test7" << endl;
int c = 12;
int d = 30;
// 把捕获列表的&改成=再测试
auto Add = [](int a, int b)->int {
cout << "d = " << d << endl; // 编译报错
return c;// 编译报错
};
d = 20;
std::cout << Add(1, 2) << std::endl;
std::cout << "c:" << c<< std::endl;
}
5 表达式捕获
上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,
而不能捕获右值。
C++14之后支持捕获右值,允许捕获的成员用任意的表达式进行初始化,被声明的捕获变量类型会根据
表达式进行判断,判断方式与使用 auto 本质上是相同的:
void test9()
{
cout << "test9" << endl;
auto important = std::make_unique<int>(1);
auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
return x + y + v1 + (*v2);
};
std::cout << add(3,4) << std::endl;
}
6 泛型 Lambda
在C++14之前,lambda表示的形参只能指定具体的类型,没法泛型化。从 C++14 开始, Lambda 函数
的形式参数可以使用 auto关键字来产生意义上的泛型:
//泛型 Lambda C++14
void test10()
{
cout << "test10" << endl;
auto add = [](auto x, auto y) {
return x+y;
};
std::cout << add(1, 2) << std::endl;
std::cout << add(1.1, 1.2) << std::endl;
}
7 可变lambda
- 采用值捕获的方式,lambda不能修改其值,如果想要修改,使用mutable修饰
- 采用引用捕获的方式,lambda可以直接修改其值
void test12() {
cout << "test12" << endl;
int v = 5;
// 值捕获方式,使用mutable修饰,可以改变捕获的变量值
auto ff = [v]() mutable {return ++v;};
v = 0;
auto j = ff(); // j为6
}
void test13() {
cout << "test13" << endl;
int v = 5;
// 采用引用捕获方式,可以直接修改变量值
auto ff = [&v] {return ++v;};
v = 0;
auto j = ff(); // v引用已修改,j为1
}
总结
- 如果捕获列表为[&],则表示所有的外部变量都按引用传递给lambda使用;
- 如果捕获列表为[=],则表示所有的外部变量都按值传递给lambda使用;
- 匿名函数构建的时候对于按值传递的捕获列表,会立即将当前可以取到的值拷贝一份作为常数,然
后将该常数作为参数传递。
Lambda捕获列表总结
[] | 空捕获列表,Lambda不能使用所在函数中的变量。 |
---|---|
[names] | names是一个逗号分隔的名字列表,这些名字都是Lambda所在函数的局部变量。默认情况这些变量会被拷贝,然后按值传递,名字前面如果使用了&,则按引用传递 |
[&] | 隐式捕获列表,Lambda体内使用的局部变量都按引用方式传递 |
[=] | 隐式捕获列表,Lanbda体内使用的局部变量都按值传递 |
[&,identifier_list] | identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量,这些变量采用值捕获的方式,其他变量则被隐式捕获,采用引用方式传递,identifier_list中的名字前面不能使用&。 |
[=,identifier_list] | identifier_list中的变量采用引用方式捕获,而被隐式捕获的变量都采用按值传递的方式捕获。identifier_list中的名字不能包含this,且这些名字面前必须使用&。 |
五 C++11标准库(STL)
STL定义了强大的、基于模板的、可复用的组件,实现了许多通用的数据结构及处理这些数据结构的算
法。其中包含三个关键组件——容器(container,流行的模板数据结构)、迭代器(iterator)和算法
(algorithm)。
组件 | 描述 |
---|---|
容器 | 容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等。 |
迭代器 | 迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。 |
算法 | 算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。 |
5.1 容器简介
STL容器,可将其分为四类:序列容器、有序关联容器、无序关联容器、容器适配器、序列容器:
标准库容器类 | 描述 |
---|---|
array | 固定大小,直接访问任意元素 |
deque | 从前部或后部进行快速插入和删除操作,直接访问任何元素 |
forward_list | 单链表,在任意位置快速插入和删除 |
list | 双向链表,在任意位置进行快速插入和删除操作 |
vector | 从后部进行快速插入和删除操作,直接访问任意元素 |
有序关联容器(键按顺序保存):
标准库容器类 | 描述 |
---|---|
set | 快速查找,无重复元素 |
multiset | 快速查找,可有重复元素 |
map | 一对一映射,无重复元素,基于键快速查找 |
multimap | 一对一映射,可有重复元素,基于键快速查找 |
无序关联容器:
标准库容器类 | 描述 |
---|---|
unordered_set | 快速查找,无重复元素 |
unordered_multiset | 快速查找,可有重复元素 |
unordered_map | 一对一映射,无重复元素,基于键快速查找 |
unordered_multimap | 一对一映射,可有重复元素,基于键快速查找 |
容器适配器:
标准库容器类 | 描述 |
---|---|
stack | 后进先出(LIFO) |
queue | 先进先出(FIFO) |
priority_queue | 优先级最高的元素先出 |
序列容器描述了线性的数据结构(也就是说,其中的元素在概念上” 排成一行"), 例如数组、向量和 链
表。
关联容器描述非线性的容器,它们通常可以快速锁定其中的元素。这种容器可以存储值的集合或者键-
值对
栈和队列都是在序列容器的基础上加以约束条件得到的,因此STL把stack和queue作为容器适配器来实
现,这样就可以使程序以一种约束方式来处理线性容器。类型string支持的功能跟线性容器一样, 但是
它只能存储字符数据.
5.2 迭代器简介
迭代器在很多方面与指针类似,也是用于指向首类容器中的元素(还有一些其他用途,后面将会提
到)。 迭代器存有它们所指的特定容器的状态信息,即迭代器对每种类型的容器都有一个实现。 有些迭
代器的操作在不同容器间是统一的。 例如,*运算符间接引用一个迭代器,这样就可以使用它所指向的
元素。++运算符使得迭代器指向容器中的下一个元素(和数组中指针递增后指向数组的下一个元素类
似)。
STL 首类容器提供了成员函数 begin 和 end。函数 begin 返回一个指向容器中第一个元素的迭代器,函
数 end 返回一个指向容器中最后一个元素的下一个元素(这个元素并不存在,常用于判断是否到达了容
器的结束位仅)的迭代器。 如果迭代器 i 指向一个特定的元素,那么 ++i 指向这个元素的下一个元素。*
i 指代的是i指向的元素。 从函数 end 中返回的迭代器只在相等或不等的比较中使用,来判断这个“移动
的迭代器” (在这里指i)是否到达了容器的末端。
使用一个 iterator 对象来指向一个可以修改的容器元素,使用一个 const_iterator 对象来指向一个不能
修改的容器元素。
类型 | 描述 |
---|---|
随机访问迭代器(randomaccess) | 在双向迭代湍基础上增加了直接访问容器中任意元素的功能, 即可以向前或 |
向后跳转任意个元素 | |
双向迭代器(bidirectional) | 在前向迭代器基础上增加了向后移动的功能。支持多遍扫描算法 |
前向迭代器(forword) | 综合输入和输出迭代器的功能,并能保持它们在容器中的位置(作为状态信息),可以使用同一个迭代器两次遍历一个容器(称为多遍扫描算法) |
输出迭代器(output) | 用于将元素写入容器。 输出迭代楛每次只能向前移动一个元索。 输出迭代器只支持一遍扫描算法,不能使用相同的输出迭代器两次遍历一个序列容器 |
输入迭代器(input) | 用于从容器读取元素。 输入迭代器每次只能向前移动一个元素。 输入迭代器只支持一遍扫描算法,不能使用相同的输入迭代器两次遍历一个序列容器 |
每种容器所支持的迭代器类型决定了这种容器是否可以在指定的 STL 算 法中使用。 支持随机访问迭代
器的容器可用于所有的 STL 算法(除了那些需要改变容器大小的算法,这样的算法不能在数组和 array
对象中使用)。 指向 数组的指针可以代替迭代器用于几乎所有的 STL 算法中,包括那些要求随机访问
迭代器的算法。 下表显示了每种 STL 容器所支持的迭代器类型。 注意, vector 、 deque 、 list 、 set、 multiset 、 map 、 multimap以及 string 和数组都可以使用迭代器遍历.
容器 | 支持的迭代器类型 | 容器 | 支持的迭代器类型 |
---|---|---|---|
vector | 随机访问迭代器 | set | 双向迭代器 |
array | 随机访问迭代器 | multiset | 双向迭代器 |
deque | 随机访问迭代器 | map | 双向迭代器 |
list | 双向迭代器 | multimap | 双向迭代器 |
forword_list | 前向迭代器 | unordered_set | 双向迭代器 |
stack | 不支持迭代器 | unordered_multiset | 双向迭代器 |
queue | 不支持迭代器 | unordered_map | 双向迭代器 |
priority_queue | 不支持迭代器 | unordered_multimap | 双向迭代器 |
下表显示了在 STL容器的类定义中出现的几种预定义的迭代器 typedef。不是每种 typedef 都出现在每
个容器中。 我们使用常量版本的迭代器来访问只读容器或不应该被更改的非只读容器,使用反向迭代器
来以相反的方向访问容器。
为迭代器预先定义的typedef | ++的方向 | 读写能力 |
---|---|---|
iterator | 向前 | 读/写 |
const_iterator | 向前 | 读 |
reverse_iterator | 向后 | 读/写 |
const_reverse_iterator | 向后 | 读 |
下表显示了可作用在每种迭代器上的操作。 除了给出的对于所有迭代器都有的运算符,迭代器还必须提
供默认构造函数、拷贝构造函数和拷贝赋值操作符。 前向迭代器支持++ 和所有的输入和输出迭代器的
功能。 双向迭代器支持–操作和前向迭代器的功能。 随机访问迭代器支持所有在表中给出的操作。 另
外, 对于输入迭代器和输出迭代器,不能在保存迭代器之后再使用保存的值
迭代器操作 | 描述 |
---|---|
适用所有迭代器的操作 | |
++p | 前置自增迭代器 |
p++ | 后置自增迭代器 |
p=p1 | 将一个迭代器赋值给另一个迭代器 |
输入迭代器 | |
*p | 间接引用一个迭代器 |
p->m | 使用迭代器读取元素m |
p==p1 | 比较两个迭代器是否相等 |
p!=p1 | 比较两个迭代器是否不相等 |
输出迭代器 | |
*p | 间接引用一个迭代器 |
p=p1 | 把一个迭代器赋值给另一个 |
前向迭代器 | 前向迭代器提供了输入和输出迭代器的所有功能 |
双向迭代器 | |
–p | q |
p– | 后置自减迭代器 |
随机访问迭代器 | |
p+=i | 迭代器p前进i个位置 |
p-=i | 迭代器p后退i个位置 |
p+i | 在迭代器p 的位置上前进i个位置 |
p-i | 在迭代器p的位置上后退i个位置 |
p-p1 | 表达式的值是一个整数,它代表同一个容器中两个元素间的距离 |
p[i] | 返回与迭代器p的位置相距i的元素 |
p<p1 | 若迭代器p小于p1(即容器中p在p1前)则返回 true, 否则返回 false |
p<=p1 | 若迭代器p小千或等于p1 (即容器中p 在p1前或位咒相同)则返回 true, 否则返回 false |
p>p1 | 若迭代器p 大于p1(即容器中p在p1后)则返回true, 否则返回false |
p>=p1 | 若迭代器p大于或等于p1(即容楛中p在p1后或位置相同)则返回 true, 否则返回 false |
5.3 map与unordered_map(红黑树VS哈希表)
C++11 增加了无序容器 unordered_map/unordered_multimap 和
unordered_set/unordered_multiset,由于这些容器中的元素是不排序的,因此,比有序容器
map/multimap 和 set/multiset 效率更高。 map 和 set 内部是红黑树,在插入元素时会自动排序,而
无序容器内部是散列表( Hash Table),通过哈希( Hash),而不是排序来快速操作元素,使得效率
更高。由于无序容器内部是散列表,因此无序容器的 key 需要提供 hash_value 函数,其他用法和
map/set 的用法是一样的。不过对于自定义的 key,需要提供 Hash 函数和比较函数。
5.3.1 map和unordered_map的差别
需要引入的头文件不同
map: #include < map >
unordered_map: #include < unordered_map >
内部实现机理不同
- map: map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜
索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点
都代表着map的一个元素。 - unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到
Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应
用)。因此,其元素的排列顺序是无序的。
5.3.2 优缺点以及适用处
map:
- 优点:
- 有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
- 红黑树,内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非
常的高
- 缺点:
- 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额
外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间
- 适用处:
对于那些有顺序要求的问题,用map会更高效一些
unordered_map:
- 优点: 因为内部实现了哈希表,因此其查找速度非常的快
- 缺点: 哈希表的建立比较耗费时间
- 适用处:对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map
5.3.3 总结
- 内存占有率的问题就转化成红黑树 VS hash表 , 还是unorder_map占用的内存要高。
- 但是unordered_map执行效率要比map高很多
- 对于unordered_map或unordered_set容器,其遍历顺序与创建该容器时输入的顺序不一定相
同,因为遍历是按照哈希表从前往后依次遍历的
推荐一个零声学院免费教程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:
服务器
音视频
dpdk
Linux内核