如何使两个在其他方面相同的指针类型不兼容
How to make two otherwise identical pointer types incompatible
在某些体系结构上,可能需要为其他方面相同的对象使用不同的指针类型。特别是对于哈佛架构 CPU,您可能需要这样的东西:
uint8_t const ram* data1;
uint8_t const rom* data2;
特别是在 PIC 的 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 数据的功能。无法区分指向 RAM 的指针和指向 ROM 的指针。
在某些情况下,这种类型安全对于捕获编程错误非常有益。
有没有办法以某种方式(例如通过预处理器,扩展到可以模仿这种行为的东西)向指针添加类似的修饰符来达到这个目的?或者甚至是警告不当访问的东西? (如果是 avr-gcc,尝试在不使用 ROM 访问函数的情况下获取值)
您可以将指针封装在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
为简洁起见使用结构
一个技巧是将指针包装在一个结构中。指向结构的指针比指向原始数据类型的指针具有更好的类型安全性。
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
真正的哈佛架构使用不同的指令来访问不同类型的内存,例如代码(AVR 上的闪存)、数据 (RAM)、硬件外围寄存器 (IO) 以及可能的其他内存。范围内地址的 值 通常重叠,即相同的值访问不同的内部设备,具体取决于指令。
来到C,如果要使用统一的指针,这意味着你不仅要在指针中编码地址(值),还要编码访问类型(以下"address space")价值。这既可以使用指针值中的附加位来完成,也可以 select run-time 处的适当指令 用于每次访问 。这对生成的代码构成了显着的开销。此外,通常至少某些地址 space 的 "natural" 值中没有备用位(例如,指针的所有 16 位已用于该地址)。所以需要额外的位,至少一个字节。这也会增加内存使用量(主要是 RAM)。
在使用此架构的典型 MCU 上,两者通常都是不可接受的,因为它们已经非常有限。幸运的是,对于大多数应用程序来说,绝对没有必要(或者至少可以轻松避免)在 run-time.
处确定地址 space
为了解决这个问题,此类平台的所有编译器都支持某种方式来告诉编译器地址 space 和对象所在的地址。标准草案 N1275 为 then-upcoming C11 提出了使用 "named address spaces" 的标准方式。不幸的是它没有进入最终版本,所以我们只剩下 compiler-extensions.
对于 gcc(请参阅其他编译器的文档),开发人员实施了原始标准提案。由于地址 space 是 target-specific,代码在不同的架构之间不可移植,但对于 bare-metal 嵌入式代码通常是这样,没有什么真正丢失。
阅读 AVR 的 documentation,地址 space 的使用类似于标准限定符。编译器会自动发出正确的指令来访问正确的 space。还有一个统一的地址 space,它决定了 run-time 的区域,如上所述。
地址 space 的工作方式类似于限定符,有更强的约束来确定兼容性,即在将不同地址 space 的指针分配给彼此时。详细说明见proposal, chapter 5.
结论:
命名地址spaces 就是你想要的。他们解决了两个问题:
- 确保指向不兼容地址 space 的指针不能在不被注意的情况下相互分配。
- 告诉编译器如何访问对象,即使用哪些指令。
关于提出 struct
s 的其他答案,您必须在访问数据后指定地址 space(以及 void *
的类型)。在声明中使用地址 space 保持代码的其余部分干净,甚至允许稍后在源代码中的单个位置更改它。
如果您追求 tool-chains 之间的可移植性,请阅读他们的文档并使用宏。您很可能只需要采用地址的实际名称 spaces.
旁注: 您引用的 PIC18 示例实际上使用命名地址 spaces 的语法。只是名称被弃用,因为实现应该为应用程序代码保留所有 non-standard 个名称。因此 gcc 中的 underscore-qualified 名称。
免责声明:我没有测试这些功能,而是依赖文档。感谢评论中的有用反馈。
由于我收到了几个答案,这些答案在提供解决方案时提供了不同的折衷方案,因此我决定将它们合并为一个,概述每个答案的优缺点。所以您可以选择最适合您的具体情况
命名地址空间
对于解决此问题的特定问题,并且只有这种 AVR-8 micro 上的 ROM 和 RAM 指针的情况,最合适的解决方案是这个。
这是针对 C11 的提案,它没有成为最终标准,但是有支持它的 C 编译器,包括用于 8 位 AVR 的 avr-gcc。
可以访问相关文档here(在线 GCC 手册的一部分,还包括使用此扩展的其他体系结构)。它优于其他解决方案(例如 AVR-8 pgmspace.h 中的类似函数的宏),编译器可以进行适当的检查,同时访问指向的数据仍然清晰和简单。
特别是,如果您在从提供某种命名地址 spaces 的编译器(如 MPLAB C18)中移植某些东西时遇到类似问题,这可能是最快和最干净的方法。
从上面移植的指针如下所示:
uint8_t const* data1;
uint8_t const __flash* data2;
char const __flash** strdptr;
(如果可能,可以使用适当的预处理器定义来简化流程)
(Olaf 的原始回答)
结构体封装,指针在里面
此方法旨在通过将指针包装在结构中来加强指针的类型化。预期用途是跨接口传递结构本身,编译器可以通过接口对它们执行类型检查。
"pointer" 类型到字节数据可能如下所示:
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 的原始回答)
结构体封装,指针在外
与上述方法类似,它旨在通过将指针包装在结构中来加强指针的类型,但以不同的方式提供更强大的约束。要指向的数据类型是封装的。
"pointer" 类型到字节数据可能如下所示:
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 的原始回答)
我应该使用哪个?
命名地址空间在这方面不需要太多讨论:如果您只想处理目标处理地址 space 的特殊性,它们是最合适的解决方案。编译器将为您提供所需的编译时检查,您不必再尝试发明任何东西。
如果您出于其他原因对结构包装感兴趣,则可能需要考虑以下事项:
这两种方法都可以很好地优化:至少 GCC 会从使用普通指针的任何一种生成相同的代码。所以你真的不必考虑性能:它们应该可以工作。
如果您有第三方接口来服务于哪些需求指针,或者如果您重构的东西太大而您无法一次性完成,那么内部指针很有用。
外部指针提供了更强大的类型安全性,因为您用它加强了指向类型本身:您拥有一个真正独特的类型,您不能轻易(意外)转换(隐式转换)。
Pointer outside 允许您在指针上使用修饰符,例如添加 const
,这对于创建健壮的接口很重要(您可以使数据仅供函数读取const
).
请记住,有些人可能不喜欢其中任何一种,因此如果您在团队中工作,或者正在创建可能被已知各方重用的代码,请先与他们讨论此事.
应该很明显,但请记住,封装并不能解决需要特殊访问代码的问题(例如 AVR-8 上的 pgmspace.h 宏),假设没有命名地址空间与该方法一起使用。如果您尝试通过在不同地址 space 上运行的函数使用指针,而不是它打算指向的地址,它仅提供一种产生编译错误的方法。
谢谢大家的回答!
在某些体系结构上,可能需要为其他方面相同的对象使用不同的指针类型。特别是对于哈佛架构 CPU,您可能需要这样的东西:
uint8_t const ram* data1;
uint8_t const rom* data2;
特别是在 PIC 的 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 数据的功能。无法区分指向 RAM 的指针和指向 ROM 的指针。
在某些情况下,这种类型安全对于捕获编程错误非常有益。
有没有办法以某种方式(例如通过预处理器,扩展到可以模仿这种行为的东西)向指针添加类似的修饰符来达到这个目的?或者甚至是警告不当访问的东西? (如果是 avr-gcc,尝试在不使用 ROM 访问函数的情况下获取值)
您可以将指针封装在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
为简洁起见使用结构
一个技巧是将指针包装在一个结构中。指向结构的指针比指向原始数据类型的指针具有更好的类型安全性。
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
真正的哈佛架构使用不同的指令来访问不同类型的内存,例如代码(AVR 上的闪存)、数据 (RAM)、硬件外围寄存器 (IO) 以及可能的其他内存。范围内地址的 值 通常重叠,即相同的值访问不同的内部设备,具体取决于指令。
来到C,如果要使用统一的指针,这意味着你不仅要在指针中编码地址(值),还要编码访问类型(以下"address space")价值。这既可以使用指针值中的附加位来完成,也可以 select run-time 处的适当指令 用于每次访问 。这对生成的代码构成了显着的开销。此外,通常至少某些地址 space 的 "natural" 值中没有备用位(例如,指针的所有 16 位已用于该地址)。所以需要额外的位,至少一个字节。这也会增加内存使用量(主要是 RAM)。
在使用此架构的典型 MCU 上,两者通常都是不可接受的,因为它们已经非常有限。幸运的是,对于大多数应用程序来说,绝对没有必要(或者至少可以轻松避免)在 run-time.
处确定地址 space为了解决这个问题,此类平台的所有编译器都支持某种方式来告诉编译器地址 space 和对象所在的地址。标准草案 N1275 为 then-upcoming C11 提出了使用 "named address spaces" 的标准方式。不幸的是它没有进入最终版本,所以我们只剩下 compiler-extensions.
对于 gcc(请参阅其他编译器的文档),开发人员实施了原始标准提案。由于地址 space 是 target-specific,代码在不同的架构之间不可移植,但对于 bare-metal 嵌入式代码通常是这样,没有什么真正丢失。
阅读 AVR 的 documentation,地址 space 的使用类似于标准限定符。编译器会自动发出正确的指令来访问正确的 space。还有一个统一的地址 space,它决定了 run-time 的区域,如上所述。
地址 space 的工作方式类似于限定符,有更强的约束来确定兼容性,即在将不同地址 space 的指针分配给彼此时。详细说明见proposal, chapter 5.
结论:
命名地址spaces 就是你想要的。他们解决了两个问题:
- 确保指向不兼容地址 space 的指针不能在不被注意的情况下相互分配。
- 告诉编译器如何访问对象,即使用哪些指令。
关于提出 struct
s 的其他答案,您必须在访问数据后指定地址 space(以及 void *
的类型)。在声明中使用地址 space 保持代码的其余部分干净,甚至允许稍后在源代码中的单个位置更改它。
如果您追求 tool-chains 之间的可移植性,请阅读他们的文档并使用宏。您很可能只需要采用地址的实际名称 spaces.
旁注: 您引用的 PIC18 示例实际上使用命名地址 spaces 的语法。只是名称被弃用,因为实现应该为应用程序代码保留所有 non-standard 个名称。因此 gcc 中的 underscore-qualified 名称。
免责声明:我没有测试这些功能,而是依赖文档。感谢评论中的有用反馈。
由于我收到了几个答案,这些答案在提供解决方案时提供了不同的折衷方案,因此我决定将它们合并为一个,概述每个答案的优缺点。所以您可以选择最适合您的具体情况
命名地址空间
对于解决此问题的特定问题,并且只有这种 AVR-8 micro 上的 ROM 和 RAM 指针的情况,最合适的解决方案是这个。
这是针对 C11 的提案,它没有成为最终标准,但是有支持它的 C 编译器,包括用于 8 位 AVR 的 avr-gcc。
可以访问相关文档here(在线 GCC 手册的一部分,还包括使用此扩展的其他体系结构)。它优于其他解决方案(例如 AVR-8 pgmspace.h 中的类似函数的宏),编译器可以进行适当的检查,同时访问指向的数据仍然清晰和简单。
特别是,如果您在从提供某种命名地址 spaces 的编译器(如 MPLAB C18)中移植某些东西时遇到类似问题,这可能是最快和最干净的方法。
从上面移植的指针如下所示:
uint8_t const* data1;
uint8_t const __flash* data2;
char const __flash** strdptr;
(如果可能,可以使用适当的预处理器定义来简化流程)
(Olaf 的原始回答)
结构体封装,指针在里面
此方法旨在通过将指针包装在结构中来加强指针的类型化。预期用途是跨接口传递结构本身,编译器可以通过接口对它们执行类型检查。
"pointer" 类型到字节数据可能如下所示:
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 的原始回答)
结构体封装,指针在外
与上述方法类似,它旨在通过将指针包装在结构中来加强指针的类型,但以不同的方式提供更强大的约束。要指向的数据类型是封装的。
"pointer" 类型到字节数据可能如下所示:
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 的原始回答)
我应该使用哪个?
命名地址空间在这方面不需要太多讨论:如果您只想处理目标处理地址 space 的特殊性,它们是最合适的解决方案。编译器将为您提供所需的编译时检查,您不必再尝试发明任何东西。
如果您出于其他原因对结构包装感兴趣,则可能需要考虑以下事项:
这两种方法都可以很好地优化:至少 GCC 会从使用普通指针的任何一种生成相同的代码。所以你真的不必考虑性能:它们应该可以工作。
如果您有第三方接口来服务于哪些需求指针,或者如果您重构的东西太大而您无法一次性完成,那么内部指针很有用。
外部指针提供了更强大的类型安全性,因为您用它加强了指向类型本身:您拥有一个真正独特的类型,您不能轻易(意外)转换(隐式转换)。
Pointer outside 允许您在指针上使用修饰符,例如添加
const
,这对于创建健壮的接口很重要(您可以使数据仅供函数读取const
).请记住,有些人可能不喜欢其中任何一种,因此如果您在团队中工作,或者正在创建可能被已知各方重用的代码,请先与他们讨论此事.
应该很明显,但请记住,封装并不能解决需要特殊访问代码的问题(例如 AVR-8 上的 pgmspace.h 宏),假设没有命名地址空间与该方法一起使用。如果您尝试通过在不同地址 space 上运行的函数使用指针,而不是它打算指向的地址,它仅提供一种产生编译错误的方法。
谢谢大家的回答!