如何在C语言中将文件内容读取为字符串?

141

在C语言中,最简单的打开文件并将其内容读取到字符串(char*,char[]或其他形式)中的方法是什么(最少出错,最少代码行数,无论您如何解释)?


13
“最简单的方法”和“最不容易出错的方法”通常是彼此相反的。 - Andy Lester
30
在我看来,“simplest way” 和 “least error prone” 实际上是同义词。例如,在 C# 中的答案是 string s = File.ReadAllText(filename);。这怎么可能更简单和更少出错呢? - Mark Lakata
13个回答

187

我倾向于将整个缓冲区作为原始内存块加载到内存中,并自行进行解析。这样,我可以更好地控制标准库在多个平台上的操作。

这是我用于此的存根。您可能还想检查fseek、ftell和fread的错误代码。(出于清晰起见而省略)

char * buffer = 0;
long length;
FILE * f = fopen (filename, "rb");

if (f)
{
  fseek (f, 0, SEEK_END);
  length = ftell (f);
  fseek (f, 0, SEEK_SET);
  buffer = malloc (length);
  if (buffer)
  {
    fread (buffer, 1, length, f);
  }
  fclose (f);
}

if (buffer)
{
  // start to process your data / extract strings here...
}

4
我会检查 fread 的返回值,因为由于错误等原因,它可能并没有实际读取整个文件。 - freespace
9
就像rmeador所说,fseek在大于4GB的文件上会失败。 - KPexEA
7
没错,对于大文件来说,这个解决方案很糟糕。 - Nils Pipenbrinck
47
由于这是一个落地页,我想指出 fread 不会给你的字符串加上结尾零。这可能会导致一些麻烦。 - ivan-k
32
正如@Manbroski所说,缓冲区需要以'\0'结尾。因此,我会更改 buffer = malloc(length + 1); 并在 fclose 之后添加 buffer[length] = '\0'; (经过Valgrind验证)。 - soywod
显示剩余10条评论

42

还有一种不幸的依赖于操作系统的解决方案是将文件映射到内存。通常情况下,这样做可以提高读取性能,并且由于应用程序视图和操作系统文件缓存可以共享物理内存,因此可以降低内存使用率。

POSIX代码如下:

int fd = open("filename", O_RDONLY);
int len = lseek(fd, 0, SEEK_END);
void *data = mmap(0, len, PROT_READ, MAP_PRIVATE, fd, 0);

另一方面,Windows相对复杂一些,不幸的是,我面前没有编译器可以测试,但功能由CreateFileMapping()MapViewOfFile()提供。


5
别忘了检查这些系统调用的返回值! - Toby Speight
3
调用lseek()函数时,必须使用off_t而不是int。 - ivan.ukr
3
请注意,如果目标是在内存中稳定地捕获文件的内容,以便在给定时间点上使用,除非您确定在映射将被使用的时间间隔内读入内存的文件不会被其他进程修改,否则应避免使用此解决方案。有关更多信息,请参见此帖子 - user001

21
如果“将其内容读入字符串”意味着文件不包含代码为0的字符,则还可以使用getdelim()函数,该函数接受一块内存并在必要时重新分配它,或者只为您分配整个缓冲区,并将文件读入其中,直到遇到指定的分隔符或文件结束。只需将'\ 0'作为分隔符传递以读取整个文件。
此函数可在GNU C库中使用,http://www.gnu.org/software/libc/manual/html_mono/libc.html#index-getdelim-994 示例代码可能看起来很简单。
char* buffer = NULL;
size_t len;
ssize_t bytes_read = getdelim( &buffer, &len, '\0', fp);
if ( bytes_read != -1) {
  /* Success, now the entire file is in the buffer */

1
我以前用过这个!它非常好用,假设你读取的文件是文本(不包含\0)。 - ephemient
不错!在读取整个文本文件时,可以节省很多问题。现在,如果有一种类似的超级简单的方法可以读取二进制文件流直到EOF而不需要任何定界字符,那就太好了! - anthony

10

如果你正在阅读类似标准输入(stdin)或管道(pipe)这样的特殊文件,使用fstat无法预先获取文件大小。此外,如果你正在读取二进制文件,由于内嵌的'\\0'字符,fgets将丢失字符串大小信息。因此,最好的读取文件的方式是使用read和realloc:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main () {
    char buf[4096];
    ssize_t n;
    char *str = NULL;
    size_t len = 0;
    while (n = read(STDIN_FILENO, buf, sizeof buf)) {
        if (n < 0) {
            if (errno == EAGAIN)
                continue;
            perror("read");
            break;
        }
        str = realloc(str, len + n + 1);
        memcpy(str + len, buf, n);
        len += n;
        str[len] = '\0';
    }
    printf("%.*s\n", len, str);
    return 0;
}

4
这是O(n^2)的时间复杂度,其中n是您的文件长度。所有得票比这更高的解决方案都是O(n)的。请不要在实践中使用此解决方案,或者使用一个具有乘法增长的修改版本。 - Clark Gaebel
3
realloc()函数可以在不复制旧内存到新大块内存的情况下扩展现有内存到新的大小。只有在调用malloc()之间有干涉时,它才需要移动内存并使解决方案O(n^2)。在这里,realloc()的调用之间没有发生malloc()的调用,因此这个解决方案应该是可行的。 - Jake
3
你可以直接读取 "str" 缓冲区(带有适当的偏移量),而无需从中间缓冲区 "buf" 复制。然而,这种技术通常会过度分配文件内容所需的内存。此外,请注意二进制文件,printf 不会正确处理它们,你可能也不想打印二进制文件! - anthony

6
注意:这是对上面被接受的答案的修改。
下面是一种方法,包含错误检查。
我添加了一个大小检查器,在文件大于1 GiB时退出。我这样做是因为程序将整个文件放入字符串中,这可能会使用太多内存并导致计算机崩溃。然而,如果你不在意这个问题,可以从代码中删除它。
#include <stdio.h>
#include <stdlib.h>

#define FILE_OK 0
#define FILE_NOT_EXIST 1
#define FILE_TOO_LARGE 2
#define FILE_READ_ERROR 3

char * c_read_file(const char * f_name, int * err, size_t * f_size) {
    char * buffer;
    size_t length;
    FILE * f = fopen(f_name, "rb");
    size_t read_length;
    
    if (f) {
        fseek(f, 0, SEEK_END);
        length = ftell(f);
        fseek(f, 0, SEEK_SET);
        
        // 1 GiB; best not to load a whole large file in one string
        if (length > 1073741824) {
            *err = FILE_TOO_LARGE;
            
            return NULL;
        }
        
        buffer = (char *)malloc(length + 1);
        
        if (length) {
            read_length = fread(buffer, 1, length, f);
            
            if (length != read_length) {
                 free(buffer);
                 *err = FILE_READ_ERROR;

                 return NULL;
            }
        }
        
        fclose(f);
        
        *err = FILE_OK;
        buffer[length] = '\0';
        *f_size = length;
    }
    else {
        *err = FILE_NOT_EXIST;
        
        return NULL;
    }
    
    return buffer;
}

同时检查错误:

int err;
size_t f_size;
char * f_data;

f_data = c_read_file("test.txt", &err, &f_size);

if (err) {
    // process error
}
else {
    // process data
    free(f_data);
}

2
只有一个问题:您使用 malloc(length + 1) 分配的 buffer 没有被释放。这是由该方法的使用者来处理,还是不需要调用 free() 来释放已分配的内存? - Pablosproject
1
如果没有发生错误,应该调用free(f_data);。感谢指出。 - Joe Cool
2
你在 FILE_TO_LARGE 中拼写了 "too" 错误。 - user11171

5
什么是在C语言中打开文件并将其内容读入字符串的最简单方法(最少错误,最少代码行数,无论你如何解释)...?
可悲的是,即使过了这么多年,答案仍然容易出错,许多人缺乏正确的字符串格式和错误检查。
#include <stdio.h>
#include <stdlib.h>

// Read the file into allocated memory.
// Return NULL on error.
char* readfile(FILE *f) {
  // f invalid? fseek() fail?
  if (f == NULL || fseek(f, 0, SEEK_END)) {
    return NULL;
  }

  long length = ftell(f);
  rewind(f);
  // Did ftell() fail?  Is the length too long?
  if (length == -1 || (unsigned long) length >= SIZE_MAX) {
    return NULL;
  }

  // Convert from long to size_t
  size_t ulength = (size_t) length;
  char *buffer = malloc(ulength + 1);
  // Allocation failed? Read incomplete?
  if (buffer == NULL || fread(buffer, 1, ulength, f) != ulength) {
    free(buffer);
    return NULL;
  }
  buffer[ulength] = '\0'; // Now buffer points to a string

  return buffer;
}

请注意,如果文本文件中包含空字符,分配的数据将包含所有文件数据,但字符串看起来会很短。更好的代码还应返回长度信息,以便调用者可以处理。
char* readfile(FILE *f, size_t *ulength_ptr) {
  ...
  if (ulength_ptr) *ulength_ptr == *ulength;
  ...
} 

在分配字符串时,请确保在完成后释放返回的指针。

4
如果文件是文本格式,而你想逐行获取文本内容,最简单的方法就是使用fgets()函数。
char buffer[100];
FILE *fp = fopen("filename", "r");                 // do not use "rb"
while (fgets(buffer, sizeof(buffer), fp)) {
... do something
}
fclose(fp);

3

刚刚对上面接受的答案进行了修改。

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

char *readFile(char *filename) {
    FILE *f = fopen(filename, "rt");
    assert(f);
    fseek(f, 0, SEEK_END);
    long length = ftell(f);
    fseek(f, 0, SEEK_SET);
    char *buffer = (char *) malloc(length + 1);
    buffer[length] = '\0';
    fread(buffer, 1, length, f);
    fclose(f);
    return buffer;
}

int main() {
    char *content = readFile("../hello.txt");
    printf("%s", content);
}

这不是C代码。问题没有标记为C++。 - Gerhardh
@Gerhardh 在我编辑时对这个问题的快速回应,已经是九年前的事情了!虽然函数部分是纯C,但是我的答案可能无法在C上运行,对此我感到抱歉。 - BaiJiFeiLong
这个古老的问题被列在活跃问题的顶部。我没有搜索它。 - Gerhardh
2
这段代码存在内存泄漏问题,请不要忘记释放你使用 malloc 分配的内存 :) - ericcurtin

3

如果你正在使用 glib,那么你可以使用 g_file_get_contents 方法。

gchar *contents;
GError *err = NULL;

g_file_get_contents ("foo.txt", &contents, NULL, &err);
g_assert ((contents == NULL && err != NULL) || (contents != NULL && err == NULL));
if (err != NULL)
  {
    // Report error to user, and free error
    g_assert (contents == NULL);
    fprintf (stderr, "Unable to read file: %s\n", err->message);
    g_error_free (err);
  }
else
  {
    // Use file contents
    g_assert (contents != NULL);
  }
}

1
// Assumes the file exists and will seg. fault otherwise.
const GLchar *load_shader_source(char *filename) {
  FILE *file = fopen(filename, "r");             // open 
  fseek(file, 0L, SEEK_END);                     // find the end
  size_t size = ftell(file);                     // get the size in bytes
  GLchar *shaderSource = calloc(1, size);        // allocate enough bytes
  rewind(file);                                  // go back to file beginning
  fread(shaderSource, size, sizeof(char), file); // read each char into ourblock
  fclose(file);                                  // close the stream
  return shaderSource;
}

这是一种相当简单的解决方案,因为没有检查null值。

这个程序只能处理磁盘文件,对于命名管道、标准输入或网络流会失败。 - anthony
哈,这也是我来这里的原因!但我认为你需要将字符串以空字符结尾,或者返回可选的glShaderSource长度。 - Ciro Santilli OurBigBook.com

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