【问题标题】:Splitting a file and passing the data on to other classes拆分文件并将数据传递给其他类
【发布时间】:2012-01-05 15:16:41
【问题描述】:

在我当前的项目中,我有很多不同格式的二进制文件。其中有几个充当简单的档案,因此我正在尝试提出一种将提取的文件数据传递给其他类的好方法。

这是我当前方法的简化示例:

class Archive {
    private:
        std::istream &fs;
        void Read();
    public:
        Archive(std::istream &fs); // Calls Read() automatically
        ~Archive();
        const char* Get(int archiveIndex);
        size_t GetSize(int archiveIndex);
};

class FileFormat {
    private:
        std::istream &fs;
        void Read();
    public:
        FileFormat(std::istream &fs); // Calls Read() automatically
        ~FileFormat();
};

Archive 类主要解析存档并将存储的文件读入char 指针。 为了从Archive 加载第一个FileFormat 文件,我目前将使用以下代码:

std::ifstream fs("somearchive.arc", std::ios::binary);
Archive arc(fs);
std::istringstream ss(std::string(arc.Get(0), arc.GetSize(0)), std::ios::binary);
FileFormat ff(ss);

(请注意,存档中的某些文件可能是其他存档,但格式不同。)

读取二进制数据时,我使用BinaryReader 类,其功能如下:

BinaryReader::BinaryReader(std::istream &fs) : fs(fs) {
}

char* BinaryReader::ReadBytes(unsigned int n) {
    char* buffer = new char[n];
    fs.read(buffer, n);
    return buffer;
}

unsigned int BinaryReader::ReadUInt32() {
    unsigned int buffer;
    fs.read((char*)&buffer, sizeof(unsigned int));
    return buffer;
}

我喜欢这种方法的简单性,但我目前正在努力解决很多内存错误和 SIGSEGV 问题,我担心这是因为这种方法。一个例子是当我在一个循环中重复创建和读取档案时。它适用于大量迭代,但一段时间后,它开始读取垃圾数据。

我的问题是这种方法是否可行(在这种情况下我会问我做错了什么),如果不可行,还有什么更好的方法?

【问题讨论】:

  • 你还没有展示 Archive 类的实现,我想用 std::ios::binary 打开 istream ?
  • 我忘记了我在这里编写的代码中的 std::ios::binary 但它在我的版本中。 istream 是从 ifstream 构造的,并且该流使用 std::ios::binary 打开,如上所示。

标签: c++ file binary


【解决方案1】:

OP中代码的缺陷是:

  1. 您正在分配堆内存并从您的一个函数返回指向它的指针。这可能会导致内存泄漏。您(目前)没有泄漏问题,但在设计类时必须考虑到这些问题。
  2. 在处理存档和文件格式类时,用户必须始终考虑存档的内部结构。基本上它妥协了数据封装的想法。

当您的类框架的用户创建一个存档对象时,他只是获得了一种方法来提取指向某些原始数据的指针。然后用户必须将此原始数据传递给完全独立的类。此外,您将拥有不止一种 FileFormat。即使不需要观察泄漏的堆分配,处理此类系统也很容易出错。

让我们尝试将一些 OOP 原则应用于任务。您的存档对象是不同格式的文件的容器。因此,存档的 Get() 等效项通常应该返回 File 对象,而不是指向原始数据的指针:

//We gonna need a way to store file type in your archive index
enum TFileType { BYTE_FILE, UINT32_FILE, /*...*/ }

class BaseFile {
public:
virtual TFileType GetFileType() const = 0;
/* Your abstract interface here */
};

class ByteFile : public BaseFile {
public:
ByteFile(istream &fs);
virtual ~ByteFile();
virtual TFileType GetFileType() const
{ return BYTE_FILE; }
unsigned char GetByte(size_t index);
protected:
/* implementation of data storage and reading procedures */
};

class UInt32File : public BaseFile {
public:
UInt32File(istream &fs);
virtual ~UInt32File();
virtual TFileType GetFileType() const
{ return UINT32_FILE; }
uint32_t GetUInt32(size_t index);
protected:
/* implementation of data storage and reading procedures */
};


class Archive {
public:
Archive(const char* filename);
~Archive();
BaseFile* Get(int archiveIndex);
{ return (m_Files.at(archiveIndex)); }
/* ... */
protected:
vector<BaseFile*> m_Files;
}

Archive::Archive(const char* filename)
{
    ifstream fs(filename);

    //Here we need to:
    //1. Read archive index
    //2. For each file in index do something like:
    switch(CurrentFileType) {
    case BYTE_FILE:
           m_Files.push_back(new ByteFile(fs));
           break;
    case UINT32_FILE:
           m_Files.push_back(new UInt32File(fs));
           break;
    //.....
    }
}  

Archive::~Archive()
{
    for(size_t i = 0; i < m_Files.size(); ++i)
        delete m_Files[i];
}

int main(int argc, char** argv)
{
     Archive arch("somearchive.arc");
     BaseFile* pbf;
     ByteFile* pByteFile;

     pbf = arch.Get(0);

     //Here we can use GetFileType() or typeid to make a proper cast
     //An example of former:

     switch ( pbf.GetFileType() ) {
     case BYTE_FILE:
         pByteFile = dynamic_cast<ByteFile*>(pbf);
         ASSERT(pByteFile != 0 );
         //Working with byte data
         break;
     /*...*/
     }

     //alternatively you may omit GetFileType() and rely solely on C++ 
     //typeid-related stuff

}

这只是可以简化应用程序中存档使用的类的一般概念。

请记住,良好的类设计可以帮助您防止内存泄漏、代码澄清等。但是无论你有什么类,你仍然会处理二进制数据存储问题。例如,如果您的存档存储了 64 个字节的字节数据和 8 个 uint32,并且您以某种方式读取了 65 个字节而不是 64 个字节,那么读取以下整数会给您带来垃圾。您可能还会遇到对齐和字节顺序问题(如果您的应用程序应该在多个平台上运行,后者很重要)。不过,良好的类设计可能会帮助您生成更好的代码来解决这些问题。

【讨论】:

  • 我真的很喜欢这种方法!一旦我尝试过,我会在这里再次写信,如果有效,我会接受你的回答。
  • 在实现这一点时,我有一个问题。如果我不想在需要之前将数据读入内存怎么办。例如,如果我有一个包含两个文件的存档,则现在需要一个,但仅在极少数情况下才需要另一个。用这种系统容易吗?这不是一个可怕的内存开销,但以后可能会很重要。
  • 另一个问题:将 ifstream 传递给文件类会使文件中的绝对搜索变得困难。我是否必须使用它们各自的偏移量来初始化它们,以便它们可以执行 fs.seekg(offset + x) 还是有更好的方法?
  • @Merigrim:无论如何,您可以使用任何方式来阅读档案。如果您不想在创建容器对象期间读取整个存档,只需读取存档索引并将某种表存储在该对象中即可。此类表可能包含“archiveIndex”、“FileType”、“FileSize”、“FileOffset”等字段。当用户调用 Archive::Get() 时,它可以使用存储在该表中的信息从 BaseFile 层次结构中创建所需的对象。这里的重点是防止内存泄漏的可能性。您可能仍希望将指向 File 对象的指针存储在 Archive 对象中。
  • @Merigrim:至于seek 部分,其想法是,当您构造 BaseFile 层次结构对象时,您传递一个文件流,该文件流设置为文件数据的开头(即第一次调用 @构造函数中的 987654323@ 将给出该文件数据的第一个字节)。这样,存档对象将处理在存档中查找单独的文件,而 BaseFile 子类将仅处理从存档实例指定的字节开始的文件数据。顺便说一句,您可能还需要将文件长度传递给 BaseFile 构造函数。
【解决方案2】:

从你的函数中传递一个指针并期望用户知道删除它是自找麻烦,除非函数名很明显这样做,例如以单词 create 开头的函数。

所以

Foo * createFoo();

很可能是一个创建用户必须删除的对象的函数。

对于初学者来说,更好的解决方案是返回std::vector&lt;char&gt; 或允许用户将std::vector&lt;char&gt; &amp; 传递给您的函数,然后将字节写入其中,并在必要时设置其大小。 (如果在可以重复使用同一个缓冲区的情况下进行多次读取,效率会更高)。

您还应该学习 const 正确性。

至于你的“过了一会儿它充满了垃圾”,你在哪里检查文件结尾?

【讨论】:

  • 我正在调查你现在写的东西。至于垃圾数据,读取档案时永远不会到达EOF(它读取文件条目并使用seekg(offset),读取指定长度的数据,然后是seekg(orig_pos))。当我创建一个存档对象(从而将文件读入内存)并在之后立即删除它时,就会出现问题。请注意,在此过程中不会发生(明显的)内存泄漏。
  • 这不是一个坏建议,但不太可能帮助 OP 解决他们最初的问题。同样返回 vector 只是 c++11 中的好建议,其中移动构造将使其成为高效操作。传入 vector & 的另一个选项在任何 c++ 实现中都是一个不错的选择。
  • 很难看到 OP 的实际问题,我猜测任何内存问题都可能源于必须进行过多的内存管理,并且在没有任何先验知识的情况下,垃圾结果可能已经出现从分配缓冲区但未能将字节读入其中(如果已达到 EOF)。
猜你喜欢
  • 1970-01-01
  • 2021-08-15
  • 2020-12-29
  • 1970-01-01
  • 1970-01-01
  • 2021-11-15
  • 2019-12-21
  • 1970-01-01
  • 2013-06-03
相关资源
最近更新 更多