6

据说我们可以写多个声明,但只能写一个定义。现在,如果我使用相同的原型实现自己的 strcpy 函数:

char * strcpy ( char * destination, const char * source );

那我不是在重新定义现有的库函数吗?这不应该显示错误吗?或者它是否与库函数以目标代码形式提供的事实有关?

编辑:在我的机器上运行以下代码会显示“分段错误(核心转储)”。我正在使用 linux 并且在没有使用任何标志的情况下进行了编译。

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

char *strcpy(char *destination, const char *source);

int main(){
    char *s = strcpy("a", "b");
    printf("\nThe function ran successfully\n");
    return 0;
}

char *strcpy(char *destination, const char *source){
    printf("in duplicate function strcpy");
    return "a";
}

请注意,我不是在尝试实现该功能。我只是想重新定义一个功能并询问后果。

编辑 2:应用 Mats 建议的更改后,程序不再给出分段错误,尽管我仍在重新定义函数。

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

char *strcpy(char *destination, const char *source);

int main(){
    char *s = strcpy("a", "b");
    printf("\nThe function ran successfully\n");
    return 0;
}

char *strcpy(char *destination, const char *source){
    printf("in duplicate function strcpy");
    return "a";
}
4

7 回答 7

11

C11(ISO/IEC 9899:201x) §7.1.3保留标识符

— 如果包含任何关联的头文件,则保留以下任何子条款(包括未来的库方向)中的每个宏名称以供指定使用;除非另有明确说明。

— 以下任何子条款(包括未来的库方向)中具有外部链接的所有标识符始终保留用作具有外部链接的标识符。

— 在以下任何子条款(包括未来的库方向)中列出的每个具有文件范围的标识符都保留用作宏名称和在同一名称空间中作为具有文件范围的标识符,如果包括其任何关联的头文件。

如果程序在保留标识符的上下文中声明或定义标识符,或将保留标识符定义为宏名称,则行为未定义。请注意,这并不意味着您不能这样做,正如这篇文章所示,它可以在 gcc 和 glibc 中完成。

glibc §1.3.3 保留名称证明了一个更清楚的原因:

无条件保留所有来自 ISO C 标准的库类型、宏、变量和函数的名称;您的程序可能不会重新定义这些名称。如果您的程序明确包含定义或声明它们的头文件,则保留所有其他库名称。这些限制有几个原因:

例如,如果您使用名为 exit 的函数执行与标准 exit 函数完全不同的操作,其他阅读您的代码的人可能会感到非常困惑。防止这种情况有助于使您的程序更易于理解,并有助于模块化和可维护性。

它避免了用户意外重新定义由其他库函数调用的库函数的可能性。如果允许重新定义,那些其他功能将无法正常工作。

它允许编译器在调用这些函数时做任何它喜欢的特殊优化,而不会被用户重新定义。一些库工具,例如那些用于处理可变参数(参见可变参数函数)和非本地出口(参见非本地出口)的工具,实际上需要 C 编译器方面的大量合作,并且关于实现,编译器可能更容易将它们视为语言的内置部分。

于 2013-07-13T15:51:42.213 回答
7

这几乎可以肯定是因为您正在传递一个“字符串文字”的目的地。

char *s = strcpy("a", "b");

随着编译器知道“我可以strcpy内联”,所以你的函数永远不会被调用。

您正在尝试复制"b"字符串 literal "a",但这是行不通的。

制作一个char a[2];并且strcpy(a, "b");它会运行 - 它可能不会调用您的函数,因为即使您没有可用的优化strcpy,编译器的内联也会很小。strcpy

于 2013-07-13T15:46:24.660 回答
4

将尝试修改不可修改内存的问题放在一边,请记住,正式不允许您重新定义标准库函数。

但是,在某些实现中,您可能会注意到为标准库函数提供另一个定义不会触发通常的“多重定义”错误。发生这种情况是因为在此类实现中标准库函数被定义为所谓的“弱符号”。例如,GCC 标准库就是为此而闻名。

这样做的直接后果是,当您使用外部链接定义自己的“版本”标准库函数时,您的定义会覆盖整个程序的“弱”标准定义。您会注意到,不仅您的代码现在调用您的函数版本,而且所有预编译的 [第三方] 库中的所有类也被分派到您的定义中。它旨在作为一项功能,但您必须注意它以避免无意中“使用”此功能。

你可以在这里阅读它,例如

如何替换 C 标准库函数?

实现的这一特性不违反语言规范,因为它在不受任何标准要求约束的未定义行为的未知区域内运行。

当然,使用某些标准库函数的内部/内联实现的调用不会受到重定义的影响。

于 2013-07-13T18:52:55.800 回答
2

你的问题具有误导性。

您看到的问题与重新实现库函数无关。

您只是在尝试编写不可写的内存,即字符串文字所在的内存a

简单地说,以下程序在我的机器上给出了分段错误(使用 编译gcc 4.7.3,没有标志):

#include <string.h>

int main(int argc, const char *argv[])
{
    strcpy("a", "b");
    return 0;
}

但是,如果您正在调用strcpy不写入不可写内存的(您的)版本,为什么会出现分段错误?仅仅因为您的函数没有被调用。

如果您使用-S标志编译代码并查看编译器为其生成的汇编代码,则不会出现callstrcpy因为编译器“内联”了该调用,这是您可以从 main 中看到的唯一相关调用,是对puts) 的调用。

.file   "test.c"
    .section    .rodata
.LC0:
    .string "a"
    .align 8
.LC1:
    .string "\nThe function ran successfully"
    .text
    .globl  main
    .type   main, @function
main:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movw    $98, .LC0(%rip)
    movq    $.LC0, -8(%rbp)
    movl    $.LC1, %edi
    call    puts
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   main, .-main
    .section    .rodata
.LC2:
    .string "in duplicate function strcpy"
    .text
    .globl  strcpy
    .type   strcpy, @function
strcpy:
.LFB3:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movq    %rdi, -8(%rbp)
    movq    %rsi, -16(%rbp)
    movl    $.LC2, %edi
    movl    $0, %eax
    call    printf
    movl    $.LC0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE3:
    .size   strcpy, .-strcpy
    .ident  "GCC: (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3"
    .

我认为余浩的回答对此有很好的解释,引用标准:

无条件保留所有来自 ISO C 标准的库类型、宏、变量和函数的名称;您的程序可能不会重新定义这些名称。如果您的程序明确包含定义或声明它们的头文件,则保留所有其他库名称。这些限制有几个原因:

[...]

它允许编译器在调用这些函数时做任何它喜欢的特殊优化,而不会被用户重新定义。

于 2013-07-13T15:57:26.277 回答
1

您的示例可以以这种方式运行:(使用 strdup

char *strcpy(char *destination, const char *source);

int main(){
    char *s = strcpy(strdup("a"), strdup("b"));
    printf("\nThe function ran successfully\n");
    return 0;
}

char *strcpy(char *destination, const char *source){
    printf("in duplicate function strcpy");
    return strdup("a");
}

输出 :

  in duplicate function strcpy
  The function ran successfully
于 2013-07-13T16:32:08.440 回答
1

解释此规则的方法是,您不能在最终链接对象(可执行文件)中结束函数的多个定义。所以,如果链接中包含的所有对象都只有一个函数定义,那么你很好。牢记这一点,请考虑以下场景。

  1. 假设您重新定义了某个库中定义的函数 somefunction() 。您的函数在 main.c (main.o) 中,而在库中,该函数位于名为 someobject.o 的对象中(在库中)。请记住,在最终链接中,链接器仅在库中查找未解析的符号。因为 somefunction() 已经从 main.o 解析,所以链接器甚至不会在库中查找它,也不会拉入 someobject.o。最后的链接只有一个函数的定义,一切都很好。
  2. 现在想象在 someobject.o 中定义了另一个符号 anotherfunction() ,您也碰巧调用了它。链接器将尝试从 someobject.o 中解析 anotherfunction(),并将其从库中拉入,它将成为最终链接的一部分。现在您在最终链接中有两个 somefunction() 定义 - 一个来自 main.o,另一个来自 someobject.o,链接器将抛出错误。
于 2013-07-13T17:05:56.983 回答
1

我经常使用这个:

void my_strcpy(char *dest, char *src)
{
    int i;

    i = 0;
    while (src[i])
    {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
}

你也可以通过修改一行来做 strncpy

void my_strncpy(char *dest, char *src, int n)
{
    int i;

    i = 0;
    while (src[i] && i < n)
    {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
}
于 2013-07-15T01:00:27.673 回答