深入C指针
计算机使用一个字(word),表示内存地址。在 32 位计算机上会把 4 个字节(byte)拼成一个字,字由 32 个位(bit)组成。在 64 位计算机上会把 8 个字节拼成一个字,字由 64 个位组成。内存地址在内存中,就是一串固定长度的整数。
可表示的内存地址范围
虽然 64 位计算机的寄存器能处理 64 位的整数,实际上的内存地址并没有 64 位。实际上地址的高 16 位始终和第 48 位一致(符号扩展),也就是虚拟地址空间只有 48 位。 而经过 MMU 映射后实际给内存的地址只有 39 位,因此如今的 x64 架构实际上只能访问 512GB 内存,如果插了超过这个大小的内存条他也不会认出来。
此外,16 位计算机实际上能通过额外的段寄存器访问到 20 位的内存地址(1MB)。
32 位计算机还能通过 PAE 技术(物理地址扩展)访问到 36 位的内存地址(64GB)。
64 位计算机反而是因为 16777216 TB 太大,内存地址被阉割到了 39 位(512GB)。
扩展:数的表示
C 语言中的整数类型
类型 | Unix 32位 | Unix 64位 | Windows 32位 | Windows 64位 |
---|---|---|---|---|
char | 8 位 | 8 位 | 8 位 | 8 位 |
short | 16 位 | 16 位 | 16 位 | 16 位 |
int | 32 位 | 32 位 | 32 位 | 32 位 |
long | 32 位 | 64 位 | 32 位 | 32 位 |
long long | 64 位 | 64 位 | 64 位 | 64 位 |
long 比较特殊,在 Unix 上随系统位数变化,Windows 上始终是 32 位。所以在编写 C 语言程序时,应该避免使用 long 类型,他会导致你的程序难以跨平台。
在数字后面追加 U 和 L 可以表示不同类型的字面常量,不区分大小写,例如:
32 是 int 类型 32L 是 long 类型 32LL 是 long long 类型 32U 是 unsigned int 类型 32UL 是 unsigned long 类型 32ULL 是 unsigned long long 类型
尽管主流操作系统上 int 都是32位的,C语言标准并没有规定 int 就是32位的。int 甚至可以是16位的。
为了解决不同操作系统上对类型定义混乱的问题,C语言标准引入了 stdint.h 这个头文件。他里面包含一系列类型别名(typedef),这些别名保证不论是什么操作系统什么架构,都是固定的大小:
1 |
|
表示内存地址的类型
指针的本质就是内存地址,所以指针的大小在 32 位系统上就 32 位,64 位系统上就 64 位。intptr_t 和 uintptr_t 自动随系统位数变化。
intptr_t 在 32 位平台上等价于 int32_t;在 64 位平台上等价于 int64_t。
uintptr_t 在 32 位平台上等价于 uint32_t;在 64 位平台上等价于 uint64_t。
另外,在主流操作系统上,size_t 和 uintptr_t 完全等价。size_t 是标准库大量使用的用于表示大小的类型,例如 vector::size() 返回类型就是 size_t。
有符号整数vs无符号整数
补码表示法,使得负数的计算可以直接使用正数的计算电路,而不用重新设计有符号位处理的新电路。
补码表示法的目的是,利用加法器的“溢出”机制,例如 -1 + 2 = 1,在计算机看来就是:11111111 + 00000010 = 100000001。溢出的1,就被丢弃,而其结果正好是1。
负数的范围反而比正数大是因为要回避 -0。
类型转换
小类型和大类型做数学运算(+-*/%)会得到两个类型中的大类型。
如果两边有一边是 unsigned 的但是基本类型是相同的,则结果是 unsigned 的:unsigned int + int = unsigned int
如果两边有一边是 unsigned 的但是基本类型是不同的,则结果是 大类型:unsigned short + int = int
浮点数表示
从下向上,可以看到浮点数被转换为计算机中二进制表示的过程。(图中127+3=130)
float 由 4 个字节组成,也就是 32 个位。最高位是符号位,接着的 8 位是指数位(e)。 剩下的 23 位是底数位(m)。
注意指数位(e)是用反码表示的。
底数位限制了浮点数可表示的有效位数。
数的计算
常用函数名称:
int | long | long long | float | double | C++ 重载版 |
---|---|---|---|---|---|
abs | labs | llabs | fabsf | fabs | std::abs |
- | - | - | fmaxf | fmax | std::max |
- | - | - | fminf | fmin | std::min |
% | % | % | fmodf | fmod | std::fmod |
- | - | - | powf | pow | std::pow |
- | - | - | sqrtf | sqrt | std::sqrt |
- | - | - | sinf | sin | std::sin |
注意函数调用中隐式的类型转换。C++中,可以使用更方便的重载函数。
指针
常见计算机架构中,表示指针的整数,在内存中以小端字节序保存在固定长度的内存中。
指针无非是一个 64 位整数,在 32 位计算机上则是个 32 位整数。表示的是指针所指向变量在内存中的起始地址(第一个字节所在的位置)。甚至可以把 int* 强制转换成 unsigned long 类型,来打印出这个地址的整数值。
C++ 的引用
C++ 的引用类型 int& 本质无非是 int* 指针。区别在于:引用不需要手动 & 来创建;不需要手动 * 来创建;无法重新赋值,指向新的变量;无法为空指针。
nullptr
在C++中根据类型调用不同的重载版本。因为C中NULL可以是 0,那么 func(int) 和 func(int*) 就区分不了。
数组指针
数组变量就是指向数组其中一个元素的指针。数组作为参数传递,会退化为指针。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!