1.原理
1.1.简述
c++中map数据结构为红黑树(平衡二叉树的一种特化),搜索的复杂度为O(logN),其他的操作不在此叙述,读者可以自行在MSDN上查找详细字段和函数的含义及用法示例。
1.2.数据结构
每个节点数据构成如下,
x64环境下,一个空的map大小为24个字节,与上图对应,
一个节点占用内存为24个字节。
1.3.初始化大小
如下图,对于一个初始化的map,其head节点,以及head节点中的left、right、parent节点值是一样的,并且是是递归重复的,由此推断,初始化仅有一个节点(下文的输出信息会佐证),节点中left、right、parent都指向了自己。
1.4.内存管理
map在erase元素或者clear之后,内存大小不会立即释放,因为也因此可以动态的向map中增删元素,重复使用map对象而同时又有较低的内存管理性能消耗,关于这个可以参考如下文章。
关于c++中map的内存占用问题
如果想立即释放map占用空间,可以用swap的方式,交换目标map与临时局部map的内存,这样离开生存范围时,临时map会被释放掉,如下,
map<int, int>().swap(mapTest);
1.5.定制比较器
map在构建的过程中(增删元素),需要对key值做排序,采用的是比较大小的方式,如果需要定制key比较器,有以下两种方法,
- 重载key类的<操作符
bool operator < (keyTest const& _A) const
- 仿函数
例如以字符串char*作为key时,
struct ptrStrLess
{
bool operator()(char const* a, char const* b) const
{
return strcmp(a, b) < 0;
}
};
1.6.添加元素时的行为
执行如下代码,
class TestData
{
public:
TestData();
TestData(int testCode);
TestData(const TestData& other);
~TestData();
TestData& operator=(const TestData& other);
public:
int mTestCode;
};
TestData::TestData()
:mTestCode(-1)
{
cout << "default structor called." << endl;
}
TestData::TestData(int testCode)
:mTestCode(testCode)
{
cout << "parametered structor called." << endl;
}
TestData::TestData(const TestData& other)
:mTestCode(other.mTestCode)
{
cout << "copy structor called." << endl;
}
TestData::~TestData()
{
cout << "destructor called." << endl;
}
TestData& TestData::operator=(const TestData& other)
{
mTestCode = other.mTestCode;
cout << "copy assign called." << endl;
return *this;
}
- 方式一,map[key] = value;方式(因为相对于insert方式,这种方式更高效些,原因如下),当不存在目标key的元素时添加元素,当存在时更新元素,
void TestFunction()
{
map<int, TestData> mapTest;
TestData item0(-1);
mapTest[0] = item0;
item0.mTestCode = 1;
int testCodeGet = mapTest[0].mTestCode;
}
输出内容为,
- 方式二,insert方式,直接插入元素,当然如果事先已存在key为目标key的元素,insert操作不会产生任何效果(会执行一些行为,比如拷贝构造函数,但是这些行为没有影响map对象的“现状”),
void TestFunction()
{
map<int, TestData> mapTest;
TestData item0(-1);
//mapTest[0] = item0;
mapTest.insert(pair<int, TestData>(0, item0));
item0.mTestCode = 1;
int testCodeGet = mapTest[0].mTestCode;
}
输出内容如下,
- 组合使用时
在insert操作前,map已存在key为目标key的元素时:
void TestFunction()
{
map<int, TestData> mapTest;
TestData item0(-1);
mapTest[0] = TestData(5);
mapTest.insert(pair<int, TestData>(0, item0));
item0.mTestCode = 1;
int testCodeGet = mapTest[0].mTestCode;
}
输出内容如下,
在insert操作前,map不存在key为目标key的元素时:
void TestFunction()
{
map<int, TestData> mapTest;
TestData item0(-1);
mapTest[1] = TestData(5);
mapTest.insert(pair<int, TestData>(0, item0));
item0.mTestCode = 1;
int testCodeGet = mapTest[0].mTestCode;
}
由此可见insert操作时不论事先有没有已存在key为目标key的元素,都会执行两次拷贝构造函数;只不过如果事先已存在时,执行了之后没有改变map的“现状”,即没有产生效果。
值得指出的是无论是debug模式还是release模式(会有代码优化),上述行为都是一致的,因为这是c++语言本身的标准。
- 其他
上文提到了构造函数、拷贝赋值、析构函数的执行,这里不妨说明下析构函数的调用顺序,由于局部变量存储在栈上,遵循着先进后出的原则:在离开局部作用域时,最先定义的局部变量总是最后被析构。
void TestFunction()
{
map<int, TestData> mapTest;
TestData item0(-1);
TestData item1(3);
mapTest[1] = TestData(5);
mapTest.insert(pair<int, TestData>(0, item0));
item0.mTestCode = 1;
int testCodeGet = mapTest[0].mTestCode;
}
1.7.构造map的过程中,随着元素的添加和删除,树枝会进行旋转,旋转时元素会被拷贝吗?
答案是否,原因如下,
- 旋转前
- 旋转后
2.总结
- 需要清楚了解map的原理及各种操作的作用和行为,参见本文和MSDN相关文章;
- 这样在不同的场景下,会根据map的作用长处和当时的场景判断是否可以用map,并且可以用的更好(有效且性能较好);
- 如后续有补充或新的认识会更新本文,如有错误欢迎指正。