NULL + int的结果是什么?

时间:2011-11-27 04:49:12

标签: c opengl pointers null pointer-arithmetic

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

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

您能否详细介绍一下这个宏的工作原理?可以用功能替换吗? 更准确地说,递增NULL指针的结果是什么?

4 个答案:

答案 0 :(得分:31)

让我们回顾一下OpenGL的肮脏历史。曾几何时,有OpenGL 1.0。您使用glBeginglEnd进行绘图,这就是全部。如果你想快速绘图,你就会把东西放在显示列表中。

然后,有人有一个聪明的主意,就是可以只使用对象数组进行渲染。因此诞生了OpenGL 1.1,它为我们带来了glVertexPointer等功能。您可能会注意到此函数以“指针”一词结束。那是因为它需要指向实际内存的指针,当调用其中一个glDraw*函数时,将会访问它。

快进几年。现在,人们希望将顶点数据放在GPU内存中,但他们不想信任显示列表。那些太隐蔽了,没有办法知道你是否会获得良好的表现。输入缓冲区对象。

然而,因为ARB的绝对政策是尽可能使所有内容都向后兼容(无论它看起来多么愚蠢),他们认为实现这一目标的最佳方法是再次使用相同的功能。只有现在,还有一个全局开关,将glVertexPointer的行为从“取指针”​​更改为“从缓冲区对象中取一个字节偏移量”。该切换是指缓冲区对象是否绑定到GL_ARRAY_BUFFER

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

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

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

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

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

无论如何都是未定义的行为。但它也比输入“BUFFER_OFFSET”短。 GCC和Visual Studio似乎觉得合理。并且它不依赖于NULL宏的值。

就个人而言,如果我更痴迷于C ++,我会在其上使用reinterpret_cast<void*>。但我不是。

你能写一个glVertexAttribBuffer来取一个偏移而不是指针吗?绝对。但这并不是什么大不了的事。做一个演员。

答案 1 :(得分:25)

#define BUFFER_OFFSET(i) ((char *)NULL + (i))

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

C定义(和C ++跟随它),指针可以转换为整数,即类型uintptr_t,如果以这种方式获得的整数,将其转换回它来自的原始指针类型,产生原始指针。

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

现在我们可以看看臭名昭着的空指针。 C定义从类型uintptr_u值0转换为指针的指针作为无效指针。 请注意,源代码中的此值始终为0 。在后端,在编译的程序中,用于实际将其表示到机器的二进制值可能完全不同!通常它不是,但它可能是。 C ++是相同的,但是C ++不允许比C更多的隐式转换,因此必须明确地将0转换为void*。另外,因为空指针不引用对象,因此没有解除引用的大小指针算法未定义为空指针。引用无对象的空指针也意味着,没有定义可以将它明智地转换为类型指针。

所以如果这一切都是未定义的,为什么这个宏毕竟会起作用?因为大多数实现(意味着编译器)都非常容易上当,而编译器编码器在最高程度上是懒惰的。大多数实现中指针的整数值只是指针本身在后端的值。所以空指针实际上是0.虽然没有检查空指针上的指针算法,但是如果指针得到某种类型的指定,大多数编译器都会默默地接受它,即使它没有意义。 char是C的“单位大小”类型,如果你想这么说的话。因此,关于强制转换的指针算法就像后端地址上的artihmetic一样。

总而言之,尝试做指针魔术并将预期结果作为C语言方面的偏移是没有意义的,它只是不起作用。

让我们退一步,记住,我们实际上要做的是:最初的问题是,gl…Pointer函数将指针作为其数据参数,但对于顶点缓冲区对象,我们实际需要在我们的数据中指定基于字节的偏移量,这是一个数字。对于C编译器,该函数采用指针(我们学习的不透明的东西)。正确的解决方案是引入新功能,尤其是与VBO一起使用(比如gl…Offset - 我想我会去ralley进行介绍)。相反,OpenGL定义的是利用编译器的工作方式。大多数编译器将指针及其整数等价物实现为相同的二进制表示。所以我们要做的是,它使编译器用我们的数字而不是指针调用那些gl…Pointer函数。

从技术上讲,我们唯一需要做的就是告诉编译器“是的,我知道你认为这个变量a是一个整数,你是对的,而且函数glVertexPointer只需要一个void*为它的数据参数。但是猜猜:这个整数是从void*“产生的,通过将其转换为(void*)然后握住拇指,编译器实际上是如此愚蠢而无法通过glVertexPointer的整数值。

所以这一切都归结为以某种方式绕过旧的函数签名。转换指针是恕我直言的脏方法。我做的有点不同:我搞乱了函数签名:

typedef void (*TFPTR_VertexOffset)(GLint, GLenum, GLsizei, uintptr_t);
TFPTR_VertexOffset myglVertexOffset = (TFPTR_VertexOffset)glVertexPointer;

现在你可以使用myglVertexOffset而不做任何愚蠢的转换,并且offset参数将被传递给函数,没有任何危险,编译器可能会搞乱它。这也是我在程序中使用的方法。

答案 2 :(得分:3)

那不是“NULL + int”,这是“对'char'的类型'指针的NULL转换”,然后用i递增该指针。

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

答案 3 :(得分:2)

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

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

通过简单的重新铸造也是可能的,但宏可能会使风格更加清晰;它也是一个方便的地方,可以捕获32/64位安全/未来的溢出

struct MyVertex { float pos[3]; u8 color[4]; }
// general purpose Macro to find the byte offset of a structure member as an 'int'

#define OFFSET(TYPE, MEMBER) ( (int)&((TYPE*)0)->MEMBER)

// assuming a VBO holding an array of 'MyVertex', 
// specify that color data is held at an offset 12 bytes from the VBO start, for every vertex.
glVertexAttribPointer(
  colorIndex,4, GL_UNSIGNED_BYTE, GL_TRUE,
  sizeof(MyVertex), 
  (GLvoid*) OFFSET(MyVertex, color) // recast offset as pointer 
);