20

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

现在看起来像这样:

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

最后一行原来是

return s;

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

4

5 回答 5

22

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

以下是 C 标准所说的:

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

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

这意味着这个程序:

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

int main(void) {
    const char *s = "hello";
    char *p = strchr(s, 'l');
    *p = 'L';
    return 0;
}

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

问题是strchr()接受一个const char*参数,这意味着它承诺不修改s指向的数据——但它返回一个 plain 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;
}

我认为,这意味着(回答您的问题)C 实现strchr()必须将其结果转换const char*char*,或做一些等效的事情。

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

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

当然C不能这样做。

另一种方法是strchr用两个函数替换,一个接受 aconst char*并返回 a const char*,另一个接受 achar*并返回 a char*。与 C++ 不同,这两个函数必须有不同的名称,也许strchrstrcchr.

(从历史上看,在已经定义const之后被添加到 C中。这可能是在不破坏现有代码的情况下保持不变的唯一方法。)strchr()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);

(全部在 中声明<string.h>)和:

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

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

于 2013-01-16T21:26:33.837 回答
14

从非修改函数返回指向 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内部修改函数参数 ( ) 是一种不好的做法。

于 2013-01-17T01:03:45.187 回答
1

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

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

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

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

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

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

于 2013-01-16T21:04:10.913 回答
0

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

strchr接受 aconst 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;
}
于 2013-01-16T21:15:01.160 回答
0

毫无疑问,每当您编写代码尝试使用 的char*结果mystrchr来修改传递mystrchr.

修改字符串文字是一种安全禁忌,因为它可能导致程序异常终止并可能导致拒绝服务攻击。当您将字符串文字传递给函数 take 时,编译器可能会警告您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;
}

看看它如何str通过指针修改字符串文字pc?那不是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。” 如果这就是您的感受,那么我从来没有遇到过使用 while 循环和 ,strchrstrrchr无法摆脱的用例。有时它实际上比正确使用 strchr 更干净。isspaceisalnum

于 2021-10-15T09:53:38.293 回答