变量与数据类型

建议先阅读:04_第一个程序与输入输出

原理

内存布局

变量是命名内存区域。类型决定了编译器为该变量分配多少字节以及如何解释这些字节:

类型字节 (x86-64)解释方式
bool10 或 1
char1ASCII 码值,-128~127
unsigned char10~255
short2-32768~32767
int4补码整数,-2³¹ ~ 2³¹-1
long long8补码整数,-2⁶³ ~ 2⁶³-1
float4IEEE 754 单精度 (符号1 + 指数8 + 尾数23)
double8IEEE 754 双精度 (符号1 + 指数11 + 尾数52)
long double16扩展精度(平台相关)

char 本质是 1 字节整数——CPU 不区分”字符”和”数字”,只是编译器在 cout 时选择输出字符还是数值。

整数类型对比(C vs C++)

C 语言依赖平台特定的 int 大小,而 C++ 引入了 <cstdint> 固定宽度类型:

#include <cstdint>
int32_t fixed_int = 42;    // 保证 32 位
uint64_t big_num = 1000000000ULL; // 保证 64 位无符号
int8_t tiny = -128;        // 保证 8 位
// C 语言: int 可能是 16/32/64 位,依赖平台
// C++11+: std::int32_t 等固定宽度类型

C 的 long 在不同平台上大小不同(Windows 32/64位都是4字节,Linux 64位是8字节),这是可移植性问题的常见来源。

内存对齐(Memory Alignment)

CPU 读取内存时按对齐边界效率最高。编译器会在结构体成员之间插入填充字节(padding):

struct Bad {
    char a;     // 1 byte + 3 padding
    int b;      // 4 bytes
    char c;     // 1 byte + 3 padding
}; // sizeof = 12 (不是 6!)
 
struct Good {
    int b;      // 4 bytes
    char a;     // 1 byte
    char c;     // 1 byte + 2 padding
}; // sizeof = 8

对齐规则:成员地址必须是 min(成员大小, 对齐上限) 的倍数。x86-64 通常对齐上限为 8 字节。

使用 alignof 查看对齐要求,offsetof 查看成员偏移:

#include <cstddef>
std::cout << alignof(char) << std::endl;    // 1
std::cout << alignof(int) << std::endl;     // 4
std::cout << alignof(double) << std::endl;  // 8
std::cout << offsetof(Bad, c) << std::endl; // 8 (偏移量)

初始化与未定义行为

局部变量不自动初始化——其值为栈上残留的任意数据。读取未初始化变量是未定义行为(UB)。全局/静态变量则自动初始化为零,因为它们存储在 .bss 段,操作系统在加载时清零。

int global_var; // 自动初始化为 0,存储在 .bss
int main() {
    int local_var; // 未初始化,栈上的随机值
    // int x = local_var; // 未定义行为!
}

名词解释未定义行为(Undefined Behavior, UB) 是指 C++ 标准未规定其结果的操作,编译器可以产生任何结果,包括看似正确的代码突然出错。

浮点精度

浮点数用二进制表示,绝大多数十进制小数无法精确表示。0.1 + 0.2 != 0.3 源于 IEEE 754 的固有限制。浮点比较应使用 epsilon 范围判断。

#include <cmath>
double a = 0.1, b = 0.2;
bool equal = std::abs(a + b - 0.3) < 1e-9; // 正确比较方式
// 而不是: if (a + b == 0.3) // 错误!

整数溢出

有符号整数溢出是未定义行为——编译器可能删除相关分支。无符号整数溢出是定义良好的(模 2^n 回绕)。

unsigned int x = UINT_MAX; // 4294967295
x++;                       // 变成 0(定义良好)
int y = INT_MAX;           // 2147483647
// y++;                    // 未定义行为! 不要这样做

整数提升(Integer Promotion)

C++ 在表达式中会自动提升小类型。charshort 参与运算时先提升为 int

char a = 100, b = 100;
// char result = a + b;   // 错误: a+b 是 int (200),超出 char 范围
int safe = a + b;          // 正确
// C 语言同样有此规则,但容易被忽视导致溢出

隐式类型转换规则

转换按”安全级别”自动进行,低级别可隐式转为高级别:

bool → char → short → int → unsigned int → long → unsigned long → long long → unsigned long long
                    ↘ float → double → long double
int i = 42;
double d = i;       // 安全: int → double,无损
float f = i;        // 安全: int → float
char c = 42;
int big = c;        // 安全: char → int
// 反向: double → int 截断,int → char 可能溢出

语法

声明与初始化

int a = 10;        // 拷贝初始化
int b(20);         // 直接初始化
int c{30};         // 列表初始化 (C++11, 推荐, 禁止窄化转换)
int d{};           // 初始化为 0
auto e = 3.14;     // 类型推导: double

列表初始化 int x{3.14} 直接编译错误,而非静默截断。这是 C++11 的安全改进。

const 与 constexpr

const int MAX = 100;           // 运行时常量,初始化后不可改
constexpr int SIZE = 50;       // 编译时常量,必须编译期可求值
const int runtime_val = get_input(); // OK: const 不要求编译期
// constexpr int bad = get_input(); // 错误: constexpr 要求编译期可求值
 
constexpr int square(int x) { return x * x; }  // 可在编译期执行
int arr[square(5)]; // arr[25]

constexpr 隐式包含 const,但约束更强:值必须在编译时确定。

类型转换

double pi = 3.14159;
int n = pi;                          // 隐式转换,截断为 3
int m = static_cast<int>(pi);        // 推荐: 显式标注意图
int bad = (int)pi;                   // C 风格: 不检查,危险
 
int a = 5, b = 2;
double result = static_cast<double>(a) / b; // 2.5,而非 2
 
// reinterpret_cast: 位级重新解释(极其危险)
int val = 65;
char ch = reinterpret_cast<char&>(val); // 可能得到 'A'

四种 C++ 强制转换static_cast(编译期检查)、const_cast(去/加 const)、dynamic_cast(运行时类型转换,需 RTTI)、reinterpret_cast(位级重新解释)。C 风格 (type)expr 等价于任意一种,无编译器检查。

sizeof

sizeof(int);           // 返回编译时常量,通常是 4
sizeof(arr);           // 整个数组的字节数
sizeof(arr) / sizeof(arr[0]); // 数组元素个数
 
// C++17: sizeof 可用于类型
sizeof(int);           // 传统写法
sizeof(int);           // C++11 后也可: sizeof(int)

auto 类型推导

auto 让编译器根据初始化表达式推导类型(C++11):

auto x = 42;         // int
auto y = 3.14;       // double
auto z = "hello";    // const char*
auto w = {1, 2, 3};  // std::initializer_list<int>
 
// 配合 STL 极大简化代码
std::vector<int> v = {1, 2, 3};
for (auto it = v.begin(); it != v.end(); ++it) {
    std::cout << *it << ' ';
}
 
// C++14: auto 返回类型
auto add(auto a, auto b) { return a + b; } // C++20 简写

auto 推导规则:忽略引用,保留 const/volatile。const int& r = 42; auto x = r; 推导为 int(不是 const int&)。

decltype(C++11)

decltype 查询表达式的类型,常用于模板和泛型编程:

int x = 42;
decltype(x) y = 0;        // int: y 的类型和 x 完全相同
decltype(x + 0.5) z = 1.0; // double: 表达式结果类型
 
// 用途: 模板返回类型
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {  // 尾置返回类型
    return a + b;
}

auto vs decltype

const int& ref = 42;
auto a = ref;       // int (去引用、去const)
decltype(ref) b = ref; // const int& (保留引用和const)
 
// auto: 推导变量类型,常用于循环、lambda
// decltype: 查询精确类型,常用于模板

C++14 简化了尾置返回类型:auto add(T a, U b) { return a + b; } 直接用 auto 推导返回类型。

结构化绑定(Structured Bindings, C++17)

#include <tuple>
#include <map>
 
// 绑定 pair
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (auto& [name, score] : scores) {
    std::cout << name << ": " << score << '\n';
}
 
// 绑定 tuple
auto [x, y, z] = std::make_tuple(1, 2.0, "three");
// x=1, y=2.0, z="three"
 
// 绑定数组
int arr[] = {10, 20, 30};
auto [a, b, c] = arr;
// a=10, b=20, c=30

变量作用域与生命周期

作用域存储位置初始化生命周期
局部变量栈 (stack)不自动初始化函数返回时销毁
static 局部.data/.bss自动初始化为0程序结束时销毁
全局变量.data/.bss自动初始化为0程序结束时销毁
auto 变量 (C++17)容器管理由构造函数决定离开作用域时
void counter() {
    static int count = 0; // 只初始化一次
    count++;
    std::cout << "调用次数: " << count << '\n';
}
// 每次调用 count 不会重置

C 语言没有 static 局部变量的概念(C89 有,但用法不同),C++ 的 static 局部变量是线程安全的初始化(C++11 起)。


实践

ASCII 码关键值:'A'=65, 'Z'=90, 'a'=97, 'z'=122, '0'=48, '9'=57。大小写转换差 32。

安全运算——将 int 转为 long long 再运算以防溢出:

long long safe = static_cast<long long>(a) * b;

常见陷阱

// 1. 大小写转换
char lower = 'A' + 32;  // 'a'
char upper = 'a' - 32;  // 'A'
 
// 2. 数字字符转数字
int digit = '7' - '0';  // 7
 
// 3. 判断奇偶 (位运算)
bool is_odd = n & 1;     // 比 n % 2 更快
 
// 4. 交换两个数 (不用临时变量)
a ^= b; b ^= a; a ^= b; // 仅适用于整数,且不能是同一变量

类型选择建议

// 竞赛/算法:
int          // 大多数情况
long long    // 可能超过 2^31 时
double       // 浮点计算
 
// 工程代码:
size_t       // 数组索引/大小
int32_t      // 需要固定宽度时
uint8_t      // 字节数据

练习

题号题目链接知识点
P1001A+B Problemhttps://www.luogu.com.cn/problem/P1001输入输出、变量
P1000超级玛丽游戏https://www.luogu.com.cn/problem/P1000顺序结构、输出
P1003铺地毯https://www.luogu.com.cn/problem/P1003数组、循环
P1012拼数https://www.luogu.com.cn/problem/P1012语法练习
202快乐数https://www.luogu.com.cn/problem/P1001整数运算、循环检测
258各位相加https://www.luogu.com.cn/problem/P1001数学运算、类型选择
412Fizz Buzzhttps://www.luogu.com.cn/problem/P1001取模运算、条件判断
415字符串相加https://www.luogu.com.cn/problem/P1001字符与数字转换