查看原文
其他

编程中的这些坑,给你挖好了

herongwei herongwei 2022-09-05

大家好,我是贺同学。

昨天,北京迎来初雪,气温急剧下降,感觉北京秋天还没怎么过,就要“一秒入冬”了。

今年北京确实冷得比较早,由于前期入秋偏晚,国庆假期又赶上持续的雨水将气温拉低,这次的冷空气势力确实很强,大幅度降低了气温,在北京的朋友们注意保暖呀。

好了,咳咳,言归正传,咋们来讲讲编程中的那些小坑。

对于一个编程初学者来说,常犯错是很正常的,就算是有了一定功底的人也会犯一些低级错误,总结一下平时工作中经常也容易犯错的编程小坑,希望大家以后多多注意。

因为小贺工作中主要用 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,一个精神小伙&鹅厂程序猿,热爱编程,热爱生活,热爱分享,在平凡的人生中追求一点不平凡,欢迎关注,一起加油,点击下方名片,了解更多。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存