NULL + int的结果是什么?

问题描述 投票:19回答:4

我已经看到以下宏在OpenGL VBO实现中使用:

#define BUFFER_OFFSET(i) ((char *)NULL + (i))
//...
glNormalPointer(GL_FLOAT, 32, BUFFER_OFFSET(x));

您能否提供有关此宏如何工作的一些详细信息?可以用功能代替吗?更确切地说,增加NULL指针的结果是什么?

c opengl pointers null pointer-arithmetic
4个回答
32
投票

让我们回顾一下OpenGL的肮脏历史。从前,有OpenGL 1.0。您使用glBeginglEnd进行绘制,仅此而已。如果要快速绘制,可以将内容粘贴在显示列表中。

然后,有人想到了一个聪明的主意,那就是可以只提取对象数组来进行渲染。从而诞生了OpenGL 1.1,为我们带来了glVertexPointer之类的功能。您可能会注意到,该函数以单词“ Pointer”结尾。这是因为它需要指向实际内存的指针,当调用glDraw*函数集之一时将访问该内存。

快进几年。现在,图形卡具有自行执行顶点T&L的能力(直到现在,固定功能的T&L由CPU完成)。最有效的方法是将顶点数据放入GPU内存中,但显示列表并不理想。这些都太隐蔽了,没有办法知道您是否可以通过它们获得良好的性能。输入缓冲区对象。

但是,由于ARB拥有绝对的政策,即使所有内容都尽可能向后兼容(无论API看起来多么愚蠢),他们决定实现此功能的最佳方法是再次使用相同的功能。只是现在,有一个全局开关将glVertexPointer的行为从“获取指针”更改为“从缓冲区对象获取字节偏移”。该开关是是否将缓冲区对象绑定到GL_ARRAY_BUFFER

当然,就C / C ++而言,该函数仍采用pointer。而且C / C ++的规则不允许您传递整数作为指针。不是没有演员。这就是为什么存在BUFFER_OBJECT之类的宏的原因。这是将整数字节偏移量转换为指针的一种方法。

(char *)NULL部分仅使用NULL指针(通常为void*)并将其转换为char*+ i只是对char*进行指针运算。因为NULL通常是零值,所以向其添加i将使字节偏移量增加i,从而生成一个指针,该指针的值就是您传入的字节偏移量。

当然,C ++规范将BUFFER_OBJECT的结果列为未定义的行为。通过使用它,您实际上是在依靠编译器来做一些合理的事情。毕竟,NULLhave为零;规范只说这是一个实现定义的空指针常量。它根本不必具有零值。在大多数实际系统上,它会。但这不是必须

这就是为什么我只使用演员表。

glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (void*)48);

两种方式都不能保证行为(有条件地支持int-> ptr-> int转换,不是必需的)。但是它比键入“ BUFFER_OFFSET”还短。 GCC和Visual Studio似乎认为合理。而且它不依赖于NULL宏的值。

就我个人而言,如果我更喜欢C ++,则应该在其上使用reinterpret_cast<void*>。但是我不是。

或者您可以放弃旧的API和use glVertexAttribFormat et. al.,这在各个方面都更好。


27
投票
glVertexAttribFormat

从技术上讲,此操作的结果是undefined,并且宏实际上是错误的。让我解释一下:

C定义(并且C ++紧随其后),指针可以转换为整数,即类型为#define BUFFER_OFFSET(i) ((char *)NULL + (i)) ,并且如果以这种方式获得的整数被转换回其原始指针类型,则将产生原始值指针。

然后是指针算法,这意味着如果我有两个指针指向相同的对象,那么我可以利用它们的差,得出一个整数(uintptr_t类型),并将该整数加或减到任一原始整数上指针,将产生另一个。还定义了,通过将1加到一个指针,可以得出指向索引对象下一个元素的指针。同样,两个ptrdiff_t的差除以同一对象的指针的uintptr_t,必须等于它们自己减去的指针。最后但并非最不重要的一点是,sizeof(type pointed to)值可以是任何值。它们也可以是不透明的手柄。不需要将它们作为地址(尽管大多数实现都是这样做,因为它很有意义)。

现在我们可以查看臭名昭著的空指针。 C将类型uintptr_t值0强制转换为的指针定义为无效指针。 请注意,它始终为0在您的源代码中。在后端,在编译程序中,用于实际将其表示给计算机的二进制值可能完全不同!通常不是,但可能是。 C ++相同,但是C ++不允许比C进行更多的隐式转换,因此必须将0显式转换为uintptr_u。同样是因为空指针没有引用对象,因此没有取消引用的大小空指针的算术未定义。指向任何对象的空指针也意味着,没有定义可合理地将其强制转换为类型化指针。

因此,如果这都是未定义的,为什么这个宏毕竟起作用?因为大多数实现(意味着编译器)都是非常容易受骗的,并且编译器编码器在很大程度上是懒惰的。在大多数实现中,指针的整数值只是后端本身的指针本身的值。因此,空指针实际上为0。尽管未检查空指针的指针算术,但即使分配了某种类型的指针,即使没有任何意义,大多数编译器也会静默接受它。如果您要说的话,void*是C的“单位大小”类型。因此,cast上的指针算术就像后端地址上的人工算术一样。

长话短说,尝试使用指针魔术来达到预期的结果是C语言方面的偏移,这根本没有用。

让我们退后一会儿,记住我们实际上正在尝试做的事情:最初的问题是,char函数将指针作为其数据参数,但是对于顶点缓冲对象,我们实际上想指定一个基于字节的偏移量到我们的数据中,它是一个数字。对于C编译器,该函数需要一个指针(如我们所知,这是不透明的)。正确的解决方案应该是引入新功能,尤其是与VBO配合使用(例如gl…Pointer –我想同他们一起介绍)。而是由OpenGL定义的是利用编译器的工作方式。大多数编译器将指针及其整数等效项实现为相同的二进制表示形式。因此,我们要做的是,使编译器使用我们的数字而不是指针来调用那些gl…Offset函数。

因此,从技术上讲,我们唯一需要做的就是告诉编译器“是的,我知道您认为此变量gl…Pointer是整数,并且您是对的,并且函数a只需要一个glVertexPointer就可以了。数据参数。但是,您猜怎么着:该整数是通过将void*“转换为void*然后握住大拇指产生的,实际上编译器是如此愚蠢,无法将整数值传递给(void*)

因此,这全部归结为以某种方式规避了旧功能签名。强制转换指针是恕我直言的脏方法。我会做些不同:我会弄乱函数签名:

glVertexPointer

现在您可以使用typedef void (*TFPTR_VertexOffset)(GLint, GLenum, GLsizei, uintptr_t); TFPTR_VertexOffset myglVertexOffset = (TFPTR_VertexOffset)glVertexPointer; 而无需执行任何愚蠢的转换,并且offset参数将被传递给函数,而不会造成编译器可能对其造成混乱的任何危险。这也是我在程序中使用的方法。


3
投票

不是“ NULL + int”,这是“将NULL转换为'指向char的指针'的类型”,然后将该指针加i。

是的,可以用一个函数代替-但是,如果您不知道它的作用,那么为什么还要关心它?首先了解它的作用,然后[[然后考虑它作为函数是否会更好。


2
投票
openGL顶点属性数据通过与内存中的指针或取决于上下文的位于顶点缓冲区对象内的偏移量相同的函数(glVertexAttribPointer)进行分配。

BUFFER_OFFSET()宏似乎是将整数字节的偏移量转换为指针,只是允许编译器安全地将其作为指针参数传递。“(char *)NULL + i”通过指针算术表示此转换;假设sizeof(char)== 1,结果应为相同的位模式,否则,该宏将失败。

也可以通过简单的重铸来实现,但是宏可以使它在样式上更加清晰地传递了什么内容;它也将是捕获溢出的便利位置,以实现32/64位安全性/未来的安全性

myglVertexOffset

© www.soinside.com 2019 - 2024. All rights reserved.