亡羊补牢还是越错越远——“C99允许在函数中的复合语句中定义变量”

时间:2021-06-17 02:18:29

作者:starwing83网友

  谭书其实和C语言有一个很相像的地方,就是都出了很多个版本。然而C语言出新的版本是为了方面程序员、适应新的变化和开发风格。而一本教材出新的版本无非就是修正错误和描述语言的新方向。然而,如果一本教材的第一版就是概念不清胡说八道,却又碍着面子不肯承认自己的错误,那最终的结果就是越错越远了。
  就比如这里:

  定义变量的位置:一般在函数开头的声明部分定义变量,也可以在函数外定义变量(即外部变量、全局变量,见第7章)。C99允许在函数中的复合语句(用一句花括号括起来)中定义变量。
————谭浩强 ,《C程序设计》(第四版),清华大学出版社, 2010年6月,(前言)p41

  我相信谭老一定没有亲自去看过C99标准,不知道是英文不好还是自信满满觉得自己比标准要牛。所以,虽然谭老的确知道有“标准化”这么一个说法,却只有错上加错的份儿。事实上,我看不出这里的“一般”有什么凭据,难道谭老从没有看过正式地C语言项目吗?一般情况下,只有八十年代到九十年代的C语言项目才会“一般”在函数开头定义变量,新的项目一般定义变量的位置是复合语句的开头。将自己的臆测煞有介事地说成业界惯例,不知道这是无耻还是做得太多已经麻木了。
  让我们来揭开谜底吧。实际上,这段话几乎每一句都是错误的。”一般在函数开头的声明部分“,很遗憾地,C99根本就没有“声明部分”的说法,而C90中,声明部分根本就不止限于函数开头!“也可以在函数外定义变量”这一句是对的,然而括号坏事了:“(即外部变量,全局变量)”,事实上C语言根本没有全局变量的说法,只有外部变量,静态变量的说法,因为事实上这里的全局是有两个含义的:文件域的全局,和整个程序范围的全局。这两个含义是根据变量的链接性来定义的,我们之前就提到了链接性的概念,而谭书对此只字不提。“C99运行在函数中的复合语句(用一对花括号括起来)中定义变量”,首先这是C90的特性,而不是C99的,其次,括号内的部分明显代表谭老根本就不知道什么是复合语句,花括号本身是复合语句的一部分,复合语句花括号内的部分在标准中是被叫做“块项列表(block-item-list)”的,而列表中的一个块项既可以是声明,也可以是语句。对基本概念都不清晰,也难怪错的这么离谱了。
  我们总结一下:
  - C99实际上是允许你在任何地方声明变量的。
  - C90声明部分实际上是可以在任何复合语句的开头的。
  也就是说,下面的代码在C90里面仍然是合法的:

    void reverse(int array[], size_t size) {
        int i = 0, j = size - 1;
        for (; i < j; ++i, --j) {
            int temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    }

   只有唯一一种情况是必须在函数开头申请变量的:即该变量在整个函数的执行过程中都必须存在。而真正有这么一个需求是很罕见的。如同这里的 temp 变量,它只需要在交换的时候才存在,因此就不该把它和 i, j 声明在一起。上面的代码是合法的 C90 代码,可以在任何严格支持 C90 (而不支持 C99)的编译器下编译。
  就算你用的是C90的编译器,你也仍然可以获得“随处声明变量”的特权,很简单,在需要使用局部变量的时候,直接用复合即可,这也是复合语句作为一种单独的语句类型出现的另一个理由。(前一个理由是作为其他语句(如if for)中的语句部分存在)
  在真正使用的时候申请变量是个好习惯,这也是 C99 真正的改变是允许变量在任意位置定义,而不仅限复合语句开头的缘故。这可以防止变量的名字冲突,简化逻辑——新申请的变量哪怕名字和之前申请的一致,也不会对旧的变量有任何影响,一旦新变量超过作用域,旧的变量仍然有效。这是C语言的一个重大特性,也被认为是C语言最优秀的一个特性,这个特性就是大名鼎鼎的“词法作用域”。它被广泛地用在了大量的语言上,是编程语言的一个约定俗成的基本特性。如果不允许在复合语句内申请变量,无异于让C语言自废武功。
  并且,我们知道,C语言的局部变量是分配在栈上的。那么局部变量的及时释放也帮助节省栈空间:对于两个平行的复合语句块,其内部的局部变量事实上是共享一块区域的,这就是词法作用域的另一个实际好处。
  为什么谭老即使是愿意让C语言自废武功,也不愿意介绍真正的C99的变量声明特性呢?原因是谭书的前一个版本言之凿凿地说明“变量只允许声明在函数开头”,记得我最开始看谭书的时候,就一直是这么申请变量的,直到自行翻阅C90标准才走出这个误区。08年的时候我帮助一个上海交大的生物系研究生修改其 DNA 检测代码,就发现一堆的 i,j, k, l, m, n 被申请在函数开头,整个程序十分难维护,后来花了很大功夫才将代码整理完成,由此可见谭书危害之深。
  程序员一个重要的素质就是肯承认错误,肯承认失误。程序本就是思维的结晶,是智慧最集中的产品。不肯承认错误,就一定会被更先进者替代。而谭书却死死抓住“权威”两字不放,宁愿错上加错也不肯承认自己不是完人。这样的心态怎么符合一个程序员的自我修养呢?又怎么培育合格的程序员呢?书的销量越好,事实上是流毒越多而已。
  下面我们介绍一下C99真正的新特性,看看标准委员会为C语言的进步做出了什么样的努力。
  C99从C++中引进了可以“随处申请变量”的特性,并修改了 for 语句的语法,添加了一个新的 for 语句形式。对C99来说,符合语句内已经不区分“声明部分”和“语句部分”,在任何地方都可以书写声明或者语句,而新加入的 for 语句的语法是允许 for 的第一个部分是声明,后跟可选的表达式(注意声明本身会带上一个分号),后跟必须的分号,再跟可选的表达式。也就是说,上面的反转函数实际上可以这么写:

    void reverse(int array[], size_t size) {
        for (int i = 0, j = size - 1; i < j; ++i, --j) {
            int temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    }

 

   这时,i, j, temp 就同属一个作用域了,即 for 语句的语句体。在 for 语句之外使用 i, j, temp 会造成编译错误。
  注意和C++不同,对C99来说只有 for 才有在第一部分申请变量的殊荣, if、while等等语句都是不允许的。而在C++里面这些语句的条件判断部分都是可以申请新变量的。
  从C99的新特性可以看出,C语言显然易见地是支持并鼓励“使用处声明”的编程方法的。而谭书却对此只字不提,挂着着C99的羊头,卖着C89的狗肉,不愿大大方方地承认自己的错误,甚至连一点点改过的意向都没有,只是一门心思的一错再错,一条路走到黑。这种态度不止是一本教科书编撰者,就算只是一个普通的程序员,恐怕也逃不过被开除的命运。