当前位置: 首页 > news >正文

C++ 中名字的作用域、概念、嵌套与实践(十八)

1. 名字的作用域基本概念

作用域(scope) 指的是程序中的一个区域(通常被花括号 {} 包围),在这里一个名字(如变量名、函数名、类名等)有其特定含义。

  • 同一个作用域 中,一个名字只能绑定到唯一的实体(变量、函数或类型)上;否则会产生重定义错误。
  • 同一个名字 可以在程序的不同作用域中定义,可能指向不同的实体。这就引出了作用域的层次性和嵌套。

1.1 作用域的生效范围

当你在某处声明并定义了一个名字,它通常在当前作用域开始处一直到该作用域结束时都可用。例如:

int main() {
    int sum = 0;   // sum 的作用域从这里开始,一直到 main 函数结束
    {
        int x = 100;  // x 的作用域仅限于当前这个花括号内
        sum += x;
    } // x 在这里“消亡”,后续无法访问 x
    return 0;
}
  • sum 一直可用到 main 函数结束。
  • x 只在它所在的内部块生效,离开这个块就“超出作用域”了。

2. 不同类型的作用域

在 C++ 中,我们可以根据作用域的定义方式和所在位置,大致分为以下几种常见的作用域类型:

  1. 全局作用域(global scope)

    • 定义在所有函数体和命名空间之外的名字,比如全局变量、函数(如 int main())等。
    • 一旦声明,全局作用域内的名字可在整个程序中被访问(如果在多文件工程中,需要借助 extern 等机制进行声明引用)。
    • 例:int globalCount = 0;
  2. 命名空间作用域(namespace scope)

    • C++ 提供了命名空间(如 namespace std { })来组织和区分名字。
    • 命名空间中的实体只在该命名空间的作用域内有效。访问时可加 std:: 之类的前缀限定符,也可以在当前作用域通过 using namespace std; 或者 using std::cout; 引入。
  3. 块作用域(block scope)

    • 也称局部作用域,通常由花括号围成的区域,如函数体、if/for/while 语句块等。
    • 例:在 main() 函数中定义的变量 sum 就只有在 main 的范围内可见。
  4. 类作用域(class scope)

    • 在类内部声明和定义的成员(成员函数、成员变量)只在类中可见;可以通过公有(public)、保护(protected)或私有(private)等访问说明符控制可见性。
    • 同样也算一种“花括号”的作用域,稍微更特殊,因为它涉及到访问权限修饰符。
  5. 函数作用域与函数原型作用域(相对较少单独提及)

    • 函数作用域主要指函数体内部。
    • 函数原型作用域指参数列表(形参)的可见范围,通常只在原型声明时可见。

通常,前 3 类是我们日常编程中最常遇到和最需关注的。

3. 嵌套作用域与隐藏规则

3.1 嵌套作用域(Inner & Outer Scope)

一个作用域可以嵌套在另一个作用域之内:

  • 外层作用域(outer scope):套在外面的大的作用域;
  • 内层作用域(inner scope):被包含在内的较小范围的作用域。

在 C++ 中:

  • 如果一个名字在外层作用域中声明,则在其所有内层作用域中都是可见的(前提是没有被隐藏);
  • 可以在内层作用域使用相同的名字“重新定义”一个变量,此时 内层的定义隐藏(shadow)外层的同名实体。

3.2 隐藏与作用域操作符

  • 隐藏(shadowing):当内层作用域中定义了一个与外层作用域同名的实体时,内层实体会覆盖外层对该名字的引用。例如:

    int reused = 42;        // 全局变量 reused(全局作用域)
    int main() {
        int reused = 0;     // 局部变量 reused(块作用域,隐藏全局的 reused)
        // ...
    }
    

    main() 函数中,使用 reused 会引用 局部变量 而非全局变量。

  • 作用域操作符 :::可以显式指定访问某个特定的作用域中的名字。例如 ::reused 表示全局命名空间中的 reused 变量(如果存在),而非局部定义的那个。

4. 示例分析

以下示例展示了全局变量与局部变量的隐藏现象,以及如何通过作用域操作符来访问全局变量:

#include <iostream>

// 全局变量 reused 拥有全局作用域
int reused = 42;

int main() {
    int unique = 0;   // unique 拥有块作用域

    // #1:此时还没有局部的 reused,所以访问的是全局 reused
    std::cout << reused << " " << unique << std::endl;  
    // 输出:42 0

    // 定义一个与全局同名的局部变量 reused
    int reused = 0;   
    // #2:此时在 main() 的块作用域中,reused 指代局部变量
    std::cout << reused << " " << unique << std::endl;  
    // 输出:0 0

    // #3:显式访问全局作用域中的 reused
    std::cout << ::reused << " " << unique << std::endl; 
    // 输出:42 0

    return 0;
}

总结:同名的局部变量会“隐藏”全局变量。若要访问被隐藏的外层名字,可用作用域操作符(::)来强行指定要访问的作用域。

5. 实践建议与注意事项

  1. 避免不必要的同名隐藏

    • 在实践中,并不推荐 定义与全局变量同名的局部变量;这样做可能导致阅读者(包括自己在内)感到困惑。
    • 若确有需要(如某些极端情况、或在模板元编程、元数据注入的特殊场景),也要在注释或命名上予以说明。
  2. 优先使用局部变量

    • 相比全局变量,局部变量的作用域更小,方便管理和控制,减少命名冲突风险,也有助于写出更可维护的代码。
  3. 明确命名

    • 如果你打算区分全局变量与局部变量,可以在命名上加前缀(例如 g_ 表示全局,m_ 表示成员变量等),但要与团队达成一致约定。
  4. 命名空间

    • 对于较大的项目,将相关的函数、类等放在一个命名空间下,避免全局命名污染。
    • 当在头文件和源文件之间共享全局变量,借助 extern 和命名空间可以使结构更清晰,也能避免重名冲突。
  5. 善用作用域来防止变量滥用

    • 只在需要的范围内定义变量,作用域越小越好,方便后续维护和安全检查。
    • 在 C++17 之后,甚至可以在 ifswitch 语句的初始化中声明变量,仅在该语句内可见。

6. 结语

  • 作用域 是 C++ 中非常重要的概念,它决定了一个名字的可见范围以及它指向的具体实体。
  • 嵌套作用域 使得外层名字可以被内层使用,但也带来了同名隐藏的可能性;合理地使用和避开这种隐藏是编写清晰代码的一大关键。
  • 当你在阅读或维护他人代码时,如果发现同名变量行为诡异,别忘了考虑是不是作用域在作怪。借助 :: 作用域操作符可以进行调试或明确访问外层名字。
  • 在实际项目中,为了维持代码可读性,建议避免在内层作用域中定义与外层同名的变量。如果有使用全局变量,也要尽可能地减少、或者明确区分其含义与命名。

希望这篇文章能让你更加明确地理解 C++ 中名字的作用域,帮助你在编码中更好地组织与管理命名,写出可读性强、可维护的优秀代码!

参考资料

  1. cppreference.com (离线亦可查阅)对作用域、命名空间及隐藏规则的介绍
  2. Modern C++ 编程实战系列书籍与视频课程
http://www.dtcms.com/a/98397.html

相关文章:

  • Java基础-22-基本语法-实体类
  • 【MySQL篇】事务管理,事务的特性及深入理解隔离级别
  • Unity功能模块一对话系统(5)-完善对话流程及功能
  • Python针对大规模数据使用”sys模块加速I/O操作“:
  • 21天Python计划:函数简单介绍
  • PCBB印刷电路板缺陷检测YOLO数据集分享
  • LeetCode hot 100—两两交换链表中的节点
  • Redis场景问题1:缓存穿透
  • 用python压缩图片大小
  • 算法-广度优先搜索
  • WSL系统找不到指定的文件
  • 接口自动化——初识pytest
  • (头歌作业—python)3.2 个人所得税计算器(project)
  • 智能发光斑马线:点亮城市道路安全之光
  • DDR(Double Data Rate)详解
  • 云边端协同
  • vue 脚手架解决跨域问题
  • 2022年12月青少年软件编程(图形化)等级考试三级编程题
  • 在树莓派5(8G版)上运行Ollama + LLM
  • NG-ZORRO中tree组件的getCheckedNodeList怎么使用
  • 26考研——排序_选择排序_选择排序的基本思想 简单选择排序(8)
  • 每天一篇目标检测文献(六)——Part One
  • 小林coding-12道Spring面试题
  • 自然语言处理(17:(第五章2.)梯度消失和LSTM(详细拆解))
  • WebSocket通信的握手阶段
  • 基于 WebAssembly 的 Game of Life 交互实现
  • ArrayList和LinkedList比较
  • 使用 Python 进行链上数据监控:让区块链数据触手可及
  • 深入解析音频:格式、同步及封装容器
  • SQL Server:sys.dm_hadr_physical_seeding_stats