9

我需要用 C++ 编写一个循环文件​​。程序必须在文件中写入行,当代码达到最大行数时,它必须覆盖文件开头的行。

有人知道吗?

4

13 回答 13

9

不幸的是,您不能在不重写整个文件的情况下截断/覆盖文件开头的行。

新建议

我刚刚想到了一种可能对你有用的新方法......

您可以在具有以下结构的文件中包含一个小标题。

编辑:垃圾,我刚刚描述了一个循环缓冲区的变体!

标题字段

  • Bytes 00 - 07 (long)- 写入文件的总(当前)行数。
  • Bytes 08 - 15 (long)- 指向文件“实际”第一行开头的指针。这最初是标头结束后的字节,但稍后会在数据被覆盖时更改。`
  • Bytes 16 - 23 (long)- 文件“结束部分”的长度。同样,这最初将为零,但稍后会在数据被覆盖时更改。

读取算法(伪代码)

读取整个文件。

读取指向“实际”第一行开头的标题字段
读取指定“结束部分”长度的标题字段
读取每一行直到文件末尾
查找标题结束后的字节
阅读每一行,直到“结束部分”被完全阅读

写算法(伪代码)

将任意数量的新行写入文件。

读取包含总数的标题字段。文件中的行数
如果(行数)+(新行数)<=(最大行数)那么
    在文件末尾追加新行
    将行数的标题字段增加(ne 行数)
别的
    将尽可能多的行(最多)附加到文件末尾
    从指向第一行的指针(在标题字段中)开始,读取仍需要写入的行数
    查找刚刚读取的行的总字节数
    将指向第一行的头字段设置为流中的下一个字节
    继续将新行写入文件末尾,一次一次,直到剩余行的字节数小于文件开头的行的字节数(可能这个条件立即为真,在这种情况下你不需要再写了)
    将剩余的新行写入文件的开头(从头后的字节开始)
    将包含文件“结束部分”长度的标题字段设置为刚刚写入标题之后的字节数。

我完全承认,这不是一个非常简单的算法!尽管如此,我认为它在某种程度上非常优雅。当然,如果有任何不清楚的地方,请告诉我。希望它现在应该完全按照您的意愿行事。

原始建议

现在,如果您保证行的长度是恒定的(以字节为单位),您可以轻松地返回到适当的点并覆盖现有数据。然而,这似乎是一个不太可能的情况。如果您不介意施加您的行必须具有最大长度的限制,并且另外将您写入的每一行填充到该最大长度,那么这可能会让您的事情变得容易。尽管如此,它也有其缺点,例如在某些情况下会大大增加文件大小(即大多数行比最大长度短得多。)这完全取决于情况是否可以接受......

最后,您可能希望考虑利用现有的日志记录系统,具体取决于您的确切目的。

于 2009-05-20T12:48:06.040 回答
8

处理大小不会爆炸的日志的常用方法是使用滚动日志文件,并且每天滚动一次或类似的,并且只保留 N 个最新的文件。

例如,每天您创建一个文件名为“application_2009_05_20.log”的新日志文件,然后开始写入,始终追加。

一旦你有 14 天的日志文件,你就开始删除最旧的。

于 2009-05-20T12:51:08.047 回答
6

由于文件是面向字节的,并且您需要面向行的服务,因此您有两种选择:

  1. 在文件周围实现一个面向行的包装器

  2. 切换到一些面向线路的设备。我最想知道的是:SQLite 有一些不错的 C++ 包装器可用。

于 2009-05-20T13:02:35.480 回答
2

使用循环缓冲区并将缓冲区写入每个添加的文件。

这是一个小而简单的代码大小解决方案。它是一个简单的字符串循环缓冲区,每次添加字符串时,它都会将整个字符串缓冲区写入文件(当然,为单个添加操作写入所有字符串会产生巨大的成本。所以这仅适用于少量字符串)。

循环缓冲区的简单实现,输出到文件:

// GLOBALS ( final implementation should not use globals )
#define MAX_CHARS_PER_LINE (1024)
#define MAX_ITEMS_IN_CIRCULARBUF (4) // must be power of two
char    lineCircBuf[MAX_ITEMS_IN_CIRCULARBUF][MAX_CHARS_PER_LINE];
int     lineCircBuf_add = 0;
int     lineCircBuf_rmv = 0; // not being used right now
uint32_t lineCircBuf_mask = MAX_ITEMS_IN_CIRCULARBUF-1;
char    FILENAME[] = "lineCircBuf.txt";
FILE *  ofp = NULL;

int addLine(char * str) {
    int i;

    // Error checking
    if( strlen(str) > MAX_CHARS_PER_LINE ) {
        return -1; // failure
    }
    if( ofp != NULL) {
        fclose(ofp);
    }

    // Copy string into circular buffer
    strncpy( &(lineCircBuf[lineCircBuf_add][0]),
             str,
             MAX_CHARS_PER_LINE );
    lineCircBuf_add = ( lineCircBuf_add + 1 ) & lineCircBuf_mask;

    // Write to file
    ofp = fopen(FILENAME,"w");
    for( i = 0; i < MAX_ITEMS_IN_CIRCULARBUF-1; i++ ) {
        fprintf( ofp, "%s\n", lineCircBuf[i] );
    }
    fprintf( ofp, "%s", lineCircBuf[i] ); // do not add a newline to the last line b/c we only want N lines in the file

    return 0; // success
}

int removeLine(int index) {
    // not implemented yet
}

void unitTest() {
    int i;

    // Dummy text to demonstrate adding string lines
    char lines[5][MAX_CHARS_PER_LINE] = {
        "Hello world.",
        "Hello world AGAIN.",
        "The world is interesting so far!",
        "The world is not interesting anymore...",
        "Goodbye world."
    };

    // Add lines to circular buffer
    for( i = 0; i < sizeof(lines)/sizeof(lines[0]); i++ ) {
        addLine(&(lines[i][0]));
    }
}

int main() {
    unitTest();
    return 0;
}

所以在上面的例子中,我们有 5 行输入,我们的缓冲区只有 4 行长。因此输出应该只有 4 行,第一行应该被最后一行“再见世界”覆盖。果然输出的第一行确实有“再见世界”:

Goodbye world.
Hello world AGAIN.
The world is interesting so far!
The world is not interesting anymore...
于 2009-05-20T14:43:23.763 回答
1

这会很棘手,因为文件 I/O 使用字节作为底层存储单元,而不是行。

我的意思是你可以 fseek() 回到开头并破坏早期的数据,但我有一种预感,这不是你想要的。

于 2009-05-20T12:48:33.013 回答
1

简单的解决方案:

  1. 有某种线的分隔符。
  2. 每次添加新行时,只需覆盖从当前行开始的所有文本,直到遇到分隔符。
  3. 文件末尾是一种特殊情况,可能有一些填充以保持文件大小不变。

此解决方案旨在提供恒定的文件长度,而不是文件中的恒定行数。行数将随时间变化,具体取决于长度。此解决方案使快速查找特定行号变得更加困难,尽管您可以将一些指标数据粘贴在文件的顶部或底部以使其更容易。

“聪明”的解决方案(上述解决方案的变体):

只需使用有时用于双端队列的相同技巧。只是明确地从文件的开头环绕到结尾,但要跟踪文件的开头/结尾在哪里。当您希望使用不支持该文件的程序读取该文件时,您可以编写一个解包实用程序将该文件转换为标准文件。这个解决方案真的很容易实现,但我更喜欢上面的版本。

丑陋的解决方案:

添加行时,为添加的每一行添加适量的填充。

每次您希望添加新行时,请执行以下操作:

  1. 确定当前行的长度,包括填充。请注意,当前行的开头等于上一行的结尾,不包括填充。
  2. 如果当前行足够长以适合您所在的行,请将其放入。在前一行的末尾添加等于任何多余空间的 1/3 的左填充,以及等于任何多余空间的 2/3 的右填充多余的空间。
  3. 如果当前行的长度不足以容纳您所在的行,请将行移到您前面(吃掉他们的填充物)直到他们有空间。
  4. 如果第 3 步达到某种阈值,则使用更多填充重写整个文件。

请注意,除非您的线条在长度上非常一致,否则这将非常糟糕。一个更简单的解决方案是保证线条的长度恒定(但以某种方式创建多行“线条”,以防超过该长度。

于 2009-05-20T13:03:30.423 回答
1

我已经看到通过将文件的当前写入位置保持在某处来完成此操作。当您需要添加一行时,您会寻找该位置,写入该行并以原子方式更新该位置。如果溢出,则在写入该行之前寻求为零。我们今天为大小受限的循环日志文件执行此操作。在线路约束的基础上做这件事有点奇怪,但可能以类似的方式完成。我们的写循环看起来像:

logFile.lockForWrite();
currentPosition = logFile.getWritePosition();
logFile.seek(currentPosition);
for each line in lineBuffer {
    if ((currentPosition+line.length()) > logFile.getMaxSize()) {
        currentPosition = 0;
        logFile.seek(0);
    }
    logFile.write(line);
    currentPosition += line.length();
}
logFile.setWritePosition(currentPosition);
logFile.unlock();

棘手的部分是保持当前的写入位置tail,并在应用程序写入文件时找到某种方式来协调读取文件(例如,使用实用程序)。您的阅读器实用程序还必须跟踪写入位置,因此它的读取循环变为:

lastPosition = logFile.getWritePosition();
while (!killed) {
    logFile.wait();
    logFile.lockForRead();
    newPosition = logFile.getWritePosition();
    logFile.seek(lastPosition);
    newLine = logFile.readFrom(lastPosition, (newPosition-lastPosition));
    lastPosition = newPosition;
    logFile.unlock();
}

这不是任何特定的语言——它只是伪代码,但想法就在那里。当然,我把处理所有有趣的边缘情况留给了读者。

综上所述……我同意其他意见。除非你有充分的理由,否则不要这样做。这听起来是个好主意,但是:

  • 实现很难写
  • 更难提高效率
  • 由于必须在某处维护写入位置,因此多个实用程序必须就如何读取、更新、初始化等达成一致。
  • 拥有非线性日志会使使用现有工具(如greptailperl等)进行日志处理变得困难。

总体而言,您最好使用一些允许可配置日志文件管理的现有包日志记录包。看看Apache 的 log4cxxPoco 的Poco::Logger.

于 2009-05-20T13:12:34.680 回答
1

如果文件需要是文本文件:
这对于不同的行长度是非常有问题的。你的前两行每行 80 个字符,你如何用 100 个字符的行覆盖它?

如果新行应该替换第一行,这将导致文件插入,这是一个非常昂贵的操作(基本上,需要读取和写入文件的整个其余部分)。除了最少量的数据,您真的不想对所有数据都这样做。

如果这是为了记录目的,请使用滚动日志文件 - 例如每天一个(如 lassevek 建议的那样)。我让它变得更简单:当文件大小超过限制时,旧文件被重命名为 .bak(旧的 .bak 被删除),然后重新开始。使用 1MB 限制,这会保留例如最后的 1 MB,而不会占用超过 2 MB。

您可以对两个或多个文件使用类似的机制。基本上,将“翻转”移动到文件,而不是行。

如果文件可能是专有格式:
使用基本数据库引擎(如建议的 SQLite)或其他结构化存储机制。

于 2009-05-20T14:02:45.063 回答
1

您可以使用log4cxxwith aRollingFileAppender将此信息写入日志文件。当RollingFileAppender日志文件达到一定大小时,它将处理滚动日志文件。我不认为这正是你想要的,但它相当简单——也许它会做。

于 2009-05-20T14:05:54.760 回答
1

只需创建所需大小的文件映射(CreateFileMapping 或 mmap),将行写入缓冲区并在达到最大数量时重新开始。

于 2009-05-20T14:08:56.610 回答
0

简单的解决方法:

  • 定义每行的最大长度为 80 个字符。将较长的“行”包装成几行。
  • 将行标题添加到行。例如“[#589] 这是第 589 行”所以你会知道什么是第一个等。
于 2009-05-20T14:03:37.057 回答
0

如果您想生成此文件以输入到另一个应用程序,我认为您最好的选择是直接登录到关系数据库(SQL Server、MySQL 等),然后根据需要从记录的数据中定期生成该文件.

于 2009-05-20T14:54:46.020 回答
0

为了解决可变大小的问题,您可能最终会得到一个间接和分配方案。这将包括一个间接块,该块具有固定数量的指向文件的“指针”,以及一个环绕 N 的“下一个要写入的”指针。

但主要技巧是添加间接性。

于 2009-05-20T17:46:39.883 回答