跳转到内容
新建笔记

C++ 声明、命名空间、作用域与对象寿命

理解 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++ 块作用域。

若问题是“两个模板能否共用同一个 T”,参见 模板形参作用域;若问题是对象销毁后仍使用其地址,参见 指针与引用的寿命。