goto语句 VS 函数调用

介绍


函数调用操作再熟悉不过了,无论是过程式、函数式还是面向对象式的编程范式里,它随处可见。
一般来说,调用一个函数需要一个函数调用操作符,大部分编程语言里它是一对小括号:()

goto语句的话,虽然基本上用不到它,不过大家肯定也很熟悉。因为它经常被当作反面教材来告诫
新手:不要在你的程序里使用goto语句!否则你的代码会很难维护。

一般新手在经历一段时间之后,确实做到了,有的甚至都忘记怎么使用goto语句了:)

GOTO语句


goto语句虽然不适合绝大部分的应用场景,但它也不是毫无用武之地。比如,在实现一个调试器的时候,
当被调试的代码出现错误或者异常时,此时就需要一个跳转逻辑,在代码出错的地方进行跳转:把现场
的错误信息及时打印出来以方便调试。注意一点,跳转处下面的代码一般是不会执行的。这是goto与函数
调用最主要的区别。

在汇编层面来看,goto语句相当于JMP指令:仅仅是简单的跳转,跳转过去的代码执行完就没了,它是不会
回来继续执行跳转处的下一行的代码的

函数调用操作


函数在调用之前,需要将它下一条指令的地址(函数的返回地址)先入栈,以方面函数调用完,回来接着继续
执行调用处的下一行代码。

总结下


  • goto:相当于汇编的jmp指令
  • 函数调用:相当于在执行jmp指令之前,先保存下返回地址(在汇编里这两个操作被抽象成一个专门的指令:call指令)

相关的题外话


GOTO:脚本异常

当时刚接触脚本语言,开发中从游戏界面的表现上来看没有任何问题。后来我发现控制台里面打印很多error,当时我想啊,这脚本语言也太不安全了吧!都出错了,程序也不闪退或者暂停啥的。我要是不看控制台打印,岂不是都发现不了这个错误!或者除非这个错误导致了很明显的游戏逻辑、UI显示异常才会被发现。

之前做纯粹的C++开发,就像网上调侃的那样,控制台的warning我一般都是视而不见的,除非编译不过,或者崩溃了。我才会尝试在日志中搜寻“error”这类关键字。。。

再说说上面那个使用脚本语言开发的游戏,当时感觉没啥问题了,基本上达到发布的质量了,开发环境控制台也没什么报错了。只不过那个时候项目还缺少一个捕获语言异常的模块,后来折腾很长时间才加上去(H5平台,不成熟,各种坑)。加上去后随便拿几台手机跑一跑,服务器上日志一拉,尼玛成吨的错误!(这里也不全是开发的坑,H5的各个平台不一致也占很大一部分原因)

函数调用:数组越界(缓冲区溢出)

最后说一说函数调用让我记忆深刻的点:数组越界。在一连串的函数调用过程中,堆栈会保存完成的调用信息。出现崩溃,调试器会根据这些信息,给出精确无误的调用堆栈信息,比如最后崩溃在哪一行,崩溃的这一行在哪个函数里,所在函数又是被另外哪个函数调用的,函数中的各个局部变量的值也都完整的保存下来了。这样调试的话就相当方便了。

但是如果是因为数组越界导致的崩溃,那上面的所有保证都会变得不可靠,因为越界时有可能把堆栈里保存的调用信息给覆盖了。这样的话,调试器给出的信息会变得有些不正常:崩溃的行号指示在一个没有代码的空行,或者是简单到不可能出错的一个赋值语句,或者是组成函数体边界的大括号上。简直让人崩溃!

而且数组越界在第一次发生时可能不会崩溃,但在后续的逻辑执行中因第一次越界而导致更多的越界错误,直到其中一个越界错误导致了崩溃,此时程序才被调试器接管或者产生core文件(linux上)之类的。要知道,现在离真正的错误(错误的源头:第一个越界的地方)可能已经十万八千里了。

  • 这个时候建议使用版本控制工具,查看下最近改了哪些东西。
  • C/C++没有运行时下标检查,是因为考虑到运行性能问题
  • 个人经验:开发环境中,最好用assert宏对下标进行检查,release时关闭宏就好了,这样既不影响性能又能第一时间发现错误,岂不是一举两得。