我需要用 C++ 编写一个循环文件。程序必须在文件中写入行,当代码达到最大行数时,它必须覆盖文件开头的行。
有人知道吗?
不幸的是,您不能在不重写整个文件的情况下截断/覆盖文件开头的行。
我刚刚想到了一种可能对你有用的新方法......
您可以在具有以下结构的文件中包含一个小标题。
编辑:垃圾,我刚刚描述了一个循环缓冲区的变体!
标题字段
Bytes 00 - 07 (long)
- 写入文件的总(当前)行数。Bytes 08 - 15 (long)
- 指向文件“实际”第一行开头的指针。这最初是标头结束后的字节,但稍后会在数据被覆盖时更改。`Bytes 16 - 23 (long)
- 文件“结束部分”的长度。同样,这最初将为零,但稍后会在数据被覆盖时更改。读取算法(伪代码)
读取整个文件。
读取指向“实际”第一行开头的标题字段 读取指定“结束部分”长度的标题字段 读取每一行直到文件末尾 查找标题结束后的字节 阅读每一行,直到“结束部分”被完全阅读
写算法(伪代码)
将任意数量的新行写入文件。
读取包含总数的标题字段。文件中的行数 如果(行数)+(新行数)<=(最大行数)那么 在文件末尾追加新行 将行数的标题字段增加(ne 行数) 别的 将尽可能多的行(最多)附加到文件末尾 从指向第一行的指针(在标题字段中)开始,读取仍需要写入的行数 查找刚刚读取的行的总字节数 将指向第一行的头字段设置为流中的下一个字节 继续将新行写入文件末尾,一次一次,直到剩余行的字节数小于文件开头的行的字节数(可能这个条件立即为真,在这种情况下你不需要再写了) 将剩余的新行写入文件的开头(从头后的字节开始) 将包含文件“结束部分”长度的标题字段设置为刚刚写入标题之后的字节数。
我完全承认,这不是一个非常简单的算法!尽管如此,我认为它在某种程度上非常优雅。当然,如果有任何不清楚的地方,请告诉我。希望它现在应该完全按照您的意愿行事。
现在,如果您保证行的长度是恒定的(以字节为单位),您可以轻松地返回到适当的点并覆盖现有数据。然而,这似乎是一个不太可能的情况。如果您不介意施加您的行必须具有最大长度的限制,并且另外将您写入的每一行填充到该最大长度,那么这可能会让您的事情变得容易。尽管如此,它也有其缺点,例如在某些情况下会大大增加文件大小(即大多数行比最大长度短得多。)这完全取决于情况是否可以接受......
最后,您可能希望考虑利用现有的日志记录系统,具体取决于您的确切目的。
处理大小不会爆炸的日志的常用方法是使用滚动日志文件,并且每天滚动一次或类似的,并且只保留 N 个最新的文件。
例如,每天您创建一个文件名为“application_2009_05_20.log”的新日志文件,然后开始写入,始终追加。
一旦你有 14 天的日志文件,你就开始删除最旧的。
由于文件是面向字节的,并且您需要面向行的服务,因此您有两种选择:
在文件周围实现一个面向行的包装器
切换到一些面向线路的设备。我最想知道的是:SQLite 有一些不错的 C++ 包装器可用。
使用循环缓冲区并将缓冲区写入每个添加的文件。
这是一个小而简单的代码大小解决方案。它是一个简单的字符串循环缓冲区,每次添加字符串时,它都会将整个字符串缓冲区写入文件(当然,为单个添加操作写入所有字符串会产生巨大的成本。所以这仅适用于少量字符串)。
循环缓冲区的简单实现,输出到文件:
// 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...
这会很棘手,因为文件 I/O 使用字节作为底层存储单元,而不是行。
我的意思是你可以 fseek() 回到开头并破坏早期的数据,但我有一种预感,这不是你想要的。
简单的解决方案:
此解决方案旨在提供恒定的文件长度,而不是文件中的恒定行数。行数将随时间变化,具体取决于长度。此解决方案使快速查找特定行号变得更加困难,尽管您可以将一些指标数据粘贴在文件的顶部或底部以使其更容易。
“聪明”的解决方案(上述解决方案的变体):
只需使用有时用于双端队列的相同技巧。只是明确地从文件的开头环绕到结尾,但要跟踪文件的开头/结尾在哪里。当您希望使用不支持该文件的程序读取该文件时,您可以编写一个解包实用程序将该文件转换为标准文件。这个解决方案真的很容易实现,但我更喜欢上面的版本。
丑陋的解决方案:
添加行时,为添加的每一行添加适量的填充。
每次您希望添加新行时,请执行以下操作:
请注意,除非您的线条在长度上非常一致,否则这将非常糟糕。一个更简单的解决方案是保证线条的长度恒定(但以某种方式创建多行“线条”,以防超过该长度。
我已经看到通过将文件的当前写入位置保持在某处来完成此操作。当您需要添加一行时,您会寻找该位置,写入该行并以原子方式更新该位置。如果溢出,则在写入该行之前寻求为零。我们今天为大小受限的循环日志文件执行此操作。在线路约束的基础上做这件事有点奇怪,但可能以类似的方式完成。我们的写循环看起来像:
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();
}
这不是任何特定的语言——它只是伪代码,但想法就在那里。当然,我把处理所有有趣的边缘情况留给了读者。
综上所述……我同意其他意见。除非你有充分的理由,否则不要这样做。这听起来是个好主意,但是:
grep
、tail
、perl
等)进行日志处理变得困难。总体而言,您最好使用一些允许可配置日志文件管理的现有包日志记录包。看看Apache 的 log4cxx或Poco 的Poco::Logger
.
如果文件需要是文本文件:
这对于不同的行长度是非常有问题的。你的前两行每行 80 个字符,你如何用 100 个字符的行覆盖它?
如果新行应该替换第一行,这将导致文件插入,这是一个非常昂贵的操作(基本上,需要读取和写入文件的整个其余部分)。除了最少量的数据,您真的不想对所有数据都这样做。
如果这是为了记录目的,请使用滚动日志文件 - 例如每天一个(如 lassevek 建议的那样)。我让它变得更简单:当文件大小超过限制时,旧文件被重命名为 .bak(旧的 .bak 被删除),然后重新开始。使用 1MB 限制,这会保留例如最后的 1 MB,而不会占用超过 2 MB。
您可以对两个或多个文件使用类似的机制。基本上,将“翻转”移动到文件,而不是行。
如果文件可能是专有格式:
使用基本数据库引擎(如建议的 SQLite)或其他结构化存储机制。
您可以使用log4cxx
with aRollingFileAppender
将此信息写入日志文件。当RollingFileAppender
日志文件达到一定大小时,它将处理滚动日志文件。我不认为这正是你想要的,但它相当简单——也许它会做。
只需创建所需大小的文件映射(CreateFileMapping 或 mmap),将行写入缓冲区并在达到最大数量时重新开始。
简单的解决方法:
如果您想生成此文件以输入到另一个应用程序,我认为您最好的选择是直接登录到关系数据库(SQL Server、MySQL 等),然后根据需要从记录的数据中定期生成该文件.
为了解决可变大小的问题,您可能最终会得到一个间接和分配方案。这将包括一个间接块,该块具有固定数量的指向文件的“指针”,以及一个环绕 N 的“下一个要写入的”指针。
但主要技巧是添加间接性。