2、基础语法与数据类型:变量、常量、基本数据类型、类型转换、auto关键字、sizeof运算符
C++ 作为一门静态类型语言,其类型系统是构建一切程序的基石。理解变量与常量的本质、掌握基本数据类型的存储特性、熟练运用类型转换规则,是写出健壮、高效代码的前提。本章将从内存视角切入,系统梳理这些核心概念,并通过可运行的代码片段与对比表格,帮助你建立扎实的语法功底。
2.1 变量:内存中的命名空间
变量本质上是内存中一段连续存储单元的抽象命名。声明变量时,编译器负责分配内存并建立名称到地址的映射。C++ 要求变量先声明后使用,且声明可以携带初始化器。从 C++11 开始,推荐使用花括号初始化(列表初始化)以避免窄化转换带来的隐患。
// 变量声明与初始化示例
int count = 10; // 拷贝初始化
double price(99.5); // 直接初始化
char grade{'A'}; // 列表初始化(C++11)
bool isReady = true; // 布尔类型
// 声明多个变量
int x = 0, y = 0, z = 0; // 每个变量独立初始化
变量的生命周期与作用域紧密相关。局部变量在进入作用域时创建,离开时销毁;全局变量在程序启动时创建,结束时销毁。理解这一点对于避免悬垂引用和内存泄漏至关重要。
| 存储类别 | 作用域 | 生命周期 | 默认值 | 存储位置 |
|---|---|---|---|---|
| 局部变量 | 块内 | 块执行期间 | 未初始化(垃圾值) | 栈 |
| 全局变量 | 整个文件 | 程序运行期间 | 零初始化 | 静态存储区 |
| static 局部变量 | 块内 | 程序运行期间 | 零初始化 | 静态存储区 |
| extern 变量 | 跨文件 | 程序运行期间 | 零初始化 | 静态存储区 |
2.2 常量:不可变的数据契约
常量在程序运行期间不可修改,C++ 提供多种定义常量的方式。最常用的是 const 关键字,它表示“只读”语义。从 C++11 开始,constexpr 用于声明编译期即可求值的常量表达式,这比 const 更严格,因为 const 变量可能在运行时才初始化。
const double PI = 3.1415926535; // 运行期常量
constexpr int MAX_SIZE = 1024; // 编译期常量
const int* ptr = &MAX_SIZE; // 指向常量的指针
int const value = 42; // 与 const int 等价
constexpr 而非 const 来定义编译期常量。这不仅能提升性能(编译器可直接替换值),还能在编译期发现错误。例如,数组大小必须使用 constexpr 或字面量,而不能使用运行期 const 变量。
2.3 基本数据类型详解
C++ 内置的基本数据类型包括整型、浮点型、字符型和布尔型。每种类型在不同平台上的字节大小可能不同,但 C++ 标准规定了最小范围。下表列出了常见平台(32/64位)上的典型值。
| 类型 | 字节数 | 取值范围 | 典型用途 |
|---|---|---|---|
| bool | 1 | true / false | 逻辑判断 |
| char | 1 | -128 ~ 127 或 0 ~ 255 | 字符存储 |
| int | 4 | -2^31 ~ 2^31-1 | 常规整数运算 |
| float | 4 | ±3.4e±38(约7位有效数字) | 单精度浮点 |
| double | 8 | ±1.7e±308(约15位有效数字) | 高精度浮点 |
浮点类型需要特别注意精度问题。由于二进制无法精确表示所有十进制小数,浮点运算会产生舍入误差。例如,0.1 + 0.2 的结果并不精确等于 0.3。在金融计算或需要精确比较的场景中,应使用定点数或整数运算。
== 比较浮点数。应使用值差小于某个极小值(如 1e-6)的方式判断近似相等。否则,由于舍入误差,看似相等的两个浮点数可能比较失败。
2.4 类型转换:显式与隐式
类型转换分为隐式转换和显式转换。隐式转换由编译器自动完成,通常发生在不同类型混合运算时。例如,int 与 double 运算时,int 会被提升为 double。这种提升是安全的,但反向转换(如 double 转 int)会丢失精度,编译器会给出警告。
// 隐式转换示例
int a = 5;
double b = 3.7;
double result = a + b; // a 被提升为 double,结果为 8.7
// 显式转换(C++风格)
int c = static_cast<int>(b); // 截断小数部分,c = 3
double d = static_cast<double>(a) / 2; // 避免整数除法
C++ 提供了四种命名的强制类型转换运算符,每种都有明确的适用场景:
- static_cast:编译期检查的通用转换,用于相关类型之间的转换(如 int 到 double、void* 到具体指针)。
- dynamic_cast:运行时类型识别,用于多态类层次结构中的安全向下转换,失败时返回 nullptr。
- const_cast:移除或添加 const/volatile 限定符,仅用于修改指针或引用的常量性。
- reinterpret_cast:低级别的位模式重新解释,不进行任何检查,仅在底层编程时使用。
实战场景:在嵌入式开发中,经常需要从寄存器读取原始数据并转换为有意义的数值。例如,一个 16 位 ADC 寄存器存储的是无符号整数,但实际表示的是有符号温度值:
uint16_t raw = 0xFFF2; // 从寄存器读取
int16_t temperature = static_cast<int16_t>(raw); // 正确解释为 -14
// 如果直接赋值给 int,会得到 65522,导致错误
2.5 auto 关键字:类型推导的便利与边界
auto 让编译器根据初始化表达式自动推导变量类型。这大大简化了复杂类型名的书写(如迭代器类型),同时保证了类型一致性。但 auto 并非“无类型”,它推导出的类型是值类型(会忽略引用和顶层 const)。
auto x = 10; // int
auto y = 3.14; // double
auto z = x + y; // double(自动提升)
const auto& ref = x; // const int&,保留引用和常量性
// 使用 auto 简化迭代器
std::vector<std::string> names = {"Alice", "Bob"};
for (auto it = names.begin(); it != names.end(); ++it) {
// it 类型为 std::vector<std::string>::iterator
}
auto 不会推导出引用类型。如果希望修改变量,必须显式使用 auto&。例如,for (auto item : vec) 会拷贝每个元素,而 for (auto& item : vec) 才能修改原容器。此外,auto 不能用于函数参数(C++20 之前)和数组声明。
2.6 sizeof 运算符:编译期内存度量
sizeof 是一个编译期运算符,返回类型或变量所占的字节数,结果类型为 size_t(无符号整数)。它不会对操作数求值,因此可以安全地用于未初始化的变量或表达式。
std::cout << sizeof(int) << "\n"; // 4(典型平台)
std::cout << sizeof(double) << "\n"; // 8
std::cout << sizeof(char) << "\n"; // 1(标准保证)
int arr[10];
std::cout << sizeof(arr) << "\n"; // 40(10 * 4)
std::cout << sizeof(arr) / sizeof(arr[0]) << "\n"; // 10(数组长度)
struct Point { int x; int y; };
std::cout << sizeof(Point) << "\n"; // 8(可能因对齐而增大)
使用 sizeof 计算数组长度是常见技巧,但要注意:当数组作为函数参数传递时,会退化为指针,此时 sizeof 得到的是指针大小而非数组大小。因此,推荐使用 std::size()(C++17)或模板函数来获取数组长度。
int 一定是 4 字节。使用 sizeof 验证或使用固定宽度类型(如 int32_t、uint64_t)来保证可移植性。结构体可能因内存对齐产生填充字节,导致 sizeof 大于成员大小之和。
2.7 综合案例:温度传感器数据解析
假设你正在开发一个环境监测系统,需要从 I2C 温度传感器读取原始数据并转换为摄氏度。传感器返回两个字节:高字节和低字节,组合成一个 12 位有符号数(左对齐),分辨率为 0.0625°C/LSB。
初始现象:直接使用 int 拼接字节得到错误温度,且负温度显示异常。
关键输入数据:传感器返回 0x01 和 0xB0,期望温度约为 26.5°C。
分析过程:
- 将两个字节拼接为 16 位整数:
0x01B0= 432。 - 该值是 12 位左对齐,需要右移 4 位得到实际 12 位值:432 >> 4 = 27。
- 乘以分辨率:27 × 0.0625 = 1.6875°C —— 这显然不对,说明符号处理有误。
- 检查符号位:0x01B0 的最高位(bit15)为 0,表示正数。但 12 位值的符号位是 bit11,此处为 0,仍为正数。
- 重新计算:实际 12 位值为 0x1B0 >> 4 = 0x1B = 27,但 0x1B0 是 16 位值,需要先取低 12 位再右移。
uint8_t high = 0x01;
uint8_t low = 0xB0;
uint16_t raw = (static_cast<uint16_t>(high) << 8) | low; // 0x01B0
int16_t temp_raw = static_cast<int16_t>(raw) >> 4; // 算术右移保留符号
double temperature = temp_raw * 0.0625;
std::cout << "温度: " << temperature << " °C\n"; // 输出 26.9375°C
结果对比:修正后输出 26.9375°C,与预期 26.5°C 接近(差异来自传感器量化误差)。若使用无符号右移,负温度会变成大正数,导致严重错误。
复盘经验:
- 处理传感器数据时,务必明确数据格式(位数、对齐方式、符号表示)。
- 使用
static_cast确保类型转换的意图明确,避免隐式转换的意外。 - 右移有符号数时,算术右移会保留符号位,这是处理负温度的关键。
- 用
sizeof验证数据类型大小,确保跨平台一致性。
2.8 常见误区与总结
初学者在数据类型上常犯的错误包括:混淆 char 的符号性(标准未规定,依赖平台)、整数除法截断、浮点精度丢失、隐式转换导致的数据截断,以及 auto 推导出非预期类型。建议遵循以下最佳实践:
- 初始化所有变量,避免使用未初始化的垃圾值。
- 优先使用
double而非float,除非内存极度受限。 - 显式使用
static_cast进行类型转换,避免隐式窄化。 - 使用
constexpr定义编译期常量,提升安全性和性能。 - 用
sizeof验证类型大小,特别是在跨平台开发中。 - 理解
auto的推导规则,需要引用时显式写auto&。