【问题标题】:Strings in C: pitfalls and techniquesC 中的字符串:陷阱和技术
【发布时间】:2009-08-17 22:36:47
【问题描述】:

下个月我将指导一个 ACM 团队(去图),现在是时候讨论 C 中的字符串了。除了讨论标准库、strcpystrcmp 等,我还会喜欢给他们一些提示(比如str[0] is equivalent to *str,等等)。

您是否知道任何清单(例如备忘单)或您自己在这件事上的经历?

我已经知道 ACM 竞赛的书籍(这些书籍很好,尤其是 this),但我正在追求交易技巧。

谢谢。

编辑:非常感谢大家。我会接受投票最多的答案,并适当地支持我认为相关的其他答案。我希望在这里做一个总结(就像我做的here,尽快)。我现在有足够的材料,我确信这极大地改善了弦乐课程。再次感谢。

【问题讨论】:

  • 很好。标题已更改。
  • @Hooked:怎么不? a[i] 等价于*(a+i),即a[0] 等价于*(a+0),后者又等价于*a
  • 我的 Google 技能找不到任何与 C 相关的短语“方向引用”。我之前说明的 C 中的数组和指针的等价性是众所周知的——甚至在维基百科上页面——所以我真的不明白你想说什么。
  • @Hooked。您的评论实际上是错误的。你介意删除它,以免混淆其他人吗?
  • a 的第一个元素是否与a 指向的元素不同?否则,听起来你刚才说它们是一样的。

标签: c string


【解决方案1】:

这很明显,但我认为重要的是要知道字符串只是一个字节数组,由零字节分隔。 正如您可能知道的那样,C 字符串并不是那么用户友好。

  • 在字符串的某处写入零字节会截断它。
  • 出界通常以坏结果告终。
  • 永远不要使用 strcpy、strcmp、strcat 等,而是使用它们的安全变体:strncmp、strncat、strndup...
  • 避免使用 strncpy。 strncpy 不会总是零分隔您的字符串!如果源字符串不适合目标缓冲区,它会截断字符串,但不会在缓冲区末尾写入 nul 字节。此外,即使源缓冲区比目标缓冲区小很多,strncpy 仍然会用零覆盖整个缓冲区。我个人使用 strlcpy。
  • 不要使用 printf(string),而是使用 printf("%s", string)。如果用户在字符串中输入 %d,请尝试考虑后果。
  • 你不能用
    if( s1 == s2 )
                doStuff(s1);
    比较字符串 您必须比较字符串中的每个字符。使用 strcmp 或更好的 strncmp。
    if( strncmp( s1, s2, BUFFER_SIZE ) == 0 )
             doStuff(s1);

【讨论】:

  • 如果您真的使用 printf 而不是执行其他操作的包装宏,那么 puts/fputs 就是您要查找的函数。
  • strlcpy() 是标准 C 吗?对于比赛来说,了解这一点可能很重要。如果没有,请准备好写它。此外,如果您可以证明目的地足够长,则 strcpy 等是安全的。
  • 我个人喜欢使用 strncpy,然后将 NUL 写入目标数组的末尾。这样我就知道它没有被覆盖,而且我知道它已经终止了。由于 strlcpy 还不是(据我所知)标准,所以当我在环境之间跳跃时,我不喜欢依赖它。
  • @David Thorley:strlcpy 确实不是标准的,那个白痴 Drepper 拒绝将它放入 glibc。但结果真的很棒,因为我写的strlcpy比strcpy快。我不喜欢 strncpy,因为它会覆盖整个数组,而不是我给出的大小。
  • 请注意,您不能安全地使用strncat(),除非您可以安全地使用memmove()memcpy()。特别是,strncat(target, source, sizeof(target)) 是不正确的,除非您知道 *target == '\0'。使用strncat() 通常是一个错误。
【解决方案2】:

滥用 strlen() 会大大降低性能。

for( int i = 0; i < strlen( string ); i++ ) {
    processChar( string[i] );
}

将至少具有 O(n2) 时间复杂度,而

int length = strlen( string );
for( int i = 0; i < length; i++ ) {
    processChar( string[i] );
}

将具有至少 O(n) 的时间复杂度。对于没有花时间思考的人来说,这并不是那么明显。

【讨论】:

  • 但是编译器不会优化它并且只真正访问一次strlen()函数吗?
  • @jaska 也许会,也许不会——取决于很多因素。标准当然不要求将其优化掉,也不禁止此类优化。
【解决方案3】:

以下函数可用于实现非变异strtok

strcspn(string, delimiters)
strspn(string, delimiters)

第一个找到您传入的分隔符集中的第一个字符。第二个找到您传入的分隔符集中not的第一个字符。

比起strpbrk,我更喜欢这些,因为如果它们不匹配,它们会返回字符串的长度。

【讨论】:

    【解决方案4】:

    str[0] 等价于0[str],或更一般的str[i]i[str]i[str]*(str + i)

    注意

    这不是特定于字符串,但它也适用于 C 数组

    【讨论】:

    • 不过,我认为这并不重要。
    • 3["hello"] 等同于 "hello"[3] 之类的东西虽然是真的,但实际上只是没有人使用过的古怪琐事。
    • 这都是因为加法是可交换的。 x[y] 是 *(x + y) 并且 y[x] 是 *(y + x)
    【解决方案5】:

    stdlib 中的strn* 变体不一定以空值终止目标字符串

    例如:来自 MSDN 的 strncpy 文档:

    strncpy 函数复制 strSource 的初始计数字符 到 strDest 并返回 strDest。 如果 计数小于或等于 strSource 的长度,一个空字符 不会自动附加到 复制的字符串。 如果计数更大 比 strSource 的长度, 目标字符串用 null 填充 不超过长度的字符。

    【讨论】:

    • 其实并不是完整的strn*家族,只有strncpy。 strncat 也有自己的问题。尽管如此,编写 null 并不一定会使您的程序更安全。如果您想传输文件 /etc/passwd-archive/public-data 的内容,但您的数据被 strncpy 截断到 /etc/passwd,该怎么办?
    • 是的,在非托管动态内存环境中安全使用字符串的一般问题本身就是硕士论文。假设你仍然想这样做:)
    【解决方案6】:

    在使用字符串时将strlen()sizeof() 混淆:

    char *p = "hello!!";
    strlen(p) != sizeof(p)
    

    sizeof(p) 在编译时产生指针的大小(4 或 8 个字节),而 strlen(p) 在运行时计算空终止字符数组的长度(本例中为 7)。

    【讨论】:

      【解决方案7】:

      strtok 不是线程安全,因为它使用可变私有缓冲区在调用之间存储数据;你也不能交错或取消strtok 电话。

      更有用的替代方法是strtok_r尽可能使用它

      【讨论】:

      • strtok 是一个来自地狱的函数。用 , 作为分隔符取出这样的字符串 "asdf",,,,"fdsa" 会得到 2 个结果而不是 5 个
      • strtok_r() 可能无法在比赛中使用。但是,如果可以,请避免使用 strtok()。
      【解决方案8】:

      kmm 已经有一个很好的列表。以下是我开始编写 C 代码时遇到的问题。

      1. 字符串文字有一个自己的内存部分并且总是可以访问的。因此,它们可以是例如函数的返回值。

      2. 字符串的内存管理,尤其是高级库(不是 libc)。如果字符串由函数返回或传递给函数,谁负责释放字符串?

      3. 何时应使用“const char *”以及何时使用“char *”。如果函数返回“const char *”,它会告诉我什么。

      所有这些问题都不是太难学,但如果你不被教导,就很难弄清楚。

      【讨论】:

      • 请记住,字符串文字是 const char *,如果您尝试更改它们,这是未定义的行为。
      【解决方案9】:

      我发现char buff[0] 技术非常有用。 考虑:

      struct foo {
         int x;
         char * payload;
      };
      

      struct foo {
         int x;
         char payload[0];
      };
      

      https://stackoverflow.com/questions/295027

      查看链接了解含义和变化

      【讨论】:

        【解决方案10】:

        我会讨论何时以及何时不使用 strcpystrncpy 以及会出现什么问题:

        char *strncpy(char* destination, const char* source, size_t n);
        
        char *strcpy(char* destination, const char* source );
        

        我还要提到 ansi C stdlib 字符串函数的返回值。例如问“这个 if 语句是通过还是失败?”

        if (stricmp("StrInG 1", "string 1")==0)
        {
            .
            .
            .
        }
        

        【讨论】:

        • stricmp() 不是 ANSI C 标准函数,它是 MS VC++ 提供的扩展,可能还有其他一些实现。在 GCC 中,该函数被称为 strcasecmp()(可能有一次我会在某件事上真正站在微软一边),但仍然不是标准的。
        【解决方案11】:

        也许你可以用下面的例子来说明 sentinel '\0' 的价值

        char* a = "你好\0世界"; 字符 b[100]; strcpy(b,a); printf(b);

        曾经在我使用 strcpy() 复制二进制数据时,我的手指被烫伤了。它大部分时间都有效,但有时会神秘地失败。当我意识到二进制输入有时包含一个零字节并且 strcpy() 会在那里终止时,谜团被揭开了。

        【讨论】:

        • 你的手指愈合了吗?&lt;/snark&gt;
        • 哦,那是很久以前的事了.. 从那以后我什至长出了新的 ;-)
        【解决方案12】:

        您可以提及索引寻址。

        元素地址是基地址+索引*元素大小

        【讨论】:

        • 您应该澄清:在 C 数组和指针中,* sizeof(element) 由编译器为您完成,生成的程序集将反映 sizeof(element) 因素。但这与字符串有什么关系? sizeof(char) == 1
        • 编译器会为你做这件事,而 char 的大小恰好是 1 并不意味着实现不重要。
        • sizeof(char) 不会发生为 1 - 它在标准中指定。
        • 你是对的,没有人应该知道这个信息,因为字符是一个字节。
        • 我不是说没关系,我是说跟字符串无关。
        【解决方案13】:

        一个常见的错误是:

        char *p;
        snprintf(p, 3, "%d", 42);
        

        在您使用最多 sizeof(p) 字节之前它一直有效。然后有趣的事情发生了(欢迎来到丛林)。

        说明

        使用 char *p 您正在分配空间以在堆栈上保存指针(sizeof(void*) 字节)。这里正确的做法是分配一个缓冲区或者只是在编译时指定指针的大小:

        char buf[12];
        char *p = buf;
        snprintf(p, sizeof(buf), "%d", 42); 
        

        【讨论】:

        • 您的第一个示例永远不会工作,即使您使用的字节数少于sizeof(*p),因为snprintf 不会将字符串复制到指针中,而是指针 指向的内存到char *pchar p[] 不同。在您的第二个示例中,*p 是多余的,因为可以将buf 直接传递给snprintf 以使代码更清晰。
        • 前一个例子有效,用编译器试试 :) 在后者我知道*p 是多余的,但它的目的是展示“如何分配内存”
        • 之所以有效,是因为*p 在声明时持有一个随机值,因此指向一个可能碰巧可写的随机内存段,从而给您一种错觉,即它在编写小文件时也有效大量的文本,因此当你尝试写太多时它会中断。
        • 另外,刚刚在我的编译器上试了一下。第一个例子:Bus error。 (GCC 4.0,OS X Leopard)
        • 你可以尝试在旧的 UNIX 上使用旧的编译器,macosx 可能会随机化段以尽量减少缓冲区溢出等坏事
        【解决方案14】:

        我会指出过度依赖内置字符串函数的性能缺陷。

        char* triple(char* source)
        {
           int n=strlen(source);
           char* dest=malloc(n*3+1);
           strcpy(dest,src);
           strcat(dest,src);
           strcat(dest,src);
           return dest;
         }
        

        【讨论】:

        • ...以及过早优化的陷阱? :-)
        【解决方案15】:

        指针和数组虽然具有相似的语法,但完全不同。给定:

        字符 a[100]; 字符 *p = a;

        对于数组a,没有指针存储在任何地方。 sizeof(a) != sizeof(p),对于数组,它是内存块的大小,对于指针,它是指针的大小。如果你使用类似的东西,这变得很重要:sizeof(a)/sizeof(a[0])。此外,您不能 ++a,并且可以将指针设为指向“const”字符的“const”指针,但数组只能是“const”字符,在这种情况下,您将首先对其进行初始化。等等等等等等

        【讨论】:

          【解决方案16】:

          如果可能,使用 strlcpy(而不是 strncpy)和 strlcat。

          更好的是,为了让生活更安全,您可以使用如下宏:

          #define strlcpy_sz(dst, src) (strlcpy(dst, src, sizeof(dst)))
          

          【讨论】:

            猜你喜欢
            • 2011-01-11
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-10-31
            • 1970-01-01
            • 2013-08-15
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多