引用自:http://queniao.blog.51cto.com/10636/126564
随着 64 位体系结构的普及,针对 64 位系统准备好您的 Linux® 软件已经变得比以前更为重要。在本文中,您将学习如何在进行语句声明、赋值、位移、类型转换、字符串格式化以及更多操作时,防止出现可移植性缺陷。
总之,64 位具有以下优点:
- 64 位的应用程序可以直接访问 4EB 的虚拟内存,Intel Itanium 处理器提供了连续的线性地址空间。
- 64 位的 Linux 允许文件大小最大达到 4 EB(2 的 63 次幂),其重要的优点之一就是可以处理对大型数据库的访问。
|
ILP32 | LP64 | LLP64 | ILP64 | |
---|---|---|---|---|
char | 8 | 8 | 8 | 8 |
short | 16 | 16 | 16 | 16 |
int | 32 | 32 | 32 | 64 |
long | 32 | 64 | 32 | 64 |
long long | 64 | 64 | 64 | 64 |
指针 | 32 | 64 | 64 | 64 |
这 3 个 64 位模型(LP64、LLP64 和 ILP64)之间的区别在于非浮点数据类型。当一个或多个 C 数据类型的宽度从一种模型变换成另外一种模型时,应用程序可能会受到很多方面的影响。这些影响主要可以分为两类:
- 数据对象的大小。编译器按照自然边界对数据类型进行对齐;换而言之,32 位的数据类型在 64 位系统上要按照 32
位边界进行对齐,而 64 位的数据类型在 64 位系统上则要按照 64 位边界进行对齐。这意味着诸如结构或联合之类的数据对象的大小在 32 位和
64 位系统上是不同的。
- 基本数据类型的大小。通常关于基本数据类型之间关系的假设在 64 位数据模型上都已经无效了。依赖于这些关系的应用程序在 64 位平台上编译也会失败。例如,
sizeof (int) = sizeof (long) = sizeof (pointer)
的假设对于 ILP32 数据模型有效,但是对于其他数据模型就无效了。
struct test { int i1; double d; int i2; long l; } |
结构成员 | 在 32 位系统上的大小 | 在 64 位系统上的大小 |
---|---|---|
struct test { | ||
int i1; | 32 位 | 32 位 |
32 位填充 | ||
double d; | 64 位 | 64 位 |
int i2; | 32 位 | 32 位 |
32 位填充 | ||
long l; | 32 位 | 64 位 |
}; | 结构大小为 20 字节 | 结构大小为 32 字节 |
d
进行对齐,尽管它是一个 64 位的对象,这是因为硬件会将其当作两个 32 位的对象进行处理。然而,64 位的系统会对 d
和 l
都进行对齐,这样会添加两个 4 字节的填充。
|
本节介绍如何解决一些常见的问题:
- 声明
- 表达式
- 赋值
- 数字常数
- Endianism
- 类型定义
- 位移
- 字符串格式化
- 函数参数
要想让您的代码在 32 位和 64 位系统上都可以工作,请注意以下有关声明的用法:
- 根据需要适当地使用 “L” 或 “U” 来声明整型常量。
- 确保使用无符号整数来防止符号扩展的问题。
- 如果有些变量在这两个平台上都需要是 32 位的,请将其类型定义为 int。
- 如果有些变量在 32 位系统上是 32 位的,在 64 位系统上是 64 位的,请将其类型定义为 long。
- 为了对齐和性能的需要,请将数字变量声明为 int 或 long 类型。不要试图使用 char 或 short 类型来保存字节。
- 将字符指针和字符字节声明为无符号类型的,这样可以防止 8 位字符的符号扩展问题。
在 C/C++ 中,表达式是基于结合律、操作符的优先级和一组数学计算规则的。要想让表达式在 32 位和 64 位系统上都可以正确工作,请注意以下规则:
- 两个有符号整数相加的结果是一个有符号整数。
- int 和 long 类型的两个数相加,结果是一个 long 类型的数。
- 如果一个操作数是无符号整数,另外一个操作数是有符号整数,那么表达式的结果就是无符号整数。
- int 和 doubule 类型的两个数相加,结果是一个 double 类型的数。此处 int 类型的数在执行加法运算之前转换成 double 类型。
- 不要交换使用 int 和 long 类型,因为这可能会导致高位数字被截断。例如,不要做下面的事情:
int i; long l; i = l;
- 不要使用 int 类型来存储指针。下面这个例子在 32 位系统上可以很好地工作,但是在 64 位系统上会失败,这是因为 32 位整数无法存放 64 位的指针。例如,不要做下面的事情:
unsigned int i, *ptr; i = (unsigned) ptr;
- 不要使用指针来存放 int 类型的值。例如,不要做下面的事情;
int *ptr; int i; ptr = (int *) i;
- 如果在表达式中混合使用无符号和有符号的 32 位整数,并将其赋值给一个有符号的 long 类型,那么将其中一个操作数转换成
64 位的类型。这会导致其他操作数也被转换成 64
位的类型,这样在对表达式进行赋值时就不需要再进行转换了。另外一种解决方案是对整个表达式进行转换,这样就可以在赋值时进行符号扩展。例如,考虑下面这
种用法可能会出现的问题:
long n; int i = -2; unsigned k = 1; n = i + k;
从数学计算上来说,上面这个黑体显示的表达式的结果应该是 -1 。但是由于表达式是无符号的,因此不会进行符号扩展。解决方案是将一个操作数转换成 64 位类型(下面的第一行就是这样),或者对整个表达式进行转换(下面第二行):n = (long) i + k; n = (int) (i + k);
long x = -1L; |
1L << ((sizeof(long) * 8) - 1); |
低地址 | 高地址 | |||||||
---|---|---|---|---|---|---|---|---|
Little endian | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 | Byte 7 |
Big endian | Byte 7 | Byte 6 | Byte 5 | Byte 4 | Byte 3 | Byte 2 | Byte 1 | Byte 0 |
表 4. 0x12345678 在 big-endian 系统上的布局
内存偏移量 | 0 | 1 | 2 | 3 |
内存内容 | 0x12 | 0x34 | 0x56 | 0x78 |
表 5. 0x12345678 在 big-endian 系统上当作两个半字来看待的情况
内存偏移量 | 0 | 2 |
内存内容 | 0x1234 | 0x5678 |
表 6. 0x12345678 在 little-endian 系统上的布局
内存偏移量 | 0 | 1 | 2 | 3 |
内存内容 | 0x78 | 0x56 | 0x34 | 0x12 |
表 7. 0x12345678 在 little-endian 系统上作为两个半字看到的情况
内存偏移量 | 0 | 2 |
内存内容 | 0x3412 | 0x7856 |
清单 2. big endian 与 little endian
#include <stdio.h> main () { int i = 0x12345678; if (*(char *)&i == 0x12) printf ("Big endian\n"); else if (*(char *)&i == 0x78) printf ("Little endian\n"); } |
Endianism 在以下情况中非常重要:
- 使用位掩码时
- 对象的间接指针地址部分
在 C 和 C++ 中有位域来帮助处理 endian 的问题。我建议使用位域,而不要使用掩码域或 16 进制的常量。有几个函数可以用来将 16 位和 32 位数据从 “主机字节顺序” 转换成 “网络字节顺序”。例如,htonl (3)
、ntohl (3)
用来转换 32 位整数。类似地,htons (3)
、ntohs (3)
用来转换 16 位整数。然而,对于 64 位整数来说,并没有标准的函数集。但是在 big endian 和 little endian 系统上,Linux 都提供了下面的几个宏:
- bswap_16
- bswap_32
- bswap_64
ptrdiff_t
:
这是一个有符号整型,是两个指针相减后的结果。
size_t
:
这是一个无符号整型,是执行sizeof
操作的结果。这在向一些函数(例如malloc (3)
)传递参数时使用,也可以从一些函数(比如fred (2)
)中返回。
int32_t
、uint32_t
等:
定义具有预定义宽度的整型。
intptr_t
和uintptr_t
:
定义整型类型,任何有效指针都可以转换成这个类型。
bufferSize
进行赋值时,从 sizeof
返回的 64 位值被截断成了 32 位。 int bufferSize = (int) sizeof (something);
size_t
对返回值进行类型转换,并将其赋给声明为 size_t
类型的 bufferSize,如下所示: size_t bufferSize = (size_t) sizeof (something);
intptr_t
和 uintptr_t
。 a
的最大值可以是 31。这是因为 1 << a
是 int 类型的。 long t = 1 << a;
1L
,如下所示: long t = 1L << a;
printf (3)
及其相关函数都可能成为问题的根源。例如,在 32 位系统上,使用 %d
来打印 int 或 long 类型的值都可以,但是在 64 位平台上,这会导致将 long 类型的值截断成低 32 位的值。对于 long 类型的变量来说,正确的用法是 %ld
。 printf (3)
时,它会扩展成 64 位的,符号会适当地进行扩展。在下面的例子中,printf (3)
假设指针是 32 位的。 char *ptr = &something;
printf (%x\n", ptr);
%p
,如下所示;这在 32 位和 64 位系统上都可以很好地工作: char *ptr = &something;
printf (%p\n", ptr);
在向函数传递参数时需要记住几件事情:
- 在参数的数据类型是由函数原型定义的情况中,参数应该根据标准规则转换成这种类型。
- 在参数类型没有指定的情况中,参数会被转换成更大的类型。
- 在 64 位系统上,整型被转换成 64 位的整型值,单精度的浮点类型被转换成双精度的浮点类型。
- 如果返回值没有指定,那么函数的缺省返回值是 int 类型的。
清单 3. 将有符号整型和无符号整型的和作为 long 类型传递
long function (long l); int main () { int i = -2; unsigned k = 1U; long n = function (i + k); } |
(i + k)
是一个无符号的 32 位表达式,在将其转换成 long 类型时,符号并没有得到扩展。解决方案是将一个操作数强制转换成 64 位的类型。 float f = 1.25;
printf ("The hex value of %f is %x", f, f);
printf ("The hex value of %f is %x", f, *(int *)&f);