首页 > 编程语言 >编程中的这些坑,给你挖好了

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

时间:2022-10-21 11:04:27浏览次数:75  
标签:容器 迭代 编程 iter 这些 erase file 失效

大家好,我是贺同学。

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

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


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

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

因为小贺工作中主要用 C++/Go/Python,所以下面的经验主要是以这三种语言举例,其它语言也没关系,编程思想都是相通的。

编程中的这些坑,给你挖好了_迭代器失效


循环遍历中删除原先容器本身

用 for 发起任何形式的遍历时,它的遍历顺序都是从最初就确定的,而在遍历中删除了原先遍历容器本身,会导致当前索引的变化,这样会带来两个危害:一是会导致漏删元素,二是会导致遍历超过链表的长度。

这个小坑,尤其对于初学者,很容易不知不觉就跳进去了。

编程中的这些坑,给你挖好了_迭代器失效_02

比如下面这段 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 中迭代器的删除需要慎重,稍有不慎,就会造成迭代器失效的问题。

编程中的这些坑,给你挖好了_迭代器失效_03

迭代器的失效问题:对容器的操作影响了元素的存放位置,称为迭代器失效。

在之前的 ​​万字长文炸裂!手撕 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
}

可能你写到这就开始专注业务代码了,最后“忘记”了写关闭文件操作的代码。殊不知,在这里埋下了一个入坑的隐患。

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

在 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 语言里,如果存在未使用的变量会导致编译失败。

编程中的这些坑,给你挖好了_迭代器_05

变量的初始化

一个变量未初始化就开始使用(如果定义在全局,变量会自动初始化,不在此列),在 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,可想而知导致的结果肯定是不一样的,如果这里没注意,涉及广告等业务的代码上线出了事故,那损失可就不是一笔小数目啊。


编程中的这些坑,给你挖好了_迭代器_06

其它小坑

忽略了“=”与“==”的区别,在大部分语言的语法规则里,“=”是赋值运算符,“==”是关系运算符。

另外对于 Python 来说。

  • 误以为 Python 是弱类型语言,其实是强类型语言;
  • 误以为 True/False 是常量值,在 Pytho2.7是两个内建(built-in)变量;
  • 对于两个等值数字比如 a = 88, b =88, 误以为 a is b 返回 True,其实返回 False;
  • 误以为函数不是对象,其实函数也是对象,也可以像其他类型的对象一样被赋值,传递,作为返回值。

当然了,上面举得例子只是我个人工作中的一些总结,限于篇幅,肯定没有涵盖所有的情况。

大家呢,还是要在平时的工作业务中积累经验,如果大家有比较印象深刻的小坑也可以留言评论一起交流学习。

好啦,本周的唠嗑就是这样了。

我是小贺,我们下期再见。


编程中的这些坑,给你挖好了_迭代器_07

·················END·················


你好,我是 herongwei,一个精神小伙&鹅厂程序猿,热爱编程,热爱生活,热爱分享,在平凡的人生中追求一点不平凡

标签:容器,迭代,编程,iter,这些,erase,file,失效
From: https://blog.51cto.com/u_15368396/5781575

相关文章

  • C++20高级编程 第五版 电子书 pdf
    作者:[比]马克·格雷戈勒(MarcGregoire)出版社:清华大学出版社原作名:ProfessionalC++,FifthEdition 链接:C++20高级编程第五版 拥抱C++的深度和复杂性,挖掘更多......
  • 学好编程的方法
    学好编程的唯一方法就是不断写代码,调试代码,大量阅读开源项目,有能力的修改开源代码,向开源项目贡献代码。正如游戏大声约翰卡马克所说的:在信息时代,进入编程领域的壁垒完......
  • 守护进程编程流程
    //服务进程/精灵进程运行时间长,不需要与用户交互,后台执行守护进程编程流程:1.fork(),创建子进程,退出父进程2.setsid(),创建新会话3.fork(),退出父进程,失去会话首进程,组长进程......
  • 《Java并发编程的艺术》读书笔记:二、Java并发机制的底层实现原理
    二、Java并发机制底层实现原理这里是我的《Java并发编程的艺术》读书笔记的第二篇,对前文有兴趣的朋友可以去这里看第一篇:一、并发编程的目的与挑战有兴趣讨论的朋友可以......
  • 熟悉编程语言
    熟悉编程语言最受欢迎的编程语言top50:常见语言的编程泛型1.命令式面向对象java,c++,c#,python,go面向过程C,Fortran,Basic,Pascal,algol,fortra,cobol2.声明式函数式Has......
  • 熟悉编程语言
    2.命令式:FORTRAN,BASIC,C,C++面向过程:C、COBOL、Fortran面向对象:C++、Java、PHP、python、go、Objective-C、C#声明式:SQL函数式:Haskell、F#、ML、Scala、lisp、logo、Scheme......
  • 熟悉编程语言
    TIOBE开发语言排行榜编程泛型-命令式:python、Java等-面向过程:C、COBOL、Fortran-面向对象:C++、Java、PHP、python、go、Objective-C、C#-声明式:SQL等-函数式:H......
  • 计算机基础与程序设计 2022-2023-1 熟悉编程语言
    从以上的内容中我们看到理想让我们确定了未来的目标,为人生的价值追求提供着自觉的目标和典范。理想好比是人的生活形象的“底片”:对过去和现在,它是人生事业现实的“曝光......
  • Shell编程与变量
    一,概述1.概念1)什么是shell:shell是一个命令解释器,它在操作系统的最外层,负责直接与用户进行对话,把用户的输入解释给操作系统,并处理各种各样的操作系统的输出结果,输出到屏......
  • 熟悉编程语言
    1.2022年10月2021年10月改变编程语言收视率改变11 Python17.08%+5.81%22 C15.21%+4.05%33 爪哇12.84%+2.38%44 C++9.92......