如何使两个相同的指针类型不兼容

在某些体系结构上,可能需要为其他相同的对象使用不同的指针类型。 特别是对于哈佛架构CPU,您可能需要以下内容:

uint8_t const ram* data1; uint8_t const rom* data2; 

特别是这对于PICs的MPLAB C18(现已停产)中ROM / RAM指针的定义如何。 它甚至可以定义如下内容:

 char const rom* ram* ram strdptr; 

这意味着RAM中的指针指向RAM中指向ROM中的字符串的指针(使用ram是没有必要的,因为默认情况下这个编译器在RAM中,为了清楚起见,只是添加了所有内容)。

这种语法的好处是编译器能够在您尝试以不兼容的方式分配时提醒您,例如ROM位置的地址到指向RAM的指针(类似于data1 = data2;或传递ROM)使用RAM指针指向函数的指针会产生错误)。

与此相反,在AVR-8的avr-gcc中,没有这种类型的安全性,因为它提供了访问ROM数据的function。 无法将指向RAM的指针与指向ROM的指针区分开来。

在某些情况下,这种类型的安全性对于捕获编程错误非常有益。

有没有办法以某种方式向指针添加类似的修饰符(例如通过预处理器,扩展到可以模仿这种行为的东西)来实现此目的? 甚至是一些警告不正当访问的东西? (在avr-gcc的情况下,尝试在不使用ROM访问function的情况下获取值)

一个技巧是将指针包装在结构中。 指向struct的指针比指向原始数据类型的指针具有更好的类型安全性。

 typedef struct { uint8_t ptr; } a_t; typedef struct { uint8_t ptr; } b_t; const volatile a_t* a = (const volatile a_t*)0x1234; const volatile b_t* b = (const volatile b_t*)0x5678; a = b; // compiler error b = a; // compiler error 

您可以将指针封装在RAM和ROM的不同结构中,使类型不兼容,但包含相同类型的值。

 struct romPtr { void *addr; }; struct ramPtr { void *addr; }; int main(int argc, char **argv) { struct romPtr data1 = {NULL}; struct romPtr data3 = data1; struct ramPtr data2 = data1; // <-- gcc would throw a compilation error here } 

在编译期间:

 $ cc struct_test.c struct_test.c: In function 'main': struct_test.c:12:24: error: invalid initializer struct ramPtr data2 = data1; ^~~~~ 

你当然可以为了简洁而输入structdef结构

由于我收到了几个答案,这些答案在提供解决方案时提供了不同的妥协,我决定将它们合并为一个,概述每个答案的优点和缺点。 因此,您可以选择最适合您的特定情况

命名地址空间

对于解决这个问题的特殊问题,以及AVR-8 micro上的ROM和RAM指针的这种情况,最合适的解决方案就是这样。

这是针对C11的提案,它没有进入最终标准,但是有C编译器支持它,包括用于8位AVR的avr-gcc。

可以在此处访问相关文档(在线GCC手册的一部分,还包括使用此扩展的其他体系结构)。 建议使用其他解决方案(例如AVR-8的pgmspace.h中类似函数的宏),编译器可以进行相应的检查,否则访问指向的数据仍然清晰简单。

特别是,如果你有类似的问题,从提供某种命名地址空间的编译器移植某些东西,比如MPLAB C18,这可能是最快,最干净的方法。

上面的移植指针如下所示:

 uint8_t const* data1; uint8_t const __flash* data2; char const __flash** strdptr; 

(如果可能,可以使用适当的预处理器定义简化流程)

(奥拉夫的原始答案)

结构封装,指针在里面

此方法旨在通过将指针包装在结构中来强化指针的类型。 预期用途是您跨接口传递结构本身,编译器可以通过它们对它们执行类型检查。

字节数据的“指针”类型可能如下所示:

 typedef struct{ uint8_t* ptr; }bytebuffer_ptr; 

可以按如下方式访问指向的数据:

 bytebuffer_ptr bbuf; (...) bbuf.ptr = allocate_bbuf(); (...) bbuf.ptr[index] = value; 

接受这种类型并返回一个类型的函数原型可能如下所示:

 bytebuffer_ptr encode_buffer(bytebuffer_ptr inbuf, size_t len); 

(dvhh的原始答案)

结构封装,指针外

与上述方法类似,它旨在通过将指针包装在结构中来强化指针的类型,但是以不同的方式,提供更强大的约束。 要指向的数据类型是封装的。

字节数据的“指针”类型可能如下所示:

 typedef struct{ uint8_t val; }byte_data; 

可以按如下方式访问指向的数据:

 byte_data* bbuf; (...) bbuf = allocate_bbuf(); (...) bbuf[index].val = value; 

接受这种类型并返回一个类型的函数原型可能如下所示:

 byte_data* encode_buffer(byte_data* inbuf, size_t len); 

(Lundin的原始答案)

我应该使用哪个?

这方面的命名地址空间不需要太多讨论:如果您只想处理目标处理地址空间的特性,它们是最合适的解决方案。 编译器将为您提供所需的编译时检查,您不必尝试进一步发明任何东西。

但是,如果由于其他原因您对结构包装感兴趣,这些是您可能需要考虑的事项:

  • 两种方法都可以很好地优化:至少GCC会使用普通指针生成相同的代码。 所以你真的不必考虑性能:他们应该工作。

  • 如果你有第三方接口来提供哪个需求指针,或者你正在重构一些你不能在一次传递中做的大事,那么指针内部很有用。

  • 外部指针提供更强大的类型安全性,因为你用它强化了尖头类型:你有一个真正的独特类型,你不能轻易(意外)转换(隐式转换)。

  • 外部指针允许您在指针上使用修饰符,例如添加const ,这对于创建健壮的接口很重要(您可以使数据仅由函数const读取)。

  • 请记住,有些人可能不喜欢这些中的任何一个,因此如果您在一个小组中工作,或者正在创建可能被已知方重用的代码,请先与他们讨论此事。

  • 应该是显而易见的,但请记住,封装不能解决需要特殊访问代码的问题(例如AVR-8上的pgmspace.h宏),假设没有命名地址空间与方法一起使用。 它只提供了一种方法来产生编译错误,如果你试图通过在不同的地址空间上操作的函数使用指针而不是它想要指向的地址空间。

谢谢你的所有答案!

真正的哈佛架构使用不同的指令来访问不同类型的存储器,如代码(AVR上的Flash),数据(RAM),硬件外设寄存器(IO)以及其他可能的存储器。 范围中的地址通常重叠,即,相同的值访问不同的内部设备,具体取决于指令。

转到C,如果要使用统一指针,这意味着您不仅需要对指针值中的地址(值)进行编码,还要对访问类型(下面的“地址空间”)进行编码。 这可以使用指针值中的附加位来完成,但也可以在运行时为每次访问选择适当的指令。 这对生成的代码构成了很大的开销。 另外,对于至少一些地址空间,通常在“自然”值中没有备用比特(例如,指针的所有16比特已经用于地址)。 因此需要额外的位,至少一个字节值。 这也会耗尽内存使用量(主要是RAM)。

在使用这种架构的典型MCU上,这两者通常都是不可接受的,因为它们已经非常有限。 幸运的是,对于大多数应用程序来说,在运行时确定地址空间是绝对不必要的(或至少可以轻易避免)。

为了解决这个问题,这种平台的所有编译器都支持某种方式告诉编译器地址空间和对象所在的位置。 当时即将到来的C11的标准草案N1275提出了使用“命名地址空间”的标准方法。 不幸的是它没有进入最终版本,因此我们留下了编译器扩展。

对于gcc(请参阅其他编译器的文档),开发人员实现了原始标准提议。 由于地址空间是特定于目标的,因此代码在不同的archittectures之间不可移植,但对于裸机嵌入式代码而言通常都是如此,没有什么真正丢失。

阅读AVR的文档 ,简单地使用类似于标准限定符的地址空间。 编译器将自动发出正确的指令以访问正确的空间。 还有一个统一的地址空间,它决定了运行时的区域,如上所述。

地址空间的工作方式类似于限定符,确定兼容性有更强的约束,即在为彼此分配不同地址空间的指针时。 有关详细说明,请参阅提案第5章 。

结论:

命名地址空间是你想要的。 他们解决了两个问题:

  • 确保指向不兼容的地址空间的指针不能被忽视。
  • 告诉编译器如何访问对象,即使用哪些指令。

关于提出struct的其他答案,您必须在访问数据后指定地址空间(以及void *的类型)。 在声明中使用地址空间可以使代码的其余部分保持干净,甚至允许在源代码中的单个位置稍后更改它。

如果您追求工具链之间的可移植性,请阅读他们的文档并使用宏。 您很可能只需要采用地址空间的实际名称。

旁注:您引用的PIC18示例实际上使用了命名地址空间的语法。 只是名称被弃用,因为实现应该为应用程序代码保留所有非标准名称。 因此gcc中的下划线限定名称。

免责声明:我没有测试function,但依赖于文档。 评论中有用的反馈意见。