变量

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(不推荐)

花括号初始化的优势:

  1. 禁止窄化转换(如 double → int、long → char),在编译期捕获精度丢失。
  2. 适用于所有类型,语法统一。
  3. 避免"最令人烦恼的解析"问题:
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

✅ 最佳实践

  1. 始终初始化变量,优先用 {}:
    int count{};
    std::string name{};
    
  2. 用 const / constexpr 修饰不变量,让编译器帮忙检查。
  3. 缩小变量作用域:在需要的地方才声明,而非集中在函数开头。
  4. 优先 auto + 范围 for,减少类型重复和越界风险。
  5. 避免全局变量;必须用时,放在命名空间中并加 const 或 static。
  6. 浮点数比较用误差范围:
    #include <cmath>
    bool nearlyEqual(double a, double b) {
        return std::fabs(a - b) < 1e-9;
    }
    
  7. 函数参数用 const& 传大对象,避免拷贝。
  8. 需要精确宽度时用 (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)实践,很多变量相关的错误能被编译器提前捕获。


版本号 #7
由 admin 创建于 2026-11-03 12:12:04 UTC
由 admin 更新于 2026-11-12 15:23:19 UTC