什么时候整数<->指针强制转换正确?


77

民间传说说:

  • 类型系统存在是有原因的。整数和指针是不同的类型,在大多数情况下,整数和指针之间的转换是不当行为,可能表示设计错误,应避免使用。

  • 即使执行了这样的强制转换,也不应假设整数和指针的大小(强制void*转换int为使代码在x64上失败的最简单方法),而不int应该使用intptr_tuintptr_tfrom stdint.h

知道这一点,什么时候执行这样的转换真正有用

(注意:对于可移植性的价格,较短的代码并不算作“实际有用”。)


我知道一种情况:

  • 一些无锁的多处理器算法利用了一个事实,即2个字节以上的指定指针具有一定的冗余性。例如,它们然后将指针的最低位用作布尔标志。对于具有适当指令集的处理器,这可以消除对锁定机制的需要(如果指针和布尔标志是分开的,则这是必要的)。
    (注意:这种做法甚至可以通过java.util.concurrent.atomic.AtomicMarkableReference在Java中安全地进行)

更多的东西?


5
指针与intptr_t实现之间的映射是定义的实现,因此,除非我确切知道它将在哪个编译器上运行,否则我也不会使用无锁算法。
安德烈亚斯·布林克

6
每个无锁算法都至少利用某些实现特定的属性...
PlasmaHH 2011年

3
@PlasmaHH:好点。C(以及C ++ 11之前的C ++)没有任何多线程程序或共享程序内存的概念。因此,如果您对无锁算法有任何用途,您已经在依赖于实现的特定属性,但是请记住这一点,因为很容易忘记,在这里执行“正常”操作不需要实现。
凯文·卡斯卡特

1
实际上,uintptr_t是在<stdint.h><cstdint>C ++ 0x中。如果您是从那里获得的,Visual C ++ 2008是错误的。
2011年

我不使用Visual C ++,这对我来说是一个明显的错误,谢谢!:)
Kos

Answers:


38

当它们某种程度上需要作为哈希值的一部分时,我有时会将指针转换为整数。我也将它们转换为整数,以在某些实现上对它们进行一些位纠缠,其中保证了指针始终剩余一或两个备用位,在这里我可以在左/右指针中编码AVL或RB树信息,而无需另外添加会员。但这一切都是针对实现的,因此我建议不要将其视为任何常见的解决方案。我也听说有时候危险指针可以用这样的东西实现。

在某些情况下,我需要将每个对象的唯一ID传递给例如服务器作为我的请求ID。根据需要何时保存一些内存(这是值得的)的上下文,我将对象的地址用作此类id,通常必须将其转换为整数。

在嵌入式系统上工作时(例如在佳能相机中,请参阅chdk),通常会出现魔咒,因此(void*)0xFFBC5235也经常在其中找到a或类似名称

编辑:

偶然发现(在我的脑海中),pthread_self()它返回一个pthread_t,通常是一个无符号整数的typedef。虽然在内部它是指向某个线程结构的指针,但它表示所讨论的线程。通常,它可能在其他地方用于不透明的句柄。


1
相反铸造指针的以整数哈希计算,你应该不是简单地看他们的代表(作为unsigned char [sizeof(T *)]),以散...
R.,GitHub上停止帮助ICE

1
正如OP所指出的那样,指针值通常具有冗余,因为低位为0。将它们移开,然后乘以1000000007,通常会产生令人惊讶的分布良好的哈希,这足以满足我的一些需求。应用程序。另外,我不喜欢盲目地将位和位加在一起以形成哈希,但一点也不认为可以在无需火箭科学努力的情况下找到更快的特定于域的哈希。
PlasmaHH 2011年

4
+1很高兴看到您了解自己正在做的事情的危险,并建议其他人不要这样做:-)我很震惊,这是在SO上被接受的,并且没有得到很多“不要进行微优化”的评论。
phkahler 2011年

5
当人们编写我的库为我优化时,我会喜欢它。是我浪费时间进行微优化的时候,
这才

15

通常,在检查类型的对齐方式时可能很有用,以使未对齐的内存被断言而不是SIGBUS / SIGSEGV捕获。

例如:

#include <xmmintrin.h>
#include <assert.h>
#include <stdint.h>

int main() {
  void *ptr = malloc(sizeof(__m128));
  assert(!((intptr_t)ptr) % __alignof__(__m128));
  return 0;
}

(在真实代码中,我不仅会赌博malloc,而且还会说明问题所在)


12

使用一半空间存储双向链表

异或链表结合next和prev指针成相同大小的单个值。它通过将两个指针异或在一起来实现此目的,这需要将它们视为整数。


1
忘记了;)内存关键型嵌入式解决方案的
绝妙

除了仅给出指向该节点的指针无法轻易将其从列表中删除的事实。
Maxim Egorushkin 2011年

1
是的,您通常需要知道两个相邻节点才能遍历或修改列表。您为了方便而交易空间。在链接的文章中进行了介绍。
Craig Gidney

8

在我看来,最有用的情况是实际上有可能使程序更高效的情况:许多标准库和公共库接口都采用单个void *参数,它们将传递回某种回调函数。假设您的回调不需要任何大量数据,只需一个整数参数。

如果回调将在函数返回之前发生,则只需传递局部(自动)int变量的地址即可,一切都很好。但是,针对这种情况的最佳现实示例是pthread_create,其中“回调”并行运行,并且您无法保证它能够在pthread_create返回之前通过指针读取参数。在这种情况下,您有3个选择:

  1. malloc一个int并读取新线程并将free其读取。
  2. 将指针传递给包含int和对象和同步对象(例如,信号量或屏障)的调用方本地结构,并在调用后让调用方对其进行等待pthread_create
  3. intto强制void *传递并按值传递。

选项3比其他任何一个选项的效率都要高,这两个选项都涉及一个额外的同步步骤(对于选项1,同步在malloc/中free,并且几乎可以肯定会涉及一些成本,因为分配和释放线程是不同的) 。


2
并且认为通过设计这些功能可以使其100%安全,因此请使用union {int i; void* p;}而不是void*
科斯

2
更安全,但使用起来更烦人。C99之前的版本(即没有复合文字),传递了一个union使温度临时变量变大的必需项。POSIX实时信号接口使用此方法(union sigval),每个人都讨厌它...
R .. GitHub停止帮助ICE,

8

Windows中就是一个示例,例如SendMessage()PostMessage()功能。它们采用HWnd(窗口的句柄),消息(整数类型)和消息的两个参数aWPARAM和an LPARAM。两种参数类型都是不可或缺的,但是有时您必须根据发送的消息传递指针。然后,您将必须将指针转换为LPARAMWPARAM

我通常会像瘟疫一样避免这种情况。如果需要存储指针,请使用指针类型(如果可能)。


1
那并不是真正的用途,那只是因为它们是遗留代码并且这种设计很普遍。在更现代的系统中,您只需提供多个回调。
小狗

我不使用WinAPI,所以我不知道有人这样做。您知道WinAPI是否保证LPARAM和WPARAM足够大以能够包含指针?
科斯,

从概念上讲,LPARAM它不是整数类型,而是LONG_PTR-指针和整数类型的并集。但这确实有点黑客。@DeadMG:您可以,在SendMessage侧面。但是问题仍然存在GetMessage。您无法超载,因为您无法预测会收到什么消息。
MSalters 2011年

@MSalters:今天,它可能是LONG_PTR,几年前,它仍然是整数类型(UINT或DWORD,IIRC)。您仍然必须使用它们来传递指针。// @ DeadMG:是你当演员。
Rudy Velthuis

1
@Kos:是的,它们一定足够大。否则,Windows将因人们无法发送带有指针值的消息而严重受阻。Windows几乎对所有GUI东西都使用消息。
Rudy Velthuis

6

在嵌入式系统中,访问内存映射的硬件设备非常普遍,这些设备的寄存器位于内存映射中的固定地址。我经常在C和C ++中对硬件进行建模(使用C ++可以利用类和模板),但是通常的思想可以同时用于两者。

一个简单的例子:假设您在硬件中有一个定时器外设,并且它有2个32位寄存器:

  • 一个自由运行的“滴答计数”寄存器,该寄存器以固定速率(例如,每微秒)递减

  • 一个控制寄存器,它允许您启动计时器,停止计时器,在我们将计数减为零时启用计时器中断等。

(请注意,真正的定时器外设通常要复杂得多)。

这些寄存器中的每一个都是32位值,定时器外设的“基地址”为0xFFFF.0000。您可以对硬件进行如下建模:

// Treat these HW regs as volatile
typedef uint32_t volatile hw_reg;

// C friendly, hence the typedef
typedef struct
{
  hw_reg TimerCount;
  hw_reg TimerControl;
} TIMER;

// Cast the integer 0xFFFF0000 as being the base address of a timer peripheral.
#define Timer1 ((TIMER *)0xFFFF0000)

// Read the current timer tick value.
// e.g. read the 32-bit value @ 0xFFFF.0000
uint32_t CurrentTicks = Timer1->TimerCount;

// Stop / reset the timer.
// e.g. write the value 0 to the 32-bit location @ 0xFFFF.0004
Timer1->TimerControl = 0;

这种方法有100种变体,其优缺点可以一再争论不休,但这仅是为了说明将整数转换为指针的常见用法。请注意,此代码不是可移植的,是绑定到特定设备的,并且假定内存区域没有超出限制,等等。


是的,从常量初始化指针是一个很好的例子,并且在嵌入式中很常见。整数->指针是两次转换中比较普遍的一种,我会说:)
Kos

3

除非您完全了解编译器+平台组合的行为,并希望利用它(您的问题场景就是这样的示例),否则执行此类强制转换永远不会有用。

我说它永远不会有用的原因是,通常来说,您没有对编译器的控制权,也没有完全了解它可能选择进行的优化。换句话说,您无法精确控制它将生成的机器代码。因此,通常来说,您不能安全地实施这种技巧。


1
您不能以可移植的方式实现它,但是如果您了解详细信息,那么在特定的体系结构/编译器上当然可以安全地实现它。
phkahler 2011年

您不需要有关编译器优化的百科全书知识。如果您想证明对类型转换的使用正确,则只需要知道一些不变式即可。例如,在所有广泛使用的malloc实现中,(uintptr_t) malloc(n) % 4 == 0当n> 2时,这将非常有用,您可以用它来做一些有趣的事情,并且在假定不变的平台上,您的代码将是正确且安全的。
杰森·奥伦多夫

3
我认为C99可以保证很多事情,例如:如果将指针转换为uintptr_t,然后再将相同的指针转换为uintptr_t,则得到的整数值是相同的。这样的转换足以用于计算哈希码。一点不变性会走很长的路。
詹森·奥伦多夫

1
@JasonOrendorff:C99不能保证。它保证了pointer-> uintptr_t-> pointer往返将产生一个与原始指针比较的指针,但是在符合标准的实现(例如48位指针和64位uintptr_t)上,类似的东西uintptr_t asUint = (uintptr_t)somePtr;可能只是写了48位其余16位保留任意值。
超级猫

2

我将a强制转换pointer为an的唯一时间integer是我想存储一个指针,但是我唯一可用的存储是整数。


3
为什么要这么做呢?在什么情况下有用?我只是将存储更改为指针。
R. Martinho Fernandes

也许像在旧的C回调系统中只有一个void *可用的地方,可能有只有size_t可用的回调系统...
PlasmaHH 2011年

size_t总是足够大吗?
Flexo

2
@R。Martinho Fernandes:当不是我的代码时。全部Components都有一个Tag属性,它是一个整数。如果我想将对象/结构/字符串/指针与关联Component,则可以通过Tag属性来实现。
伊恩·博伊德

3
示例:在“经典” MacOS中,许多结构WindowRecord都具有一个4字节的userInfo字段,可用于存储所需的任何信息,并且通常用于存储指向辅助结构的指针。在这种情况下,您必须将指针转换为int或long(我不记得是哪个),然后再次返回以使编译器满意。
Caleb,

2

什么时候将指针存储为int是正确的?当您将其视为真实时,它是正确的:使用平台或编译器特定的行为。

仅当您在整个应用程序中散布了特定于平台/编译器的代码并且必须将代码移植到另一个平台时才出现问题,因为您所做的假设不再成立。通过隔离该代码并将其隐藏在不对基础平台进行任何假设的接口后面,可以消除此问题。

因此,只要您记录了实现,就可以使用句柄或不依赖于幕后工作方式的东西将其隔离在平台无关的界面后面,然后仅在经过测试和测试的平台/编译器上有条件地编译代码。起作用,那么就没有理由不使用遇到的任何伏都教魔法了。如果需要,甚至可以包括大量汇编语言,专有API调用和内核系统调用。

就是说,如果您的“便携式”接口使用整数句柄,则整数与某个平台上实现的指针大小相同,并且该实现在内部使用指针,为什么不简单地将指针用作整数句柄呢?在这种情况下,将简单的类型转换为整数是有意义的,因为您无需使用某种类型的句柄/指针查找表。


1

您可能需要使用固定的已知地址访问内存,然后您的地址是整数,并且需要将其分配给指针。这在嵌入式系统中很常见。相反,您可能需要打印一个内存地址,因此需要将其转换为整数。

哦,别忘了您需要分配和比较指向NULL的指针,该指针通常为0L


那好吧。在编写打印指针的库例程时。h!
deStrangis 2011年

在C ++中,0是空指针文字,并且不涉及任何强制转换。实际上,空指针的位模式甚至不必与值为0的大小相同的整数相同……
PlasmaHH 2011年

是的,很好,但是C并非如此(请注意,我通常使用该词)。如果您涉及固定的固定内存地址-当您需要对指针强制转换进行整数处理时,我会想到这种情况-我会说您比C ++更可能使用C。
deStrangis 2011年

无论如何,在C ++中强制转换比在C中使用少得多,因为在C ++中强制转换是必需的。
deStrangis 2011年

1

我在对象的网络范围ID中有这样一种用途。这样的ID将结合机器的标识(例如IP地址),进程ID和对象的地址。要通过套接字发送,此类ID的指针部分必须放入足够宽的整数,这样它才能在来回传输中幸免。指针部分仅在有意义的上下文中(同一台机器,同一进程),在其他机器上或在其他进程中被解释为指针(=强制返回指针),仅用于区分不同的对象。

工作需要做的事情是存在uintptr_tuint64_t作为固定宽度的整数类型。(只能在最多具有64个地址的机器上使用:)


1

在x64下,on可以使用指针的高位进行标记(因为实际的指针仅使用47位)。这对于运行时代码生成(例如,LuaJIT使用此技术,根据评论是一种古老的技术)之类的东西非常有用,执行此标记和标记检查您是否需要强制转换或a union,基本上等于同一件事。

在使用binning的内存管理系统中,将整数的指针转换也非常有用,例如:一个人可以通过一些数学运算轻松找到地址的bin /页面,这是我写过一段时间的无锁分配器的一个示例背部:

inline Page* GetPage(void* pMemory)
{
    return &pPages[((UINT_PTR)pMemory - (UINT_PTR)pReserve) >> nPageShift];
}

1
嘿。Mike Pall没有发明这项技术。我相信它可以追溯到Lisp的早期实现。
詹森·奥伦多夫

4
AMD特别警告不要这样做,因为当地址空间扩展时,它会严重损坏。就像在68000上一样,地址空间从24位扩展到32位。
Bo Persson

就像杰森所说的那样,这种技术确实很古老,并且已经在无数的语言运行时中使用。
斯蒂芬·佳能

@Bo:有链接吗?好奇它可能还包含什么。杰森:更新以反映您的评论:)
Necrolis

1
@Eonil:很明显,如果你打算做指针标记或进行内存管理系统,你需要知道你的底层架构,我的回答是主要集中在x86和x86_64的下,所有的地址空间是线性的,所以它保证:)
Necrolis

0

当我试图逐字节遍历数组时,我曾使用过这样的系统。通常,指针一次会遍历多个字节,这会导致很难诊断的问题。

例如,int指针:

int* my_pointer;

移动my_pointer++将导致前进4个字节(在标准32位系统中)。但是,移动((int)my_pointer)++会将其前进一个字节。

除了将指针投射到(char *)之外,这实际上是完成此操作的唯一方法。((char*)my_pointer)++

诚然,(char *)是我常用的方法,因为它更有意义。


(char*)是唯一可以保证定义明确的方法。
GManNickG 2011年

0

指针值还可以作为有用的熵源,以作为随机数生成器的种子:

int* p = new int();
seed(intptr_t(p) ^ *p);
delete p;

boost UUID库使用此技巧以及其他一些技巧。


不保证在后续运行中new int()(顺便说一句,无需初始化)会产生不同的值。熵有明确定义的来源,例如/dev/random
Maxim Egorushkin 2011年

0

将对象的指针用作无类型句柄是一种古老而良好的传统。例如,有人使用它使用平面C样式API在两个C ++单元之间实现交互。在那种情况下,句柄类型被定义为整数类型之一,任何方法都必须先将指针转换为整数,然后才能将其转换为另一个将抽象的无类型句柄作为其参数之一的方法。另外,有时没有其他方法可以打破循环依赖。


我无法想象这种情况……您能否提供代码示例?
科斯

无法想象,因为它不是抽象情况。这是一个非常具体的案例,很难用简短的示例来说明。一般规则是:如果可以在没有无类型句柄的情况下实现交互,请不要使用它们。但是有一天,您可能会面临一个事实,那就是别无选择。在这种情况下,请毫无疑问地使用它。这是合法的方法,如果您在从整数取消广播指针之后在运行时检查对象的类型(例如,使用get_type_id()方法)。
谢尔盖·沙莫夫

他们经常使用指针和整数的并集。见struct epoll_dataepoll_ctl例如。
Maxim Egorushkin 2011年

工会在这种情况下很有用。就像强制转换一样,但是目标类型集受到限制。
谢尔盖·沙莫夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.