【C++ 变量与常量】变量的定义、初始化、const 与 constexpr

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. 常量:constconstexpr

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. 练习题

题目:请编写一个程序,完成以下要求:

  1. 定义一个 constexpr 整数 kStudentCount = 5
  2. 定义一个 const 浮点数 kPi = 3.14159
  3. 定义一个未初始化的局部整数 uninit_val,然后直接输出它(观察结果,理解垃圾值)。
  4. 定义一个静态局部整数 counter,初始化为 0;每次调用一个函数 Increment() 时,counter 增加 1 并返回。在 main 中调用三次,输出每次的返回值。
  5. 尝试修改 kPi,观察编译错误。

输出示例uninit_val 的值每次运行可能不同):

复制代码
未初始化的值:4200136
第1次调用:1
第2次调用:2
第3次调用:3

附加挑战 :尝试用 constconstexpr 分别定义一个数组大小,验证 constexpr 可以而普通 const(如果初始化不是编译时常量)不行。
提示 :关于附加挑战中的 arr2[size_const],在标准 C++ 中,数组大小必须是编译时常量。虽然 size_constconst,但如果它的初始化值在编译时已知,有些编译器(如 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 强调编译时求值。理解内存分区(栈、数据段、只读段)有助于写出更高效的代码。下一篇文章我们将学习基本数据类型,进一步夯实基础。

赶快动手写代码,试试修改常量的后果,并观察静态局部变量的神奇行为吧!

相关推荐
i220818 Faiz Ul2 小时前
教育资源共享平台|基于springboot + vue教育资源共享平台系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·教育资源共享平台
玛卡巴卡ldf2 小时前
【Springboot7】ApachePOI文件导入导出
java·spring boot·sql
编程大师哥2 小时前
VSCode中如何搭建JAVA+MAVEN
java·vscode·maven
不会写DN2 小时前
SQL 单表操作全解
java·服务器·开发语言·数据库·sql
John_ToDebug2 小时前
Chrome 首次启动引导页里触发 Pref 设置,为什么主进程收不到 IPC
c++·chrome
Devin~Y2 小时前
大厂 Java 面试实战:从电商微服务到 AI 智能客服(含 Spring 全家桶、Redis、Kafka、RAG/Agent 解析)
java·spring boot·redis·elasticsearch·spring cloud·docker·kafka
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第十五期 - 策略模式】策略模式 —— 算法封装与动态替换实现、优缺点与适用场景
java·后端·设计模式·软件工程·策略模式
珍朱(珠)奶茶2 小时前
Spring Boot3整合FreeMark、itextpdf 5/7 实现pdf文件导出及注意问题
java·spring boot·后端·pdf·itextpdf
我头发多我先学2 小时前
C++ STL vector 原理到模拟实现
c++·算法