变量
C++ 变量教程
变量是程序中存储数据的具名内存单元。本教程从声明与初始化讲起,覆盖类型系统、初始化方式、作用域与生命周期、类型推导、存储类说明符,以及现代 C++ 的变量特性。
一、变量的声明、定义与初始化
1.1 三者的区别
概念 含义 示例 声明 告诉编译器"有这么一个名字和类型",不分配内存 extern int x; 定义 分配内存,可同时初始化(一个变量只能定义一次) int x = 5; 初始化 定义时赋初值 int x{5};
extern int g; // 声明(通常在头文件)
int g = 10; // 定义(在某个 .cpp 中)
int main() {
int a; // 定义但未初始化 → 值不确定(局部变量)
int b = 5; // 定义并拷贝初始化
return 0;
}
⚠️ 未初始化的局部变量值是不确定的(俗称"垃圾值"),读取它是未定义行为。全局/静态变量则会被零初始化。
1.2 初始化方式(C++11 起)
C++ 提供多种初始化语法,推荐使用花括号初始化:
int a = 5; // 拷贝初始化
int b(5); // 直接初始化
int c{5}; // 列表初始化(统一初始化)✅ 推荐
int d = {5}; // 拷贝列表初始化
double e{5}; // 允许:5 → 5.0
// int f{5.7}; // ❌ 编译错误:窄化转换被禁止
int g(5.7); // 允许但会截断为 5(不推荐)
花括号初始化的优势:
- 禁止窄化转换(如 double → int、long → char),在编译期捕获精度丢失。
- 适用于所有类型,语法统一。
- 避免"最令人烦恼的解析"问题:
int x(10); // 变量
int y(); // ⚠️ 这是函数声明,不是变量!
int z{}; // 明确是变量,值为 0
默认初始化陷阱:
int a; // 不确定值(局部变量)
int b{}; // 0
int c = int(); // 0
现代 C++ 核心准则建议:始终初始化变量,优先 {}。
二、基本数据类型
2.1 整型
类型 典型大小 范围(典型) short 2 字节 -32768 ~ 32767 int 4 字节 ≈ ±21 亿 long 4/8 字节 平台相关 long long 8 字节 ≈ ±9.2×10¹⁸ unsigned 各类型 同上 0 到 2ⁿ-1
int i = 42;
unsigned int u = 42u;
long long big = 9'000'000'000LL; // C++14 数字分隔符,提高可读性
unsigned char c = 255;
要点:
· int 至少 16 位,实际大小依赖平台;需要精确宽度用 中的 int32_t、uint64_t。 · 数字后缀:u(无符号)、l/ll(长整型)、f(浮点)。
#include <cstdint>
int32_t a = 100; // 精确 32 位有符号
uint64_t b = 1ULL << 40; // 精确 64 位无符号
2.2 浮点型
类型 典型大小 精度
float 4 字节 约 67 位有效数字
double 8 字节 约 1516 位
long double 8/16 字节 平台相关
float f = 3.14f;
double d = 3.141592653589793;
long double ld = 3.141592653589793238L;
⚠️ 浮点数不要用 == 比较,用误差范围(见第五节)。
2.3 字符与布尔
char ch = 'A'; // 单个字符,1 字节
bool flag = true; // true / false
char 是有符号还是无符号取决于平台;需要明确时用 signed char / unsigned char。
2.4 sizeof 运算符
#include <iostream>
std::cout << sizeof(int) << "\n"; // 字节数
std::cout << sizeof(long long) << "\n";
std::cout << sizeof(std::string) << "\n"; // 对象大小,非内容长度
三、const 与 constexpr
3.1 const 常量
const int MAX = 100;
// MAX = 200; // ❌ 编译错误
const double PI = 3.14159;
const std::string NAME = "C++";
const 与指针的组合(经典难点):
int x = 10;
const int* p1 = &x; // 指向常量的指针:不能改 *p1,能改 p1
int* const p2 = &x; // 常量指针:能改 *p2,不能改 p2
const int* const p3 = &x; // 都不能改
读法技巧:从右往左读。const int* p → "p 是指针,指向 const int"。
3.2 constexpr(C++11)
表示"编译期常量表达式",可用于数组大小、模板参数等编译期场景。
constexpr int SQUARE(int n) { return n * n; }
constexpr int N = SQUARE(5); // 编译期计算,N = 25
int arr[SQUARE(3)]; // 数组大小必须是编译期常量
const vs constexpr:
const constexpr 保证 运行期不可修改 编译期可求值(更强) 可用于数组大小 仅当初始化为编译期常量 ✅ 总是 函数 不能修饰为编译期函数 ✅ 可修饰函数
能用 constexpr 就用 constexpr,它隐含 const。
3.3 consteval 与 constinit(C++20)
consteval int cube(int n) { return n * n * n; } // 必须在编译期求值
constinit int global = 42; // 保证静态初始化(无动态初始化开销)
四、类型推导:auto 与 decltype
4.1 auto(C++11)
让编译器根据初始化表达式推导类型。
auto a = 42; // int
auto b = 3.14; // double
auto c = 'x'; // char
auto d = std::string("hi"); // std::string
auto e = 1 + 2.0; // double
必须初始化,且推导时会丢弃顶层 const 和引用:
const int ci = 10;
auto x = ci; // int(const 被丢弃)
auto& r = ci; // const int&(引用保留 const)
int arr[3] = {1, 2, 3};
auto p = arr; // int*(数组退化为指针)
典型用途:
// 迭代器类型冗长
for (auto it = v.begin(); it != v.end(); ++it) { ... }
// 范围 for 中
for (const auto& item : container) { ... }
// 复杂返回类型
auto result = computeSomething();
现代 C++ 核心准则:用 auto 避免重复类型名,但不要滥用导致可读性下降(如 auto x = getValue(); 看不出是什么类型时,可考虑显式写出)。
4.2 decltype(C++11)
推导表达式的类型,保留 const 和引用。
int i = 0;
const int ci = 0;
decltype(i) a = 5; // int
decltype(ci) b = 5; // const int
decltype((i)) c = i; // int&(带括号表示左值引用)
// 常见用途:函数返回类型
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) { // C++11 尾置返回类型
return a + b;
}
C++14 起函数返回类型可直接 auto 推导,简化上述写法。
4.3 结构化绑定(C++17)
一次性解包 pair、tuple、结构体、数组:
#include <map>
#include <tuple>
std::map<std::string, int> ages = {{"Alice", 25}};
for (const auto& [name, age] : ages) {
std::cout << name << ": " << age << "\n";
}
auto [x, y] = std::make_pair(1, 2);
五、作用域与生命周期
5.1 作用域类型
作用域 说明 示例 块作用域 {} 内 函数体、if/for 块内的变量 函数作用域 函数参数 void f(int x) 命名空间作用域 命名空间内(全局变量属于全局命名空间) namespace N { int x; } 类作用域 类成员 class C { int x; }; 全局作用域 所有命名空间外 int g;
int g = 1; // 全局
void f() {
int a = 2; // 函数块
{
int b = 3; // 内层块
// 此处可访问 a、b、g
}
// 此处 b 已销毁
}
名字遮蔽(shadowing):
int x = 1;
void f() {
int x = 2; // 遮蔽全局 x
std::cout << x; // 2
std::cout << ::x; // 1(用 :: 访问全局)
}
遮蔽易导致 bug,现代准则建议避免用同名变量遮蔽外层。
5.2 生命周期与存储期
存储期 说明 典型对象 自动存储期 进入作用域创建,离开销毁 局部变量 静态存储期 程序运行期间一直存在 全局变量、static 局部变量 线程存储期 线程存活期间 thread_local 变量 动态存储期 由 new/delete 手动管理 堆对象
六、存储类说明符
6.1 static
在函数内:只初始化一次,生命周期延长到程序结束。
void counter() {
static int count = 0; // 只初始化一次
++count;
std::cout << count << "\n";
}
// 多次调用依次输出 1, 2, 3...
在文件/命名空间作用域:内部链接,仅当前翻译单元可见。
static int filePrivate = 42; // 其他 .cpp 无法访问
在类中:属于类而非对象,所有实例共享。
class Widget {
static int instanceCount; // 声明
};
int Widget::instanceCount = 0; // 定义(C++17 可用 inline static 直接在类内定义)
6.2 extern
声明在其他翻译单元中定义的变量:
extern int sharedVar; // 声明,定义在别处
6.3 thread_local(C++11)
每个线程拥有独立副本:
thread_local int tlsCounter = 0;
6.4 register(C++17 已废弃)
曾用于建议放入寄存器,现代编译器自动优化,不要使用。
七、引用
引用是变量的别名,必须初始化且不能重新绑定。
int a = 10;
int& ref = a; // ref 是 a 的别名
ref = 20; // a 也变成 20
常量引用:可绑定临时值,常用于函数参数避免拷贝:
void print(const std::string& s) { // 不拷贝,且不能修改
std::cout << s << "\n";
}
print("hello"); // 临时 string 可绑定到 const 引用
引用 vs 指针:
引用 指针 必须初始化 ✅ ❌ 可重新绑定 ❌ ✅ 可为空 ❌ ✅ 语法 像普通变量 需 * / &
优先用引用,需要"可空"或"重新指向"时才用指针。
八、类型别名
8.1 using(C++11,推荐)
using Age = int;
using StringList = std::vector<std::string>;
using Callback = void(*)(int);
Age a = 25;
StringList names;
8.2 typedef(旧式)
typedef int Age;
typedef std::vector<std::string> StringList;
现代 C++ 推荐 using,语法更清晰,且支持模板别名(typedef 不支持)。
8.3 模板别名
template <typename T>
using Vec = std::vector<T>;
Vec<int> v; // 等价于 std::vector<int>
九、类型转换
9.1 C 风格转换(不推荐)
double d = 3.7;
int i = (int)d; // 难看出意图,且可能掩盖错误
9.2 四种具名转换(推荐)
// static_cast:相关类型间的转换(数值转换、向上转型等)
double d = 3.7;
int i = static_cast<int>(d); // 3(截断)
// dynamic_cast:多态类型的安全向下转型(运行期检查)
Base* b = new Derived();
Derived* dp = dynamic_cast<Derived*>(b); // 失败返回 nullptr
// const_cast:去除/添加 const(谨慎使用)
const int ci = 10;
int* p = const_cast<int*>(&ci);
// reinterpret_cast:底层重新解释(危险,平台相关)
int x = 42;
char* bytes = reinterpret_cast<char*>(&x);
优先 static_cast;reinterpret_cast 和 const_cast 仅在确实需要时使用。
9.3 隐式转换与窄化
int i = 3.99; // 隐式截断为 3(可能告警)
char c = 1000; // 窄化,值不确定
int j{3.99}; // ❌ 花括号禁止窄化,编译错误
十、常见陷阱与最佳实践
❌ 常见错误
错误 说明 int a; 后直接使用 值不确定,未定义行为 if (x = 5) 赋值而非比较 有符号与无符号混用比较 int i = -1; unsigned u = 1; i < u 为 true(i 被转换为无符号) 浮点数 == 比较 精度问题 返回局部变量的引用/指针 悬垂引用 全局变量滥用 难以维护、线程不安全、初始化顺序问题
有符号/无符号陷阱示例:
std::vector<int> v = {1, 2, 3};
for (int i = 0; i < v.size(); ++i) // ⚠️ 有符号与 size_t 比较告警
;
for (size_t i = 0; i < v.size(); ++i) // ✅
;
// 或使用范围 for
✅ 最佳实践
- 始终初始化变量,优先用 {}:
int count{}; std::string name{}; - 用 const / constexpr 修饰不变量,让编译器帮忙检查。
- 缩小变量作用域:在需要的地方才声明,而非集中在函数开头。
- 优先 auto + 范围 for,减少类型重复和越界风险。
- 避免全局变量;必须用时,放在命名空间中并加 const 或 static。
- 浮点数比较用误差范围:
#include <cmath> bool nearlyEqual(double a, double b) { return std::fabs(a - b) < 1e-9; } - 函数参数用 const& 传大对象,避免拷贝。
- 需要精确宽度时用 (int32_t、uint8_t 等),而非假设 int 大小。
十一、综合示例
一个综合运用变量的示例:
#include <iostream>
#include <string>
#include <vector>
#include <cstdint>
// 常量:编译期确定
constexpr double TAX_RATE = 0.08;
constexpr int MAX_ITEMS = 100;
// 类型别名
using Price = double;
using ItemList = std::vector<std::string>;
// 静态成员计数
class Order {
public:
explicit Order(std::string name) : customer_{std::move(name)} {
++totalOrders;
}
static int totalOrders; // 声明
private:
std::string customer_;
};
int Order::totalOrders = 0; // 定义
int main() {
ItemList items{"apple", "banana", "cherry"};
Price subtotal{0.0};
// 范围 for + const auto&
for (const auto& item : items) {
constexpr Price unitPrice = 2.5; // 每种假设 2.5 元
subtotal += unitPrice;
}
const Price tax = subtotal * TAX_RATE;
const Price total = subtotal + tax;
std::cout << "商品数:" << items.size() << "(上限 " << MAX_ITEMS << ")\n";
std::cout << "小计:" << subtotal << "\n";
std::cout << "税费:" << tax << "\n";
std::cout << "总计:" << total << "\n";
// 结构化绑定
Order o1{"Alice"}, o2{"Bob"};
std::cout << "订单总数:" << Order::totalOrders << "\n";
return 0;
}
十二、小结
主题 关键点 声明 vs 定义 定义分配内存,只能一次;声明可多次 初始化 优先 {},禁止窄化,避免未初始化 类型 固定宽度用 ;浮点勿用 == const / constexpr 修饰不变量;能编译期求值用 constexpr auto / decltype 简化类型,注意 auto 丢弃顶层 const 作用域 尽量缩小,避免遮蔽 static 局部:只初始化一次;全局:内部链接 引用 别名,必须初始化;const& 避免拷贝 类型别名 用 using,支持模板别名 转换 用 static_cast 等具名转换,慎用 reinterpret_cast
变量是程序的基础,理解类型、初始化、作用域和生命周期,才能写出正确、高效、可维护的 C++ 代码。建议结合编译器警告(-Wall -Wextra)实践,很多变量相关的错误能被编译器提前捕获。