strchr的实现方式是什么?

21

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

现在看起来像这样:

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

最后一行原本是

return s;

但是这并不起作用,因为s是const类型。我发现需要进行这种强制转换(char*),但是我真的不知道我在做什么:( 有人能解释一下吗?

5个回答

23

我认为这实际上是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;
}
即使它将指针定义为指向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;
}

我的理解是,(回答你的问题)C语言中的strchr()实现必须将其结果转换为char*类型,以将其从const 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

(历史上,在定义strchr()之后才添加了const关键字以保持向后兼容性。这可能是保留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指针。


不确定您的 UB 示例在这里是否相关。strchr 返回指向 c 的第一个出现位置的指针。它并没有说您可以修改 strchr 返回的指针所指向的内容。去掉 const 并通过指针修改 const 字符串并不是 strchr 特有的。strchr 的签名仅确保字符串不会在 strchr 内部意外修改。 - P.P
5
尽管有人可能认为这是一个“缺陷”,但这仍然是故意设计的,以便允许函数与const和非const数据一起使用。这种做法在C语言中被广泛采用(正如我在我的答案中所描述的)。当然,这种方法存在明显的潜在危险,但只要调用者确保正确使用该函数,它仍然只是“潜在”的。调用者不能允许该函数产生“消除const”的效果进行传递。如果参数是常量,则接收方指针也应声明为const char * - AnT stands with Russia
2
顺便提一下,这个问题的规范解决方案是返回一个 size_t 类型的 偏移量 而不是指针。然后调用者有责任使用该偏移量(将其添加到指针或以其他方式使用),并且调用者自然可以以 const 安全的方式使类型匹配。 - R.. GitHub STOP HELPING ICE
@R.. 谢谢,这听起来比“惯用”的方式好多了 :) - Navin
1
尽管它仔细地将指针定义为const字符串,但这是错误的。它定义了一个指向const char的指针。指向char的const指针应该是 char *const str - DeftlyHacked
@DeftlyHacked:谢谢,已修复。(我知道我的意思,只是表达方式有些马虎。) - Keith Thompson

14

在 C 语言中,从非修改函数返回指向 const 数据的非 const 指针实际上是一种很常用的惯用法,它并不总是很优美,但已经相当成熟。

这里的理由很简单:`strchr` 本身是一种非修改操作。然而我们需要 `strchr` 的功能来处理常量字符串和非常量字符串,同时也要将输入的 const 属性传递到输出的 const 属性中。C 和 C++ 都没有提供任何优雅的支持此概念,这意味着在两种语言中,你都必须编写两个几乎完全相同的函数,以避免出现 const-correctness 的风险。

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

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

C语言没有函数重载,因此在这种情况下要完全执行const-correctness, 你必须提供两个具有不同名称的函数,类似于:

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正确性的责任从函数本身委托给调用者。 只要调用者知道正在发生什么并记得“友好地玩耍”,即使用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)是一种不好的做法。


1

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”则不会。 - Marc
3
你有选择的余地,要么将变量s转换为char*类型,要么将返回值改为const char*类型[或将参数类型改为char*]。 - Daniel Fischer
1
@MarcMosby 不完全是这样。这是标准库中的一种不一致性。 - user529758
好的,既然已经给出了签名,那我想我就得继续使用我的实现方法。 - Marc
1
不。在某些时候,你需要显式地转换 const char* 和 char* 之间的类型,因为它们可以被视为不同的类型。 - Alberto Miranda
@Marc Mosby:无论如何,总有一点需要进行强制类型转换。最好在结束时而不是开始时进行转换。strchr的实现是非修改性的,这就是为什么保持整个函数的常量性是可能(也更好)的原因。像您在原始代码中所做的那样,在最后删除常量性。 - AnT stands with Russia

0

每当您编写试图使用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 的?这可不好。

以下是正确的方法:

#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返回的指针是如何用于计算字符串中匹配字符的索引的吗?该索引(也等于到那个点的长度减一)然后用于将所需部分的字符串复制到输出缓冲区。

你可能会觉得:“哦,这很愚蠢!如果它只是让我memcpy,我不想使用strchr。”如果你这样感觉,那么我从来没有遇到过strchrstrrchr等的用例,我不能用while循环和isspaceisalnum等来代替。有时它实际上比正确使用strchr更清晰。


0

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

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;
}

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