C++的数据类型你真的了解吗
前言
「C++ 有多少种数据类型」是个看起来很简单的问题,但真正写代码时出错的,往往不是不知道有 int 和 double,而是对它们的保证范围和平台差异有错误的直觉。比如认为 int 一定是 4 字节、char 一定是无符号、long 一定比 int 大、两个 double 用 == 比较没问题。这些直觉在某个平台上碰巧成立,换一个编译器或换一个操作系统就会翻车。
标准对基本类型只规定最小范围,不规定字节数。sizeof(int) 可以是 2、4 或 8,只要它能表示至少 16 位;char 是否有符号由实现决定;long 在 Windows 的 LLP64 模型下是 4 字节,在 Linux 的 LP64 模型下是 8 字节。这些差异不是冷知识,它们直接影响序列化格式、二进制协议和跨平台接口。
本文从「标准保证什么、实现决定什么」出发,依次讲基本类型的范围、整型转换与溢出、浮点比较、定宽类型与类型推导,最后给出可读的检查代码。全文以 C++17 为基准,涉及 C++20 的变化会单独标注。文中结论均按标准语义描述,本机没有编译环境,未做运行验证。
一、标准只保证最小范围
C++ 的基本类型分为几组:布尔 bool、字符类型(char、signed char、unsigned char、wchar_t、C++11 的 char16_t/char32_t、C++20 的 char8_t)、整数类型(short、int、long、long long 及各自的 unsigned 版本)、浮点类型(float、double、long double)以及 void。
标准给出的保证可以归纳为下表。注意「最小位数」一列,这是标准的下限,不是实际大小:
| 类型 | 标准保证的最小范围 | 常见实现(LP64 / LLP64) |
|---|
char | 至少 8 位,恰好 1 字节 | 1 字节,符号由实现决定 |
short | 至少 16 位 | 2 字节 |
int | 至少 16 位 | 4 字节 |
long | 至少 32 位 | Linux 8 字节 / Windows 4 字节 |
long long | 至少 64 位 | 8 字节 |
float | 至少 6 位有效数字 | 4 字节,通常 IEEE 754 单精度 |
double | 至少 10 位有效数字 | 8 字节,通常 IEEE 754 双精度 |
wchar_t | 能表示实现支持的最大字符集 | Windows 2 字节 / Linux 4 字节 |
sizeof(char) 恒为 1,这是标准的定义方式:一个字节就是一个 char 的大小,而一个字节有多少位由 CHAR_BIT 给出,标准要求至少 8 位。绝大多数平台是 8 位,但不能假设所有平台都如此。
char、signed char、unsigned char 是三种不同的类型。char 的符号性是实现定义行为:x86 上的 GCC 和 MSVC 默认有符号,ARM 上的 GCC 默认无符号,可以用 -funsigned-char / -fsigned-char 改变。因此把 char 直接传给 std::tolower 或用作数组下标时,负值会带来未定义行为;正确做法是先转成 unsigned char。
浮点类型也有类似的坑。标准并不强制要求 IEEE 754,只是大多数实现遵循它;std::numeric_limits<double>::is_iec559 可以用来在编译期判断。即使遵循 IEEE 754,0.1 + 0.2 == 0.3 也不成立,因为这三个数都无法用二进制精确表示。
二、整型转换:提升、截断与符号
理解整型运算,要记住三条规则。
第一条是整型提升(integral promotion)。 比 int 小的类型(char、short、bool 等)参与算术运算前会先提升为 int(如果 int 能表示其全部值),否则提升为 unsigned int。所以 char a = 200; char b = 100; a + b 的结果类型是 int,值为 300,不会在 8 位处回绕。这也解释了一个常见疑问:为什么两个 unsigned char 相加不会溢出。
第二条是寻常算术转换(usual arithmetic conversions)。 当二元运算符两侧类型不同时,会转换到能容纳两者的公共类型。危险就出在有符号与无符号混用:如果无符号类型的秩不低于有符号类型,有符号操作数会被转换成无符号。int a = -1; unsigned b = 1; a < b 的结果是 false,因为 -1 被转换成了一个极大的无符号数。这类比较在开启 -Wsign-compare 时会有警告,不要忽略。
第三条是溢出规则不同。 无符号整数的运算按模 2^N 回绕,这是标准明确规定的;有符号整数溢出是未定义行为(undefined behavior, UB),编译器可以假设它不会发生,从而做出在溢出时看似荒谬的优化。不要写 if (x + 1 < x) 来判断溢出,这个条件在优化后可能被直接删除。需要检查时用宽类型计算,或用编译器内建函数(如 GCC 的 __builtin_add_overflow,属扩展)。
C++20 起,有符号整数被要求采用二补码表示,但有符号溢出仍然是 UB,这一点没有改变。
三、浮点数与字面量
浮点数的比较要分场景。判断两个计算结果的「相等」,通常用相对误差或绝对误差;判断某个值是否恰好等于常量(例如哨兵值、解析结果),直接用 == 是合理的。把所有浮点比较都换成 epsilon 比较,反而会让「两个不同的值」被判为相等。
字面量的后缀决定类型:1 是 int,1U 是 unsigned int,1L 是 long,1LL 是 long long,1.0 是 double,1.0f 是 float,1.0L 是 long double。C++14 起可以用单引号分隔数字(1'000'000),也支持二进制字面量(0b1010)。
// 适用于 C++17
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <limits>
#include <type_traits>
template <typename T>
void show_limits(const char* name) {
std::cout << std::left << std::setw(12) << name
<< " sizeof=" << sizeof(T)
<< " bits=" << std::numeric_limits<T>::digits
<< " signed=" << std::numeric_limits<T>::is_signed
<< " min=" << static_cast<long long>(std::numeric_limits<T>::min())
<< " max=" << static_cast<long long>(std::numeric_limits<T>::max())
<< '\n';
}
int main() {
show_limits<signed char>("signed char");
show_limits<short>("short");
show_limits<int>("int");
show_limits<unsigned>("unsigned");
std::cout << "long sizeof=" << sizeof(long) << '\n';
std::cout << "long long sizeof=" << sizeof(long long) << '\n';
std::cout << "pointer sizeof=" << sizeof(void*) << '\n';
std::cout << "wchar_t sizeof=" << sizeof(wchar_t) << '\n';
std::cout << "size_t sizeof=" << sizeof(std::size_t) << '\n';
// 判断浮点是否遵循 IEEE 754
std::cout << "double is_iec559=" << std::numeric_limits<double>::is_iec559 << '\n';
// 有符号与无符号混用的经典陷阱
int negative = -1;
unsigned positive = 1;
std::cout << "(-1 < 1u) = " << std::boolalpha << (negative < positive) << '\n';
return 0;
}把 char 安全地传给字符分类函数,写法是两步转换:
// 适用于 C++17
#include <cctype>
#include <string>
std::string to_upper_ascii(const std::string& input) {
std::string out = input;
for (char& ch : out) {
unsigned char uch = static_cast<unsigned char>(ch);
if (uch >= 'a' && uch <= 'z') {
ch = static_cast<char>(uch - 'a' + 'A');
}
}
return out;
}这里没有调用 std::toupper,是因为 std::toupper 的行为依赖当前的 C 区域设置(locale),默认的 "C" 区域只保证处理 ASCII;同时它的参数必须能表示成 unsigned char 或等于 EOF,直接传负的 char 是 UB。手写 ASCII 判断没有这些问题,也便于理解边界。
四、定宽类型、size_t 与推导
需要精确位宽时,用 <cstdint> 里的 std::int8_t、std::int32_t、std::uint64_t 等。注意这些类型是可选的:只有在平台恰好存在 8/16/32/64 位且无填充位的整数类型时才会定义。绝大多数平台都有,但标准不保证。需要「至少 N 位」时用 std::int_least32_t 或 std::int_fast32_t。
表示大小和下标用 std::size_t(<cstddef>),表示指针差值用 std::ptrdiff_t。sizeof 的结果类型就是 std::size_t,它是无符号的,所以 for (int i = 0; i < v.size(); ++i) 会有符号比较警告,容器为空时把 size() - 1 赋给有符号数还会得到巨大的值。
auto 推导会丢弃顶层 const 和引用,decltype(auto) 则保留。这两者的差异在返回引用时特别重要:
// 适用于 C++17
#include <string>
#include <vector>
std::string& first_name(std::vector<std::string>& names) {
return names.at(0); // 越界时抛 std::out_of_range
}
auto copy_of_first(std::vector<std::string>& names) {
return first_name(names); // auto 推导为 std::string,发生拷贝
}
decltype(auto) ref_of_first(std::vector<std::string>& names) {
return first_name(names); // 推导为 std::string&,不拷贝
}auto 按值推导,返回类型是 std::string,函数返回时拷贝一次;decltype(auto) 保留表达式的引用性。如果误写成 auto& 而函数返回的是局部变量,就会产生悬垂引用。C++17 的结构化绑定(structured binding)也遵循类似规则,auto [a, b] = pair; 是拷贝,auto& [a, b] = pair; 才是引用。
常见坑点
坑点 1:假设 int 是 4 字节。
❌ 直接把 int 写入二进制文件或网络包。
✅ 用 std::int32_t 显式指定宽度,并统一字节序;或者用文本格式。
坑点 2:有符号与无符号直接比较。
❌ for (int i = 0; i < vec.size(); ++i),空容器时 size() 参与比较会出现符号转换。
✅ 用 std::size_t i = 0,或使用范围 for;比较前确认两边符号一致。
坑点 3:依赖有符号溢出回绕。
❌ if (x + 1 < x) { /* 认为发生了溢出 */ }。
✅ 有符号溢出是 UB,判断要用宽类型或编译器内建函数。
坑点 4:把负的 char 传给 std::toupper / std::tolower。
❌ std::toupper(ch),当 ch 为负值时是 UB。
✅ 先 static_cast<unsigned char>(ch) 再传;或像示例那样只处理 ASCII 范围。
坑点 5:用 == 比较浮点计算结果。
❌ if (a + b == 0.3)。
✅ 用相对误差比较,或改用整数/定点表示金额等需要精确的场景。
坑点 6:以为 long 一定等于指针宽度。
❌ 在 Windows 上把 long 当指针大小的整数使用。
✅ 用 std::intptr_t / std::uintptr_t 表示能容纳指针的整数。
坑点 7:auto 意外拷贝。
❌ for (auto s : vec_of_strings),每个元素拷贝一次。
✅ 只读用 const auto&,需要修改用 auto&,确实要副本才用 auto。
坑点 8:窄化初始化被忽略。
❌ char c = 300; 静默截断。
✅ 用列表初始化 char c{300};,编译器会报窄化错误;确实要转换时写 static_cast 并确认范围。
总结
| 主题 | 标准规定 | 实践建议 |
|---|
| 类型大小 | 只保证最小范围 | 跨平台数据用定宽类型 |
char 符号 | 实现定义 | 转 unsigned char 再参与字符分类 |
| 无符号溢出 | 按模 2^N 回绕 | 可用于哈希等场景,但要写清意图 |
| 有符号溢出 | 未定义行为 | 不做任何依赖,用宽类型检查 |
| 浮点表示 | 不强制 IEEE 754 | 用 is_iec559 判断,比较用容差 |
| 指针大小 | 实现定义 | 用 std::uintptr_t,不要用 long |
数据类型的关键不是记住一张字节数表,而是分清「标准保证的」「实现决定的」「未定义的」三层。凡是跨越编译单元、跨越平台、跨越进程边界的数据,都应该用定宽类型并显式处理字节序;凡是涉及符号混合的表达式,都应该让两边的类型先对齐。把这两条落实到代码里,大部分「换个平台就出错」的问题会在写的时候就被挡住。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)