3、常量与作用域:字面常量、const关键字、constexpr常量表达式、宏定义(#define)、全局作用域、局部作用域、命名空间(namespace)基础

常量与作用域,说白了就是两件事:什么东西不能变,以及什么东西在哪儿能用。我刚开始学C++时,觉得这俩概念太简单了,不就是“固定值”和“花括号”嘛。结果后来在项目里被宏定义坑过一次,又被命名空间冲突折磨过一回……嗯,从那以后我再也不敢小看它们了。

今天咱们就把这块彻底捋清楚。你想想看,一个程序里变量满天飞,哪些是只读的?哪些只在某个函数里有效?哪些全局可见?搞明白这些,你写出来的代码才能既安全又清晰。

核心要点: 常量保证数据不被意外修改,作用域控制名字的可见范围。两者结合,是写出健壮代码的基石。

3.1 字面常量:最直接的“死值”

字面常量,就是直接写在代码里的值。比如 423.14'A'"hello"。它们没有名字,就是赤裸裸的数据。

我个人习惯把字面常量分成几类:

类型 示例 说明
整型字面量 42, 0xFF, 077 十进制、十六进制(0x)、八进制(0)
浮点字面量 3.14, 1.2e5 小数或科学计数法
字符字面量 'A', '\n' 单个字符,用单引号
字符串字面量 "hello", "C++" 多个字符,用双引号
布尔字面量 true, false C++ 关键字
指针字面量 nullptr 空指针(C++11 起)

这里有个小坑:字符串字面量在C++里是 const char[] 类型,也就是说它本质上是个常量数组。我曾经试图修改字符串字面量的内容,结果程序直接崩溃——嗯,这是未定义行为,千万别干。

3.2 const 关键字:让变量变成只读

const 是C++里最常用的常量修饰符。它的意思很简单:这个变量一旦初始化,就不能再改了。

const int MAX_SIZE = 1024;
// MAX_SIZE = 2048;  // 编译错误!不能修改 const 变量

const double PI = 3.1415926;
// PI = 3.14;        // 同样不行

我在项目中见过不少新手把 const 当成“可有可无”的装饰。其实不然。你想想看,一个函数如果接受一个 const & 参数,调用者就知道这个参数不会被修改,读代码的人也能放心。这就是一种契约。

我的习惯: 只要一个变量在初始化后不再改变,我就加 const。这不仅能防止意外修改,还能让编译器做更多优化。

另外,const 还可以修饰指针,这里容易搞混:

int value = 10;
const int* p1 = &value;   // 指向 const int 的指针:不能通过 p1 修改 value
int* const p2 = &value;   // const 指针:p2 本身不能指向别处
const int* const p3 = &value; // 两者都 const

怎么记?我有个笨办法:从右往左读。比如 const int* —— “pointer to const int”,指针指向的东西是 const。而 int* const —— “const pointer to int”,指针本身是 const。

3.3 constexpr 常量表达式:编译期就定下来

constexpr 是C++11引入的关键字,它比 const 更严格。它要求变量的值在编译期就能算出来。说白了,就是让编译器帮你把常量算好,运行时直接拿来用。

constexpr int SQUARE(int x) {
    return x * x;
}

constexpr int result = SQUARE(5);  // 编译期就算出 25
int arr[result];                   // 可以用作数组大小

我记得有一次在嵌入式项目里,需要定义一些查表用的常量数组。如果用 const,有些编译器会在运行时才初始化,浪费RAM。换成 constexpr 后,所有数据都放在只读区,既省空间又安全。

注意: constexpr 函数体内不能有运行时才能确定的操作,比如动态内存分配、抛出异常等。如果传入的参数不是常量表达式,它会退化为普通函数。

简单总结:const 是“运行时只读”,constexpr 是“编译期常量”。能用 constexpr 的地方,尽量用它。

3.4 宏定义(#define):预处理阶段的文本替换

宏定义是C语言的老遗产,C++里依然能用。它本质上是预处理阶段的文本替换,跟编译器没什么关系。

#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int area = PI * r * r;
int bigger = MAX(x, y);

嗯,这里我要多说几句。宏定义虽然方便,但坑特别多。我曾经在项目里遇到过一个bug:

#define SQUARE(x) x * x
int result = SQUARE(2 + 3);  // 期望 25,实际得到 2 + 3 * 2 + 3 = 11

看到了吗?因为宏只是文本替换,没有括号保护,优先级全乱了。所以写宏的时候,参数和整个表达式都要加括号。但即使这样,宏还是有很多问题:没有类型检查、不能调试、容易污染命名空间。

我的建议: 在C++里,尽量用 const / constexpr 代替宏常量,用 inline 函数或模板代替宏函数。宏只适合做条件编译(#ifdef)和头文件保护(#pragma once#ifndef)。

3.5 全局作用域与局部作用域

作用域,说白了就是名字的可见范围。C++里最常见的就是全局作用域和局部作用域。

  • 全局作用域:在所有函数和类之外定义的名字,从定义点开始到文件末尾都可见。
  • 局部作用域:在函数内部或块语句(花括号内)定义的名字,只在那个块里有效。
#include <iostream>

int globalVar = 100;  // 全局作用域

void func() {
    int localVar = 200;  // 局部作用域
    std::cout << globalVar << std::endl;  // 可以访问全局
    // std::cout << localVar2;  // 错误!localVar2 还没定义
    {
        int innerVar = 300;  // 更内层的局部作用域
        std::cout << innerVar << std::endl;
    }
    // std::cout << innerVar;  // 错误!innerVar 已超出作用域
}

int main() {
    func();
    std::cout << globalVar << std::endl;  // 可以
    // std::cout << localVar;  // 错误!localVar 在 func 内部
    return 0;
}

这里有个容易忽略的点:全局变量和局部变量同名时,局部变量会隐藏全局变量。我见过有人写出这样的代码:

int value = 1;
void test() {
    int value = 2;
    std::cout << value;  // 输出 2,全局的 value 被隐藏了
    // 想访问全局 value?用 ::value
    std::cout << ::value;  // 输出 1
}

我个人不太建议滥用全局变量。全局变量让代码的依赖关系变得隐晦,多线程下更是灾难。如果非要用,记得加 const 或者用命名空间包起来。

3.6 命名空间(namespace)基础

命名空间就是为了解决名字冲突的。你想啊,如果两个库都定义了一个 Logger 类,没有命名空间的话就炸了。

namespace MyProject {
    int value = 42;
    void print() {
        std::cout << "MyProject::print()" << std::endl;
    }
}

namespace YourProject {
    int value = 100;
    void print() {
        std::cout << "YourProject::print()" << std::endl;
    }
}

int main() {
    std::cout << MyProject::value << std::endl;   // 42
    std::cout << YourProject::value << std::endl; // 100
    MyProject::print();
    YourProject::print();
    return 0;
}

使用 using 可以偷懒:

using namespace MyProject;  // 之后可以直接用 value、print
// 但小心!如果同时 using 两个命名空间,冲突又回来了
避坑指南: 不要在头文件里写 using namespace std; 或任何 using namespace。这会把整个命名空间暴露给所有包含该头文件的代码,极易引发冲突。我曾经在一个大型项目里排查了半天,最后发现是某个头文件里的 using namespace 污染了全局命名空间。

命名空间还可以嵌套:

namespace A::B::C {  // C++17 语法
    int x = 10;
}
// 等价于 namespace A { namespace B { namespace C { int x = 10; } } }

我个人习惯把项目中的所有代码放在一个顶层命名空间里(比如 namespace myapp),然后按模块分内层命名空间。这样既避免了冲突,也让代码结构更清晰。

知识体系总览

下面这张图把本章的核心概念串起来了。你可以看到常量从字面量到编译期常量的演进,以及作用域从全局到局部的层次关系。

常量与作用域知识体系 常量(不可变数据) 字面常量 42, 3.14, 'A', "hello" const 变量 运行时只读 constexpr 编译期常量 #define 宏 预处理替换(慎用) 作用域(名字可见范围) 全局作用域 文件级可见 局部作用域 块内可见 命名空间 逻辑分组 隐藏规则 局部覆盖全局 实践建议 • 能用 constexpr 就不用 const,能用 const 就不用 #define • 全局变量尽量少用,必要时放入命名空间 • 头文件里不要写 using namespace,避免污染 • 局部变量生命周期短,优先使用

小结

常量与作用域,看似基础,但贯穿了整个C++编程。我见过太多因为 const 漏写导致的意外修改,也见过因为命名空间冲突导致的编译错误。记住一句话:能加 const 就加,能用命名空间就包,能不用宏就不用。这样写出来的代码,你自己看着舒服,别人维护也省心。

好了,这一章就到这里。下一章我们会深入函数与重载,到时候再聊。


专注资料整理