在C程序中将文本文件作为char[]包含进来

166
有没有一种方法可以在编译时将整个文本文件作为字符串包含在 C 程序中?
类似于:
  • file.txt:

    This is
    a little
    text file
    
    #include <stdio.h>
    int main(void) {
       #blackmagicinclude("file.txt", content)
       /*
       equiv: char[] content = "This is\na little\ntext file";
       */
       printf("%s", content);
    }
    

我想获取一个简单的程序,在标准输出上打印"This is a little text file"。

目前使用了一个不太优雅的Python脚本,而且只能限定一个变量名称,你能告诉我另一种方法吗?


在这里查看将文件读入char[]的方法。https://dev59.com/YXRC5IYBdhLWcg3wFdJx 这里有一些关于使用C预处理器宏的技巧。http://gcc.gnu.org/onlinedocs/cpp/Macros.html - Daniel A. White
3
为什么你想这样做?为什么不在运行时读取文件?(答案:可能是因为在运行时很难知道文件在哪里,或者因为只应该有一个文件需要安装。) - Jonathan Leffler
1
或者,文件可能仅在编译时可用,例如源代码。 - TMS
4
有时候在开发中,您希望将数据作为单独的文件进行访问,但是在编译成二进制文件时将其内容编译进去。例如,在Arduino上运行Web服务器时,由于没有本地存储,您希望将html文件分开保存以便编辑,但在编译时它们需要存在于源代码中作为字符串。 - Geordie
C23将拥有#embed,这听起来会对此非常有用。 - starball
22个回答

151

我建议使用(Unix 实用程序)xxd 来完成此操作。 你可以像这样使用它。

$ echo hello world > a
$ xxd -i a

输出:

unsigned char a[] = {
  0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x0a
};
unsigned int a_len = 12;

28
注意:由xxd创建的char[]没有以NULL结尾!因此我这样做:$ xxd -i < file.txt > file.xxd $ echo ', 0' >> file.xxd在main.c中:char file_content[] = { #include "file.xxd" }; - ZeD
3
我从未听说过xxd。太棒了! - anon
4
嵌入 GLSL 着色器时,这非常有用。 - linello
6
给 xxd 生成的 C 代码添加 0x00 结尾的另一种方法:xxd -i file.txt | sed 's/\([0-9a-f]\)$/\0, 0x00/' > file.h - vleo
1
是否有像 xxd -i 这样的程序,可以输出转义(可读)的 C 字符串文字,而不是字节数组? - Justin Meiners
显示剩余5条评论

138
这个问题是关于C语言的,但是如果有人想使用C++11,那么只需对包含的文本文件进行少量更改即可,感谢新的原始字符串字面值
在C++中,请执行以下操作:
const char *s =
#include "test.txt"
;

在文本文件中执行以下操作:
R"(Line 1
Line 2
Line 3
Line 4
Line 5
Line 6)"

所以文件顶部只能有前缀,在文件末尾只能有后缀。在其中间,您可以随意操作,只要您不需要字符序列 )" 就不需要进行特殊的转义。但是,如果您指定自定义分隔符,即使这也可以工作。
R"=====(Line 1
Line 2
Line 3
Now you can use "( and )" in the text file, too.
Line 5
Line 6)====="

8
谢谢,我选择了这里提出的方法将长的 [tag:SQL] 片段嵌入到我的 C++ 11 代码中。这使我可以将 SQL 独立地保存在它自己的文件中,并使用适当的语法检查、高亮等进行编辑。 - Isac Casapu
4
这非常接近我想要的东西,特别是用户定义的分隔符,非常有用。但我希望更进一步:是否有方法可以从您想要包含的文件中完全删除前缀 R"( 和后缀 ) "?我尝试使用两个文件bra.in和ket.in,它们中都有前缀和后缀,先将bra.in、file.txt和ket.in依次包含进来。但编译器在包含下一个文件之前会评估bra.in的内容(只是R"()),因此会出现错误。如果有人知道如何从file.txt中去掉前缀和后缀,请告诉我。谢谢。 - TMS
我猜C++不允许R"(<newline>#include...)"吧?最好的方式是在编译时将文件摄入,而无需任何编码...例如直接使用json、xml、csv等。 - Brian Chrisman
如果您使用1+R"...作为起始分隔符,而不是R"...,并在Line 1之前添加一个换行符,那么您可以使原始文本的文本更易读。这将把表达式从数组转换为指针,但这里并不是真正的问题,因为您正在初始化指针,而不是数组。 - Ruslan
1
@Brian Chrisman 在GCC中似乎可以正常工作。 - 0xB00B
我曾使用这种方法将JS嵌入到C++中。我曾担心代码编辑器会将整个文件解释为单个字符串,因为它以R"开头,但事实并非如此,因为在第一个换行符之后,它只是一个“未终止的字符串文字”,文件的其余部分被正确处理。 - Jan

32

我喜欢kayahr的回答。但是如果你不想动输入文件,并且如果你使用的是CMake,你可以在文件中添加分隔符字符序列。例如,下面的CMake代码将复制输入文件并相应地包装它们的内容:

function(make_includable input_file output_file)
    file(READ ${input_file} content)
    set(delim "for_c++_include")
    set(content "R\"${delim}(\n${content})${delim}\"")
    file(WRITE ${output_file} "${content}")
endfunction(make_includable)

# Use like
make_includable(external/shaders/cool.frag generated/cool.frag)

然后在C++代码中像这样包含:

constexpr char *test =
#include "generated/cool.frag"
;

14

你有两种选择:

  1. 利用编译器/链接器扩展将一个文件转换为二进制文件,使其具有指向二进制数据开头和结尾的正确符号。请参考这个回答:使用GNU ld链接脚本包含二进制文件
  2. 将你的文件转换为一系列字符常量,可以初始化一个数组。注意你不能只写 "" 并跨多行。你需要使用行继续字符 (\)、转义 " 字符等方法使其正常工作。更容易的方式是编写一个小程序将字节转换为类似于 '\xFF', '\xAB', ...., '\0' 的序列(或者如果您有可用的unix工具xxd,可以使用另一个答案中描述的方法):

代码:

#include <stdio.h>

int main() {
    int c;
    while((c = fgetc(stdin)) != EOF) {
        printf("'\\x%X',", (unsigned)c);
    }
    printf("'\\0'"); // put terminating zero
}

(未经测试)。然后执行:

char my_file[] = {
#include "data.h"
};

data.h由何生成?

cat file.bin | ./bin2c > data.h

1
最后一行应该是“cat file.bin | ./bin2c > data.h”或“./bin2c < file.bin > data.h”。 - Hasturkun
我使用了http://www.codeproject.com/Tips/845393/Convert-a-Binary-File-to-a-Hex-Encoded-Text-File来从二进制文件创建一个十六进制文件(在Windows上),然后使用了您的建议:“char my_file[] = { #include my_large_file.h };” 谢谢! - Someone Somewhere
bin2c 不是来自 Debian 的 hxtools 中的 bin2c,请注意。 - ThorSummoner
如果是这样的话,那么调用就更奇怪了:bin2c -H myoutput.h myinput1.txt myinputN.txt - ThorSummoner

11

你可以使用objcopy来完成此操作:

objcopy --input binary --output elf64-x86-64 myfile.txt myfile.o

现在您有一个对象文件,可以将其链接到可执行文件中,其中包含来自myfile.txt的内容的开头、结尾和大小的符号。


4
你能告诉我们符号的名称将是什么吗? - Mark Ch
根据文档,符号名称是从输入文件名生成的。@MarkCh - John Zwinck
我猜这个在非x86-64架构的机器上不会工作,对吗? - ThorSummoner
1
只有输出格式说明符elf64-x86-64不会将生成的二进制文件绑定到特定的架构(这就是为什么file命令显示“no machine”的原因)。 - ThorSummoner

9

好的,受到Daemin帖子的启发,我测试了以下简单的例子:

a.data:

"this is test\n file\n"

test.c:

int main(void)
{
    char *test = 
#include "a.data"
    ;
    return 0;
}

gcc -E test.c 输出:


# 1 "test.c"
# 1 "<built-in>"
# 1 "<command line>"
# 1 "test.c"

int main(void)
{
    char *test =
# 1 "a.data" 1
"this is test\n file\n"
# 6 "test.c" 2
    ;
    return 0;
}

所以它正在工作,但需要用引号括起来的数据。

这就是我在答案的最后一部分所暗示的。 - Dominik Grabiec
引号,或者它被称为什么,不好意思我的英语。 - Ilya
1
这需要对数据进行C转义。我认为这不是帖子所要寻找的内容。如果这有某种包含宏,可以对文件内容进行C转义,那就没问题了。 - Brian Chrisman

9
如果你愿意使用一些奇技淫巧,你可以在某些类型的文件中使用原始字符串字面量和 #include 进行创造性操作。例如,假设我想在项目中包含一些 SQLite 的 SQL 脚本,并且希望获得语法高亮,但不想要任何特殊的构建基础设施。我可以有一个名为 test.sql 的文件,其中包含对 SQLite 有效的 SQL,其中 -- 开始一个注释。
--x, R"(--
SELECT * from TestTable
WHERE field = 5
--)"

然后在我的 C++ 代码中,可以有以下内容:

int main()
{
    auto x = 0;
    const char* mysql = (
#include "test.sql"
    );

    cout << mysql << endl;
}

输出结果为:
--
SELECT * from TestTable
WHERE field = 5
--

或者,您可以从一个名为test.py的文件中包含一些 Python 代码,该文件是有效的 Python 脚本(因为在 Python 中#表示注释符号,而pass是无操作):
#define pass R"(
pass
def myfunc():
    print("Some Python code")

myfunc()
#undef pass
#define pass )"
pass

然后在C++代码中:

int main()
{
    const char* mypython = (
#include "test.py"
    );

    cout << mypython << endl;
}

这将输出:

pass
def myfunc():
    print("Some Python code")

myfunc()
#undef pass
#define pass

你可以尝试类似的技巧来将其他类型的代码作为字符串包含在其中。无论这是否是一个好主意,我都不确定。这是一种非常巧妙的黑客技巧,但可能不适合实际生产代码。对于周末黑客项目来说,可能还可以接受。


我也用过这种方法将OpenGL着色器放入文本文件中! - yano
嗯,着色器文本文件仍然应该有以R和双引号开头和结尾的"非着色器"行。 - x4444
我们可以使用"const char* mysql {"而不是"const char* mysql = (" - VSCode分析器更喜欢第一种选项。 - x4444
我们可以使用"const char* mysql {"而不是"const char* mysql = (" - VSCode分析器更喜欢第一种选项。 - undefined

3
您可以使用汇编语言来实现这个功能:
asm("fileData:    .incbin \"filename.ext\"");
asm("fileDataEnd: db 0x00");

extern char fileData[];
extern char fileDataEnd[];
const int fileDataSize = fileDataEnd - fileData + 1;

整洁的回答,你能提供一些额外的支持来说明如何使用它吗?被复制的文件是什么?复制过程是否对文件内容敏感,即特定的特殊字符会引起问题吗? - undefined

2
我用Python3重新实现了xxd,并修复了xxd的所有烦恼:
  • 常量正确性
  • 字符串长度数据类型:int → size_t
  • 空终止符(如果需要的话)
  • C字符串兼容:在数组上去掉无符号unsigned
  • 更小、更易读的输出,就像您编写的那样:可打印的ASCII字符按原样输出;其他字节以十六进制编码。

这是脚本,通过自身过滤,所以您可以看到它的功能:

pyxxd.c

#include <stddef.h>

extern const char pyxxd[];
extern const size_t pyxxd_len;

const char pyxxd[] =
"#!/usr/bin/env python3\n"
"\n"
"import sys\n"
"import re\n"
"\n"
"def is_printable_ascii(byte):\n"
"    return byte >= ord(' ') and byte <= ord('~')\n"
"\n"
"def needs_escaping(byte):\n"
"    return byte == ord('\\\"') or byte == ord('\\\\')\n"
"\n"
"def stringify_nibble(nibble):\n"
"    if nibble < 10:\n"
"        return chr(nibble + ord('0'))\n"
"    return chr(nibble - 10 + ord('a'))\n"
"\n"
"def write_byte(of, byte):\n"
"    if is_printable_ascii(byte):\n"
"        if needs_escaping(byte):\n"
"            of.write('\\\\')\n"
"        of.write(chr(byte))\n"
"    elif byte == ord('\\n'):\n"
"        of.write('\\\\n\"\\n\"')\n"
"    else:\n"
"        of.write('\\\\x')\n"
"        of.write(stringify_nibble(byte >> 4))\n"
"        of.write(stringify_nibble(byte & 0xf))\n"
"\n"
"def mk_valid_identifier(s):\n"
"    s = re.sub('^[^_a-z]', '_', s)\n"
"    s = re.sub('[^_a-z0-9]', '_', s)\n"
"    return s\n"
"\n"
"def main():\n"
"    # `xxd -i` compatibility\n"
"    if len(sys.argv) != 4 or sys.argv[1] != \"-i\":\n"
"        print(\"Usage: xxd -i infile outfile\")\n"
"        exit(2)\n"
"\n"
"    with open(sys.argv[2], \"rb\") as infile:\n"
"        with open(sys.argv[3], \"w\") as outfile:\n"
"\n"
"            identifier = mk_valid_identifier(sys.argv[2]);\n"
"            outfile.write('#include <stddef.h>\\n\\n');\n"
"            outfile.write('extern const char {}[];\\n'.format(identifier));\n"
"            outfile.write('extern const size_t {}_len;\\n\\n'.format(identifier));\n"
"            outfile.write('const char {}[] =\\n\"'.format(identifier));\n"
"\n"
"            while True:\n"
"                byte = infile.read(1)\n"
"                if byte == b\"\":\n"
"                    break\n"
"                write_byte(outfile, ord(byte))\n"
"\n"
"            outfile.write('\";\\n\\n');\n"
"            outfile.write('const size_t {}_len = sizeof({}) - 1;\\n'.format(identifier, identifier));\n"
"\n"
"if __name__ == '__main__':\n"
"    main()\n"
"";

const size_t pyxxd_len = sizeof(pyxxd) - 1;

使用方法(提取脚本):

#include <stdio.h>

extern const char pyxxd[];
extern const size_t pyxxd_len;

int main()
{
    fwrite(pyxxd, 1, pyxxd_len, stdout);
}

2
为什么不将文本链接到程序中并将其用作全局变量!这里有一个例子。 我正在考虑使用这种方法在可执行文件中包含Open GL着色器文件,因为GL着色器需要在运行时编译为GPU。

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