如何在迭代文件的同时从文件中读取指定数量的字符?

How do I read in a specified number of characters from a file while still iterating through it?

我有这样一个数据文件:

    Judy Henn      2 Oaklyn Road       Saturday 2001
    Norman Malnark 15 Manor Drive      Saturday 2500
    Rita Fish      210 Sunbury Road    Friday   750

我需要分配前 20 个字符作为名称,接下来的 20 个字符作为地址,接下来的 10 个字符作为日期,数字作为 yardSize,使用 istream::get() 方法。我的教授要求使用 .get() 来完成此操作。

我真的很难弄清楚如何在仍在循环的同时将文件中的数据分配给正确的变量。

struct Customer{
    char name[21];
    char address[21];
    char day[11];
    int yardSize;
};

int main(){
    const int arrSize = 50;
    Customer custArr[arrSize];
    int i = 0;
    
    //set up file
    ifstream dataFile;
    dataFile.open("Data.txt");
       
    //try to open file
    if(!dataFile){
        cout << "couldn't open file";
    }
       
    //while dataFile hasn't ended
    while(!dataFile.eof()){
        dataFile.get(custArr[i].name, 21);   
        cout << custArr[i].name;
        i++;
    }
}; //end  

我原以为 while 循环会将前 21 个字符分配给 custArr[i].name,然后一遍又一遍地循环直到文件末尾。但是,当我打印出 custArr[i].name 时,我得到了这个并且只有这个:

Judy Henn           2 Oaklyn Road       Saturday   2001

我不确定如何将指定数量的字符分配给变量,同时仍然遍历整个文件。

首先,您提到的字符数与您显示的数据文件不匹配。姓名只能使用 19 个字符,而不是 20 个。并且当天只能使用 9 个字符,而不是 10 个。

修复后,您的代码仍然有问题,因为它只读入 Customer::name 字段。所以它将尝试将 Judy Henn 读入 custArr[0].name,然后将 2 Oaklyn Road 读入 custArr[1].name,然后将 Saturday 读入 custArr[2].name,依此类推。

我会建议更像这样的东西:

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
using namespace std;

struct Customer
{
    char name[21];
    char address[21];
    char day[11];
    int yardSize;
};

int main()
{
    const int arrSize = 50;
    Customer custArr[arrSize];
    string line;
    int i = 0;
    
    //set up file
    ifstream dataFile("Data.txt");
    if (!dataFile)
    {
        cout << "couldn't open file";
        return 0;
    }
       
    //while dataFile hasn't ended
    while ((i < arrSize) && getline(dataFile, line))
    {
        istringstream iss(line);
        if (iss.get(custArr[i].name, 21) &&
            iss.get(custArr[i].address, 21) &&
            iss.get(custArr[i].day, 11) &&
            iss >> custArr[i].yardSize)
        {
            cout << custArr[i].name;
            ++i;
        }
    }

    return 0;
}

读取固定宽度(大型机类型)记录并不是 C++ 专门编写的。虽然 C++ 提供了丰富的字符串操作函数,但读取固定宽度的记录仍然是您必须使用基本的 I/O 函数自己组合起来的东西。

除了使用 @RemyLebeau, a similar approach using std::vector<Customer> instead of an array of customers eliminates bounds concerns. By using a std::vector 的出色答案而不是数组之外,您还可以调整代码以根据需要读取尽可能多的记录(不超过物理内存的限制),而不必担心添加超出数组范围的信息。

此外,如当前所写,您在每个数组中保留前导和尾随的白色space。例如,您的 name 数组将包含 " Judy Henn " 而不仅仅是 "Judy Henn"。通常,您总是希望 trim 前导和尾随白色 space 来自您存储为变量的内容。否则,当您使用存储的字符时,每次使用内容时都必须以某种方式处理 whitespace 。虽然 std::string 提供了许多方法,您可以使用这些方法来 trim 前导和尾随白色 space,但您对普通旧 char[] 的使用将需要手动删除。

将代码添加到 trim Customer 集合中的字符数组中多余的前导和尾随白色space 可以编写如下。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <cstring>

#define NAMLEN  20      /* if you need a constant, #define one (or more)  */
#define ADDRLEN 21      /* (these marking the fixed-widths of the fields) */
#define DAYLEN  10

struct Customer {
   char name[21];
   char address[21];
   char day[11];
   int yardSize;
};

int main (int argc, char **argv) {
    
    if (argc < 2) { /* validate at least one argument given for filename */
        std::cerr << "error: insufficient no. of arguments\n"
                     "usage: " << argv[0] << " <filename>\n";
        return 1;
    }
    
    std::string line {};                    /* string to hold each line read from file */
    std::vector<Customer> customers {};     /* vector of Customer struct */
    std::ifstream f (argv[1]);              /* file stream (filename in 1st arg) */
    
    if (!f.is_open()) { /* validate file open for reading */
        std::cerr << "error: file open failed '" << argv[1] << "'.\n"
                  << "usage: " << argv[0] << " <filename>\n";
        return 1;
    }
    
    while (getline (f, line)) {             /* read each line into line */
        std::stringstream ss (line);        /* create stringstream from line */
        Customer tmp {};                    /* declare temporary instance */
        char *p;                            /* pointer to trim leading ws from name */
        size_t wslen;                       /* whitespace len to use in trim */
        
        ss.get (tmp.name, NAMLEN);          /* read up to NAMLEN chars from ss */
        if (ss.gcount() != NAMLEN - 1) {    /* validate gcount()-1 chars read */
            std::cerr << "error: invalid format for name.\n";
            continue;
        }
        for (int i = NAMLEN - 2; tmp.name[i] == ' '; i--)   /* loop from end of name */
            tmp.name[i] = 0;                        /* overwrite spaces with nul-char */
        for (p = tmp.name; *p == ' '; p++) {}       /* count leading spaces */
        wslen = strlen (p);                         /* get remaining length */
        memmove (tmp.name, p, wslen + 1);           /* move name to front of array */
        
        ss.get (tmp.address, ADDRLEN);      /* read up to ADDRLEN chars from ss */
        if (ss.gcount() != ADDRLEN - 1) {   /* validate gcount()-1 chars read */
            std::cerr << "error: invalid format for address.\n";
            continue;
        }
        for (int i = ADDRLEN - 2; tmp.address[i] == ' '; i--)/* loop from end of name */
            tmp.address[i] = 0;                     /* overwrite spaces with nul-char */
        
        ss.get (tmp.day, DAYLEN);           /* read up to DAYLEN chars from ss */
        if (ss.gcount() != DAYLEN - 1) {    /* validate gcount()-1 chars read */
            std::cerr << "error: invalid format for day.\n";
            continue;
        }
        for (int i = DAYLEN - 2; tmp.day[i] == ' '; i--)    /* loop from end of name */
            tmp.day[i] = 0;                         /* overwrite spaces with nul-char */
        
        if (!(ss >> tmp.yardSize)) {        /* extract final int value from ss */
            std::cerr << "error: invalid format for yardSize.\n";
            continue;
        }
        
        customers.push_back(tmp);           /* add temp to vector */
    }
    
    for (Customer c : customers)    /* output information */
        std::cout << "\n'" << c.name << "'\n'" << c.address << "'\n'" << 
                    c.day << "'\n'" << c.yardSize << "'\n";
}

(注意: 程序希望在命令行上提供要读取的文件名作为第一个参数。您可以更改提供文件名的方式以满足您的需要,但你不应该硬编码文件名或在你的代码中使用 MagicNumbers。你不应该为了从另一个文件名读取而重新编译你的程序)

另请注意,在 for() 循环 trimming whitespace 中,您正在处理基于 0 的索引而不是基于 1 的字符计数,这就是为什么您正在使用 gcount() - 1 或字符总数减去两个,例如NAMLEN - 2 从数组中的最后一个字符开始循环。

删除尾随的白色space只是从每个数组末尾的每个字符串中的最后一个字符循环回到开头,用nul-覆盖每个space终止 字符。从name开始trim前导whitespace,统计whitespace字符的个数,然后用Cmemmove()把名字移回开头数组的

例子Use/Output

$ ./bin/read_customer_day_get dat/customer_day_get.txt

'Judy Henn'
'2 Oaklyn Road'
'Saturday'
'2001'

'Norman Malnark'
'15 Manor Drive'
'Saturday'
'2500'

'Rita Fish'
'210 Sunbury Road'
'Friday'
'750'

每个值的输出都包含在单引号中,以提供视觉确认,即 name 字段已删除前导和尾随白色 space,而 addressday 都删除了尾随白色space。