用C++编写循环文件

9
我需要在c++中编写一个循环文件。程序必须将行写入文件,当代码达到最大行数时,它必须覆盖文件开头的行。
有任何想法吗?

3
为什么需要实施这个?你试图解决什么实际问题? - anon
我需要在文件中编写标识符列表。每个列表占据一行。但不能超过最大行数,超出时应删除最旧的行。另一个可能的解决方案是创建一个窗口文件。这样就可以删除第一行并将新行添加到末尾。你知道如何删除文件的第一行吗? - Kram
3
抱歉,您还没有说出您想要解决的问题 - 您已经勾勒出了一种可能的(而且在我看来是不好的)解决方案。 - anon
1
文件是否必须是文本文件?固定行长度可行吗? - peterchen
它必须立即写入文件吗?维护循环缓冲区并将其写出是一回事。写出循环文件则是另一个更加混乱的问题。从文件前面删除通常涉及复制文件的其余部分。简而言之,如果这是作业,那么你应该说出来;如果这是一个真正糟糕的想法,那么你应该告诉我们你想做什么,以便我们可以帮助你;或者有一个合法的原因,我们真的需要知道。 - David Thornley
显示剩余2条评论
13个回答

9

很遗憾,你不能在不重写整个文件的情况下截断/覆盖文件开头的行。

新建议

我刚想到一个新方法,可能能解决你的问题...

你可以向文件中添加一个小标题,其结构如下。

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

标题字段

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

读取算法(伪代码)

读取整个文件。

读取指向文件“实际”第一行的标题字段
读取指定“结尾部分”长度的标题字段
读取文件末尾之前的每一行
定位到标题结束后的下一个字节
读取每行,直到完全读取“结尾部分”

写入算法(伪代码)

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

读取包含文件总行数的标题字段
如果(行数)+(新行数)≤(最大行数),则
    将新行附加到文件末尾
    将线路计数的标题字段递增(按新行数)
否则
    将尽可能多的行(最多为最大值)附加到文件末尾
    从指向第一行的标题字段开始,读取仍需编写的行数
    查找刚刚读取的行的总字节数
    将指向第一行的标题字段设置为流中的下一个字节
    继续将新行写入文件末尾,每次一个,直到剩余行的字节计数小于文件开头行的字节计数(可能立即满足此条件,在这种情况下,您不需要再写入任何内容)
    将剩余的新行写入文件开头(从标题后的下一个字节开始)
    将包含文件“结尾部分”长度的标题字段设置为刚刚写入标题后的字节数。

我承认,这不是一个非常简单的算法!然而,我仍然认为它在某种程度上相当优雅。当然,如果有任何不清楚的地方,请告诉我。希望现在它能够完全满足你的需求。

原始建议

现在,如果你的行保证长度恒定(按字节计),那么你可以很容易地回到合适的位置并覆盖现有数据。然而,这似乎是一个相当不太可能的情况。如果你不介意强制要求你的行必须具有最大长度,并且额外填充每个写入此最大长度的行,那么这可能会让事情变得容易。但是,它也有其缺点,例如在某些情况下大幅增加文件大小(即大多数行远比最大长度短)。是否可以接受取决于具体情况...

最后,你可能想考虑利用现有的日志记录系统,这取决于你确切的目的。


这些行的大小非常不稳定 :( 同时感谢您的回复 :) - Kram
@Kram:没问题。:) 请查看我的更新答案,另有一种方法。 - Noldorin
如果您使用固定大小的行(例如每行80个字节),则可以使用file = fopen(...,"rb+"),然后fseek(file,line_no*80,SEEK_SET),接着进行一些memcpy/strcpy等操作,最后使用fwrite(charbuffer,80,file)写入文件。 - KitsuneYMG

8
通常处理不会爆炸式增长的日志记录的方法是使用滚动日志文件,并在一天或类似时间滚动它们,只保留最新的N个文件。
例如,每天创建一个名为“application_2009_05_20.log”的新日志文件,并开始写入它,始终进行追加。
一旦您拥有14天的日志文件,就开始删除最旧的日志文件。

大多数Linux系统都有一个叫做“logrotate”的实用程序,可以为您完成此操作。也许可以利用它来非常简单地实现这个目标。 - rmeador

6

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

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

  2. 切换到某些面向行的设备。我脑海中首先想到的是:SQLite有一些不错的C++包装器可用。


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"覆盖。不出所料,输出的第一行确实包含了"Goodbye world":

Goodbye world.
Hello world AGAIN.
The world is interesting so far!
The world is not interesting anymore...

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's log4cxxPoco的 Poco::Logger


1

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

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


1

简单解决方案:

  1. 为每行设置某种分隔符。
  2. 每次添加新行时,只需覆盖当前行及其之后的所有文本,直到遇到分隔符为止。
  3. 文件末尾是一个特殊情况,可能会有一些填充以保持文件大小恒定。

此解决方案旨在提供恒定的文件长度,而不是文件中恒定数量的行。行数将随时间而变化,取决于长度。这个解决方案使得快速查找特定行号变得更加困难,但您可以在文件顶部或底部放置一些指示数据,以使这更容易。

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

只需使用有时用于双端队列的相同技巧即可。从文件开头明确地绕到结尾,但要跟踪文件的开头/结尾位置。您可以编写一个取消包装实用程序,将此文件转换为标准文件,以便使用不支持它的程序读取它。这个解决方案非常容易实现,但我更喜欢上面的版本。

丑陋的解决方案:

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

每当您想添加一行新内容时,请按照以下步骤进行:

  1. 确定当前行的长度,包括填充。请注意,当前行的开头等于上一行的结尾(不包括填充)。
  2. 如果当前行足够长,可以放进去。在上一行末尾添加左填充,其大小等于多余空间的1/3,并添加右填充,其大小等于多余空间的2/3。
  3. 如果当前行的长度超出了所在行的长度,则将前面的行向前移动(将它们的填充删除),直到有足够的空间容纳该行。
  4. 如果第3步达到某种阈值,则用更多的填充重新编写整个文件。

请注意,除非您的行长度相当一致,否则这种方法的效果可能会很差。更简单的解决方案是保证所有行的长度都相同(但要提供某种方式来创建多行“行”,以防您超过了长度限制)。


1

如果文件需要是文本文件:
这会带来很多问题,因为每行的长度都不同。你的前两行都有80个字符,如何用100个字符的行覆盖它们?

如果新行应该替换第一行,那么这将导致文件插入,这是一种非常昂贵的操作(基本上需要读取和写入文件的剩余部分)。除了最小量的数据外,你真的不想这样做。

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

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

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


1
你可以使用log4cxxRollingFileAppender来将这些信息写入日志文件中。当日志文件达到一定大小时,RollingFileAppender会自动进行滚动。我不认为这完全符合你的要求,但它相当简单,也许可以胜任。

1

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接