STL中的map是一个key-value的数据结构 ,每一个key对应着一个值,而且key是唯一的,底层采用红黑树的数据结构实现。在使用map的过程中,有两种方式进行数据的插入,第一种是使用下标的方式,也就是[],第二种是使用insert接口,它们主要的区别如下:
- 下标的方式插入,如果原本key不存在则会先创建对应的记录,然后再进行赋值;
- insert方式插入,如果key不存在,则插入记录,如果存在则什么都不做。
map<int, char> m;
m[1] = 'a'; // 首先初始化话m[1],然后在给m[1]赋值为'a'
m[1] = 'b'; // **修改m[1]的值为'b'**
m.insert<pair<int, char>(2, 'c')> // key=2不存在,则插入该记录,m[2]的值为'c'
m.insert<pair<int, char>(2, 'd')> // **key=2存在,则什么也不做,m[2]的值仍为'c'**
下面从源码的角度对map的[]和insert进行分析,首先看一个例子,代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <map>
using namespace std;
class Entry
{
public:
Entry(int a)
: _a (a)
{
printf("Entry (int a): addr:%p\n", this);
}
Entry ()
: _a(0)
{
printf("Entry (): addr:%p\n", this);
}
Entry (const Entry & e)
{
_a = e._a;
printf("Entry (const Entry & e) addr:%p source:%p\n", this, &e);
}
~Entry(){printf("~Entry() addr:%p\n", this);}
void SetA(int a) { _a = a; }
int GetA() { return _a; }
private:
int _a;
};
void* operator new (size_t size)
{
cout << "new (size_t size): size:" << size <<endl;
void* p = malloc(size);
return p;
}
void operator delete(void *p)
{
free(p);
}
void* operator new[] (size_t size)
{
cout << "new[] (size_t size): size:" << size <<endl;
void* p = malloc(size);
return p;
}
void operator delete[](void* p)
{
free(p);
}
int main()
{
map<int, Entry> m;
printf("------Entry(int)------\n");
Entry e1(1);
Entry e2(2);
printf("------end------\n\n");
printf("------[] not exist------\n");
m[0] = 1;
printf("m[0].a: %d\n", m[0].GetA());
printf("------end------\n\n");
printf("------[] exist------\n");
m[0] = e2;
printf("m[0].a: %d\n", m[0].GetA());
printf("------end------\n\n");
printf("------insert------\n");
m.insert(pair<int, Entry>(1, e1));
printf("m[1].a: %d\n", m[1].GetA());
printf("------end------\n\n");
printf("------insert: exist key------\n");
m.insert(pair<int, Entry>(1, e2));
printf("m[1].a: %d\n", m[1].GetA());
printf("------end------\n\n");
e2.SetA(100);
printf("e2._a: %d m[0]._a: %d\n\n", e2.GetA(), m[0].GetA());
return 0;
}
输出结果:
------Entry(int)------
Entry (int a): addr:0x7fff750e46f0
Entry (int a): addr:0x7fff750e46e0
------end------
------[] not exist------
Entry (): addr:0x7fff750e4670
Entry (const Entry & e) addr:0x7fff750e4664 source:0x7fff750e4670
new (size_t size): size:40
Entry (const Entry & e) addr:0x1e1c034 source:0x7fff750e4664
~Entry() addr:0x7fff750e4664
~Entry() addr:0x7fff750e4670
Entry (int a): addr:0x7fff750e4700
~Entry() addr:0x7fff750e4700
m[0].a: 1
------end------
------[] exist------
m[0].a: 2
------end------
------insert------
Entry (const Entry & e) addr:0x7fff750e4734 source:0x7fff750e46f0
Entry (const Entry & e) addr:0x7fff750e4724 source:0x7fff750e4734
new (size_t size): size:40
Entry (const Entry & e) addr:0x1e1c064 source:0x7fff750e4724
~Entry() addr:0x7fff750e4724
~Entry() addr:0x7fff750e4734
m[1].a: 1
------end------
------insert: exist key------
Entry (const Entry & e) addr:0x7fff750e4764 source:0x7fff750e46e0
Entry (const Entry & e) addr:0x7fff750e4754 source:0x7fff750e4764
~Entry() addr:0x7fff750e4754
~Entry() addr:0x7fff750e4764
m[1].a: 1
------end------
e2._a: 100 m[0]._a: 2
~Entry() addr:0x7fff750e46e0
~Entry() addr:0x7fff750e46f0
~Entry() addr:0x1e1c064
~Entry() addr:0x1e1c034
分析:
1.第一个输出:
——Entry(int)——
Entry (int a): addr:0x7fffc1d21c50
Entry (int a): addr:0x7fffc1d21c40
——end——
首先创建Entry对像e,因此会调用Entry的构造函数,所以会有以上输出
2.第二个输出(map对应key=0的记录不存在):
——[] not exist——
Entry (): addr:0x7fffc1d21bd0
Entry (const Entry & e) addr:0x7fffc1d21bc4 source:0x7fffc1d21bd0
new (size_t size): size:40
Entry (const Entry & e) addr:0x2141034 source:0x7fffc1d21bc4
~Entry() addr:0x7fffc1d21bc4
~Entry() addr:0x7fffc1d21bd0
Entry (int a): addr:0x7fffc1d21c60
~Entry() addr:0x7fffc1d21c60
m[0].a: 1
——end——
再看看map的[]源码:
_Tp& operator[](const key_type& __k) {
iterator __i = lower_bound(__k);
// __i->first is greater than or equivalent to __k.
if (__i == end() || key_comp()(__k, (*__i).first))
__i = insert(__i, value_type(__k, _Tp()));
return (*__i).second; // 如果已经存在则直接返回
}
从上面可以看出,当key不存在的时候,会调用map的insert接口进程初始化,在调用insert的时候会创建一个默认的对象
insert(__i, value_type(__k, _Tp()))
_Tp()的作用是创建一个默认的value,在这里就是创建一个默认的Entry对应,因此会调用Entry的无参数构造函数,所以有第一行输出:Entry (): addr:0x7fff1461d2d0。然后通过value_type创建一个pair(value_type其实是一个pair类型,map中有这样的声明:typedef pair<const _Key, _Tp> value_type;
),在pair的构造函数中会调用Entry的拷贝构造函数,因此会输出
Entry (const Entry & e) addr:0x7fff1461d2c4 source:0x7fff1461d2d0
。
接着就进入到map的insert函数中,请看insert函数的实现(注意insert的参数类型):
iterator insert(iterator position, const value_type& __x)
{ return _M_t.insert_unique(position, __x); }
_M_t是map的底层数据结构,它是一个红黑树,具体的实现这里不进行详细讨论。insert里面会调用红黑树_M_t的insert_unique,insert_unique接口里面会新建一个树的节点,因此会调用new,所以有了第三行的输出
new (size_t size): size:40
这时会把value_type拷贝到新节点上,因此会再次调用Entry的拷贝构造函数,所以有第四行输出:
Entry (const Entry & e) addr:0x1fa0034 source:0x7fff1461d2c4
红黑树执行完插入操作后,返回到map的[]函数中,函数返回_Tp&类型,然后退出,销毁相应的临时变量,因此后两行输出。然后对m[0]进行赋值操作,因此m[0]就被赋值为e1,所以有m[0].a: 1
3.第三个输出(key=0记录已存在):
------[] exist------
m[0].a: 2
------end------
因为key=0的记录已经存在,所以调用map的[]时就没有其他插入操作,直接返回m[0]的对象,然后在把e2赋值给它,因此会有m[0].a: 2
- 第四个输出(key=1不存在)
------insert------
Entry (const Entry & e) addr:0x7fff750e4734 source:0x7fff750e46f0
Entry (const Entry & e) addr:0x7fff750e4724 source:0x7fff750e4734
new (size_t size): size:40
Entry (const Entry & e) addr:0x1e1c064 source:0x7fff750e4724
~Entry() addr:0x7fff750e4724
~Entry() addr:0x7fff750e4734
m[1].a: 1
------end------
首先,在插入数据之前需要创建一个pair对象pair<int, Entry>(1, e1)
,在pair的构造函数中会调用e1的拷贝构造函数,因此会有第一行输出。然后调用map的insert接口(注意参数类型,与上面有所区别):
//插入元素节点,调用RB-Tree的insert_unique(__x);
//不能插入相同键值的元素
pair<iterator,bool> insert(const value_type& __x)
{ return _M_t.insert_unique(__x); }
在insert会调用红黑树_M_t的insert_unique接口(参数与上面的[]的insert不同,注意观察),然后接着的三行都是在红黑树的接口中产生的输出:
Entry (const Entry & e) addr:0x7fff750e4724 source:0x7fff750e4734
new (size_t size): size:40
Entry (const Entry & e) addr:0x1e1c064 source:0x7fff750e4724
你会发现上面的输出比[]的多了一次构造函数的调用,其实原理和上面的分析一样,具体请看红黑书的实现。
插入成功,这是m[1]的a的值为1。
5.第五个输出(key=1已经存在):
------insert: exist key------
Entry (const Entry & e) addr:0x7fff750e4764 source:0x7fff750e46e0
Entry (const Entry & e) addr:0x7fff750e4754 source:0x7fff750e4764
~Entry() addr:0x7fff750e4754
~Entry() addr:0x7fff750e4764
m[1].a: 1
------end------
这是key=1已经存在,上面已经说过,当key存在的时候调用map的insert不会做任何操作,但是为什么还有这些输出呢?这些输出是因为在调用对应的接口的时候参数传递时产生的临时变量,可以看出输出中并没有new的操作,m[1]的a值仍为1.
6.最后的输出:
e2._a: 100 map[0]._a: 2
~Entry() addr:0x7fff750e46e0
~Entry() addr:0x7fff750e46f0
~Entry() addr:0x1e1c064
~Entry() addr:0x1e1c034
主要是看第一行,后面的都是程序退出时回收对象调用对应的析构行数。在程序中,调用了e2的SetA接口,更改了e2的a的值为100
e2.SetA(100);
但是输出的时候发现e2._a是为100,而m[0]._a等于2(在程序的70行m[0]=e2),可以分析出m[0]对应的对象和e2不是同一个(其实从程序输出时输出的地址也可以看出),这一点在实际使用的时候需要注意。
参考:
C++拷贝构造函数及重写operator =的区别
《STL源码剖析》—stl_pair.h阅读笔记
STL源码剖析——关联容器之map
STL源码剖析——RB-Tree(红黑树)
lower_bound()返回值
std::map的insert和下标[]访问