NULL + int的结果是什么?

Dan*_*tor 19 c opengl null pointers pointer-arithmetic

我在OpenGL VBO实现中看到了以下宏:

#define BUFFER_OFFSET(i) ((char *)NULL + (i))
//...
glNormalPointer(GL_FLOAT, 32, BUFFER_OFFSET(x));
Run Code Online (Sandbox Code Playgroud)

你能提供一下这个宏的工作原理吗?可以用功能替换吗?更确切地说,递增NULL指针的结果是什么?

Nic*_*las 32

让我们回顾一下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指针(通常是a void*)并将其转换为a char*.该+ i少了点上的指针运算char*.因为NULL通常是零值,所以添加i它会增加字节偏移量i,从而生成一个指针,其值是您传入的字节偏移量.

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

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

glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (void*)48);
Run Code Online (Sandbox Code Playgroud)

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

就个人而言,如果我更狡猾的C++,我会使用reinterpret_cast<void*>它.但我不是.

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

  • @JimBalter:`BUFFER_OFFSET`不是API的一部分.您将在实际的OpenGL标题中找到此宏*无处*.仅仅因为在网上看到它是一个相当普遍的事情并不能使它成为OpenGL的一部分.OpenGL规范明确指出需要`gl*Pointer`调用将`void*'转换为整数值,该值表示从缓冲区开始的字节偏移量.从此值中减去`(char*)NULL`是*not*规范的一部分.所以这样的实现将违反OpenGL规范. (5认同)
  • @datenwolf`BULL`可能是`0`(或`(42-42)`),但`(char*)NULL`在理论上可能具有非零表示.在实践中,有太多的程序只调用`memset`或`bzero`来对所有内容(int,float,pointer)进行零初始化,使其具有零指针的非零表示. (2认同)
  • @datenwolf:宏`NULL` 不必*必须是数字0。它只需要是一个空指针。C++ 规范从未声明空指针 * 是 * 零。它只是说整数文字 0 在转换为指针时是空指针。但这与说空指针的值为零不同。或者对空的 `char*` 进行指针运算意味着从零开始递增。 (2认同)

dat*_*olf 27

#define BUFFER_OFFSET(i) ((char *)NULL + (i))
Run Code Online (Sandbox Code Playgroud)

从技术上讲,此操作的结果是未定义的,并且宏实际上是错误的.让我解释:

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编译器,该函数采用指针(我们学习的不透明的东西).正确的解决方案是引入新的功能,特别是与gl…OffsetVBO 一起使用(比方说- 我想我会去ralley介绍).相反,OpenGL定义的是利用编译器的工作方式.大多数编译器将指针及其整数等价物实现为相同的二进制表示.所以我们要做的是,它使编译器用gl…Pointer我们的数字而不是指针来调用这些函数.

所以从技术上来说,我们唯一需要做的就是告诉编译器"是的,我知道你认为这个变量a是一个整数,你是对的,而且这个函数glVertexPointer只需要一个void*for的数据参数.但是猜猜是:那个整数是屈服的从一个void*",通过将其转换为(void*)然后握住拇指,编译器实际上是如此愚蠢,以传递整数值glVertexPointer.

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

typedef void (*TFPTR_VertexOffset)(GLint, GLenum, GLsizei, uintptr_t);
TFPTR_VertexOffset myglVertexOffset = (TFPTR_VertexOffset)glVertexPointer;
Run Code Online (Sandbox Code Playgroud)

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

  • 如果可以实际暴露OpenGL标准中的这个小嘲笑,我会给出+50.客户端顶点阵列已经出现了一段时间,但即使在OpenGL 4中它们仍然没有清除这种结构. (2认同)