2、基础语法与数据类型:变量、常量、基本数据类型、类型转换、auto关键字、sizeof运算符

C++ 作为一门静态类型语言,其类型系统是构建一切程序的基石。理解变量与常量的本质、掌握基本数据类型的存储特性、熟练运用类型转换规则,是写出健壮、高效代码的前提。本章将从内存视角切入,系统梳理这些核心概念,并通过可运行的代码片段与对比表格,帮助你建立扎实的语法功底。

C++ 基础语法与数据类型知识结构 数据类型体系 基本数据类型 • int:4字节,整数运算 • float:4字节,单精度浮点 • double:8字节,双精度浮点 • char:1字节,字符/小整数 变量与常量 • 变量:可修改,有作用域 • const:编译期常量 • constexpr:编译期求值 • 初始化:拷贝/直接/列表 类型转换 • 隐式转换:自动提升 • static_cast:编译期检查 • dynamic_cast:运行时检查 • const_cast / reinterpret_cast auto 与 sizeof • auto:类型推导,必须初始化 • sizeof:编译期求字节大小

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位)上的典型值。

类型字节数取值范围典型用途
bool1true / false逻辑判断
char1-128 ~ 127 或 0 ~ 255字符存储
int4-2^31 ~ 2^31-1常规整数运算
float4±3.4e±38(约7位有效数字)单精度浮点
double8±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。

分析过程:

  1. 将两个字节拼接为 16 位整数:0x01B0 = 432。
  2. 该值是 12 位左对齐,需要右移 4 位得到实际 12 位值:432 >> 4 = 27。
  3. 乘以分辨率:27 × 0.0625 = 1.6875°C —— 这显然不对,说明符号处理有误。
  4. 检查符号位:0x01B0 的最高位(bit15)为 0,表示正数。但 12 位值的符号位是 bit11,此处为 0,仍为正数。
  5. 重新计算:实际 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&。