在C语言中将一个由4个元素组成的字符数组复制到一个整数中

9
一个char占用1字节,一个int占用4个字节。我想将char[4]中的每个字节逐字节复制到int中。我考虑了不同的方法,但得到了不同的答案。
char str[4]="abc";
unsigned int a = *(unsigned int*)str;
unsigned int b = str[0]<<24 | str[1]<<16 | str[2]<<8 | str[3];
unsigned int c;
memcpy(&c, str, 4);
printf("%u %u %u\n", a, b, c);

输出结果为6513249 1633837824 6513249。

哪一个是正确的?出了什么问题?


第一种方法类似于执行“union”,正如下面的答案所说,它依赖于处理器的字节序。 - Iddillian
5
使用 printf("%08X %08X %08X\n", a, b, c);,注意相同的字节都在,但顺序不同。 - chux - Reinstate Monica
6个回答

15

这是一个关于字节序的问题。当您将char*解释为int*时,字符串的第一个字节成为整数的最低有效字节(因为您在x86上运行此代码,它是小端序),而手动转换则使第一个字节成为最高有效字节。

为了更好地理解,以下是源数组:

   a      b      c      \0
+------+------+------+------+
| 0x61 | 0x62 | 0x63 | 0x00 |  <---- bytes in memory
+------+------+------+------+

当这些字节在小端架构中被解释为整数时,结果是0x00636261,即十进制6513249。另一方面,手动放置每个字节会产生0x61626300 -- 十进制1633837824。
当然,将char*视为int*是未定义的行为,因此实际上不允许使用第一种转换,因此差异并不重要。但是,有一种方法可以实现相同的结果,称为type punning
union {
    char str[4];
    unsigned int ui;
} u;

strcpy(u.str, "abc");
printf("%u\n", u.ui);

谢谢。图片非常清晰明了。我想要的答案是手动放置字节的那个。 顺便说一下,您在数组图片中打错了——0x64应该是0x63。 - avmohan

6

以上两种方法都不正确。

第一种方法违反了别名规则,可能因为str的地址没有正确对齐到unsigned int而导致失败。要将字符串的字节重新解释为主机系统字节顺序的unsigned int,您可以使用memcpy进行复制:

unsigned int a; memcpy(&a, &str, sizeof a);

(假设无符号整数的大小和str的大小相同。)第二个可能会因为整数溢出而失败,因为str [0] 提升为int类型,所以 str [0]<<24 的类型是int,但移位需要的值可能大于int类型可以表示的范围。要解决这个问题,请使用:
unsigned int b = (unsigned int) str[0] << 24 | …;

第二种方法会按照大端序解释来自str的字节,而不管主机系统中unsigned int的字节顺序。


1
unsigned int a = *(unsigned int*)str;

这个初始化不正确,会导致未定义的行为。它违反了C语言别名规则,可能会违反处理器对齐。


1
你说你想逐字节复制。这意味着行unsigned int a = *(unsigned int*)str;不被允许。但是,当你以不同的类型(例如,从磁盘读取流时)读取数组时,你所做的是一种相当常见的方式。只需要稍作调整即可:
 char * str ="abc";
int i;
unsigned a;
char * c = (char * )&a;
for(i = 0; i < sizeof(unsigned); i++){
   c[i] = str[i];
}
printf("%d\n", a);

请记住,您正在阅读的数据可能与您正在读取的计算机具有不同的字节序。这可能会有所帮助:
void 
changeEndian32(void * data)
{
    uint8_t * cp = (uint8_t *) data;
    union 
    {
        uint32_t word;
        uint8_t bytes[4];
    }temp;

    temp.bytes[0] = cp[3];
    temp.bytes[1] = cp[2];
    temp.bytes[2] = cp[1];
    temp.bytes[3] = cp[0];
    *((uint32_t *)data) = temp.word;
}

对于联合成员,如果将某些内容存储为一种类型并提取为另一种类型,则结果取决于实现。 - David Ranieri
@AlterMann - 我不知道这个。我很想了解更多。你有参考资料吗?我的C语言几乎总是“实现相关的”,所以很高兴有这些事情被指出。 - user2363448

1

两种方法都是正确的:

  • 你的第一种解决方案按本机字节顺序(即CPU使用的字节顺序)复制,因此可能会根据CPU类型而产生不同的结果。

  • 你的第二种解决方案无论CPU使用什么字节顺序,都会以大端字节顺序(即最高有效字节在最低地址)进行复制。它将在所有类型的CPU上产生相同的值。

哪种方法是正确的取决于如何解释原始数据(char数组)。
例如,Java代码(class文件)始终使用大端字节顺序(无论CPU使用什么),因此如果要从Java类文件中读取ints,则必须使用第二种方式。在其他情况下,你可能希望使用依赖于CPU的方式(我认为Matlab将ints以本机字节顺序写入文件,参见this question)。


第一和第二种都可能导致崩溃。这应该在任何答案中提到。两者都不正确。 - Eric Postpischil
@Eric Postpischil:第一种方式:对齐是一个完全不同的问题,与OP最初的问题无关。在很多情况下(即在许多硬件平台上),对齐根本不重要,像这样的代码完全没有问题。第二种方式:这绝对不会在任何情况下导致崩溃(无论int是否足够大来移位24位的值)。 - Curd
对齐确实很重要,与 OP 的原始问题有关:将 char 数组别名为 int 不能保证符合对齐要求,并且在某些 C 实现中可能会崩溃。它在许多平台上不崩溃的事实并不意味着它是可以接受的,因为它并没有抹去它在某些平台上崩溃的事实。 - Eric Postpischil
第二种方式在 str[0] << 24 可能会溢出。因为 str[0] 是一个 char 类型,它被转换为 int 类型(除了一些奇怪的 C 实现中 int 类型不比 char 更宽)。这是一个带符号的整数。然后将其向左移动 24 位可能超过了 int 的范围。例如,如果 str[0] 是 128,则 str[0] << 24 将是 2147483648,但由于 32 位有符号 int 可以表示的最大值是 2147483647,因此程序可能会崩溃或产生错误的结果。在 C 标准中,有符号整数的溢出行为未定义。 - Eric Postpischil

0
如果您使用CVI(National Instruments)编译器,可以使用Scan函数来实现此功能: unsigned int a;
对于大端字节序: Scan(str,“%1i[b4uzi1o3210]>%i”,&a);
对于小端字节序: Scan(str,“%1i[b4uzi1o0123]>%i”,&a);
o修饰符指定字节顺序。 方括号内的i表示在str数组中开始的位置。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接