使用 seekg() 和 seekp() 在二进制文件中搜索错误的输出

wrong output searching in binary files using seekg() & seekp()

我写了这段非常简单的代码,但它的搜索部分不起作用。

我只是把人的名字和年龄保存在一个二进制文件中,我想找到他们,它给了我错误的记录。

这是搜索部分:

fstream myfile(filename , ios::binary | ios::out | ios::in);

data d;


int searchage;


cout << "age : ";
cin >> searchage;


myfile.seekg(sizeof(data)* (searchage) );

myfile.read((char*)&d , sizeof(data));

myfile.close();

cout << "name : " << d.getname() << '\t' << d.getage() << endl;

您可能需要 class :

    class data{

private:
    int age;
    char name[15];


public:


    data (){
    // null

    }
    void setname(string tempname){
    int len = tempname.size();
    len = (len < 15 ? len : 14);
    tempname.copy(name , len);

    name[len] = '[=11=]';

    }

    string getname(){
    return name;
    }



    void setage(int tempage){
    age = tempage;
    }


    int getage(){

    return age;
    }

};

并保存部分:

fstream myfile(filename , ios::binary | ios::out | ios::app);


data d;

string name ;
int age;


cout << "name : ";
cin >> name;

cout << "age : ";
cin >> age;


d.setname(name);
d.setage(age);



myfile.seekp(sizeof(data) * age , ios::beg);

myfile.write((char*)&d , sizeof(data));

myfile.close();

完整代码也在这里


    #include <iostream>
#include <fstream>
#include <iomanip>


using namespace std;

#define filename "D:\data.dat"


class data{

private:
    int age;
    char name[15];


public:


    data (string tempname = "null" , int tempage = 1){

    setname(tempname);
    setage(tempage);

    }
    void setname(string tempname){
    int len = tempname.size();
    len = (len < 15 ? len : 14);
    tempname.copy(name , len);

    name[len] = '[=13=]';

    }

    string getname(){
    return name;
    }



    void setage(int tempage){
    age = tempage;
    }


    int getage(){

    return age;
    }

};



int main(){

 int a;

    cout << "1 - save \n2 - search \nchose : ";
   
    cin >> a;

    if(a == 1){

        fstream myfile("D:\data.dat", ios::out | ios::in | ios::app);


        data d , d2;

        if(!myfile)
        {
            cout << "creat file \n";
            ofstream myfile2(filename , ios::out | ios::binary);

            myfile2.write((char*)&d2 , sizeof(data));

            myfile2.close();
        }

        fstream myfile3(filename , ios::binary | ios::out | ios::in | ios::app);


        string name ;
        int age;


        cout << "name : ";
        cin >> name;

        cout << "age : ";
        cin >> age;


        d.setname(name);
        d.setage(age);



        myfile3.seekp(sizeof(data) * age , ios::beg);

        myfile3.write((char*)&d , sizeof(data));

        myfile3.close();

    }else if(a == 2){
        fstream myfile(filename , ios::binary | ios::out | ios::in);

        data d;


        int searchage;


        cout << "age : ";
        cin >> searchage;


        myfile.seekg(sizeof(data)* searchage );

        myfile.read((char*)&d , sizeof(data));

        myfile.close();

        cout << "name : " << d.getname() << '\t' << d.getage() << endl;


    }







}

不知道怎么回事

首先,上面的代码不可能在没有错误或警告的情况下编译。特别是由于您使用:

    void setname(string tempname){
      ...
      tempname.copy(name , len);

.copy() 成员函数不是 std::string 的成员,而是 std::basic_string_view (C++17) 的成员。在这种情况下,编译器不知道 .copy() 是什么。相反,您需要使用 std::basic_string_view<char> 作为 tempname 的类型,例如

  void setname (std::basic_string_view<char> tempname) {
    int len = tempname.size();
    len = (len < 15 ? len : 14);
    tempname.copy(name , len);
    name[len] = '[=11=]';
  }

(注意: len 应为 size_t)

您的构造函数可以使用相同的方法来填充 name,例如

  data (std::basic_string_view<char> tempname = "null" , int tempage = 1) : 
  age{tempage} {
    tempname.copy (name, tempname.size());
  }

虽然应避免使用 char[] 而不是 std::string,但很明显 data 中使用它来确保 fixed-size 对象具有一个 15 个字符C-string 和一个 int)

.seekp().seekg() 的基本问题 - age 不是偏移量

如上面评论所述,当您保存时,除非 D:\data.dat 至少有 age 个对象已存储在文件中,否则您的保存和搜索将尝试移动文件位置超出文件末尾的指示符。如果您在文件不存在时尝试在保存之前进行搜索,这绝对会发生。例如:

    myfile3.seekp (sizeof(data) * age , std::ios::beg);

if (!myfile) { ... } 的情况下,只有一个 data 对象写入文件。这是在这里创建的:

      myfile2.write((char*)&d2 , sizeof(data));

只将 d2 写入文件。

在搜索的情况下,同样的问题也适用。当您阅读 searchage 时,您无法保证文件中至少有那么多 data 个对象。您需要一些方法来确保您有有效的数据可供读取。如评论中所述,确保 seekp()seekg() 成功的一种方法是检查每个调用的 stream-state(return)。参见 std::basic_ostream::seekpseekg() 也是如此)

例如,将单个输出写入文件后,尝试搜索将尝试 seekg() 超出文件末尾:

    myfile.seekg (sizeof(data)* searchage );

检查是否设置了 failbit 会告诉您搜索是否成功。目前,seekg() 在文件末尾留下 read-position 指示符,您什么也看不到。 (但由于您在读取后不检查流状态——您同样不知道它失败了)

(只需注释掉 seekg() 调用即可让您重新读取第一条记录)

要解决 seekg() 的问题,您需要跟踪文件中可用的 data 个对象的数量(比如 n)并且最多只能偏移 (n - 1) * sizeof(data) 字节开始。如果覆盖现有数据,则需要将 seekp() 限制为相同数量,否则只需查找文件末尾以将新对象写入文件。

没有您的样本输入,就无法测试您的文件。但是,修复代码上方的问题确实会创建一个空文件并将 nameage 写入该文件,在注释掉您的 seekg() 调用后将其读回到程序中 d 很好....例如,将您的搜索代码更改为以下针对 seekg()read() 的最低限度验证检查将允许您读取记录号(data 对象号)从你的文件。下面使用 record 代替 searchage

  else if (a == 2) {
    std::fstream myfile (filename , std::ios::binary | std::ios::out | 
                                    std::ios::in);

    data d;
    int record;

    std::cout << "record no. : ";
    if (!(std::cin >> record)) {
      std::cerr << "error: invalid input - record.\n";
      return 1;
    }

    if (!myfile.seekg (sizeof(data)* (record - 1))) {
      std::cerr << "error: seekg() failed.\n";
      return 1;
    }

    if (!myfile.read ((char*)&d , sizeof(data))) {
      std::cerr << "error: read failed.\n";
      return 1;
    }

    myfile.close();

    std::cout << "name : " << d.getname() << '\t' << d.getage() << '\n';
  }

示例Use/Output

将两条记录写入数据文件("Henry", 10"Mike", 20),使用正确的记录偏移量即可毫无问题地检索数据,例如

$ hexdump -Cv dat/nameage.dat
00000000  0a 00 00 00 48 65 6e 72  79 00 f8 2e fd 7e 00 00  |....Henry....~..|
00000010  01 c9 d9 2e 14 00 00 00  4d 69 6b 65 00 5e 7b 17  |........Mike.^{.|
00000020  2c 7f 00 00 01 b9 5c 17                           |,.....\.|
00000028

第一个可以阅读:

$ ./bin/name_age
1 - save
2 - search
chose : 2
record no. : 1
name : Henry    10

第二个是:

$ ./bin/name_age
1 - save
2 - search
chose : 2
record no. : 2
name : Mike     20

尝试读取比现有数据更多的数据,会产生错误,例如

$ ./bin/name_age
1 - save
2 - search
chose : 2
record no. : 3
error: read failed.

工作示例 - 从文件中检索 data 条记录

稍微清理一下代码,这样您只需要打开一个文件并获取以字节为单位的大小来计算文件中可用的 data 记录的数量,这样您就可以防止尝试超出结尾进行查找你的文件,你可以做类似下面的事情。请注意,您必须将编译器语言标准设置为 C++17。另请注意,已知 data 记录的数量——没有理由将空的 data 结构写入文件。您只需在 search 代码中添加检查以检查减少的记录是否可用,如果不可用则处理错误:

/* requires compiling with the C++17 language standard */
#include <iostream>
#include <fstream>
#include <iomanip>
#include <string_view>

#define filename "D:\data.dat"
#define MAXNAME  15

class data {

 private:
  int age;
  char name[MAXNAME];

 public:

  data (std::basic_string_view<char> tempname = "null" , int tempage = 1) : 
  age{tempage} {
    tempname.copy (name, tempname.size());
  }
  
  void setname (std::basic_string_view<char> tempname) {
    int len = tempname.size();
    len = (len < 15 ? len : 14);
    tempname.copy(name , len);
    name[len] = '[=21=]';
  }

  std::string getname() { return name; }
  void setage (int tempage) { age = tempage; }
  int getage() { return age; }
};

int main() {

  int a;
  size_t nrecords = 0;        /* number of records in file */
  data d;

  /* open file in r/w, bin, app mode (a+b), will create if doesn't exist */
  std::fstream myfile (filename, std::ios::out | std::ios::in | 
                                 std::ios::app | std::ios::binary);

  if (!myfile.is_open()) {    /* validate file is open */
    std::cerr << "error: file open/create failed.\n";
    return 1;
  }
  
  myfile.seekp (0, std::ios::end);            /* seek to end */
  nrecords = myfile.tellp() / sizeof(data);   /* get number of records */
  
  std::cout << "File number of records : " << nrecords << "\n\n";
  
  if (!nrecords)  /* if no records, new file */
    std::cout << "creat file \n";   /* optional - your output */
  
  std::cout << "1 - save \n2 - search \nchose : ";  /* menu */
  
  if (!(std::cin >> a) || a < 1 || 2 < a) {   /* validate EVERY input */
    std::cerr << "error: invalid input.\n";
    return 1;
  }

  if (a == 1) {     /* save */
    
    std::string name {};
    int age;

    std::cout << "name : ";
    if (!(std::cin >> name)) {    /* validate EVERY input */
      std::cerr << "error: name not read.\n";
      return 1;
    }

    std::cout << "age : ";
    if (!(std::cin >> age)) {     /* validate EVERY input */
      std::cerr << "error: invalid input - age.\n";
      return 1;
    }

    d.setname(name);          /* set class members */
    d.setage(age);
    
    /* validate EVRY write */
    if (!myfile.write ((char*)&d , sizeof(data))) {
      std::cerr << "error: write failed.\n";
      return 1;
    }

  }
  else if (a == 2) {    /* search */
    
    if (nrecords == 0) {      /* validate records available to search */
      std::cout << "file-empty, nothing to search.\n";
      return 1;
    }
    
    size_t record;            /* record number to read */

    std::cout << "record no. : ";
    if (!(std::cin >> record)) {  /* validate EVERY input */
      std::cerr << "error: invalid input - searchage.\n";
      return 1;
    }
    
    if (record > nrecords) {  /* check requested record in range */
      std::cerr << "error: requested record out-of-range.\n";
      return 1;
    }
    
    /* validate seek from beginning */
    if (!myfile.seekg (sizeof(data) * (record - 1))) {
      std::cerr << "error: seekg() failed.\n";
      return 1;
    }
    
    /* validate EVERY read */
    if (!myfile.read ((char*)&d , sizeof(data))) {
      std::cerr << "error: read failed.\n";
      return 1;
    }

    std::cout << "name : " << d.getname() << '\t' << d.getage() << '\n';
  }
}

一般观察

您需要在阅读后通过检查 stream-state 来验证所有 user-inputs。这可以很简单:

  if (!(std::cin >> a)) {   /* validate EVERY input */
    std::cerr << "error: invalid input.\n";
    return 1;
  }

或者您也可以包括 a 的值检查,例如

  if (!(std::cin >> a) || a < 1 || 2 < a) {   /* validate EVERY input */
    std::cerr << "error: invalid input.\n";
    return 1;
  }

也不鼓励在您的代码中包含整个标准命名空间。参见:Why is “using namespace std;” considered bad practice?。虽然对于运动来说它没有伤害,但要注意应该避免向前移动。

我确定还有其他错误无法在没有输入的情况下进行测试,但上述错误是阻止您的代码正常工作的主要问题。