C strcpy 函数在典型的 C 程序中很常见。它也是缓冲区溢出缺陷的来源,因此 linters 和 codereviewers 通常推荐替代方案,例如 strncpy(难以正确使用;语义不匹配)、strlcpy(非标准)或C11 的可选 strcpy_s (没有正确或实用的实现)。除了他们各自的缺点之外,这些答案是不正确的。 strcpy 和朋友充其量是令人难以置信的小众,正确的替代品是 memcpy。如果 strcpy 不容易被 memcpy 替换,那么代码是根本错误的。要么它没有安全地使用 strcpy 要么它在做一些愚蠢的事情,应该重写。突出这些问题是使 memcpy 成为有效替代品的部分原因。当目标小于源时会发生缓冲区溢出。安全使用 strcpy 需要对源字符串长度的长度有先验知识。通常这个知识是确切的源字符串长度。如果是这样, memcpy 不仅是一个微不足道的替代品,而且速度更快,因为它不会同时搜索空终止符。 char * my_strdup ( const char * s ) { size_t len = strlen ( s ) + 1 ; char * c = malloc ( len ); if ( c ) { strcpy ( c , s ); // 不好 } return c ; } char * my_strdup_v2 ( const char * s ) { size_t len = strlen (s) + 1 ; char * c = malloc ( len ); if ( c ) { memcpy ( c , s , len ); // 好 } return c ;大小是一个编译时间常数,所以利用它!更重要的是,非静态断言 (C11) 可以在编译时而不是运行时捕获错误。 void set_oom_v2 ( struct err * err ) { static const char oom [] = "out of memory" ; static_assert ( sizeof ( err -> message ) >= sizeof ( oom )); memcpy ( err -> message , oom , sizeof ( oom )); } // 或者使用宏: void set_oom_v3 ( struct err * err ) { #define OOM "out of memory" static_assert ( sizeof ( err -> message ) >= sizeof ( OOM )); memcpy ( err -> message , OOM , sizeof ( OOM )); } // 或者赋值(隐式 memcpy): void set_oom_v4 ( struct err * err ) { static const struct err oom = { "out of memory" }; * 错误 = oom ;在不知道确切的源字符串长度的情况下,strcpy 仍然可以是安全的。知道它的上限不超过目标长度就足够了。在这个例子中——假设输入保证以空值终止——这个 strcpy 是安全的,而无需知道源字符串长度:
结构回复 { 字符消息 [ 32 ];整数 x , y ; };结构日志 { time_t 时间戳;字符消息 [32]; }; void log_reply ( struct log * e , const struct reply * r ) { e -> timestamp = time ( 0 ); strcpy ( e -> message , r -> message );这是 strncpy 具有正确语义的罕见情况。它将未使用的目标字节归零,破坏任何先前的内容。这不是一般的 strcpy 替换,因为 strncpy 可能不会写空终止符。如果源字符串在目标长度内不以空字符结尾,则目标字符串也不会。这无条件地复制了 32 个字节。但是它不会浪费时间复制它不需要的字节吗?不!在现代硬件上,复制大量固定数量的字节比复制少量可变字节要好得多。毕竟,分支是昂贵的。搜索和处理该空终止符是有代价的。这个固定大小的副本实际上是 x86-64 上的两条指令(clang -march=x86-64-v3 -O3 的输出):读取超过该长度是不可取的。也许 readextra 不安全,或者上限非常大,所以无条件复制太贵了。源字符串太长,函数太热,值得避免两次传递:strlen 后跟 memcpy。这些情况非常不寻常,这使得 strcpy 成为您可能不需要的利基功能。这是我能想象到的最好的情况,而且非常愚蠢:
struct doc { unsigned long long id ;炭体[ 1L << 20 ]; }; // 从缓冲区创建一个新文档。 // // 如果主体大于 1MiB,则行为未定义。 struct doc * doc_create ( const char * body ) { struct doc * c = calloc ( 1 , sizeof ( * c )); if ( c ) { c -> id = id_gen (); assert ( strlen ( body ) < sizeof ( c -> body )); strcpy ( c -> body , body ); } 返回 c ;如果您正在处理如此大的以空字符结尾的字符串(2)和(3),那么您已经在做一些根本错误且自相矛盾的事情。指针和长度应该一起保存和传递。这对于热函数尤其重要。 C11 引入了“安全”字符串函数作为可选的“附件 K”,每个函数都以 _s 后缀命名为“不安全”对应项。下面是 strcpy_s 的原型: rsize_t 是一个 size_t 具有“受限”范围(RSIZE_MAX,可能是 SIZE_MAX/2),用于捕获整数下溢。如果您不小心计算出一个负长度,它将是一个非常大的无符号数。 (size_t 最初应该定义为有符号的指示符。)这将超出限制范围,因此由于可能的下溢,不会尝试操作。这些“安全”函数以 MSVC 中的同名函数为模型。但是,如前所述,附件K 没有实际实现。 MSVC 中的函数具有不同的语义和行为,并且它们不尝试实现标准。更糟糕的是,他们甚至没有做他们文档中承诺的事情。以下程序应该会导致违反运行时约束,因为 -1 在任何合理的实现中都是无效的 rsize_t:#define __STDC_WANT_LIB_EXT1__ 1#include <stdio.h>#include < string.h> int main ( void ) { char buf [ 8 ] = { 0 }; errno_t r = strcpy_s ( buf , - 1 , "hello" ); printf ( "%d %s \n ", ( int ) r , buf ); }
使用截至撰写本文时的最新 MSVC(VS 2019),该程序打印“0hello”。使用 strcpy_s 并没有使我的程序比我刚使用 strcpy 时更安全。如果有的话,由于错误的安全感,它不太安全。不要使用这些功能。对这篇文章有评论吗?通过向 ~skeeto/[email protected] [邮件列表礼仪] 发送电子邮件,在我的公共收件箱中发起讨论,或查看现有讨论。