← 目录 / 第二章 · 变量与数据类型 / 2.6 作用域与常量

2.6 作用域与常量

变量并不是在程序的任何地方都能被访问的——本节先讲清楚"作用域"这个概念,再介绍 C++ 中定义"不能被修改的数据"(常量)的两种写法,以及为什么其中一种明显更值得优先使用。

作用域与常量

2.6.1 作用域(Scope):变量的有效范围

变量并不是在程序的任何地方都能被访问的——它有自己的可见范围,这就是作用域(Scope)。搞清楚一个变量"活在哪儿",是理解后面很多报错信息的关键。

局部作用域:在函数(比如 main 函数)或代码块 {} 内部定义的变量,只能在该函数或该代码块内部使用,一旦离开这对花括号,这个变量就"不存在"了。

C++ · 局部作用域
1int main()
2{
3 {
4 int temp = 10; // temp 只在这对花括号内有效
5 cout << temp; // 正常,输出 10
6 }
7 cout << temp; // ✗ 编译报错:temp 已经超出作用域
8 return 0;
9}

全局作用域:在所有函数之外定义的变量,可以在整个程序文件中被访问,不局限于某一个函数内部。

C++ · 全局作用域
1int totalScore = 0; // 全局变量,定义在所有函数之外
2
3int main()
4{
5 totalScore = 10; // 函数内部可以直接访问、修改全局变量
6 cout << totalScore; // 输出 10
7 return 0;
8}
⚠️
但要注意声明顺序:C++ 代码是从上到下依次编译的,全局变量也必须先声明,再使用。如果把 int totalScore = 0; 写在 main 函数下面,编译器读到 main 里的 totalScore 时还不认识这个变量,同样会报错。以后学到"函数"章节会看到,即使是另一个函数(不只是 main),只要写在全局变量声明之后,同样可以直接访问和修改它,不需要额外传递。

作用域嵌套与优先级:如果局部变量和全局变量恰好同名,会发生什么?规则是:局部变量会暂时"遮蔽"(shadowing)同名的全局变量——在这个局部作用域范围内,所有对这个变量的访问,指的都是局部变量,直到这个作用域结束。

C++ · 同名变量的遮蔽
1int score = 100; // 全局变量 score
2
3int main()
4{
5 cout << score << endl; // 输出 100,这里还没有同名局部变量
6
7 int score = 60; // 局部变量 score,"遮蔽"了全局的 score
8 cout << score << endl; // 输出 60,局部变量优先
9 return 0;
10}
全局作用域(整个文件)
score = 100
局部作用域(main 函数内部)
score = 60(遮蔽了外层的 score)
📌
(拓展,可以先跳过)static 关键字:static 可以用来改变变量的"生命周期"或"链接属性"——比如让一个局部变量的值在多次调用之间被保留下来,或者让一个全局变量只能在当前文件内访问。这是一个进阶话题,现在只需要知道有这么个关键字存在就够了,之后用到时我们会再详细介绍。

2.6.2 为什么需要常量

前面讲的变量,值是可以随时改变的,作用域决定了它"在哪里能被访问"。但有些数据,不仅需要在某个作用域内有效,而且它们的值从头到尾都不应该被改变——比如圆周率 3.14159、一个数组的最大长度 100——这类"定义好之后就不允许再修改"的数据,就应该定义成常量。C++ 里定义常量主要有两种写法:const 关键字和 #define 宏定义。

2.6.3 写法一:const(推荐)

const 是 C++ 的关键字,加在类型前面,表示"这个变量的值一旦初始化就不能再修改"。语法是:const 类型 常量名 = 值;

C++ · const 常量
1const double PI = 3.14159;
2const int MAX_N = 100;
3
4cout << PI * 2 << endl; // 可以正常参与运算,输出 6.28318
5PI = 3.14; // ✗ 编译报错!const 变量初始化后不允许再赋值
💡
const 变量和普通变量一样有类型、有作用域:PI 是一个真正的 double 类型变量,只是多了"不能被修改"这条限制——编译器会检查类型是否匹配,调试时也能像普通变量一样在调试器里看到它的名字和值。这也是 const 相比下面 #define 的核心优势。

2.6.4 写法二:#define(宏定义)

虽然现代 C++ 强烈推荐使用 const,但 #define 在旧代码、竞赛题解或某些配置文件中仍然非常常见,因此了解它的工作原理和缺点,对于阅读和理解别人的代码很有必要。

#define预处理指令,不是 C++ 语句——它在程序真正编译之前,由预处理器进行一次纯文本替换:把代码里所有出现的"常量名",原样替换成后面写的内容。语法是:#define 常量名 值(注意末尾不加分号)。

C++ · #define 宏定义
1#define PI 3.14159 // 注意:末尾没有分号
2#define MAX_N 100
3
4int main()
5{
6 int a[MAX_N]; // 预处理后,这一行实际变成 int a[100];
7 cout << PI; // 预处理后,这一行实际变成 cout << 3.14159;
8}
⚠️
#define 只是"文本替换",编译器根本不知道 PI、MAX_N 的存在:预处理阶段结束后,代码里已经找不到 PI 这个名字了——它已经被替换成了 3.14159 本身。所以 #define 定义的"常量"没有类型,编译器也不会做任何类型检查;如果代码写错了,报错信息里也只会显示替换后的内容(比如一长串 3.14159),而不会提示"PI 这里有问题",调试起来更麻烦。

2.6.5 两种写法怎么选

对比项 const #define
处理阶段 编译阶段,是真正的 C++ 语句 预处理阶段,编译之前就已经被替换掉
类型检查 有类型,编译器会检查类型是否匹配 没有类型,只是字符串替换,不做任何检查
作用域 遵守 C++ 正常的作用域规则 没有作用域概念,从定义处到文件结尾(或被 #undef)全局生效
调试可见性 调试器里能看到变量名和值 调试器里只能看到替换后的字面值,看不到宏名
竞赛中的建议 优先使用 了解即可,老代码或某些场景仍会见到
📌
命名习惯:不管用哪种写法,常量名通常都全部大写,多个单词用下划线连接,例如 MAX_NPIARRAY_SIZE——一看到全大写的名字,就知道这是一个不会变的常量,而不是普通变量,这是 C/C++ 里非常通行的约定。
⚠️
常见陷阱:#define 末尾如果多写了分号(#define MAX_N 100;),替换后分号也会一起被带入代码,可能导致语法错误,且报错位置常常和实际写错的地方对不上,容易让人摸不着头脑;② 试图给 const 变量重新赋值会在编译期直接报错,这其实是优点——比起运行到一半才发现数值被意外改动,编译期报错能更早、更清楚地暴露问题。