《C++覆辙录》——2.4 for语句引发的理解障碍

本节书摘来自异步社区出版社《C++覆辙录》一书中的第2章,第2.4节,作者: 【美】Stephen C. Dewhurst(史蒂芬 C. 杜赫斯特),更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.4 :for语句引发的理解障碍

C++语言中有若干位置可以合法地在一个受限辖域(restricted scope)内,而不仅仅是平凡的一个嵌套闭合语句区块(nested block,即一对大括号之间的部分)中做一个变量声明。举例来说,在if语句的判别式部分就可以做一个变量声明。该变量在根据判别式控制跳转到的语句,无论true的部分还是false的部分内都有效:

if (char *theName = lookup(name) ){
  // 做一些有关theName的操作
}```
// 这里就越过了theName的辖域(`theName`不再有效)
以前的岁月里,此种变量很可能在if语句之外声明,在我们已经不需要它的时候仍然赖着不走,并带来麻烦。

char *theName = lookup(name);
if (theName){
  // 做一些有关theName的操作
}`
// theName在这里仍然有效
一般来讲,把一个变量的辖域在其所在的代码内加以限制是个好主意。因为在进行代码维护时,出于一些超出我个人理解能力的原因,这样辖域太广的变量会死灰复燃,用于一些压根无关的目的。这给文档簿记和后期维护带来的影响,说实话,相当负面(参见常见错误48)。

theName = new char[ISBN_LEN]; // theName又被用来存储ISBN号了
对for语句而言以上说法依然成立。一个迭代变量的声明可以作为语句的第一部分:

for (int i = 0; i < bufSize; ++i){ 
  if (!buffer[i])
    break;
}
if (i == bufSize) // 原先是合法的,现在不合法了,i超出了其辖域  
  // ...```
上面的代码在许多年间都是合法的C++代码,但是迭代变量的辖域后来作了调整。以前,迭代变量的辖域规定为从它被声明的那个位置(恰在初始化运算符之前,参见常见错误21)一直到包含该for语句的那个闭合语句区块的结束位置12。在C++语言新规定的语义中,迭代变量的辖域被限定到了for语句本身的结束位置。尽管大多数C++软件工程师都觉得这个调整于情于理皆无可指摘——它和语言的其余部分更加正交13,也使得循环更加容易得以优化,诸如此类——但是不得不去收拾for语句旧用法的烂摊子这一事实也实实在在地给一些维护工程师造成了一些落枕般的痛苦。

有时候这个痛苦就不止像落枕那个程度了,考虑下面的代码片段中悄然变化的变量含义:

int i = 0;
void f(){
  for (int i = 0; i < bufSize; i++){
    if (!buffer[i])
    break;
}
if (i == bufSize) // 这个i是整个文件辖域里的i
    // ...
}`
幸运的是,犯这种错误的机会毕竟不多,何况任何一个有质量可言的编译器都会就此大声警告。严肃对待编译器警告(不要关闭编译器警告)14,避免让外层辖域的变量遮掩了内层辖域里的同名变量。还有,坚决让全局变量下岗(参见常见错误3)。

让人想不通的是,迭代变量的辖域调整造成的最具破坏性的后果是它居然使一些C++软件工程师在写for语句时养成了一些坏毛病:

int i;
for (i = 0; i < bufSize; ++i){ 
  if (isprint(buffer[i]))
    massage(buffer[i]); // 译注:“按摩”内存?
  // ...
  if (some_condition) 
    continue;
  // ...
}```
这是C代码,不是C++代码。没错,这段代码的好处是在迭代变量辖域定义的调整前后具有相同的语义。但看看我们付出了什么代价:首先,迭代变量在for语句结束时仍然保持有效15;其次,i没有被初始化。这两点在代码初成时都没有问题,但是在维护时期,缺乏经验的维护工程师会在i被初始化之前就使用它,或是for语句结束之后违反作者想让i“挥挥手不带走一片云彩”的本意而继续使用它。

另一个问题是有些软件工程师干脆就不用for语句了:

int i = 0;
while (i < bufSize){
  if (isprint(buffer[i]))
    massage(buffer[i]);
  // ...
  if (some_condition)
    continue; // 错误!
  // ...
  ++i;
}`
for语句和while语句并非完全等价。比如,如果循环体内有一个continue语句,整个程序就有了一个难以察觉的语义改变。本例中,会引起死循环,它提醒我们哪里肯定出了什么毛病16。我们并非总是幸运儿。

如果你很走运地工作在一个支持for语句新语义的平台上,最好的做法就是从善如流。

不过,不幸的现实是我们的代码恐怕必须在不同的、在对for语句语义的解释相互矛盾的平台上编译。“保持两种解释下的兼容性”似乎是一个写出以下代码的好理由:

int i; 
for (i = 0; i < bufSize; ++i){ 
  if (isprint(buffer[i]))
     massage(buffer[i]);
  // ...
}```
无论如何,我都大声呼吁所有的for语句都应该在新语义下书写,为避免迭代变量的辖域过大的问题,你可以把for语句置入一个闭合语句区块内(即在for语句外面套一对大括号):

{for (int i = 0; i < bufSize; ++i){
  if (isprint(buffer[i]))
     massage(buffer[i]);
  // ...
}}`
这种写法丑陋得可以,所以当编译器的改进使得它失去存在的必要时,一定会被维护工程师发觉并移除。它还有其他的优点:他鼓励撰写初次代码的软件工程师使用for语句的新语义,并给这段代码的维护工程师省却了不少额外的麻烦。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值