在C语言中将`void *`转换为`int`的问题

12

我正在尝试重新掌握我的C语言技能。我想在不同的线程中对一个序列求和,每个线程将返回部分序列的总和指针。然而,当我尝试将 void* 类型的值 local_sum 转换为 int 时,出现了问题。

我曾试图使用 sum += *(int*)local_sum; 进行转换,但是出现了段错误,程序退出时显示 Process finished with exit code 11

后来我发现,如果我使用 sum += (int)local_sum;,那么就没有问题了。但我无法说服自己:难道 local_sum 不应该是一个 void * 吗?为什么可以使用 (int)local_sum 来将其转换为 int

非常感谢您能够解答这个问题。

对每个进程返回值进行求和的部分如下:

int sum = 0;
for (int i = 0; i < NUM_THREADS; i ++) {
    void * local_sum;
    pthread_join(count_threads[i], (&local_sum));
    sum += (int)local_sum;
}

线程的作用在这里:

void * count_thr(void *arg) {
    int terminal = ARRAY_SIZE / NUM_THREADS;
    int sum = 0;
    for (int i = 0; i < terminal; i ++) {
        sum += *((int*)arg + i);
    }
    return (void*)sum;
}

12
将一个 int 传递给 pthreads 中的 void*,而不是指向它,一直以来都是一种肮脏的技巧。而且像所有肮脏的技巧一样,它们最终会失效。将整数转换为指针具有实现定义的行为(至少使用 uintptr_t)。解决方案不是使用肮脏的技巧,而是传递一个指向 malloc 分配的数据的指针。动态分配的开销执行时间与创建和关闭线程的成本相比微不足道。 - Lundin
请参考c89: Convert an int to void* and back和相关问题Is it safe to cast an int to void pointer and back to int again?,这些问题涉及将int类型转换为void指针并再次转换回int类型的安全性问题。 - Sander De Dycker
3个回答

10

您正在通过将 void * 地址设置为 int sum 的值来 return 它。 在这种情况下,该地址无效。 但是,如果您记住这一点,并通过将 void * 转换为 int 来获取 sum 的值,它将有效。

有时会使用 void * 以此方式来 return 值(例如 int)或指向某些东西的地址(例如 struct)。

为了说明这一点:

int a = 5;
void *p = (void *)a;
int b = (int)p;

apb 的值都为 5p 没有指向有效的地址。试图对 p 进行解引用将导致未定义行为:

b = *(int *)p; // Undefined Behavior!

考虑以下程序:

#include <limits.h>
#include <stdio.h>

int main(void)
{
    int a, b;
    void *p;

    a = 5;
    p = (void *)a;
    b = (int)p;

    printf("%d %p %d\n", a, p, b);

    a = INT_MAX;
    p = (void *)a + 1;
    b = (int)p;

    printf("%d %p %d\n", a, p, b);

    return 0;
}

当编译时,我会收到以下警告: $ gcc main.c -o main.exe main.c: 在函数‘main’中: main.c:9:9: 警告:将不同大小的整数强制转换为指针[-Wint-to-pointer-cast] p = (void *)a; ^ main.c:10:9: 警告:将指针强制转换为不同大小的整数[-Wpointer-to-int-cast] b = (int)p;
...
警告是由于 @Gerhardh 指出的 sizeof(int) 和 sizeof(void *) 可能是不同的。如果 void * 的值超过 int 所能存储的最大值,则可能会发生数据丢失。
输出: $ ./main.exe 5 0x5 5 2147483647 0x80000000 -2147483648

1
在我们的情况下,这种行为是期望的。真的吗?这不仅仅是有关转换的警告,而是涉及到不同的大小。那不是期望的部分。你可能有4字节整数和8字节指针。 - Gerhardh
2
此行为的定义取决于实现。 - Sander De Dycker
a、p 和 b 的值都为 5。a 和 b 的值不保证相同。 - David Brown
@DavidBrown 当然,这是未定义行为。在我的机器上,这种行为是这样的。 - Fiddling Bits
@FiddlingBits:更正:就像我之前所说的那样,它是“实现定义”。它可以在许多(如果不是大多数)平台上工作,就像你描述的那样,但不能保证。 - Sander De Dycker

8
由于local_sum是一个转换为void*int,而不是转换为void*int*,所以您无法执行*(int*)local_sum。它是一个数字重新解释为地址,但仅用于传输目的,因为pthread_exit只允许您返回void*而不是int,并且因为标准明确允许实现定义的转换(6.3.2.3p56.3.2.3p6)在整数和数字之间(如果值匹配,则进行转换;如果不匹配,则 UB)。例如,如果返回0x42,则地址0x42上极有可能没有任何内容,因此您应该忘记对其进行解引用,并尽快将其转换回整数,可以使用(int)local_sum;或者更好地使用(int)(intptr_t)local_sum;(虽然intptr_t不能保证存在),或者(最好的方式)使用(int)(intmax_t)local_sum;,以避免在LP64平台上可能出现的有关将其转换为不同大小的整数的编译器警告。

1

一种安全且可移植的解决方案可能是使用 union:

union void_cast {
    void* ptr;
    int value;
};

例如,您可以使用以下方式安全地重新解释void*指针:

int VOID_TO_INT(void* ptr) {
    union void_cast u;
    u.ptr = ptr;
    return u.value;
}

void* INT_TO_VOID(int value) {
    union void_cast u;
    u.value = value;
    return u.ptr;
}

所以你的代码可以更改为:

sum += VOID_TO_INT(local_sum);

2
强制类型转换应该不受字节顺序的影响。在一个(非常理论化的)大端机器上,其中 int 类型的宽度是指针宽度的一半,使用 union 的方法无法提取该值,而强制类型转换则可以。 - Petr Skocik

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