跳转到内容
新建笔记

Python、C 与 C++:作用域、链接与对象生存期

先分开作用域、链接与生存期

跳转到“先分开作用域、链接与生存期”

Python 与 C 都不能用“哪个函数调用了我”来决定普通名字的词法归属,但二者绑定名字、组织文件和管理对象的规则有明显差异。本文保留原笔记逐项对照的学习方式,以 Python 3.11、C11 为主要范围;原文夹在 C 段落里的类示例单独标为 C++17。

概念需要回答的问题不能直接推出的结论
作用域在哪一段代码中可以使用这个名字对象一定在离开该段代码时消失
链接属性,主要用于这里的 C 讨论不同声明是否指向同一个对象或函数另一个源文件无需声明就能访问
对象生存期/存储期对象在执行过程中的存在区间对象的名字在整个区间都可见
Python 属性查找obj.name 从哪里取得属性与方法中的裸名字 name 使用同一套查找路径

1. 普通语句块:C 的新声明与 Python 的赋值

跳转到“1. 普通语句块:C 的新声明与 Python 的赋值”

C 的复合语句块可以引入局部声明,内层同名对象会遮蔽外层对象。下面保存为 block_scope.c,独立编译运行。

#include <assert.h>
#include <stdio.h>
int main(void)
{
int x = 10;
if (x > 5) {
int y = 20;
assert(x == 10 && y == 20);
printf("inside if: x=%d y=%d\n", x, y);
}
/* y is not declared here. */
assert(x == 10);
{
int x = 110;
assert(x == 110);
printf("inner x=%d\n", x);
}
assert(x == 10);
printf("outer x=%d\n", x);
return 0;
}

内层 int x = 110 声明了另一个对象,不会把外层 x 改为 110。块外直接写 printf("%d", y) 则缺少可见声明,应由编译器诊断。声明的可见范围还与声明出现的位置有关,不能把它粗略说成从函数第一行就开始。

Python 的普通 if、for、while 不因缩进建立同类的局部作用域。同一个函数中的普通赋值会更新相应绑定;分支从未执行时,名字也可能从未绑定值。

def block_demo(flag):
x = 10
if flag:
y = 20
x = 110
return x, y
assert block_demo(True) == (110, 20)
try:
block_demo(False)
except UnboundLocalError:
print("false branch: y has no value")
else:
raise AssertionError("expected UnboundLocalError")
assert "y" not in globals()

这里 y 属于函数,不是模块全局变量。推导式、函数、类定义有另外的名字规则,不能把“普通 if 不产生作用域”推广为所有缩进结构都一样。Python 名字绑定与解析

2. 函数中的局部名字:术语不要混用

跳转到“2. 函数中的局部名字:术语不要混用”

C 日常表达中的“函数局部变量”,在标准术语里通常属于块作用域。C 的正式“函数作用域”专用于标签名;此外还有文件作用域和函数原型作用域。函数形参在函数定义中的名字也属于相应的块作用域。

Python 函数则确实有自己的局部名字环境。下面同时说明局部变量不能由调用者直接访问,以及被调用函数不会去调用者的局部变量中寻找同名值。

value = 40
def read_value():
local_value = 30
assert local_value == 30
return value
def caller():
value = 999
result = read_value()
assert value == 999
return result
assert caller() == 40
assert "local_value" not in globals()
print("callee uses its defining module:", caller())

caller 的 value=999 不会动态改变 read_value 的名字解析环境。更完整的 global、nonlocal 和 a += 1 例子见 Python 作用域与名字绑定。

3. “全局”不是整个程序无条件可见

跳转到“3. “全局”不是整个程序无条件可见”

C 在函数外声明的对象通常具有文件作用域;其他翻译单元若要访问具有外部链接的对象,需要可见且兼容的声明。文件作用域加 static 则常用于限制为内部链接。这里要分别考虑可见声明与链接关系,而非一句“所有函数和类之外的变量处处可访问”;C 本身没有类。

Python 的全局名字属于具体模块。一个模块定义函数后,即使其他模块导入并调用该函数,它的普通全局查找仍指向定义它的模块。global 也不会将名字发布到所有模块。Python 模块教程

4. 多文件:C 头文件/源文件与 Python 模块

跳转到“4. 多文件:C 头文件/源文件与 Python 模块”

下面的 C 示例由三个文件组成。头文件提供声明,源文件提供唯一的外部对象定义;另有内部链接的调用计数供访问器使用。

mymodule.h:

#ifndef MYMODULE_H
#define MYMODULE_H
extern int global_value;
int get_global_value(void);
int get_read_count(void);
#endif

mymodule.c:

#include "mymodule.h"
int global_value = 60;
static int read_count = 0;
int get_global_value(void)
{
++read_count;
return global_value;
}
int get_read_count(void)
{
return read_count;
}

module_main.c:

#include <assert.h>
#include <stdio.h>
#include "mymodule.h"
int main(void)
{
assert(get_global_value() == 60);
assert(get_read_count() == 1);
global_value = 61;
assert(get_global_value() == 61);
assert(get_read_count() == 2);
printf("value=%d reads=%d\n", global_value, get_read_count());
return 0;
}

编译时需要把 mymodule.c 和 module_main.c 一起链接,例如 gcc -std=c11 -Wall -Wextra -Werror -pedantic-errors mymodule.c module_main.c -o module_demo。只包含头文件不会自动把定义链接进来;遗漏源文件将产生未解析符号。read_count 的定义具有内部链接,另一个源文件不能仅写一个外部声明就取得这个内部对象。

Python 版本同样用两个文件,但导入建立的是模块对象及名字绑定关系。

mymodule.py:

global_value = 60
def get_global_value():
return global_value
def increment():
global global_value
global_value += 1

同目录下的 module_main.py:

import mymodule
from mymodule import global_value as imported_value
assert mymodule.get_global_value() == 60
assert imported_value == 60
mymodule.increment()
assert mymodule.global_value == 61
assert imported_value == 60
print("module:", mymodule.global_value, "imported binding:", imported_value)

from ... import ... 在本模块创建名字绑定,不是将两个变量名永久连接。increment() 重新绑定了 mymodule.global_value,本例中先前导入的整数绑定仍为 60。若共享的是可变对象,修改对象与重新绑定又有不同结果;参见 copy 与对象身份。Python 模块也不都来自普通 .py 文件,内置模块与扩展模块同样可以参与导入。

5. 类的对照:这是 C++,不是 C

跳转到“5. 类的对照:这是 C++,不是 C”

原笔记在“C 语言”标题下使用了 class、public 和 std::cout。这些属于 C++。此外,“类成员只能在类内部访问”也不准确:下面的公开成员可以经对象在类外访问。

#include <cassert>
#include <iostream>
class Example {
public:
int classValue = 50;
int readValue() const { return classValue; }
};
int main()
{
Example object;
assert(object.classValue == 50 && object.readValue() == 50);
object.classValue = 51;
assert(object.readValue() == 51);
std::cout << object.classValue << '\n';
}

这是 C++17 的独立程序,可用 g++ -std=c++17 编译。类内名字查找与 public / private / protected 访问控制是相关但不同的问题;这里的非静态成员值属于每个实例。C++ 类作用域、成员访问控制

Python 的同名对照也不能直接照搬 C++:类体赋值通常创建类属性,而方法里的裸名字不自动去类属性表查找。

class_value = 5
class Example:
class_value = 50
def read_values(self):
return class_value, self.class_value, type(self).class_value
first = Example()
second = Example()
assert first.read_values() == (5, 50, 50)
first.class_value = 51
assert first.read_values() == (5, 51, 50)
assert second.class_value == 50 and Example.class_value == 50
print(first.read_values())

为 first.class_value 赋值在此例创建实例属性,并未修改 Example.class_value。描述符、继承和自定义属性访问还会进一步影响行为;这个例子只对照普通类与实例属性。Python 类与实例变量

6. C 有标识符名字空间,但没有 C++ 的 namespace 语法

跳转到“6. C 有标识符名字空间,但没有 C++ 的 namespace 语法”

原说法“C 没有命名空间”容易产生歧义。C 没有 C++ 那样的 namespace group { ... } 组织语法,却在语言规则中区分标签、结构/联合/枚举标签、成员及普通标识符等名字空间。下面刻意使用同一个拼写,展示语法上下文如何区分它们。

#include <assert.h>
#include <stdio.h>
struct reading { int reading; };
int main(void)
{
int reading = 70;
struct reading sample = {80};
assert(reading == 70 && sample.reading == 80);
goto reading;
reading:
printf("ordinary=%d member=%d\n", reading, sample.reading);
return 0;
}

这不是推荐同名命名风格,只用于验证规则。struct reading、变量 reading、成员 sample.reading 和标签 reading: 各自靠不同上下文识别。C 项目使用模块前缀减少外部符号冲突仍然很有用,但这是命名约定,不等同于语言新增了一层用户命名空间。

7. 名字遮蔽与普通赋值

跳转到“7. 名字遮蔽与普通赋值”

前面的 C 程序在嵌套块内重新声明 int x,退出内层后又能用原来的外层 x。Python 在普通 if 内写 x=... 则没有声明另一个块级局部名字,后续同一函数中的 x 看到的是更新后的绑定。原文的两组“作用域链”和“变量隐藏”例子主要展示的就是这个差别。

不要因此推断 Python 不会遮蔽名字:内层函数的局部名字可以遮蔽外层函数或模块中的同名名字,函数内赋值还可能导致 UnboundLocalError。也不要把 C 作用域链机械写成“块→函数局部→全局”,忽略正式术语中的标签作用域和声明位置。C 的有关规则可对照委员会草案 N1570,6.2.1–6.2.3。

8. 词法作用域、闭包和 nonlocal

跳转到“8. 词法作用域、闭包和 nonlocal”

Python 的 global 与 nonlocal 不会切换为动态作用域。它们指定名字应绑定到哪一个词法环境。闭包可以在创建它的外层调用结束后继续访问所保留的绑定。

global_value = 150
def outer_function():
nonlocal_value = 160
def inner_function():
nonlocal nonlocal_value
nonlocal_value += 10
return nonlocal_value
return inner_function
increment = outer_function()
assert increment() == 170
assert increment() == 180
assert global_value == 150
print("closure survives the outer return")

C11 不支持在函数体内定义普通嵌套函数;某些编译器扩展不能作为通用 C 规则。需要把状态与操作一起传递时,可以显式使用结构体和函数指针,而不是假设 C 会捕获外层局部变量。本例没有通过返回悬空指针模拟闭包。Python nonlocal

9. 块结束不意味着所有 C 对象都结束生存期

跳转到“9. 块结束不意味着所有 C 对象都结束生存期”

原总结“C 的变量在块结束时被销毁”只适合特定自动存储期对象,不能覆盖块内 static。下面 calls 的名字只能在 next_call 内使用,但对象在函数调用之间保留值。

#include <assert.h>
#include <stdio.h>
static int next_call(void)
{
static int calls = 0;
++calls;
return calls;
}
int main(void)
{
int first = next_call();
int second = next_call();
assert(first == 1 && second == 2);
printf("calls: %d %d\n", first, second);
return 0;
}

C 的自动、静态、线程、分配存储期分别有各自规则,作用域与生存期需要分开讨论;指向已经结束生存期的自动对象的指针不能再用于访问该对象。Python 的对象也可能因为闭包、容器或其他引用而继续存在,离开某个函数不等于所有曾由局部名字引用的对象都立即消失。本文的程序验证名字可见性与这些受控状态变化,没有执行越界生存期访问。

原文来源:CSDN:Python 和 C 中作用域的差异,首次发布于 2025-03-29。