c++的坑
一、snprntf的困惑
有经验的程序员会拒绝使用sprintf。凭着对输出buffer长度的严格检查,snprintf得到了众多程序员的青睐。但使用了snprintf就万事无忧了么,下面举例说明一个比较容易忽略的细节:
intSendMsg(int fd, constchar* msg)
{
char buffer[32];
int len = snprintf(buffer, sizeof(buffer), "MsgBody: %s", msg);
buffer[len] = '\0';
write(fd, buffer, len);
return len;
}
假设该场景中以字符串进行协议交换,也不必关心write能否成功写入,仅把关注重点放到snprintf的基本使用上来,这样的代码是正确的么?
首先buffer[len] = '\0' 这一行代码是多余的,snprintf会保证目标字符串以\0结尾,即便在目标字符串被截断的情况下。
其次,更加重要也是致命的一点,snprintf的返回值,并非是实际写入的长度,而是它原本应该写入的长度。也就是说,它返回的是格式化后的完整字符串长度,因此当发生截断时,返回值将超过buffer长度。同时从函数手册上来看,仍然存在返回负数的可能。
综合以上,两个错误,在置结束符时发生了写越界,在随后write时发生了读越界。同时还有另一个可恶之处,就是当没有发生截断时,他总是运行正确。
或许有人认为snprintf返回值的设定有点说不过去,但考虑到将返回值与buffer长度进行比较,是一种有效识别是否截断的手段,这样的设计也就合情合理了。
四. 迭代器失效
STL是C++对泛型编程思想的实现,广义上有算法、容器和迭代器三个部分组成。从作用上来说,迭代器是STL的最基本部分,使得算法和容器能够解耦分离,并用迭代器精妙的连携起来。迭代器提供了比下标操作更一般化的方式,现代C++更倾向于使用迭代器而不是下标访问容器。
即便迭代器具有如此重要的地位,提供更加一般化的操作,但稍不留心,也还是会搞出点儿麻烦。至少在笔者所经历过的项目中,不止一次的遇到,很多老手也未能幸免。
1. vector的遍历删除
for (std::vector<int>::iterator iter = numbers.begin();
iter != numbers.end(); ++iter)
{
if (NeedDelete(*iter))
{
numbers.erase(iter);
}
}
上述代码忽略了一个细节,vector的存储空间是连续分配的,删除当前元素时,会导致后续的元素集体前移。erase过后iter已经指向下一个元素,而for循环中的++iter,则再次跳过一个元素。跳过一个也许只会导致结果错误,但令人更加担心的情形是,如果恰好跳过了尾端指示器end()呢。干得漂亮,一个不会结束的循环产生了,直到越界访问到非法区域而崩溃。
这里推荐一种针对vector的有效处理方式,当执行erase时通过返回值修正iter,否则++iter:
for (std::vector<int>::iterator iter = numbers.begin();
iter != numbers.end(); )
{
if (*iter ==value)
{
iter = numbers.erase(iter);// 通过返回值修正
}
else
{
++iter;
}
}
除了删除操作,增加操作也同样存在问题,或许问题更大。因为当vector元素增加超过现有空间时,需要重新申请一片连续空间并作迁移,此时所有迭代器都将失效。
vector因为有内存空间连续的要求,对迭代器也有了如上的限制,那对于结点空间独立分配的map或是list,是不是就不存在问题了呢?
上述的正确用法,对于list容器也完全奏效,但对于map,set等关联容器时,上面的方式行不通,原因是标准的C++关联容器,erase()方法返回void而并非下一个结点(某些特殊版本STL实现会返回结点,如VC,但这并不符合标准)。
2. map的遍历删除
针对map的遍历删除,即便结点独立存在,但以下方式仍然是错的:
for (std::map<int, int>::iterator iter = numbers.begin();
iter != numbers.end(); ++iter)
{
if (NeedDelete(iter->second))
{
numbers.erase(iter);// erase会导致iter失效
}
}
其根本原因在于,删除当前iter指向元素后,该iter也立即失效,随后在循环体中执行的++iter,其结果是不确定的。然而能够仿照vector,使用iter = numbers.erase(iter) 么? 答案也是否定的,原因是标准的C++关联容器,erase()方法返回值是void,而不是迭代器(尽管也有某些特殊STL实现也会返回迭代器,但这并不符合标准)。
针对本例错误,推荐使用以下的方式:
for (std::map<int, int>::iterator iter = numbers.begin();
iter != numbers.end(); )
{
if (NeedDelete(iter->second))
{
numbers.erase(iter++);// 先取值,然后++,再erase
}
else
{
++iter;
}
}
理解该方式的关键点在于理解numbers.erase(iter++)的行为。按照后自增操作的定义,该条语句拆解如下:
1. iterator temp_iter = iter;
2. iter++;
3. numbers.erase(temp_iter);
可见在erase操作前iter已经指向为下一个结点,随后的erase也无法对iter造成影响。前面的实效问题得到化解。
1、遵循编程规范,例如公司的编程规范、Google C++ 编程规范等;
2、小就是美、简单就是美;
3、尽可能多的使用 const 修饰符;
4、声明即初始化:变量、对象声明时就初始化;
5、结构、类等实例变量都以指针变量的方式使用;
6、始终在使用前检测指针变量的有效性;
7、指针和标量类型使用值传递,其它都使用指针和引用传递;
8、多用智能指针: auto_ptr, shared_ptr,少用原始指针;
9、多用 new/delete/new[]/delete[],少用malloc/free/realloc;
10、多用只读常量、局部变量,少用全局变量、静态变量;
11、识别无符号数和有符号数的应用场景并正确选择数据类型;
12、重试编译器警告:重视并修复编译器警告;

浙公网安备 33010602011771号