我有一个二进制文件,我知道一些布局。例如,让格式如下:
该文件应该看起来像(为了便于阅读,我添加了空格):
5 hello 3 0.0 0.1 0.2 -0.3 -0.4 -0.5
这里5 - 是2个字节:0x05 0x00。 “你好” - 5个字节,依此类推。
现在我想读这个文件。目前我这样做:
char buffer[2]
unsigned short len{ *((unsigned short*)buffer) };
。现在我有一个字符串的长度。vector<char>
并从这个向量创建一个std::string
。现在我有字符串ID。char bufferFloat[4]
并为每个浮点数投射*((float*)bufferFloat)
。这有效,但对我来说它看起来很难看。我可以直接阅读unsigned short
或float
或string
等没有char [x]
创建吗?如果不是,那么正确投射的方式是什么(我读过我正在使用的风格 - 是旧风格)?
P.S。:当我写一个问题时,我头脑中提出的解释更加清晰 - 如何从char [x]
中的任意位置投射任意数量的字节?
更新:我忘了明确提到字符串和浮点数据长度在编译时是未知的并且是可变的。
在C ++中可以正常工作的C方式是声明一个结构:
#pragma pack(1)
struct contents {
// data members;
};
注意
然后将读缓冲区直接转换为struct类型:
std::vector<char> buf(sizeof(contents));
file.read(buf.data(), buf.size());
contents *stuff = reinterpret_cast<contents *>(buf.data());
现在,如果您的数据大小是可变的,您可以分成几个块。要从缓冲区读取单个二进制对象,读取器功能非常方便:
template<typename T>
const char *read_object(const char *buffer, T& target) {
target = *reinterpret_cast<const T*>(buffer);
return buffer + sizeof(T);
}
主要优点是这样的阅读器可以专门用于更高级的c ++对象:
template<typename CT>
const char *read_object(const char *buffer, std::vector<CT>& target) {
size_t size = target.size();
CT const *buf_start = reinterpret_cast<const CT*>(buffer);
std::copy(buf_start, buf_start + size, target.begin());
return buffer + size * sizeof(CT);
}
现在在你的主解析器中:
int n_floats;
iter = read_object(iter, n_floats);
std::vector<float> my_floats(n_floats);
iter = read_object(iter, my_floats);
注意:正如Tony D观察到的那样,即使您可以通过#pragma
指令和手动填充(如果需要)获得对齐,您仍可能遇到与处理器对齐不兼容的问题,以(最佳情况)性能问题或(最坏情况)陷阱信号。只有在您控制文件格式的情况下,此方法才有意义。
我使用ragel
工具为具有1-2K RAM的微控制器生成纯C程序源代码(无表)。它没有使用任何文件io,缓冲,并生成易于调试的代码和带状态机图的.dot / .pdf文件。
ragel还可以输出go,Java,..代码进行解析,但我没有使用这些功能。
ragel
的关键特性是能够解析任何字节构建数据,但你不能深入研究位字段。其他问题是ragel能够解析常规结构但没有递归和语法语法解析。
如果它不是出于学习目的,并且如果您可以自由选择二进制格式,那么最好考虑使用类似protobuf的东西,它将为您处理序列化并允许与其他平台和语言进行互操作。
如果您不能使用第三方API,您可以查看QDataStream
的灵感
目前我这样做:
- 将文件加载到ifstream
- 将此流读取到char缓冲区[2]
- 把它投到
unsigned short
:unsigned short len{ *((unsigned short*)buffer) };
。现在我有一个字符串的长度。
最后冒险使用SIGBUS
(如果您的字符阵列恰好从奇数地址开始,而您的CPU只能读取在偶数地址处对齐的16位值),性能(某些CPU将读取未对齐的值但速度较慢;其他类似于现代x86s很好而且很快)和/或endianness问题。我建议阅读这两个字符然后你可以说(x[0] << 8) | x[1]
反之亦然,使用htons
如果需要纠正字节顺序。
- 读一个流到
vector<char>
并从这个std::string
创建一个vector
。现在我有字符串ID。
不需要......只需直接读入字符串:
std::string s(the_size, ' ');
if (input_fstream.read(&s[0], s.size()) &&
input_stream.gcount() == s.size())
...use s...
- 同样的方式
read
接下来的4个字节并将它们投射到unsigned int
。现在我有一个进步。while
不是文件的结尾read
float
s以同样的方式 - 创建一个char bufferFloat[4]
并为每个*((float*)bufferFloat)
施放float
。
最好直接通过unsigned int
s和floats
读取数据,因为编译器将确保正确对齐。
这有效,但对我来说它看起来很难看。我可以直接阅读
unsigned short
或float
或string
等没有char [x]
创建吗?如果不是,那么正确投射的方式是什么(我读过我正在使用的风格 - 是旧风格)?
struct Data
{
uint32_t x;
float y[6];
};
Data data;
if (input_stream.read((char*)&data, sizeof data) &&
input_stream.gcount() == sizeof data)
...use x and y...
请注意,上面的代码避免将数据读入可能未对齐的字符数组,其中由于对齐问题,它对于未对齐的reinterpret_cast
数组(包括char
内部)中的std::string
数据不安全。同样,如果文件内容有可能以字节顺序显示,则可能需要使用htonl
进行一些读后转换。如果有一个未知数量的float
s,你需要计算和分配足够的存储,并且至少有4个字节的对齐,然后将Data*
瞄准它......只要符合y
的声明数组大小就可以合法地索引。访问地址处的内存内容是分配的一部分,并保存从流中读入的有效float
表示。更简单 - 但有一个额外的阅读,所以可能更慢 - 首先阅读uint32_t
然后new float[n]
并进一步read
进入那里....
实际上,这种类型的方法可以工作,并且许多低级别和C代码正是这样做的。可能帮助您阅读文件的“清洁”高级库最终必须在内部执行类似操作....
我实际上在上个月实现了一个快速而又脏的二进制格式解析器来读取.zip
文件(遵循维基百科的格式描述),并且现代化我决定使用C ++模板。
在某些特定的平台上,一个打包的struct
可以工作,但有些东西它处理得不好......比如可变长度的字段。但是,对于模板,没有这样的问题:您可以获得任意复杂的结构(和返回类型)。
幸运的是,.zip
存档相对简单,所以我实现了一些简单的东西。脱离我的头顶:
using Buffer = std::pair<unsigned char const*, size_t>;
template <typename OffsetReader>
class UInt16LEReader: private OffsetReader {
public:
UInt16LEReader() {}
explicit UInt16LEReader(OffsetReader const or): OffsetReader(or) {}
uint16_t read(Buffer const& buffer) const {
OffsetReader const& or = *this;
size_t const offset = or.read(buffer);
assert(offset <= buffer.second && "Incorrect offset");
assert(offset + 2 <= buffer.second && "Too short buffer");
unsigned char const* begin = buffer.first + offset;
// http://commandcenter.blogspot.fr/2012/04/byte-order-fallacy.html
return (uint16_t(begin[0]) << 0)
+ (uint16_t(begin[1]) << 8);
}
}; // class UInt16LEReader
// Declined for UInt[8|16|32][LE|BE]...
当然,基本的OffsetReader
实际上有一个恒定的结果:
template <size_t O>
class FixedOffsetReader {
public:
size_t read(Buffer const&) const { return O; }
}; // class FixedOffsetReader
因为我们正在谈论模板,你可以在闲暇时切换类型(你可以实现一个代理阅读器,它将所有读取委托给一个记忆它们的shared_ptr
)。
然而,有趣的是最终结果:
// http://en.wikipedia.org/wiki/Zip_%28file_format%29#File_headers
class LocalFileHeader {
public:
template <size_t O>
using UInt32 = UInt32LEReader<FixedOffsetReader<O>>;
template <size_t O>
using UInt16 = UInt16LEReader<FixedOffsetReader<O>>;
UInt32< 0> signature;
UInt16< 4> versionNeededToExtract;
UInt16< 6> generalPurposeBitFlag;
UInt16< 8> compressionMethod;
UInt16<10> fileLastModificationTime;
UInt16<12> fileLastModificationDate;
UInt32<14> crc32;
UInt32<18> compressedSize;
UInt32<22> uncompressedSize;
using FileNameLength = UInt16<26>;
using ExtraFieldLength = UInt16<28>;
using FileName = StringReader<FixedOffsetReader<30>, FileNameLength>;
using ExtraField = StringReader<
CombinedAdd<FixedOffsetReader<30>, FileNameLength>,
ExtraFieldLength
>;
FileName filename;
ExtraField extraField;
}; // class LocalFileHeader
显然,这是相当简单的,但同时又极其灵活。
一个明显的改进轴线是改进链接,因为这里存在意外重叠的风险。我的存档阅读代码在我第一次尝试时工作,这足以证明这个代码足以完成手头的任务。
我必须解决这个问题一次。数据文件打包为FORTRAN输出。对齐都错了。我成功地使用了预处理器技巧,自动完成了手动操作:将原始数据从字节缓冲区解压缩到结构。想法是描述包含文件中的数据:
BEGIN_STRUCT(foo)
UNSIGNED_SHORT(length)
STRING_FIELD(length, label)
UNSIGNED_INT(stride)
FLOAT_ARRAY(3 * stride)
END_STRUCT(foo)
现在你可以定义这些宏来生成你需要的代码,比如struct声明,包括上面的,undef并再次定义宏来生成解包函数,然后是另一个include等。
NB我第一次看到gcc中用于抽象语法树相关代码生成的这种技术。
如果CPP不够强大(或者这种预处理器滥用不适合你),请替换一个小的lex / yacc程序(或选择你喜欢的工具)。
令人惊讶的是,在生成代码而不是手工编写代码方面需要多少考虑,至少在像这样的低级基础代码中。
你应该更好地声明一个结构(使用1字节填充 - 如何 - 取决于编译器)。使用该结构编写,并使用相同的结构读取。只将POD放在结构中,因此没有std::string
等。仅将此结构用于文件I / O或其他进程间通信 - 使用普通的struct
或class
来保存它以便在C ++程序中进一步使用。
由于您的所有数据都是可变的,因此您可以单独读取这两个块并仍使用强制转换:
struct id_contents
{
uint16_t len;
char id[];
} __attribute__((packed)); // assuming gcc, ymmv
struct data_contents
{
uint32_t stride;
float data[];
} __attribute__((packed)); // assuming gcc, ymmv
class my_row
{
const id_contents* id_;
const data_contents* data_;
size_t len;
public:
my_row(const char* buffer) {
id_= reinterpret_cast<const id_contents*>(buffer);
size_ = sizeof(*id_) + id_->len;
data_ = reinterpret_cast<const data_contents*>(buffer + size_);
size_ += sizeof(*data_) +
data_->stride * sizeof(float); // or however many, 3*float?
}
size_t size() const { return size_; }
};
这样你就可以使用kbok先生的答案来正确解析:
const char* buffer = getPointerToDataSomehow();
my_row data1(buffer);
buffer += data1.size();
my_row data2(buffer);
buffer += data2.size();
// etc.
我个人这样做:
// some code which loads the file in memory
#pragma pack(push, 1)
struct someFile { int a, b, c; char d[0xEF]; };
#pragma pack(pop)
someFile* f = (someFile*) (file_in_memory);
int filePropertyA = f->a;
对于文件开头的固定大小的结构非常有效的方法。
使用序列化库。以下是一些: