变量并不是在程序的任何地方都能被访问的——本节先讲清楚"作用域"这个概念,再介绍 C++ 中定义"不能被修改的数据"(常量)的两种写法,以及为什么其中一种明显更值得优先使用。
2.6
变量并不是在程序的任何地方都能被访问的——它有自己的可见范围,这就是作用域(Scope)。搞清楚一个变量"活在哪儿",是理解后面很多报错信息的关键。
局部作用域:在函数(比如 main 函数)或代码块 {} 内部定义的变量,只能在该函数或该代码块内部使用,一旦离开这对花括号,这个变量就"不存在"了。
| 1 | int main() |
| 2 | { |
| 3 | { |
| 4 | int temp = 10; // temp 只在这对花括号内有效 |
| 5 | cout << temp; // 正常,输出 10 |
| 6 | } |
| 7 | cout << temp; // ✗ 编译报错:temp 已经超出作用域 |
| 8 | return 0; |
| 9 | } |
全局作用域:在所有函数之外定义的变量,可以在整个程序文件中被访问,不局限于某一个函数内部。
| 1 | int totalScore = 0; // 全局变量,定义在所有函数之外 |
| 2 | |
| 3 | int main() |
| 4 | { |
| 5 | totalScore = 10; // 函数内部可以直接访问、修改全局变量 |
| 6 | cout << totalScore; // 输出 10 |
| 7 | return 0; |
| 8 | } |
int totalScore = 0; 写在 main 函数下面,编译器读到 main 里的 totalScore 时还不认识这个变量,同样会报错。以后学到"函数"章节会看到,即使是另一个函数(不只是 main),只要写在全局变量声明之后,同样可以直接访问和修改它,不需要额外传递。作用域嵌套与优先级:如果局部变量和全局变量恰好同名,会发生什么?规则是:局部变量会暂时"遮蔽"(shadowing)同名的全局变量——在这个局部作用域范围内,所有对这个变量的访问,指的都是局部变量,直到这个作用域结束。
| 1 | int score = 100; // 全局变量 score |
| 2 | |
| 3 | int main() |
| 4 | { |
| 5 | cout << score << endl; // 输出 100,这里还没有同名局部变量 |
| 6 | |
| 7 | int score = 60; // 局部变量 score,"遮蔽"了全局的 score |
| 8 | cout << score << endl; // 输出 60,局部变量优先 |
| 9 | return 0; |
| 10 | } |
static 可以用来改变变量的"生命周期"或"链接属性"——比如让一个局部变量的值在多次调用之间被保留下来,或者让一个全局变量只能在当前文件内访问。这是一个进阶话题,现在只需要知道有这么个关键字存在就够了,之后用到时我们会再详细介绍。前面讲的变量,值是可以随时改变的,作用域决定了它"在哪里能被访问"。但有些数据,不仅需要在某个作用域内有效,而且它们的值从头到尾都不应该被改变——比如圆周率 3.14159、一个数组的最大长度 100——这类"定义好之后就不允许再修改"的数据,就应该定义成常量。C++ 里定义常量主要有两种写法:const 关键字和 #define 宏定义。
const 是 C++ 的关键字,加在类型前面,表示"这个变量的值一旦初始化就不能再修改"。语法是:const 类型 常量名 = 值;
| 1 | const double PI = 3.14159; |
| 2 | const int MAX_N = 100; |
| 3 | |
| 4 | cout << PI * 2 << endl; // 可以正常参与运算,输出 6.28318 |
| 5 | PI = 3.14; // ✗ 编译报错!const 变量初始化后不允许再赋值 |
PI 是一个真正的 double 类型变量,只是多了"不能被修改"这条限制——编译器会检查类型是否匹配,调试时也能像普通变量一样在调试器里看到它的名字和值。这也是 const 相比下面 #define 的核心优势。虽然现代 C++ 强烈推荐使用 const,但 #define 在旧代码、竞赛题解或某些配置文件中仍然非常常见,因此了解它的工作原理和缺点,对于阅读和理解别人的代码很有必要。
#define 是预处理指令,不是 C++ 语句——它在程序真正编译之前,由预处理器进行一次纯文本替换:把代码里所有出现的"常量名",原样替换成后面写的内容。语法是:#define 常量名 值(注意末尾不加分号)。
| 1 | #define PI 3.14159 // 注意:末尾没有分号 |
| 2 | #define MAX_N 100 |
| 3 | |
| 4 | int main() |
| 5 | { |
| 6 | int a[MAX_N]; // 预处理后,这一行实际变成 int a[100]; |
| 7 | cout << PI; // 预处理后,这一行实际变成 cout << 3.14159; |
| 8 | } |
PI 这个名字了——它已经被替换成了 3.14159 本身。所以 #define 定义的"常量"没有类型,编译器也不会做任何类型检查;如果代码写错了,报错信息里也只会显示替换后的内容(比如一长串 3.14159),而不会提示"PI 这里有问题",调试起来更麻烦。| 对比项 | const | #define |
|---|---|---|
| 处理阶段 | 编译阶段,是真正的 C++ 语句 | 预处理阶段,编译之前就已经被替换掉 |
| 类型检查 | 有类型,编译器会检查类型是否匹配 | 没有类型,只是字符串替换,不做任何检查 |
| 作用域 | 遵守 C++ 正常的作用域规则 | 没有作用域概念,从定义处到文件结尾(或被 #undef)全局生效 |
| 调试可见性 | 调试器里能看到变量名和值 | 调试器里只能看到替换后的字面值,看不到宏名 |
| 竞赛中的建议 | 优先使用 | 了解即可,老代码或某些场景仍会见到 |
MAX_N、PI、ARRAY_SIZE——一看到全大写的名字,就知道这是一个不会变的常量,而不是普通变量,这是 C/C++ 里非常通行的约定。#define 末尾如果多写了分号(#define MAX_N 100;),替换后分号也会一起被带入代码,可能导致语法错误,且报错位置常常和实际写错的地方对不上,容易让人摸不着头脑;② 试图给 const 变量重新赋值会在编译期直接报错,这其实是优点——比起运行到一半才发现数值被意外改动,编译期报错能更早、更清楚地暴露问题。