理解 C++ 名字时,要分开四件事:声明了什么、在何处能找到名字、对象存在多久、不同翻译单元是否指同一实体。它们分别涉及声明/定义、作用域与查找、存储期与寿命、链接性,不能放在同一张“变量作用域表”中混讲。本篇以 C++17 为例。
声明与定义
跳转到“声明与定义”声明向编译器描述实体的名字和性质;定义提供实体的完整内容或创建对象。定义通常也是声明,一个名字出现并不总是再次创建对象。
| 形式 | 示例 | 作用 |
|---|---|---|
| 变量声明 | extern int count; | 声明由别处定义的变量,不在这里定义它 |
| 变量定义 | int count = 0; | 定义变量 |
| 函数声明 | int add(int, int); | 声明可调用接口 |
| 函数定义 | int add(int a, int b) { return a + b; } | 提供函数体 |
| 类前置声明 | class Sensor; | 使类名可见,尚不提供完整布局 |
| 类定义 | struct Sensor { int value; }; | 定义类型,不同时创建某个 Sensor 对象 |
| 枚举定义 | enum class Mode { Idle, Running }; | 定义枚举类型与枚举项 |
| 类型别名 | using Count = unsigned; | 为已有类型提供别名,不创建新对象 |
| 模板声明/定义 | template<class T> struct Box { T value; }; | 描述一族类型 |
| 命名空间定义 | namespace App { int count = 0; } | 将成员名字放入命名空间 |
| 宏定义 | #define CAPACITY 16 | 预处理记号替换,不遵循 C++ 名字作用域规则 |
同一实体的多个声明必须一致;能否有多份定义还受单一定义规则约束。头文件中的函数定义通常需要 inline 等合适安排,C++17 的 inline 变量也可用于头文件定义;不能把普通全局变量定义反复放进被多个源文件包含的头文件。声明与定义、单一定义规则
命名空间、限定名与成员访问
跳转到“命名空间、限定名与成员访问”Library::Sensor 使用 :: 在命名空间中查找类型;sensor.value 使用 . 在一个对象中访问成员;指针使用 sensorPointer->value。这三种表达式解决的问题不同。
#include <cassert>
int count = 1;namespace Library { int count = 2; struct Sensor { int value = 3; };}
int main() { int count = 4; Library::Sensor sensor; Library::Sensor* pointer = &sensor; assert(count == 4); // 最内层局部名字 assert(::count == 1); // 全局命名空间 assert(Library::count == 2); // 指定命名空间 sensor.value = 8; assert(pointer->value == 8);}using Library::Sensor; 引入特定名字,namespace Lib = Library; 声明命名空间别名。全局命名空间中的名字也要先有可见声明,并不自动对整个程序的所有源文件可见。避免在公共头文件中使用宽泛的 using namespace ...;,以减少查找冲突。
配置结构体为什么只在类型位置加命名空间
跳转到“配置结构体为什么只在类型位置加命名空间”原问答把 MeasurementItem 声明在 SYSCONFIGS 中。需要写限定名的是类型名:SYSCONFIGS::MeasurementItem。访问已经存在的配置对象的成员时,普通 . 连续访问即可,不需要再凭空插入命名空间。
下面给出 Qt 6 的完整例子,保留原来的单例配置、name/value 和 QString::toStdString() 使用场景:
#include <QString>#include <cassert>#include <string>
namespace SYSCONFIGS {struct MeasurementItem { QString name; double value = 0;};}
class SystemConfigs {public: static SystemConfigs& instance() { static SystemConfigs config; return config; } SYSCONFIGS::MeasurementItem measurement{QStringLiteral("speed"), 1200}; SystemConfigs(const SystemConfigs&) = delete; SystemConfigs& operator=(const SystemConfigs&) = delete;private: SystemConfigs() = default;};
int main() { SystemConfigs& config = SystemConfigs::instance(); SYSCONFIGS::MeasurementItem item = config.measurement; std::string name = config.measurement.name.toStdString(); assert(name == "speed" && item.value == 1200); config.measurement.value = 1500; assert(item.value == 1200); // item 是按值复制出来的独立对象 assert(&SystemConfigs::instance() == &config);}instance() 返回引用,所以用 SystemConfigs& 接收它不会复制单例。函数局部静态对象的初始化具有语言规定的并发保证,但这不保证后续修改 measurement 是线程安全的;共享可变配置仍需要同步。本例只验证名称查找和访问,不推荐以可变全局单例替代所有依赖设计。原问题见 CSDN 配置结构体问答。
作用域不等于对象寿命
跳转到“作用域不等于对象寿命”| 概念 | 回答的问题 | 例子 |
|---|---|---|
| 作用域 | 在哪段代码中可查找名字? | 块、命名空间、类、模板形参作用域 |
| 存储期 | 保存对象的存储可持续多久? | 自动、静态、线程、动态存储期 |
| 对象寿命 | 对象何时开始存在,何时结束? | 类对象完成初始化后开始;销毁时结束 |
| 链接性 | 不同声明是否可表示同一实体? | 无链接、内部链接、外部链接 |
名字的作用域可能很短,对象却活得很久。例如函数局部 static 变量只有局部名字,但存储具有静态存储期;动态对象的指针变量离开作用域,也不等于动态对象被自动删除。相反,借用对象的引用可以在对象销毁后继续留下一个悬空名字。作用域、存储期、对象寿命
#include <cassert>
int nextId() { static int counter = 0; // 局部名字,跨调用保存状态 return ++counter;}
int main() { int outer = 1; { int outer = 2; // 遮蔽,不是修改外层 outer assert(outer == 2); } assert(outer == 1); assert(nextId() == 1 && nextId() == 2);}普通局部类对象在执行到定义并完成初始化时开始寿命,不是刚进入所在大括号就全部构造。类型别名、命名空间、枚举类型和模板形参也不能笼统写成“拥有程序全程对象寿命”,因为它们不都是运行时对象。
命名空间作用域的 static 函数/变量通常具有内部链接;函数内的 static 主要改变局部变量的存储期;类中的 static 成员又表示该成员不属于某个单独实例。它们不是一个统一的“static 作用域”。宏则按预处理指令和 #undef 控制,不拥有普通 C++ 块作用域。