STL库安全实验
实验目的
了解STL标准模板库
了解C++中的STL库安全
掌握安全的C++中的STL库操作
实验原理
1.STL(标准模板库)
STL是Standard Template Library的简称,中文名标准模板库,惠普实验室开发的一系列软件的统称。它是由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室工作时所开发出来的。从根本上说,STL是一些"容器"的集合,这些"容器"有list,vector,set,map等,STL也是算法和其他一些组件的集合。这里的"容器"和算法的集合指的是世界上很多聪明人很多年的杰作。STL的目的是标准化组件,这样就不用重新开发,可以使用现成的组件。STL现在是C++的一部分,因此不用安装额外的库文件。
STL的版本很多,常见的有HP STL、PJ STL、 SGI STL等。
2.STL库安全
STL(Standard Template Library标准模板库)是C++开发中的常用组件,它通过容器+迭代子+算法的灵活组合,可以满足很多应用场景,但使用不好则易留下安全隐患。
实验步骤
步骤1:引用容器前后元素时要确保容器元素存在
1.1 没有判断是否为空就直接通过引用STL容器首尾元素,这在容器为空时会导致程序异常。
错误示例:
bool NoCompliant(const NodeKeyList &srcList, const NodeKeyList &snkList)
{
NodeKey srcNode = srcList.front();
NodeKey snkNode = snkList.back();
/* ...do something... */
}
示例代码对函数的入参srcList没有判断长度直接通过front()和back()方法取了第一个和最后一个元素,在容器列表为空的情况下,会导致程序异常,与front()类似的还有通过begin()数据下标获取对应元素,比如*srcList.begin(),或者srcList.begin()->GetID(),或者是srcList[0](在为vector时)。
推荐做法:
bool Compliant(const NodeKeyList &srcList, const NodeKeyList &snkList)
{
if (srcList.empty() || snkList.empty()) //【修改】确保STL容器内有元素存在
{
return false;
}
NodeKey srcNode = srcList.front();
NodeKey snkNode = snkList.back();
/* ...do something... */
}
步骤2:迭代子使用前必须保证迭代子有效
2.1 STL算法std::find()、std::find_if()和std::set::find()等有可能返回容器的end()位置,迭代子定义时可以不初始化,或者初始化指向容find()等方法返回的位置,与指针类似地,若未判断迭代子有效性,直接引用迭代子有可能导致程序崩溃。
错误示例:
void STLIterTest::IterReference_NoCompliant(int CmdCode, MAP_GENKEY_VALUE& allResult)
{
TEGenKey tmpKey;
tmpKey.attrID = DWDMTL1_ATTRPORT_CLIENTPRO;
tmpKey.objectID = it->first.objectID;
MAP_GENKEY_VALUE::iterator iter = allResult.find( tmpKey );
if ("FC100" != iter->second.sValue) //非FC100设置为无效
{
it->second.access = TEGenVar::ACCESS_INVALID;
}
}
示例代码通过map的find函数返回的迭代子iter,if语句直接通过iter->second来引用变量,如果迭代子iter指向为allResult的end()位置,则程序会崩溃。
推荐做法:
void STLIterTest::IterReference_Compliant(int CmdCode, MAP_GENKEY_VALUE& allResult)
{
TEGenKey tmpKey;
tmpKey.attrID = DWDMTL1_ATTRPORT_CLIENTPRO;
tmpKey.objectID = it->first.objectID;
MAP_GENKEY_VALUE::iterator iter = allResult.find(tmpKey);
if (iter != allResult.end()) //【修改】确保迭代子有效后再进行操作
{
if ( "FC100" != iter->second.sValue)
{
it->second.access = TEGenVar::ACCESS_INVALID;
}
}
}
步骤3:必须确保迭代子指向的内容有效
3.1 在理解上迭代子可以视为C指针,迭代子只有在指向了容器中一个存在的对象时,访问才是安全有效的,其他情况的访问都可能存在风险。典型问题:
对连续内存容器来说(如std::vector)会分配一块固定内存来保存连续对象,在插入新元素后(成员函数包括:reserve(),resize(), push_back(),insert()等),可能会引起容器重新分配内存和数据迁移,如果在插入元素之前使用迭代子保存了迭代子位置,那么插入新元素之后,前面保存的迭代子就可能是无效的。
错误示例:下列代码的迭代子在操作过程中失效。
void NoCompliant()
{
deque<double> d;
double data[5] = { 2.3, 3.7, 1.4, 0.8, 9.6 };
deque<double>::iterator pos = d.begin();
for (size_t i = 0; i < 5; ++i)
{
d.insert(pos++, data[i] + 41); //【错误】insert操作后,pos已失效
}
}
Insert操作后,迭代子pos已经失效,执行自增操作导致异常。
推荐做法:
void Compliant()
{
double data[5] = { 2.3, 3.7, 1.4, 0.8, 9.6 };
deque<double> d;
deque<double>::iterator pos = d.begin();
for (size_t i = 0; i < 5; ++i)
{
pos = d.insert(pos, data[i] + 41); /*【修改】通过返回值获得新的有效的迭代子 */
++pos;
}
}
std::remove和std::remove_if仅会将被删除元素后移并返回应该被删除元素位置的迭代子,并没有正在从容器中删除对象,需要另配合erase函数才能删除,所以一般建议配合一起使用。
错误示例2:下列代码中错误的仅使用remove()函数来删除容器中元素。
void NoCompliant()
{
vector<int> container;
int value = 42;
iterator end = remove( container.begin(), container.end(), value);
for (iterator i = container.begin(); i != container.end(); ++i)
{
cout << "Container element: " << *i << endl;
}
}
remove() 删除任一个成员后返回值将指向任一个成员,值将不可预知。所以被删除后需要立即调用 erase()抹去,防止不可预知的数据访问。
推荐做法:
void Compliant()
{
vector<int> container;
int value = 42;
container.erase(remove(container.begin(),container.end(),value), container.end()); /*【修改】remove删除成员后立即调用erase,确保迭代子指向的内容是有效的*/
for (iterator i = container.begin(); i != container.end(); ++i)
{
cout << "Container element: " << *i << endl;
}
}
步骤4:正确处理容器的erase()方法与迭代子的关系
4.1 调用容器的erase(iter)方法后,迭代子指向的对象被析构,迭代子已经失效,如果再对迭代子执行递增递减或者引用操作会导致程序崩溃。
错误示例:下列代码中的迭代子在执行删除操作过程中已失效。
void STLIterTest::IterVisitContainer_NoCompliant()
{
std::map<oid,NE>::iterator it = m_mapID2NE.begin();
for (; it != m_mapID2NE.end(); )
{
if (pNEInfo->GetNEState(ulNEID) == NESTATE_LOGIN)
{
m_mapID2NE.erase(iter);
iter++; //【错误】erase后,iter指向的对象可能已失效
}
else {++iter;}
}
}
推荐做法:将迭代子后置递增作为erase()的参数。
void STLIterTest::IterVisitContainer_Compliant()
{
std::map<oid,NE>::iterator it = m_mapID2NE.begin();
for (; it != m_mapID2NE.end(); )
{
if ( pNEInfo->GetNEState(ulNEID) == NESTATE_LOGIN)
{
m_mapID2NE.erase(iter++); //【修改】将迭代子后置递增作为erase参数
}
else {++iter;}
}
}
也可以使用earse方法的返回值来保存迭代子,因为返回的是被删除元素迭代子指向的下一个元素位置:
iter = erase(iter)。
注意这种用法可以用于list和vector的erase(),但不适用于map。因为std::map::erase()的返回值在不同STL实现版本是有差异的,有的有返回值,有的没有返回值,所以对map只能使用推荐做法。