大家好,我是贺同学。
昨天,北京迎来初雪,气温急剧下降,感觉北京秋天还没怎么过,就要“一秒入冬”了。
今年北京确实冷得比较早,由于前期入秋偏晚,国庆假期又赶上持续的雨水将气温拉低,这次的冷空气势力确实很强,大幅度降低了气温,在北京的朋友们注意保暖呀。
好了,咳咳,言归正传,咋们来讲讲编程中的那些小坑。
对于一个编程初学者来说,常犯错是很正常的,就算是有了一定功底的人也会犯一些低级错误,总结一下平时工作中经常也容易犯错的编程小坑,希望大家以后多多注意。
因为小贺工作中主要用 C++/Go/Python,所以下面的经验主要是以这三种语言举例,其它语言也没关系,编程思想都是相通的。
循环遍历中删除原先容器本身
用 for 发起任何形式的遍历时,它的遍历顺序都是从最初就确定的,而在遍历中删除了原先遍历容器本身,会导致当前索引的变化,这样会带来两个危害:一是会导致漏删元素,二是会导致遍历超过链表的长度。
这个小坑,尤其对于初学者,很容易不知不觉就跳进去了。
比如下面这段 Python 代码:
# 每个词中间是空格,用正则过滤掉英文和数字,最终输出"每一天 你"
import re
a = "I cherish 每一天 with 你 for 10000"
list1 = a.split(" ")
print(list1)
res = re.findall(r'\d+|[a-zA-Z]+',a)
for i in list1:
if i in res:
list1.remove(i)
print(list1) #输出是 ['cherish', '每一天', '你', '10000']
原先用正则匹配出来的四个要删除的字符分别是 ['I', 'cherish', 'with', 'for', '10000'] ,在原先空格分隔索引之后分别对应[0 1 3 5 6] 可以验证发现每次 for 循环遍历删除了原先列表里的元素之后这个索引就变化了。
那么,对于上面的代码,优雅的做法是用 join 重新拼接字符串,或者以英文字符和数字作为分隔符,得到一个空字符和中文的列表,再用 join 来拼接。
迭代器失效问题
这个知识点对 C++ 代码的初学者来说,包括老选手,都是容易犯的错误。对 C++ STL 中迭代器的删除需要慎重,稍有不慎,就会造成迭代器失效的问题。
迭代器的失效问题:对容器的操作影响了元素的存放位置,称为迭代器失效。
在之前的 万字长文炸裂!手撕 STL 迭代器源码与 traits 编程技法 中,其实已经深入讲解了一部分,那这里在简单提一下吧。
失效情况:
- 当容器调用erase()方法后,当前位置到容器末尾元素的所有迭代器全部失效。
- 当容器调用insert()方法后,当前位置到容器末尾元素的所有迭代器全部失效。
- 如果容器扩容,在其他地方重新又开辟了一块内存。原来容器底层的内存上所保存的迭代器全都失效了。
迭代器失效的原因是:因为 vetor、deque 使用了连续分配的内存,erase 操作删除一个元素导致后面所有的元素都会向前移动一个位置,这些元素的地址发生了变化,所以当前位置到容器末尾元素的所有迭代器全部失效。
分三种情况:
对于序列式容器(如 vector, deque)的迭代器失效示例如下:
// 在这里想把大于2的元素都删除
for (auto it = q.begin(); it != q.end(); it++) {
if (*it > 2)
q.erase(it); // 这里就会发生迭代器失效
}
解决方法是利用 erase 方法可以返回下一个有效的 iterator,所以代码做如下修改即可:
// 在这里想把大于 2 的元素都删除
for(auto iter=vec.begin();iter!=vec.end();){
if(*iter>2) {
iter=vec.erase(iter); // 这里会返回指向下一个元素的迭代器,因此不需要再自加了
}
else {
iter++;
}
}
对于链表式容器(如 list),删除当前的 iterator,仅仅会使当前的 iterator 失效,这是因为 list 之类的容器,使用了链表来实现,插入、删除一个结点不会对其他结点造成影响。
只要在 erase 时,递增当前 iterator 即可,并且 erase 方法可以返回下一个有效的 iterator。
for (iter = dataList.begin(); iter != dataList.end();)
{
(*it)->doSomething();
if (shouldDelete(*iter))
iter = dataList.erase(iter); //erase删除元素,返回下一个迭代器
else
++iter;
}
对于关联容器(如 map, set,multimap,multiset),只要在 erase 时,递增当前 iterator 即可。
这是因为 map 之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。
erase 迭代器只是被删元素的迭代器失效,但是返回值为 void,所以要采用 erase(iter++) 的方式删除迭代器,因为传给 erase 的是 iter 的一个副本,iter++ 是下一个有效的迭代器。
for (iter = dataMap.begin; iter != dataMap.end();)
{
(*it)->doSomething();
if (shouldDelete(*iter))
dataMap.erase(iter++); //erase删除元素,返回下一个迭代器
else
++iter;
}
资源关闭
这里的资源包括文件、数据库连接和 socket 连接等,我们以文件操作为例,说明一下常见的资源关闭错误。
我们来以 Go 举例,文件操作的一个代码示例:
file, err := os.Open("file.go")
if err != nil {
fmt.Println("open file failed:", err)
return
}
可能你写到这就开始专注业务代码了,最后“忘记”了写关闭文件操作的代码。殊不知,在这里埋下了一个入坑的隐患。
在 Linux 中,一切皆文件,当打开的文件数过多时,就会触发 "too many open files“ 的系统错误,从而让整个系统陷入崩溃。
我们增加上关闭文件操作的代码,如下所示:
file, err := os.Open("file.go")
defer file.Close()
if err != nil {
fmt.Println("open file failed:", err)
return
}
Golang 提供了一个很好用的关键字 defer,defer 语句的含义是不管程序是否出现异常,均在函数退出时自动执行相关代码。
但值得注意的是上面的修改又引入了新问题,即如果文件打开错误,调用 file.Close 会导致程序抛出异常(panic),所以正确的修改应该将 file.Close 放到错误检查之后,如下:
file, err := os.Open("file.go")
if err != nil {
fmt.Println("open file failed:", err)
return
}
defer file.Close()
变量的大小写/初始化
变量的大小写
在 C++,Python等语言中对于关键字的增加没有 Golang那么严格,相比之下,Golang 对关键字的增加非常吝啬,其中没有 private、protected 和 public 这样的关键字。
要使某个符号对其他包(package)可见(即可以访问),需要将该符号定义为以大写字母开头,这些符号包括接口,类型,函数和变量等。
另外对于未使用的变量,在其他语言,可能会警告,但在 Go 语言里,如果存在未使用的变量会导致编译失败。
变量的初始化
一个变量未初始化就开始使用(如果定义在全局,变量会自动初始化,不在此列),在 Go 里面,另一个常见错误是,局部变量有可能遮盖或隐藏全局变量,因为通过 “:=” 方式初始化的局部变量看不到全局变量。
精度转化
在对一些数据做处理的时候,也会遇到一些特殊 case 情况需要排查,最终你会发现精度转化函数调用的不对也会掉坑里了。
比如在 C++ 里面,string to float and double Conversion 就提供了三个函数,那么具体到实战层面来说,每个函数的保留的位数也是不一样的。
- std::stof() - convert string to float
- std::stod() - convert string to double
不同等级的精度转化不一样。
string s = "116.8"
// 若调用 stof 转化为 float 则结果是 116.80000305175781
// 若调用 stod 转化为 double 则结果是 116.8
如果转换的数在后续需要进一步的数学运算,比如每个数乘以 1e6,可想而知导致的结果肯定是不一样的,如果这里没注意,涉及广告等业务的代码上线出了事故,那损失可就不是一笔小数目啊。
其它小坑
忽略了“=”与“==”的区别,在大部分语言的语法规则里,“=”是赋值运算符,“==”是关系运算符。
另外对于 Python 来说。
- 误以为 Python 是弱类型语言,其实是强类型语言;
- 误以为 True/False 是常量值,在 Pytho2.7是两个内建(built-in)变量;
- 对于两个等值数字比如 a = 88, b =88, 误以为 a is b 返回 True,其实返回 False;
- 误以为函数不是对象,其实函数也是对象,也可以像其他类型的对象一样被赋值,传递,作为返回值。
当然了,上面举得例子只是我个人工作中的一些总结,限于篇幅,肯定没有涵盖所有的情况。
大家呢,还是要在平时的工作业务中积累经验,如果大家有比较印象深刻的小坑也可以留言评论一起交流学习。
好啦,本周的唠嗑就是这样了。
我是小贺,我们下期再见。
·················END·················
你好,我是 herongwei,一个精神小伙&鹅厂程序猿,热爱编程,热爱生活,热爱分享,在平凡的人生中追求一点不平凡
标签:容器,迭代,编程,iter,这些,erase,file,失效 From: https://blog.51cto.com/u_15368396/5781575