数据
C语言有5种基本数据类型:char
、int
、float
、double
、void
(不能被修饰)
通过2个长度修饰符:
short
:只能修饰int
long
:可修饰1~2次int
或1次double
以及2个符号修饰符(只能修饰整型类型,可以和长度修饰符组合):
signed
:可以缺省,不写符号修饰符等同于signed
。有符号则表示正数和负数,绝对值范围减半。unsigned
:不可以缺省。无符号则只表示正数,不表示负数。
可以组合出14种基本数据类型。
一级分类 | 二级分类 | 字节数 | 类型 | 完整形式 | 简化形式 | 取值范围 | 描述 |
---|---|---|---|---|---|---|---|
整型类型 | 字符型 | 1 | char | signed char | char | -2⁷ ~ 2⁷-1 | 字符类型(8位,不支持中文) |
整型类型 | 字符型 | 1 | unsigned char | unsigned char | unsigned char | 0 ~ 2⁸-1 | 无符号字符型(8位,不支持中文) |
整型类型 | 短整型 | 2 | short | signed short int | short /short int | -2¹⁵ ~ 2¹⁵-1 | 有符号短整型(16位) |
整型类型 | 短整型 | 2 | unsigned short | unsigned short int | unsigned short | 0 ~ 2¹⁶-1 | 无符号短整型(16位) |
整型类型 | 标准整型 | 4 | int | signed int | int /signed | -2³¹ ~ 2³¹-1 | 有符号整型(32位,默认) |
整型类型 | 标准整型 | 4 | unsigned int | unsigned int | unsigned int /unsigned | 0 ~ 2³²-1 | 无符号整型(32位) |
整型类型 | 长整型 | 4/8 | long | signed long int | long | -2³¹ ~ 2³¹-1 或 -2⁶³ ~ 2⁶³-1 | 有符号长整型(32位或64位) |
整型类型 | 长整型 | 4/8 | unsigned long | unsigned long int | unsigned long | 0 ~ 2³²-1 或 0 ~ 2⁶⁴-1 | 无符号长整型(32位或64位) |
整型类型 | 长长整型 | 8 | long long | signed long long int | long long int /signed long long /long long | -2⁶³ ~ 2⁶³-1 | 有符号长长整型(64位) |
整型类型 | 长长整型 | 8 | unsigned long long | unsigned long long int | unsigned long long | 0 ~ 2⁶⁴-1 | 无符号长长整型(64位) |
浮点型类型 | 单精度 | 4 | float | float | float | ±1.18×2⁻¹²⁶ ~ ±3.40×2¹²⁷ | 单精度浮点型(32位IEEE754) |
浮点型类型 | 双精度 | 8 | double | double | double | ±2.23×2⁻¹⁰²² ~ ±1.80×2¹⁰²³ | 小数默认类型,双精度浮点型(64位IEEE754) |
浮点型类型 | 扩展精度 | 8/12/16 | long double | long double | long double | 平台相关 | 扩展精度浮点型(64/80/128位) |
特殊类型 | - | 0 | void | void | void | - | 空类型,不占存储空间 |
char
用于存储一个基本的 ASCII 字符(不可以存储中文)。
ASCII 码表很小,只包含大小写字母、数字、标点、打印机用的换行、换页等符号。
- 用单引号括起来:例如
'a'
,'1'
,'$'
- 转义字符:例如
'\n'
(换行),'\t'
(制表符),'\\'
(反斜杠)
#include <stdio.h>
int main(){
const char letter = 'A';
const char newline = '\n';
const char tab = '\t';
printf("字符: %c\n", letter);
printf("换行前%c换行后\n", newline);
printf("制表符前%c制表符后\n", tab);
return 0;
}
char
和int
类型转换时,int
转为char
时会是对应的ASCII
码表中字符。char
转为int
时会是对应的ASCII
码表中对应的数字。
ASCII 表
想要存储中文可以试试字符串数组。
- 用双引号括起来:例如
"Hello"
,"C语言"
- 系统会自动在字符串末尾添加 '\0' 作为结束标志
#include <stdio.h>
#include <string.h>
int main(){
const char greeting[] = "Hello World";
const char empty[] = "";
printf("问候语: %s\n", greeting);
printf("空字符串长度: %lu\n", strlen(empty));
return 0;
}
int
定义时可以带正负号,默认是正数。支持四种进制格式:
- 十进制整数:例如
666
,-120
,0
- 二进制整数:以
0b
开头,例如0b1010
(十进制的10) - 八进制整数:以
0
开头,例如0123
(十进制的83) - 十六进制整数:以
0x
开头,例如0x123
(十进制的291)
#include <stdio.h>
int main(){
printf("十进制: %d\n", 123); // 123
printf("八进制: %d\n", 0123); // 83
printf("十六进制: %d\n", 0x123); // 291
printf("二进制: %d\n", 0b1010); // 10
return 0;
}
判断下列数字 是否合理
+178
0b325 // 二进制只能是0-1
0b0010
0986 // 八进制只能是0-7
00011
0x001
0x7h4 // 十六进制只能是0-9,a-f,A-F
0xffdc
signed 和 unsigned
符号修饰符(仅适用于整数类型):
signed
- 有符号位unsigned
- 无符号位
signed
和 unsigned
的区别就是它们的最高位是否要当做符号位,并不会像 short
和 long
那样改变数据的长度,即所占的字节数。
浮点数类型(float
、double
、long double
)天然支持正负值,不能使用符号修饰符
signed
表示有符号的,也就是说最高位要当做符号位。刚好 int
的最高位本来就是符号位。
因此 signed
等价 int
等价 signed int
。
short 和 long
short
:只能修饰int
long
:可修饰1~2次int
或1次double
🤔 为什么 long
不一定比 int
大?有时是4字节,有时是8字节?
核心原因:C标准只规定了相对大小关系,没有规定绝对大小!
// C标准只保证这些相对关系:
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
// 1字节 ≥2字节 ≥2字节 ≥4字节 ≥8字节
1980年代-1990年代(32位时代):
// 当时的"标准"配置
char = 1字节 (8位)
short = 2字节 (16位)
int = 4字节 (32位) ← 与CPU字长匹配
long = 4字节 (32位) ← 也是32位,够用了
2000年代后(64位时代):
// 两种不同的选择
// Linux/macOS (LP64模型)
int = 4字节 (32位) ← 保持兼容性
long = 8字节 (64位) ← 升级到64位
// Windows (LLP64模型)
int = 4字节 (32位) ← 保持兼容性
long = 4字节 (32位) ← 与32位系统保持一致,向前兼容
long long = 8字节 (64位) ← 用long long做64位
如果你需要特定大小:
// ✅ 推荐:使用标准库的固定大小类型
#include <stdint.h>
int32_t x; // 明确32位
int64_t y; // 明确64位
uint32_t z; // 明确32位无符号
// ⚠️ 不推荐:依赖long的大小
long maybe_big; // 大小不确定
float与double
实型常量
- 单精度小数?:以字母
f
或F
结尾,例如3.14f
- 双精度小数?:默认的小数形式,例如
3.14159
- 指数形式:使用
e
或E
表示科学计数法,例如1.23e5
(表示123000)
#include <stdio.h>
int main(){
const float pi_f = 3.14f; // 单精度
const double pi_d = 3.14159; // 双精度(默认的小数形式就是双精度)
const double big_num = 1.23e5; // 科学计数法
const double small_num = 1.23e-3; // 0.00123
printf("单精度PI: %.7f\n", pi_f);
printf("双精度PI: %.15f\n", pi_d);
printf("大数: %.0f\n", big_num);
printf("小数: %.5f\n", small_num);
return 0;
}
判断下列数字是否合理
10.98 // ✅ 合理:标准的十进制小数
.089 // ✅ 合理:浮点数,前面的0可以省略,等同于0.089
-.003 // ✅ 合理:负数浮点常量
96.0f // ✅ 合理:单精度浮点常量
3.14E-2 // ✅ 合理:科学计数法,等同于0.0314
96f // ❌ 错误:缺少小数点
96.oF // ❌ 错误:用了字母'o'而不是数字'0',应该是96.0F
3.14.15 // ❌ 错误:多个小数点
long double
精度差异的技术演进
如果说long int
字节不一致是处理器字长升级导致的兼容性问题,那么long double
字节差异则反映了不同厂商在"精度与效率"之间的技术权衡。
C标准只规定了相对大小关系,没有规定绝对大小,因此long double
的精度取决于各大硬件厂商的具体实现。
1980年代初 期
Intel开发了x87浮点协处理器(8087),采用80位(10字节)扩展精度格式,提供了比标准64位double更高的计算精度。这在当时的科学计算领域具有重要意义。
Linux和GCC 选择充分利用Intel的80位精度能力,为需要高精度计算的应用提供支持。
Microsoft 则采用了更为保守的策略,在Visual Studio中将long double
实现为64位,与double
相同。这样做的考虑是:
- 简化编译器实现和调试
- 保持与早期Windows系统的兼容性
- 64位精度已能满足大多数应用需求
2000年代后期
随着移动设备兴起,ARM架构 开始在嵌入式和移动领域占主导地位。ARM处理器专注于功耗优化:
- 80位运算增加功耗和复杂度
- 移动设备电池容量有限
- 64位精度足够满足移动应用需求
苹果、Google、高通、三星 等移动平台厂商都选择了ARM的64位实现方案。
同时期:高性能计算的野心
就在移动厂商追求效率的同时,高性能计算领域却在追求更极致的精度:
IBM AIX/PowerPC 拿出了"双倍双精度"方案:"既然单个64位不够,那就用两个64位组合!" 这种Double-Double格式提供了约31位十进制精度。
GCC社区 更进一步,推出了__float128
:"我们要真正的128位IEEE标准实现!" 34位十进制精度,满足最苛刻的科学计算需求。
现在的状况:
平台/编译器 | long double大小 | 设计考量 |
---|---|---|
Windows + Visual Studio | 8字节(64位) | 兼容性优先,简化实现 |
Linux + GCC (x86) | 12/16字节(80位) | 充分利用x87硬件能力 |
Mac + Clang (Intel) | 16字节(80位) | 遵循x86-64 ABI标准 |
Mac + Clang (Apple Silicon) | 8字节(64位) | ARM架构的效率优化 |
Android开发 | 8字节(64位) | 移动平台功耗考量 |
IBM AIX/PowerPC | 16字节(128位) | 双倍双精度,超高精度计算 |
GCC __float128 | 16字节(128位) | IEEE 754四精度扩展 |
32位处理器一次操作处理32位(4字节)数据,64位处理器一次操作处理64位(8字节)数据。
80位(10字节)数据在32位处理器需要操作3次,等价处理了3x4=12字节。在64位处理器需要操作2次,等价处理了2x8=16字节。
开发建议:
- 不要假设
long double
一定比double
大 - 需要特定精度时,考虑使用专门的高精度数学库
- 跨平台项目应该测试各平台的精度差异
void
void
的主要用途:
用途 | 语法 | 示例 | 说明 |
---|---|---|---|
函数返回类型 | void 函数名() | void print_hello() { printf("Hello"); } | 表示函数不返回任何值 |
函数参数 | 函数名(void) | int get_random(void) { return 42; } | 表示函数不接受任何参数 |
通用指针 | void* | void *ptr; ptr = &some_variable; | 可以指向任何类型的数据 |
// ✅ void 的正确用法
void print_message(void) { // 无返回值,无参数
printf("Hello World\n");
}
int main(void) { // 返回int,无参数
void *generic_ptr; // 通用指针
int num = 10;
generic_ptr = # // 指向int类型数据
print_message(); // 调用void函数
return 0;
}
// ❌ void 的错误用法
void variable; // 错误!不能声明void类型变量
void array[10]; // 错误!不能创建void数组
重要提醒:
void
本身不能存储数据,只用于类型声明void*
是通用指针,使用前需要强制类型转换- 函数参数写
void
比省略更明确(C89标准推荐)
类型转换
显式转换
显式转换也叫强制类型转换:(需要转换的类型)(表达式)
// 将double转换为int
int a = (int)(10.5 + 0.6);
// C 取整时,总是向0取整。(计算余数和四舍五入时也总是向0取整)
// 所以结果为11
// 当只有一个数时,括号可以省略
int a = (int) 10.5;
// 结果为10
// 括号省略会导致可读性变差
int a = (int) 10.5 + 0.6;
// 结 果为10
// 第一步:(int) 10.5 被强制转换为 10(int类型)
// 第二步:10(int)+ 0.6(double)→ 10(int)被自动提升为10.0(double),计算得10.6(double)
// 第三步:将10.6(double)隐式转换为int类型赋值给a,结果为10
// C 取整时,总是向0取整(截断小数部分)
赋值转换
// 赋值时左边是什么类型,就会自动将右边转换为什么类型再保存
int a = 10.6;
// 结果为10
// C 取整时,总是向0取整。(计算余数和四舍五入时也总是向0取整)
运算时转换
当运算符左右两边类型不一致时,系统会自动对占用内存较少的类型做一个“自动类型提升”的操作。
先将其转换为当前算数表达式中占用内存高的类型, 然后再参与运算。
// 结果为0, 因为参与运算的都是整型,先得出0,再转换为double类型
double a = (double)(1 / 2);
// 结果为0.5, 因为1被强制转换为了double类型, 2也会被自动提升为double类型
double b = (double)1 / 2;
// 等价于
double b = 1.0 / 2;
- 类型转换并不会影响到原有变量的值
#include <stdio.h>
int main(){
double d = 3.14;
int num = (int)d;
printf("num = %i\n", num); // 3
printf("d = %lf\n", d); // 3.140000
}