1. 什么是变量?什么是常量?
- 变量:在程序运行过程中,其值可以改变的量。可以把它想象成一个有名字的"盒子",盒子里可以放不同类型的数据。
- 常量 :其值在程序运行过程中不能被改变。一旦初始化后,试图修改常量会导致编译错误。
2. 变量的定义与初始化
2.1 变量的定义
语法:类型 变量名; 或 类型 变量名 = 初始值;
cpp
#include <iostream>
using namespace std;
int main() {
int age; // 定义变量 age,类型为 int,此时值未初始化(垃圾值)
age = 18; // 赋值
int score = 95; // 定义并初始化(推荐写法)
double pi = 3.14;
char grade = 'A';
cout << "年龄: " << age << ", 分数: " << score << endl;
return 0;
}
2.2 初始化方式
C++ 提供了多种初始化语法:
| 方式 | 示例 | 说明 |
|---|---|---|
| 赋值初始化 | int a = 10; |
最常用,直观 |
| 函数形式初始化 | int a(10); |
构造函数风格 |
| 统一初始化(列表初始化) | int a{10}; |
C++11 起推荐,防止窄化转换 |
cpp
int x = 5; // 赋值初始化
int y(5); // 直接初始化
int z{5}; // 统一初始化(推荐)
int w = {5}; // 等号加花括号也可以
// 列表初始化会防止数据丢失(窄化转换)
int a = 3.14; // 允许,但 a 变成 3(丢失小数部分)
int b{3.14}; // 编译错误:窄化转换,小数转整数不允许
2.3 默认初始化与未初始化变量
- 全局变量、静态变量会被默认初始化为 0。
- 局部变量不会自动初始化,其值是未定义的(垃圾值),使用前必须显式赋值。
cpp
int global_var; // 全局变量,默认值为 0
int main() {
int local_var; // 未初始化,值不确定(危险!)
static int static_var; // 静态局部变量,默认值为 0
cout << global_var << endl; // 0
cout << static_var << endl; // 0
// cout << local_var << endl; // 输出垃圾值,行为未定义
return 0;
}
⚠️ 警告:使用未初始化的局部变量会导致不可预测的结果,甚至程序崩溃。务必养成定义时初始化的习惯。
3. 常量:const 与 constexpr
3.1 const 常量
const 修饰的变量一旦初始化后,就不能再修改。
cpp
const int DAYS_IN_WEEK = 7; // 必须初始化
// DAYS_IN_WEEK = 8; // 错误:不能修改常量
const double PI = 3.1415926;
const 可以修饰函数参数、返回值、成员函数等,后续文章会深入。
3.2 constexpr 常量表达式(C++11 起)
constexpr 表示该变量的值在编译时就能确定,可用于数组大小、模板参数等需要编译时常量的场景。
cpp
constexpr int ARRAY_SIZE = 10; // 编译时常量
int arr[ARRAY_SIZE]; // 合法,因为 ARRAY_SIZE 编译时已知
int getSize() { return 20; }
constexpr int size2 = getSize(); // 错误!getSize() 不是 constexpr 函数
区别总结:
| 特性 | const |
constexpr |
|---|---|---|
| 初始化时机 | 运行时或编译时 | 必须是编译时 |
| 能否修改 | 否 | 否 |
| 能否用于数组大小 | 仅在部分编译器支持 VLA 时 | 可以(因为编译时已知) |
| 修饰函数 | 表示函数不会修改成员 | 表示函数可在编译时求值 |
简单记忆:
const是"运行时只读",constexpr是"编译时已知"。
4. 内存模型讲解(浅显易懂)
当程序运行时,变量被存放在内存的不同区域。让我们画一张简单清晰的图。
高地址
┌─────────────────┐
│ 栈 (Stack) │ ← 局部变量(如 main 中的 age, score)
│ (向下增长) │ 函数调用时的参数、返回地址
├─────────────────┤
│ 堆 (Heap) │ ← 动态分配的内存(new/malloc,本章暂未涉及)
├─────────────────┤
│ 数据段 (Data) │
│ ┌─────────────┐ │
│ │ .rodata │ │ ← 只读数据段:字符串字面量、const 常量(取决于编译器)
│ ├─────────────┤ │
│ │ .data │ │ ← 已初始化的全局变量、静态变量
│ ├─────────────┤ │
│ │ .bss │ │ ← 未初始化的全局变量、静态变量(会被清零)
│ └─────────────┘ │
├─────────────────┤
│ 代码段 (Text) │ ← 程序的二进制指令
└─────────────────┘
低地址
具体例子:
cpp
const int GLOBAL_CONST = 100; // 通常放在 .rodata(只读)
int global_var = 42; // 放在 .data
int global_uninit; // 放在 .bss(默认 0)
int main() {
const int local_const = 10; // 通常放在栈上,但编译器可能优化为直接替换
int local_var = 20; // 放在栈上
static int static_var = 5; // 放在 .data(静态局部)
return 0;
}
栈上的变量 :当 main 函数被调用时,系统在栈上分配一块空间(栈帧)。local_var 就在其中,函数返回后这块空间被回收。
.rodata 段:只读,任何试图修改该段内存的行为都会导致运行时错误(段错误)。
.bss 段:不占用可执行文件的实际空间,但程序加载时会被清零。
🔍 关键点 :
const变量不一定放在只读内存段(取决于编译器优化),但编译器会阻止你修改它。constexpr变量则几乎肯定在编译时就被求值,可能根本不占用内存(直接作为立即数嵌入指令)。
5. 常见错误与避坑
| 错误示例 | 问题 | 正确做法 |
|---|---|---|
const int a; |
const 变量未初始化 | 定义时必须初始化 |
int arr[n]; 其中 n 是普通变量 |
标准 C++ 不允许变长数组 | 使用 constexpr 或动态分配 |
int x = 10; const int &r = x; 然后尝试通过 r 修改 |
不能通过常量引用修改 | 需要修改就不要用 const |
将 constexpr 函数在运行时调用 |
可以,但失去了编译时优势 | 确保参数也是编译时常量 |
6. 练习题
题目:请编写一个程序,完成以下要求:
- 定义一个
constexpr整数kStudentCount = 5。- 定义一个
const浮点数kPi = 3.14159。- 定义一个未初始化的局部整数
uninit_val,然后直接输出它(观察结果,理解垃圾值)。- 定义一个静态局部整数
counter,初始化为 0;每次调用一个函数Increment()时,counter增加 1 并返回。在main中调用三次,输出每次的返回值。- 尝试修改
kPi,观察编译错误。输出示例 (
uninit_val的值每次运行可能不同):
未初始化的值:4200136 第1次调用:1 第2次调用:2 第3次调用:3附加挑战 :尝试用
const和constexpr分别定义一个数组大小,验证constexpr可以而普通const(如果初始化不是编译时常量)不行。
提示 :关于附加挑战中的arr2[size_const],在标准 C++ 中,数组大小必须是编译时常量。虽然size_const是const,但如果它的初始化值在编译时已知,有些编译器(如 GCC)会将其视为常量表达式,但为了可移植性,应使用constexpr。
上期答案
cpp
/**
* @file sum.cpp
* @brief 读取两个整数,输出它们的和
* @author (你的名字)
* @date 2025-01-15
*/
#include <iostream>
using namespace std;
/**
* @brief 计算两个整数的和
* @param a 第一个整数
* @param b 第二个整数
* @return 两数之和
*/
int Add(int a, int b) {
int sum = a + b; // 临时存储和
return sum;
}
int main() {
int first_num, second_num;
cout << "请输入两个整数:";
cin >> first_num >> second_num;
int result = Add(first_num, second_num);
cout << "它们的和是:" << result << endl;
return 0;
}
总结 :变量是存储数据的基本单元,常量让数据不可变,增强代码安全性和可读性。const 保证运行时只读,constexpr 强调编译时求值。理解内存分区(栈、数据段、只读段)有助于写出更高效的代码。下一篇文章我们将学习基本数据类型,进一步夯实基础。
赶快动手写代码,试试修改常量的后果,并观察静态局部变量的神奇行为吧!