3、常量与作用域:字面常量、const关键字、constexpr常量表达式、宏定义(#define)、全局作用域、局部作用域、命名空间(namespace)基础
常量与作用域,说白了就是两件事:什么东西不能变,以及什么东西在哪儿能用。我刚开始学C++时,觉得这俩概念太简单了,不就是“固定值”和“花括号”嘛。结果后来在项目里被宏定义坑过一次,又被命名空间冲突折磨过一回……嗯,从那以后我再也不敢小看它们了。
今天咱们就把这块彻底捋清楚。你想想看,一个程序里变量满天飞,哪些是只读的?哪些只在某个函数里有效?哪些全局可见?搞明白这些,你写出来的代码才能既安全又清晰。
3.1 字面常量:最直接的“死值”
字面常量,就是直接写在代码里的值。比如 42、3.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
看到了吗?因为宏只是文本替换,没有括号保护,优先级全乱了。所以写宏的时候,参数和整个表达式都要加括号。但即使这样,宏还是有很多问题:没有类型检查、不能调试、容易污染命名空间。
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),然后按模块分内层命名空间。这样既避免了冲突,也让代码结构更清晰。
知识体系总览
下面这张图把本章的核心概念串起来了。你可以看到常量从字面量到编译期常量的演进,以及作用域从全局到局部的层次关系。
小结
常量与作用域,看似基础,但贯穿了整个C++编程。我见过太多因为 const 漏写导致的意外修改,也见过因为命名空间冲突导致的编译错误。记住一句话:能加 const 就加,能用命名空间就包,能不用宏就不用。这样写出来的代码,你自己看着舒服,别人维护也省心。
好了,这一章就到这里。下一章我们会深入函数与重载,到时候再聊。