【问题标题】:Properly delimiting strings in C在 C 中正确分隔字符串
【发布时间】:2010-11-04 01:52:50
【问题描述】:

我想知道,什么是分隔基本上可以包含任何字符的字符串的好/有效的方法。因此,例如,我需要连接 n 个字符串,如下所示:

char *str_1 = "foo; for|* 1.234+\"@!`";
char *str_n = "bar; for|* 1.234+%\"@`";

对于最终字符串为:

char *str_final = "foo; for|* 1.234+\"@!`bar; for|* 1.234+%\"@`"; // split?

我可以使用哪个分隔符来正确拆分它?

请注意,要连接的字符串可能超过 2 个。

我愿意接受建议。

谢谢

【问题讨论】:

    标签: c concatenation delimiter


    【解决方案1】:

    因为我的cmets越来越长,这里有一个完整的答案:

    您的char * 缓冲区应该在前 X 个字节中存储字符串的长度(就像 Pascal 的做法一样)。在该长度之后是字符串数据,它可以包含您喜欢的任何字符。之后,接下来的 X 个字节告诉您 next 字符串的长度。以此类推,直到结尾,由空字符串分隔(即最后 X 个字节声称下一个字符串的长度为零,您的应用程序将此作为停止寻找更多字符串的信号)。

    一个好处是您不需要扫描字符串数据 - 从第一个字符串的开头查找下一个字符串需要 O(1) 时间,查找列表中有多少个字符串需要 O(n ) 时间,但仍然会非常快(如果 O(n) 不可接受,您可以解决这个问题,但我认为现在不值得这样做)。

    另一个好处是字符串数据可以包含任何你喜欢的字符。这可能是一个骗局——如果你的字符串可能包含 NUL 字符,你可以安全地提取它,但你必须小心不要将它传递给 C 字符串函数(如 strlen()strcat()),它会看到NUL 字符作为数据的结尾(它可能是也可能不是)。您将不得不依赖 memcpy() 和指针算法。

    问题在于 X 的值(用于存储字符串长度的字节数)。最简单的是 1,它将绕过所有字节顺序和对齐问题,但会将您的字符串限制为 255 个字符。如果这是您可以忍受的限制,那太好了,但 255 对我来说似乎有点低。

    X 可以是 2 或 4 个字节,但您需要确保您的(无符号)数据类型至少有那么多字节(stdint.huint16_tuint32_t,或者可能是 @ 987654328@ 或 uint_least32_t)。更好的解决方案是创建X = sizeof(size_t),因为size_t 类型保证能够存储您想要存储的任何字符串的长度。

    拥有X > 1 引入对齐,如果网络可移植性是一个问题,则引入字节序。将前 X 个字节读取为 size_t 变量的最简单方法是将您的 char * 数据转换为 size_t * 并取消引用。但是,除非您可以保证您的 char * 数据正确对齐,否则这将在某些系统上中断。即使您确实保证了 char * 数据的对齐,您也必须在大多数字符串的末尾浪费一些字节来确保下一个字符串的长度值是对齐的。

    克服对齐的最简单方法是将第一个 sizeof(size_t) 字节手动转换为 size_t 值。您必须决定是否要以小端或大端方式存储数据。大多数计算机本机都是 little-endian,但对于手动转换,这无关紧要 - 只需选择一个。数字 65537 (2 ^ 16 + 2) 存储在 4 个字节中,大端,看起来像 { 0, 1, 0, 2 };小端,{ 2, 0, 1, 0 }.

    一旦你决定了(没关系,选择你喜欢的那个),你只需将数据的前 X 个点投射到 unsigned chars,然后投射到 size_t,然后进行位移通过适当的指数将它们放在适当的位置,然后将它们加在一起。在上面的例子中,0 将乘以 2 ^ 32、1 乘以 2 ^ 16、0 乘以 2 ^ 8、2 乘以 2 ^ 0(或 1),得到 0 + 65536 + 0 + 2 或 65537。可能有如果您进行手动转换,大端和小端之间的效率差异将为零 - 我想(再次)指出,据我所知,选择完全是任意的。

    进行手动转换避免了对齐问题,并且完全绕过了对跨系统字节序的担忧,因此从小端计算机传输到大端计算机的数据将被读取相同。数据从sizeof(size_t) == 4 的系统传输到sizeof(size_t) == 8 的系统仍然存在潜在问题。如果这是一个问题,您可以 a) 抛弃 size_t 并选择一个不变的大小,或者 b) 将发送者的 sizeof(size_t) 的值编码(您只需要一个字节)作为数据的第一个字节,并让接收器进行任何必要的调整。选择 a) 可能更容易,但可能会导致问题(如果您选择的尺寸太小而无法容纳网络上的旧计算机,并且随着它们被逐步淘汰,您开始没有空间来存储您的数据?),所以我更喜欢选择 b),因为它可以随您运行的任何系统(16 位、32 位、64 位,甚至未来 128 位)进行扩展,但您可能不需要这种努力.

    </vomit>我把它留给读者来整理我刚刚写的所有乱七八糟的东西。

    【讨论】:

      【解决方案2】:

      也许您可以对字符串的长度进行编码,然后在每个字符串前面加上一个特殊字符?这样您就不必担心接下来的 N 个字符中有哪些字符。将每个子字符串也以空值终止可能是个好主意。

      这种方法的一个优点是您可以非常快速地解析字符串。

      编辑:更好的方法是使用 Chris 在下面的评论中建议的前 2-4 个字节,而不是编码长度 + 特殊字符。

      【讨论】:

      • 您可以将长度编码为恒定大小(如 2 或 4 个字节)以消除特殊字符。
      • @GWW - 我会说使用 sizeof(size_t) 字节以获得最大的正确性,但这将取决于平台,并且如果(这可能是一个很大的)数据被写入要共享的文件对于其他计算机,这种方法是不可移植的(尽管它可能会工作一段时间)。使用sizeof(size_t) 是我会做的,但它需要一些额外的工作才能使其正确可移植(即文件中的第一个数据字节编码sizeof(size_t) 的实际值,因此读取应用程序可以调整来自不同的数据大小的系统),但它可能不值得(OP 的电话)。
      • (我怎么会忘记这个!?)另外,决定你的尺寸是小端还是大端对于便携性也很重要。您可能不应该只是将其转换为 short * 并读取整数,因为这会产生大量的可移植性问题(以及潜在的对齐问题)。
      • @GWW 为什么他们不只是扩展字节大小以一劳永逸地杀死字节序? :)
      • 应该注意,转换为short *size_t * 等以访问嵌入的长度整数是未定义的行为。从实际的角度来看,它可能会在不支持非对齐访问的机器上中断。其他一些问题是这种方法会生成嵌入的空字节(但仅限于某些密钥大小),这可能会导致无法立即检测到的错误,当然还有字节序问题。如果你想嵌入尺寸,你真的需要想出一个干净、定义明确的编码来避免这些问题,而不是使用快速破解。
      【解决方案3】:

      一种选择是使用空字符作为分隔符并使用双空字符终止列表。的字符串。它看起来像这样:

      const char* str_final = "foo; for|* 1.234+\"@!`\0bar; for|* 1.234+%\"@`\0";
                                           delimiter ^             delimiter ^ 
      

      Raymond Chen 很好地概述了双空终止字符串in a blog post. 它被 Windows API 中的多个函数使用。

      【讨论】:

        【解决方案4】:

        如果您知道您的字符串将始终是有效的 UTF-8 文本(或 ASCII),您可以使用不能出现在有效 UTF-8(或 ASCII)中的字节作为分隔符。在 UTF-8 中,字节 C0、C1、F5、F6、F7、F8、F9、FA、FB、FC、FD、FE 和 FF 无效。在 ASCII 中,任何设置高位的字节都是无效的。

        【讨论】:

        • FF 和 FE 可能出现在字节顺序掩码 (en.wikipedia.org/wiki/Byte_order_mark)
        • @pmg:错误。 U+FEFF 可能会出现,但在不包含字节 FE 和 FF 的 UTF-8 中。它在 UTF-8 中的表示是EF BB BF
        • 猜你从未见过混合 UTF-8 文件 :-) 但你是对的,字节不应该永远不存在。
        • “混合 UTF-8 文件”不是 UTF-8。 :-) 见我回答的第一句话。也许我应该在那里强调“有效”这个词..
        【解决方案5】:

        一种解决方案是选择转义字符和分隔符。通常,反斜杠 \ 用作转义字符,但这可能会导致混淆,因为它已经是字符串文字的转义字符。选择真的无关紧要,让我们将正斜杠/ 作为转义符,将分号; 作为分隔符。理想情况下选择两个最不可能出现在您的字符串中的字符。

        当你连接字符串时,第一步是在未编码的字符串中搜索两个字符并用转义版本替换它们:

        str1 = "foo;bar;baz";
        str2 = "foo/bar/baz";
        

        变成

        estr1 = "foo/;bar/;baz";
        estr2 = "foo//bar//baz";
        

        然后将它们与分隔符连接起来:

        res = "foo/;bar/;baz;foo//bar//baz";
        

        就是这样。拆分是通过搜索分隔符没有前导转义字符,然后将单个字符串中的转义字符替换回未转义的版本来完成的。

        如果您想使用等待单个零终止字符串的函数来处理字符串,这是一个不错的选择,例如使用str 函数或使用printf 函数打印它们。如果您可以保证只有您自己的函数才能使用这些字符串,那么提到的用零分隔 \0 会更有效,特别是因为您实际上不需要拆分它,您可以使用指向完整字符串的指针来使用 strprintf 函数时使用其中的单个部分字符串。

        【讨论】:

          【解决方案6】:

          2 个想法:

          1) 使用标准的“转义”方法,类似于在 C 中定义 char* 文字。

          2) 使用一个'\0' 字符作为分隔符,其中两个作为字符串结束标记。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-07-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-04-18
            • 2012-03-01
            • 2018-05-05
            相关资源
            最近更新 更多