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

时间:2018-04-09 12:39:11

标签: c pointers types avr-gcc harvard-architecture

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

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

特别是对于PIC来说,指向ROM / RAM的指针的定义与MPLAB C18(现已停产)相似。它甚至可以定义如下内容:

char const rom* ram* ram strdptr;

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

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

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

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

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

4 个答案:

答案 0 :(得分:7)

一个技巧是将指针包装在结构中。指向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

答案 1 :(得分:4)

您可以将指针封装在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;
                    ^~~~~

当然,你可以typedef简化结构

答案 2 :(得分:2)

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

命名地址空间

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

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

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

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

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

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

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

(奥拉夫的原始答案)

结构封装,内部指针

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

A&#34;指针&#34;类型到字节数据可能如下所示:

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的原始答案)

结构封装,指针

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

A&#34;指针&#34;类型到字节数据可能如下所示:

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会使用普通指针生成相同的代码。所以你不必考虑表现:他们应该工作。

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

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

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

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

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

感谢您的所有答案!

答案 3 :(得分:-5)

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

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

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

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

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

为AVR读取documentation,地址空间的使用类似于标准限定符。编译器将自动发出正确的指令以访问正确的空间。此外,还有一个统一的地址空间,可以在运行时确定区域,如上所述。

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

结论:

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

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

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

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

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

免责声明:我没有测试这些功能,但依赖于文档。评论中的有用反馈表示赞赏。