`
RednaxelaFX
  • 浏览: 3053119 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

函数/方法的局部作用域与for的作用域问题

阅读更多
前天跟axx大聊起那个do..while(0)的宏的时候顺带聊到了别的一些语法结构的诡异地方。
觉得在C或者C-like语言里很麻烦的一个语法结构是for语句。比较常见的定义方式会是:
ForStatement -> "for" "(" ForInitialize ";" ForCondition ";" ForIncrement ")" ForBody
              ;

ForInitialize -> VariableDeclarationList
               | ExpressionList
               ;

ForCondition -> Expression
              ;

ForIncrement -> ExpressionList
              ;

ForBody -> Statement
         ;

也就是,一般来说for语句头部的括号里,第二部分是一个表达式,第三部分是一个表达式列表,而第一部分可能是一个变量声明列表或者一个表达式列表。按照局部作用域的规则,一般来说在这第一部分里声明的变量都是局部与for语句内的;如果与外部作用域已定义的变量重名,则可能:
1、不允许这样重定义(Java、C#、D等);
2、在for语句的局部作用域内创建一个新的局部变量,遮盖外部作用域原本的同名变量(C99/C++98);
3、不允许在for语句的头部定义新变量——所有局部变量都必须在局部作用域的一开头定义。(C99以前的C);
4、由于同一个局部作用域允许同一个名字的变量多次声明,所以实际上声明与不声明都没啥区别;for的头部里声明的变量与外部作用域的同名变量可以看成是“同一个”(ECMAScript 3)。

让我们看看C-like语言里具体是怎么定义的。关键要留意一下for头部的第一部分的规定。

------------------------------------------------------------------------------

C99:ISO/IEC 9899:1999, 6.8.5.3
引用
1 The statement
for ( clause-1 ; expression-2 ; expression-3 ) statement

behaves as follows: The expression expression-2 is the controlling expression that is evaluated before each execution of the loop body. The expression expression-3 is evaluated as a void expression after each execution of the loop body. If clause-1 is a declaration, the scope of any variables it declares is the remainder of the declaration and the entire loop, including the other two expressions; it is reached in the order of execution before the first evaluation of the controlling expression. If clause-1 is an expression, it is evaluated as a void expression before the first evaluation of the controlling expression.134)
2 Both clause-1 and expression-3 can be omitted. An omitted expression-2 is replaced by a nonzero constant.

C99里的for语句与前面说的“一般情况”吻合。第一部分的子句可以是变量声明或者表达式,但不能是语句。

演示代码:
testCScope.c:
#include <stdio.h>

int main( ) {
    int c = 0;
    for ( int i = 0; i < 2; ++i ) {
        // ... do something
    }
    printf( "%d\n", c ); // 0
}

/*
rednaxela@META-FX /d/experiment
$ gcc -std=c99 testCScope.c -o testCScope.exe
*/

用GCC 3.4.5编译出来的结果。跟预期一样,for里创建了一个新的局部变量c,遮蔽了main()里的c。

------------------------------------------------------------------------------

C++98:ISO/IEC 14882:1998, 6.5.3
(这PDF复制不了……懒得打字,截图代替)

可以看到,C++98里对for语句头部第一部分的定义与C99的写法不一样——第一部分是一个语句,而那个分号是语句的一部分。
不过还得结合另外一部分的规定来看:
引用
for-init-statement:
        expression-statement
        simple-declaration

结合这个来看,其实它与C99的规定并没有多少区别。只是写法上的差异而已。

演示代码:
testCppScope.cpp:
#include <iostream>

int main( ) {
    int c = 0;
    for ( int i = 0, c = 1; i < 2; ++i ) {
        // ... do something
    }
    std::cout <<  c << std::endl; // 0
}

/*
D:\experiment>cl testCppScope.cpp
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

testCppScope.cpp
C:\Program Files\Microsoft Visual Studio 9.0\VC\INCLUDE\xlocale(342) : warning C
4530: C++ exception handler used, but unwind semantics are not enabled. Specify
/EHsc
Microsoft (R) Incremental Linker Version 9.00.21022.08
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:testCppScope.exe
testCppScope.obj
*/

用GCC 3.4.5和VC++2008编译都一样。运行结果是0,没问题,跟预期一样,与C99也吻合。

------------------------------------------------------------------------------

Java:Java Language Specification, 3rd Edition
引用
BasicForStatement:
        for ( ForInitopt ; Expressionopt ; ForUpdateopt ) Statement

ForStatementNoShortIf:
        for ( ForInitopt ; Expressionopt ; ForUpdateopt )
         StatementNoShortIf

ForInit:
        StatementExpressionList
        LocalVariableDeclaration

ForUpdate:
        StatementExpressionList

StatementExpressionList:
        StatementExpression
        StatementExpressionList , StatementExpression

Java的语法也与“一般情况”吻合。但是它不允许在for的头部对方法的局部变量进行再次声明,所以下面的代码在编译时会出现错误。

演示代码:
testJavaScope.java:
public class testJavaScope {
    public static void main( String[ ] args ) {
        int c = 0;
        for ( int i = 0, c = 1; i < 2; ++i ) { // error
            // ... do something
        }
        System.out.println( c );
    }
}

/*
D:\experiment>javac testJavaScope.java
testJavaScope.java:4: 已在 main(java.lang.String[]) 中定义 c
        for ( int i = 0, c = 1; i < 2; ++i ) {
                         ^
1 错误
*/


------------------------------------------------------------------------------

C#:ECMA-334 4th Edition, A.2.5
引用
for-statement:
    for ( for-initializeropt ; for-conditionopt ; for-iteratoropt ) embedded-statement

for-initializer:
    local-variable-declaration
    statement-expression-list

for-condition:
    boolean-expression

for-iterator:
    statement-expression-list

statement-expression-list:
    statement-expression
    statement-expression-list , statement-expression

于是C#的for语句在语法上也跟C99、C++98、Java等相似,属于“一般情况”。

演示代码:
testCSharpScope.cs:
sealed class Test {
    public static void Main( string[ ] args ) {
        int c = 0;
        for ( int i = 0, c = 1; i < 2; ++i ) { // error
            // ... do something
        }
        System.Console.WriteLine( c );
    }
}

/*
D:\experiment>csc testCSharpScope.cs
适用于 Microsoft(R) .NET Framework 3.5 版的 Microsoft(R) Visual C# 2008 编译器 3.5.21022.8 版
版权所有 (C) Microsoft Corporation。保留所有权利。

testCSharpScope.cs(4,26): error CS0136:
       不能在此范围内声明名为“c”的局部变量,因为这样会使“c”具有不同的含义,
       而它已在“父级或当前”范围中表示其他内容了
*/

这段代码编译出错了。但是出错的原因与Java的版本并不完全相同,因为Java与C#的作用域规则并不完全一样。这里我们暂时不关心那个问题,至少在for语句头部的第一部分表现相似就是了。

------------------------------------------------------------------------------

吉里吉里2的TJS2
引用
2.28,\kirikiri2\src\core\tjs2\syntax\tjs.y,第298行开始
/* a for loop */
for
    : "for" "("
      for_first_clause ";"
      for_second_clause ";"
      for_third_clause ")"
      block_or_statement                    { cc->ExitForCode(); }
;


/* the first clause of a for statement */
for_first_clause
    : /* empty */                           { cc->EnterForCode(false); }
    |                                       { cc->EnterForCode(true); }
      variable_def_inner
    | expr                                  { cc->EnterForCode(false);
                                              cc->CreateExprCode($1); }
;

/* the second clause of a for statement */
for_second_clause
    : /* empty */                           { cc->CreateForExprCode(NULL); }
    | expr                                  { cc->CreateForExprCode($1); }
;

/* the third clause of a for statement */
for_third_clause
    : /* empty */                           { cc->SetForThirdExprCode(NULL); }
    | expr                                  { cc->SetForThirdExprCode($1); }
;

语法上也属于“一般情况。看看运行时如何?

演示代码:
startup.tjs:
function foo() {
    var c = 0;
    for ( var i = 0, c = 1; i < 2; ++i ) {
        // ... do something
        // System.inform( c );
    }
    System.inform( c );
}

foo();

运行结果是c == 0。去掉中间的注释的话,可以看到for循环中c是1,没问题。
于是TJS2在这个地方的行为与C99/C++98更相似。

------------------------------------------------------------------------------

D语言在这里比较诡异。
D 1.0
D 2.0
引用
ForStatement:
	for (Initialize Test; Increment) ScopeStatement

Initialize:
	;
	NoScopeNonEmptyStatement

Test:
	empty
	Expression

Increment:
	empty
	Expression


演示代码1:
testDScope.d:
void main(char[][] args) {
    int c = 0;
    for (int i = 0, c = 1; i < 2; ++i) { // error
        // ...do something
    }
    printf("%d", c);
}

/*
D:\experiment>dmd testDScope.d
testDScope.d(3): Error: shadowing declaration testDScope.main.c is deprecated
*/

OK,编译时出现错误。跟前面Java和C#的行为差不多。但是……

演示代码2:
testDScope.d:
void main(char[][] args) {
    int c = 0;
    for ({int i = 0; c = 1;} i < 2; ++i) {
        // ...do something
    }
    printf("%d", c); // 1
}

这段代码可以顺利通过编译(DMD 2.012),而且运行的结果与C/C++不一样……
诡异吧?

------------------------------------------------------------------------------

ECMAScript:ECMA-262 3rd Edition, 12.6
引用
for (ExpressionNoInopt; Expressionopt ; Expressionopt ) Statement
for ( var VariableDeclarationListNoIn; Expressionopt ; Expressionopt ) Statement

看上去语法与“一般情况”吻合。但这ECMAScript实际上也不乖……

让我们用Rhino 1.7R1来测试一下:
Rhino 1.7 release 1 2008 03 06
js> var c = 0
js> for ( var i = 0, c = 1; i < 2; ++i ) { /* ... */ }
js> c
1
js> i
2

看到了吧,c的值变为1了。这跟ECMAScript对作用域的规定相关:同一个作用域内同一个名字的变量可以多次声明;多次声明的同名变量还是“同一个”;var关键字声明的变量拥有的是function scoping。所以……要是按照Java或者C#的习惯来写JavaScript代码,这里就危险了……
从JavaScript 1.7开始增加了let关键字,相应增加了let语句、let表达式和let声明。以let关键字而不是var关键字声明的变量的作用域就是局部于最小的语句块的,而不是函数的。但是for循环的初始化部分却无法用let关键字声明循环变量……

===========================================================================

真的是自己不写语言的语法都不觉得,真到要自己写语法的时候就会注意到很多这种诡异的地方 T T
3
1
分享到:
评论

相关推荐

    函数的全局和局部作用域和变量-作用域.html

    函数的变量和作用域 /全局变量和全局作用域指的是变量或者函数的位置 ... // 局部作用域:任何一个函数的内部都有一个局部作用域,在局部作用域中定义的变量 局部变量。局部变量只有在定义该变量的函数中可以访问

    C语言、变量和函数的作用域与生存期

    2. **局部作用域(Local Scope)**:局部变量在函数内部定义,仅在该函数内部可见。函数调用结束后,局部变量的存储空间会被释放,它们的值也会丢失。 3. **块级作用域(Block Scope)**:在C99引入了复合语句...

    javascript 闭包、匿名函数、作用域链

    JavaScript中的闭包、匿名函数和作用域链是编程中至关重要的概念,它们是理解JavaScript运行机制的关键。在本文中,我们将深入探讨这三个概念,并通过实际示例来展示它们的运用。 首先,我们来讨论“闭包”。闭包是...

    JavaScript: 函数与作用域深入解析及应用场景

    接着讨论了 JavaScript 中的全局作用域、局部作用域、块级作用域和函数作用域,特别是闭包的概念。随后,文章探讨了函数的高级用法,如递归函数、高阶函数和立即执行函数表达式(IIFE)。最后,通过实际应用示例,如...

    python 包、模块、函数与变量作用域视频讲解

    2. 局部作用域(Local Scope):在函数内部定义的变量,只在该函数内部可见。 3. 块作用域(Block Scope):如`for`和`while`循环,`if`语句等,它们有自己的作用域。 4. 闭包作用域(Closure Scope):当一个内嵌...

    Python语言基础:作用域.pptx

    - **局部作用域(Local, L)**:这是最内层的作用域,通常在函数或方法内部,只包含在此区域内定义的变量。这些变量在函数执行完毕后会被销毁。 - **包围作用域(Enclosing, E)**:当一个函数或类内部包含另一个...

    作用域的定义及应用

    2. **局部作用域**:只在特定代码块(如函数内部)中可访问的变量。 3. **块级作用域**:在特定的代码块(如循环体、条件语句等)中可访问的变量。 #### 三、函数原型作用域 函数原型中的参数具有特定的作用域。...

    深入理解js函数的作用域与this指向.docx

    2. **局部作用域**:在函数内部定义的变量仅在该函数内部可见,无法在函数外部访问。此外,每个函数都拥有自己的作用域链,用于在执行过程中查找变量。 **作用域链**:当尝试访问一个变量时,JavaScript会沿着作用...

    学习python中变量作用域及嵌套作用域.pdf

    局部作用域是指当前函数或代码块中的变量作用域。局部作用域中的变量优先级最高,只有在局部作用域中找不到变量时,Python 才会去外部作用域和全局作用域中查找。 外部作用域是指当前函数或代码块的外部函数或代码...

    深入理解变量作用域

    - 定义:当一个变量在函数内部声明时,该变量仅在该函数内有效,这就是局部作用域。 - 特性: - 函数内部声明的变量不会污染全局命名空间。 - 如果函数内部声明的变量与全局变量同名,局部变量会覆盖全局变量。 ...

    05-JavaScript作用域.pdf

    函数作用域通常与局部作用域相重叠,但指的是通过函数定义的方式(使用function关键字或函数表达式)来创建的函数,它们作用域的范围被限制在函数体内。函数作用域内的变量可以是局部的,也可以是从外部作用域中引入...

    深入理解JavaScript作用域和作用域链

    局部作用域则限定在特定代码块,如函数内部。在函数内部定义的变量只有在该函数内部可以访问,外部无法直接访问。这样的设计有助于防止变量污染全局空间,提高代码的可维护性和性能。 作用域链是JavaScript中另一个...

    JavaScript:函数与作用域

    - **定义**:在函数内部声明的变量拥有局部作用域。 - **访问**:这些变量仅在该函数内部可访问。 - **示例**: ```javascript function checkLocal() { var localVar = "我是局部变量"; console.log(localVar)...

    PHP函数 -变量的作用域.ppt

    一、局部变量 局部变量是在函数内部定义的变量,其作用域仅限于函数内部,离开该函数后再使用此变量是非法的。另外,函数定义中的普通形参由于只能在本函数内部使用,因此也是局部变量。 二、全局变量 全局变量是指...

    c#从入门到实践; 示例 使用变量记录用户的登录名;变量的作用域

    2. **局部作用域**:在函数内部声明的变量是局部的,它们只在该函数内可见。当函数执行结束时,这些变量会被销毁。 3. **类成员作用域**:在类的定义中,但不在任何方法内的变量称为类成员变量,它们对类的所有实例...

    javascript变量作用域

    JavaScript 还没有块级作用域,这意味着在 if 语句、for 循环、while 循环等语句块中定义的变量,在整个函数体内都是可见的,而不是只在语句块中可见。 在 JavaScript 中,函数中声明的变量在整个函数中都有定义。...

    3.8 函数参数与变量的作用域(ppt).zip

    2. **局部作用域**:在函数内部定义的变量具有局部作用域,只在函数内部有效。一旦函数执行完毕,这些变量就会被销毁。例如: ```python def local_example(): y = 5 print(y) # 输出5 local_example() print(y)...

    四讲函数及变量的作用域PPT课件.pptx

    关于变量的作用域,C++中有全局作用域和局部作用域。全局变量在整个程序中都是可见的,而局部变量只在定义它的函数或代码块内部可见。例如: ```cpp int global_var = 10; // 全局变量 void func() { int local_...

    什么是作用域?

    ### 什么是作用域? 作用域是编程语言中的一个重要概念,它定义...通过掌握不同的作用域类型,如函数原型作用域、局部变量和全局变量,开发者能够更好地控制变量的生命周期和可见性,从而编写出更加清晰和高效的程序。

    JavaScript 基础函数_深入剖析变量和作用域

    - **局部作用域**:在函数内部声明的变量,只在函数内部可见,函数执行完毕后,局部变量会被销毁。 在ES6中,引入了`let`和`const`两个新的关键字,用于声明块级作用域的变量,它们的使用更加严格,不允许变量提升...

Global site tag (gtag.js) - Google Analytics