【问题标题】:How does strchr implementation workstrchr 实现是如何工作的
【发布时间】:2012-12-31 07:43:36
【问题描述】:

我尝试编写自己的 strchr() 方法实现。

现在看起来像这样:

char *mystrchr(const char *s, int c) {
    while (*s != (char) c) {
        if (!*s++) {
            return NULL;
        }
    }
    return (char *)s;
}

最后一行原来是

return s;

但这不起作用,因为 s 是 const。我发现需要有这个演员表(char *),但老实说我不知道​​我在那里做什么:(有人可以解释一下吗?

【问题讨论】:

    标签: c pointers constants strchr


    【解决方案1】:

    我相信这实际上是 C 标准对 strchr() 函数的定义中的一个缺陷。 (我很高兴被证明是错误的。)(回复 cmets,它是否真的是一个缺陷值得商榷;恕我直言,它仍然是糟糕的设计。它可以安全使用,但是太容易了不安全地使用它。)

    这是 C 标准所说的:

    char *strchr(const char *s, int c);
    

    strchr 函数定位 c 的第一次出现 (转换为 char)在 s 指向的字符串中。这 终止空字符被认为是字符串的一部分。

    这意味着这个程序:

    #include <stdio.h>
    #include <string.h>
    
    int main(void) {
        const char *s = "hello";
        char *p = strchr(s, 'l');
        *p = 'L';
        return 0;
    }
    

    尽管它小心地将指向字符串文字的指针定义为指向 const char 的指针,但它具有未定义的行为,因为它修改了字符串文字。至少 gcc 不会对此发出警告,并且程序会因分段错误而死。

    问题在于strchr() 接受const char* 参数,这意味着它承诺不会修改s 指向的数据——但它返回一个普通的char*,它允许调用者修改相同的数据。

    这是另一个例子; 它没有未定义的行为,但是它悄悄地修改了 const 限定的对象,没有任何强制转换(进一步考虑,我认为它具有未定义的行为):

    #include <stdio.h>
    #include <string.h>
    
    int main(void) {
        const char s[] = "hello";
        char *p = strchr(s, 'l');
        *p = 'L';
        printf("s = \"%s\"\n", s);
        return 0;
    }
    

    我认为,这意味着(回答您的问题)strchr() 的 C 实现必须强制转换其结果以将其从 const char* 转换为 char*,或执行等效操作。

    这就是为什么 C++ 在它对 C 标准库所做的少数更改之一中,将 strchr() 替换为两个同名的重载函数:

    const char * strchr ( const char * str, int character );
          char * strchr (       char * str, int character );
    

    当然 C 做不到。

    另一种方法是将strchr 替换为两个函数,一个采用const char* 并返回const char*,另一个采用char* 并返回char*。与 C++ 不同,这两个函数必须有不同的名称,可能是 strchrstrcchr

    (历史上,const 是在 strchr() 已经定义之后添加到 C 中的。这可能是在不破坏现有代码的情况下保留 strchr() 的唯一方法。)

    strchr() 并不是唯一存在此问题的 C 标准库函数。受影响的功能列表(我认为这个列表是完整的,但我不保证它)是:

    void *memchr(const void *s, int c, size_t n);
    char *strchr(const char *s, int c);
    char *strpbrk(const char *s1, const char *s2);
    char *strrchr(const char *s, int c);
    char *strstr(const char *s1, const char *s2);
    

    (全部在&lt;string.h&gt;中声明)和:

    void *bsearch(const void *key, const void *base,
        size_t nmemb, size_t size,
        int (*compar)(const void *, const void *));
    

    (在&lt;stdlib.h&gt; 中声明)。所有这些函数都采用指向数组初始元素的const 数据的指针,并返回指向该数组元素的非const 指针。

    【讨论】:

    • 不确定您的 UB 示例是否与此处相关。 strchr 返回指向第一次出现的 c 的指针。它并不是说您可以修改 strchr 返回的指针的指针。抛弃 const 并通过指针修改 const 字符串并不是 strchr 特有的。 strchr 的签名只是确保字符串不会在 strchr 内被意外修改。
    • 好吧,虽然有人可能会说这是一个“缺陷”,但它仍然是故意这样做的,以便让函数同时处理 const 和非 const 数据。这样做在 C 语言中几乎是惯用的(正如我在回答中所描述的那样)。当然,这种方法存在明显的潜在危险,但只要调用者确保正确使用此函数,它就仍然是“潜在的”。调用者不得允许此函数产生的“抛弃 const”效果传播。如果参数是常量,接收者指针也应该声明为const char *
    • 顺便说一句,这个问题的典型解决方案是返回 size_t 类型的 offset 而不是指针。然后调用者有责任使用偏移量(将其添加到指针或以其他方式),调用者自然可以使类型以 const 安全的方式匹配。
    • @R.. 谢谢,这听起来比“惯用”的方式要少得多:)
    • " 即使它小心地将指向字符串文字的指针定义为 const, " -- 这是错误的。它定义了一个指向 const char 的指针。指向 char 的 const 指针是 char *const str
    【解决方案2】:

    从非修改函数返回指向 const 数据的非 const 指针的做法实际上是一个在 C 语言中相当广泛使用的习语。它并不总是很漂亮,但它已经相当成熟了。

    这里的原理很简单:strchr 本身就是一个非修改操作。然而,对于常量字符串和非常量字符串,我们需要strchr 功能,这也会将输入的常量传播到输出的常量。 C 和 C++ 都没有为这个概念提供任何优雅的支持,这意味着在这两种语言中,您必须编写 两个 几乎相同的函数,以避免冒任何 const 正确性的风险。

    在 C++ 中,您可以通过声明两个具有相同名称的函数来使用函数重载

    const char *strchr(const char *s, int c);
    char *strchr(char *s, int c);
    

    在 C 中你没有函数重载,所以为了在这种情况下完全执行 const 正确性,你必须提供两个具有 不同 名称的函数,类似于

    const char *strchr_c(const char *s, int c);
    char *strchr(char *s, int c);
    

    虽然在某些情况下这可能是正确的做法,但通常(并且理所当然地)认为它过于繁琐并且涉及到 C 标准。您可以通过仅实现一个功能以更紧凑(尽管风险更大)的方式解决这种情况

    char *strchr(const char *s, int c);
    

    它将非常量指针返回到输入字符串中(通过在出口处使用强制转换,就像您所做的那样)。请注意,这种方法不违反语言的任何规则,尽管它为调用者提供了违反它们的方法。通过抛弃数据的常量性,这种方法只是将观察常量正确性的责任从函数本身委托给调用者。只要调用者知道发生了什么并记得“玩得好”,即使用 const 限定的指针指向 const 数据,由此类函数创建的 const 正确性墙中的任何临时漏洞都会立即修复。

    我认为这个技巧是一种完全可以接受的方法,可以减少不必要的代码重复(尤其是在没有函数重载的情况下)。标准库使用它。假设您了解自己在做什么,您也没有理由避免它。

    现在,至于您对strchr 的实现,从风格的角度来看,我觉得它很奇怪。我会使用循环头来遍历我们正在操作的整个范围(完整的字符串),并使用内部的if 来捕获提前终止条件

    for (; *s != '\0'; ++s)
      if (*s == c)
        return (char *) s;
    
    return NULL;
    

    但这样的事情始终是个人喜好问题。有人可能更喜欢只是

    for (; *s != '\0' && *s != c; ++s)
      ;
    
    return *s == c ? (char *) s : NULL;
    

    有人可能会说在函数内部修改函数参数(s)是一种不好的做法。

    【讨论】:

      【解决方案3】:

      const关键字表示不能修改参数。

      您不能直接返回s,因为s 被声明为const char *s,并且函数的返回类型是char *。如果编译器允许您这样做,则可以覆盖 const 限制。

      char* 添加显式强制转换告诉编译器您知道自己在做什么(尽管正如 Eric 解释的那样,如果您不这样做会更好)。

      更新:为了上下文,我引用了 Eric 的回答,因为他似乎已经删除了它:

      你不应该修改 s,因为它是一个 const char *。

      相反,定义一个表示 char * 类型结果的局部变量,并在方法体中使用它代替 s。

      【讨论】:

      • 谢谢.. 但是有没有更好的方法来实现它?只有当我在将 s 分配给临时变量时使用相同的演员表时,Erics 解决方案才有效。就像 "char *temp = s" 仍然会出错,而 "char *temp = (char *)s" 不会
      • @MarcMosby 您可以选择将s 转换为char*,或者将返回类型更改为const char* [或参数类型更改为char*]。
      • @MarcMosby 不是真的。这是标准库中的不一致。
      • 签名已经给了,所以我想我得继续我的实现了
      • 没有。在某些时候,您需要在const char *char * 之间进行显式转换,因为它们可以被视为不同的类型。
      【解决方案4】:

      函数返回值应该是一个指向字符的常量指针:

      strchr 接受 const char* 并且应该也返回 const char*。您正在返回一个具有潜在危险的非常量,因为返回值指向输入字符数组(调用者可能希望常量参数保持不变,但如果它的任何部分作为char * 返回,则它是可修改的指针)。

      如果未找到匹配的字符,则函数返回值应为 NULL:

      如果没有找到寻找的字符,strchr 也应该返回NULL。如果在找不到字符时返回非NULL,或者在这种情况下为s,调用者(如果他认为行为与strchr相同) 可能假设结果中的第一个字符实际上匹配(没有 NULL 返回值 没有办法判断是否匹配)。

      (我不确定你是否打算这样做。)

      以下是执行此操作的函数示例:

      我为此功能编写并运行了几个测试;我添加了一些非常明显的完整性检查以避免潜在的崩溃:

      const char *mystrchr1(const char *s, int c) {
          if (s == NULL) {
              return NULL;
          }
          if ((c > 255) || (c < 0)) {
              return NULL;
          }
          int s_len;
          int i;
          s_len = strlen(s);
          for (i = 0; i < s_len; i++) {
              if ((char) c == s[i]) {
                  return (const char*) &s[i];
              }
          }
          return NULL;
      }
      

      【讨论】:

        【解决方案5】:

        当您编写代码尝试使用mystrchrchar* 结果来修改传递给mystrchr字符串文字 时,您无疑会看到编译器错误。

        修改字符串文字是安全禁忌,因为它可能导致程序异常终止并可能导致拒绝服务攻击。当您将字符串文字传递给采用 char* 的函数时,编译器可能会警告您,但它们不是必需的。

        如何正确使用 strchr?让我们看一个例子。

        这是一个不该做什么的例子:

        #include <stdio.h>
        #include <string.h>
        
        /** Truncate a null-terminated string $str starting at the first occurence 
         *  of a character $c. Return the string after truncating it.
         */
        const char* trunc(const char* str, char c){
          char* pc = strchr(str, c);
          if(pc && *pc && *(pc+1)) *(pc+1)=0;
          return str;
        }
        

        看看它如何通过指针pc修改字符串文字str?那不是bueno。

        这是正确的做法:

        #include <stdio.h>
        #include <string.h>
        
        /** Truncate a null-terminated string $str of $sz bytes starting at the first 
         *  occurrence of a character $c. Write the truncated string to the output buffer 
         *  $out.
         */
        char* trunc(size_t sz, const char* str, char c, char* out){
          char* c_pos = strchr(str, c);
          if(c_pos){
            ptrdiff_t c_idx = c_pos - str;
            if((size_t)n < sz){
              memcpy(out, str, c_idx); // copy out all chars before c
              out[c_idx]=0; // terminate with null byte
            }
          }
           return 0; // strchr couldn't find c, or had serious problems
        }
        

        看看strchr返回的指针是如何用来计算字符串中匹配字符的索引的?然后使用索引(也等于到该点的长度减去一)将字符串的所需部分复制到输出缓冲区。

        你可能会想“噢,这太愚蠢了!我不想使用 strchr,如果它只会让我成为 memcpy。”如果这就是您的感受,我从来没有遇到过 strchrstrrchr 等的用例,而我无法使用 while 循环和 isspaceisalnum 等。有时它实际上比正确使用 strchr 更干净。

        【讨论】:

          猜你喜欢
          • 2021-02-03
          • 1970-01-01
          • 2020-07-31
          • 2020-07-24
          • 2014-12-18
          • 2013-07-04
          • 2018-07-21
          • 2020-11-28
          • 1970-01-01
          相关资源
          最近更新 更多