深入浅出 C/C++ 数据类型:从入门到踩坑
原创不易,先赞后看,养成习惯。这篇文章我尽量把 C/C++ 的数据类型讲透,顺便带上一些我平时写代码时踩过的坑。
前言
刚学 C/C++ 的时候,总觉得数据类型就是 int、char、float 那么简单。后来项目做大了,才发现类型系统是 C/C++ 的基石,也是很多诡异 bug 的源头。今天我就从头捋一遍,把常用的、不常用的,以及那些容易混淆的点都过一下。
一、基本内置类型(Primary Types)
1. 整型家族
| 类型 | 典型大小(32位平台) | 范围(有符号) |
|---|---|---|
char |
1 字节 | -128 ~ 127 |
short |
2 字节 | -32768 ~ 32767 |
int |
4 字节 | -2^31 ~ 2^31-1 |
long |
4 字节(Windows)/ 8 字节(Linux 64位) | 视平台 |
long long |
8 字节 | -2^63 ~ 2^63-1 |
注意 :char 比较特殊,它虽然本质是整型,但通常用来表示字符。而且 char 是否有符号是实现定义 的,有的编译器默认 char 为 signed char,有的默认 unsigned char。所以如果你需要明确符号,请显式写 signed char 或 unsigned char。
C++ 11 起 ,多了固定宽度的类型,在 <cstdint> 里:
cpp
int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t
我强烈建议你在写跨平台代码时用这些,别再迷信 int 一定占 4 字节了。
2. 浮点类型
| 类型 | 精度 | 通常字节数 |
|---|---|---|
float |
约 7 位十进制有效数字 | 4 |
double |
约 15~16 位 | 8 |
long double |
更高精度(实现定义) | 常见 12/16 字节 |
坑点 :浮点数比较大小千万别用 ==,要用 fabs(a - b) < eps。
- 布尔类型(C++)
C 语言没有真正的布尔类型(C99 有 _Bool),C++ 有 bool,取值 true / false,本质是 1 字节。
二、修饰符(Modifiers)
可以搭配整型使用的修饰符有:
signed/unsigned------ 有无符号short/long------ 长度
组合示例:
cpp
unsigned int ui = 42u; // 后缀 u 表示 unsigned
long long ll = 123LL; // LL 后缀
重要规则 :unsigned 和 signed 混合运算时,会发生隐式类型提升 ,通常 signed 会被转为 unsigned。这是很多死循环的元凶。
举个例子:
cpp
for (int i = 10; i >= 0; --i) { ... } // 正常
for (unsigned i = 10; i >= 0; --i) { ... } // 死循环!因为 i>=0 永远为真
三、构造类型(Derived/Compound Types)
1. 数组(Array)
cpp
int arr[10]; // 10 个 int
int arr2[] = {1,2,3}; // 自动推导大小为 3
char str[] = "hello"; // 大小 6(含 '\0')
数组名在大多数情况下会退化为指向首元素的指针,除了 sizeof 和 & 操作。
坑 :数组作为函数参数时,会退化为指针,所以 sizeof 拿不到数组长度。通常你需要额外传一个长度参数。
2. 结构体(struct)/ 联合体(union)/ 枚举(enum)
struct:字段按声明顺序排列,但存在内存对齐,字节数不一定等于各字段大小之和。union:所有成员共用一块内存,大小等于最大成员的大小。enum:枚举常量底层是int,C++11 支持枚举类enum class,更安全。
四、指针与引用(C++)
指针
cpp
int a = 10;
int* p = &a; // p 保存 a 的地址
*p = 20; // 解引用
指针的大小在 32 位平台是 4 字节,64 位是 8 字节。void* 可以指向任意类型,但不能直接解引用。
引用(C++ only)
cpp
int a = 10;
int& ref = a; // ref 是 a 的别名,不占额外内存(逻辑上)
ref = 20; // a 变为 20
引用必须在声明时初始化,且不能改变绑定。引用比指针更安全,但底层还是指针实现的。
五、类型别名
typedef int my_int;using my_int = int;(C++11)
建议用 using,语法更直观,尤其在模板别名时。
六、类型转换(重中之重)
C 风格强制转换
cpp
int a = 10;
double b = (double)a;
C++ 四种 cast
static_cast:最常用,编译时检查,用于相关类型转换(如 int->double,基类子类指针)。dynamic_cast:运行时检查,用于多态类型向下转型(需要 RTTI)。const_cast:去掉 const 属性(谨慎使用,只有在你确定原始对象非 const 时才能改)。reinterpret_cast:最危险,重新解释二进制位,基本属于"我明确知道我在做什么"时才用。
建议 :日常开发尽量少用强制转换,设计良好的接口应该避免转换。实在需要,优先 static_cast。
七、字面量后缀
| 后缀 | 含义 |
|---|---|
u / U |
unsigned |
l / L |
long |
ll / LL |
long long |
f / F |
float |
lu / UL |
unsigned long |
z (C++23) |
size_t |
例:
cpp
auto x = 3.14f; // float
auto y = 42ULL; // unsigned long long
八、类型推导(C++11 起)
auto:自动推导类型(会忽略引用和 const,除非显式写const auto&)。decltype:推导表达式的类型,保留引用和 const。
cpp
int a = 10;
const int& ra = a;
auto b = ra; // b 是 int,不是 const int&
decltype(ra) c = a; // c 是 const int&
九、完整示例:一个综合演练
下面我写一个小程序,把上面大部分知识点串起来,并附带一些容易犯错的点。
cpp
#include <iostream>
#include <cstdint>
#include <iomanip>
#include <cmath>
// 自定义类型别名
using byte_t = uint8_t; // 无符号 8 位
// 结构体,注意内存对齐
struct Student {
char name[20]; // 20 字节
uint32_t id; // 4 字节,但可能偏移到 20 的倍数(对齐)
float score; // 4 字节
// 理论总大小 28,但实际可能 32(取决于对齐)
};
// 枚举类,更安全
enum class Color : uint8_t {
Red,
Green,
Blue
};
int main() {
// 1. 固定宽度整型
int32_t x = 100;
uint64_t y = 0xFFFFFFFFFFFFFFFFULL; // 最大无符号64位
std::cout << "x = " << x << ", y = " << y << std::endl;
// 2. 浮点数精度问题
double a = 0.1 + 0.2;
double b = 0.3;
const double eps = 1e-9;
if (std::fabs(a - b) < eps) {
std::cout << "0.1+0.2 == 0.3 (within tolerance)" << std::endl;
} else {
std::cout << "Not equal!" << std::endl;
}
// 3. unsigned 陷阱演示
unsigned int count = 5;
// 注意:下面这个循环不会执行?错!因为 i 是 int,但 count 是 unsigned,
// 比较时 count 被转为 unsigned,i 也被转为 unsigned,-1 变成很大的数,条件为假
// 但更常见的坑是 for (int i = count - 1; i >= 0; --i) 当 count=0 时死循环
// 这里我们演示一个安全的写法
std::cout << "逆序输出 0~4: ";
for (int i = static_cast<int>(count) - 1; i >= 0; --i) {
std::cout << i << " ";
}
std::cout << std::endl;
// 4. 数组与指针
int arr[] = {1, 2, 3, 4, 5};
int* p = arr; // 退化为指针
std::cout << "数组大小: " << sizeof(arr) / sizeof(arr[0]) << std::endl;
std::cout << "指针大小: " << sizeof(p) << std::endl; // 64位下 8
// 5. 结构体大小(对齐)
std::cout << "Student 大小: " << sizeof(Student) << " 字节" << std::endl;
// 6. 引用与 auto
int value = 42;
int& ref = value;
auto copy = ref; // copy 是 int
decltype(ref) ref2 = value; // ref2 是 int&
ref2 = 100;
std::cout << "value = " << value << std::endl;
// 7. 枚举类
Color c = Color::Red;
// 不能隐式转为 int
// int colorValue = c; // 编译错误
int colorValue = static_cast<int>(c);
std::cout << "Color Red = " << colorValue << std::endl;
// 8. const_cast 示例(谨慎)
const int constVal = 30;
// 注意:下面这种修改实际上会导致未定义行为,因为 constVal 可能存于只读区
// 这里只是演示语法,实际项目中不要这么写
// int* pConst = const_cast<int*>(&constVal);
// *pConst = 50; // 危险!
// 安全用法:去除函数形参中的 const(但实参本身非常量)
auto func = [](const int* p) {
// 已知 p 指向的是非常量内存
int* q = const_cast<int*>(p);
*q = 999;
};
int normal = 123;
func(&normal);
std::cout << "normal after const_cast: " << normal << std::endl;
return 0;
}
输出示例(64位 Linux):
x = 100, y = 18446744073709551615
0.1+0.2 == 0.3 (within tolerance)
逆序输出 0~4: 4 3 2 1 0
数组大小: 5
指针大小: 8
Student 大小: 32 字节
value = 100
Color Red = 0
normal after const_cast: 999
十、总结与建议
- 跨平台优先用固定宽度类型 (
int32_t、uint64_t等)。 - 小心 unsigned 和 signed 混用,循环变量尤其注意。
- 浮点数永远不要直接
==,用eps比较。 - 结构体对齐 可以用
#pragma pack或__attribute__((packed)),但会影响性能,非必要不碰。 - 类型转换优先使用 C++ cast,让意图明确。
auto很好用 ,但要记得引用和 const 会被剥离,需要时可以显式写const auto&。
写这篇的时候,我尽可能把日常开发中容易翻车的地方都点到了。如果还有我没说清楚的,欢迎评论区交流,我看到都会回。
如果觉得有用,点个赞再走呗~