我正在开发一个 RAM 非常紧张的嵌入式应用程序。 为此,我需要创建一个 24 位无符号整数数据类型。我正在使用结构来执行此操作:
typedef struct
{
uint32_t v : 24;
} uint24_t;
但是,当我询问这种类型的变量的大小时,它返回“4”,即:
uint24_t x;
x.v = 0;
printf("Size = %u", sizeof(x));
有没有办法可以强制这个变量有3个字节?
最初我认为这是因为它强制数据类型进行字对齐,但我可以这样做:
typedef struct
{
uint8_t blah[3];
} mytype;
在这种情况下,尺寸为 3。
好吧,您可以尝试确保该结构仅占用您需要的空间,例如:
#pragma pack(push, 1)
typedef struct { uint8_t byt[3]; } UInt24;
#pragma pack(pop)
您可能必须提供这些编译器指令(如上面的
#pragma
行)以确保没有填充,但这将可能成为仅具有八位字段的结构的默认值(a)。
然后,您可能必须将实际值打包到结构中或从结构中解包,例如:
// Inline suggestion used to (hopefully) reduce overhead.
inline uint32_t unpack(UInt24 x) {
uint32_t retVal = x.byt[0];
retVal = retVal << 8 | x.byt[1];
retVal = retVal << 8 | x.byt[2];
return retVal;
}
inline UInt24 pack(uint32_t x) {
UInt24 retVal;
retVal.byt[0] = (x >> 16) & 0xff;
retVal.byt[1] = (x >> 8) & 0xff;
retVal.byt[2] = x & 0xff;
return retVal;
}
请注意,无论您的实际架构如何,这都会为您提供大端值。如果您自己专门打包和解包,这并不重要,但如果您想在特定布局中的其他地方使用内存块,那么它可能会成为问题(在这种情况下,您只需更改打包/解包即可)代码以使用所需的格式)。
此方法会向您的系统添加一些代码(并且性能损失可能很小),因此您必须决定这是否值得节省所用的数据空间。
(a) 例如,对于以下程序,
gcc 7.3
和 clang 6.0
均显示 3 6
,表明结构内或结构后没有填充:
#include <stdio.h>
#include <stdint.h>
typedef struct { uint8_t byt[3]; } UInt24;
int main() {
UInt24 x, y[2];
printf("%zd %zd\n", sizeof(x), sizeof(y));
return 0;
}
但是,只是一个示例,因此为了可移植代码,您可能需要考虑使用
#pragma pack(1)
之类的东西,或者放入代码来捕获可能不是这种情况的环境。
João Baptista 在 this site 上的评论说您可以使用
#pragma pack
。另一种选择是使用 __attribute__((packed))
:
#ifndef __GNUC__
# define __attribute__(x)
#endif
struct uint24_t { unsigned long v:24; };
typedef struct uint24_t __attribute__((packed)) uint24_t;
这应该适用于 GCC 和 Clang。
但是请注意,除非您的处理器支持未对齐访问,否则这可能会搞砸对齐。
最初我认为这是因为它强制数据类型进行字对齐
不同的数据类型可以有不同的对齐方式。例如,请参阅对象和对齐文档。
您可以使用
alignof
进行检查,但 char
或 uint8_t
具有 1 字节(即实际上没有)对齐是完全正常的,但 uint32_t
具有 4 字节对齐。我不知道位域的对齐方式是否被明确描述,但从存储类型继承它似乎足够合理。
注意。具有对齐要求的原因通常是它与底层硬件配合得更好。如果您使用
#pragma pack
或__attribute__((packed))
或其他什么,您可能会受到性能影响,因为编译器或内存硬件会默默地处理未对齐的访问。
仅显式存储 3 字节数组可能更好,IMO。
首先,不要使用位字段或结构。它们可以根据需要包含填充,并且位字段通常是不可移植的。
除非您的 CPU 明确获得 24 位算术指令(这似乎不太可能,除非它是一些奇怪的 DSP),否则自定义数据类型只会导致额外的堆栈膨胀。
很可能您必须使用
uint32_t
来进行所有算术运算。这意味着您的 24 位类型在节省 RAM 方面可能效果不佳。如果您发明一些具有 setter/getter(序列化/反序列化)访问权限的自定义 ADT,您可能只是浪费 RAM,因为如果函数无法内联,您会获得更高的堆栈峰值使用率。
要真正节省 RAM,您应该修改程序设计。
话虽如此,您可以基于数组创建自定义类型:
typedef unsigned char u24_t[3];
每当您需要访问数据时,您都可以将其与 32 位类型进行
memcpy
转换,然后在 32 位上进行所有算术运算:
u24_t u24;
uint32_t u32;
...
memcpy(&u32, u24, sizeof(u24));
...
memcpy(&u24, &u32, sizeof(u24));
但请注意,这假设是小端字节序,因为我们只使用位 0 到 2。在大端字节序系统的情况下,您必须执行
memcpy((uint8_t*)&u32+1, ...
来丢弃 MS 字节。