【问题标题】:strcpy behaving differently when two pointers are assigned strings in different ways当以不同方式为两个指针分配字符串时,strcpy 的行为会有所不同
【发布时间】:2015-05-09 19:03:18
【问题描述】:

对不起,我可能会问一个愚蠢的问题,但我想了解以下作业有什么不同吗? strcpy 在第一种情况下有效,但在第二种情况下无效。

char *str1;
*str1 = "Hello";
char *str2 = "World";
strcpy(str1,str2);    //Works as expected

char *str1 = "Hello";
char *str2 = "World";
strcpy(str1,str2);    //SEGMENTATION FAULT

编译器如何理解每个赋值?请澄清。

【问题讨论】:

  • 您应该使用char str1[] = "Hello"; 或动态分配内存。
  • 你的第一个例子甚至没有编译,应该是str1 = "Hello";。说了这么多,修改一个字符串字面量就是UB。
  • 您好 Vane,第一个示例在我的 cygwin 上按预期编译和工作。

标签: c


【解决方案1】:

编辑:在第一个sn-p中你写*str1 = "Hello"相当于赋值给str[0],这显然是错误的,因为str1是未初始化的,因此是一个无效的指针.如果我们假设您的意思是str1 = "Hello",那么您仍然错了:

根据 C 规范,尝试修改字符串文字会导致未定义的行为:它们可能存储在只读存储中(例如 .rodata)或与其他字符串文字组合您提供的 -ps 将产生未定义的行为。

我只能猜测,在第二个 sn-p 中,编译器将字符串存储在一些只读存储中,而在第一个中它没有,所以它可以工作,但不能保证。

【讨论】:

  • 除非 OP 输入错误的问题,否则第一个代码甚至不应该编译。
  • @chqrlie 根据规格,它可能不应该。但它确实 - ideone.com/tYN3Lj - 然而,正如我所说,一些编译器(可能如 OP 的)可能不会将字符串放在只读存储中,这可以解释为什么它适用于 OP。
  • 重读代码:*str1 = "Hello"; 正在尝试将指针分配给str1 指向的char,它确实未初始化。
  • @chqrlie 哎呀,你是绝对正确的。运行时错误发生在 *str1 = "Hello" 而不是 strcpy
  • 遗憾的是,此类错误仅由编译器标记为带有令人困惑的警告warning: incompatible pointer to integer conversion assigning to 'char' from 'char [6]',并且只有在明确请求警告时才会如此 (clang -Wall)。当然strcpy()也是个问题。
【解决方案2】:

抱歉,这两个示例都非常错误,并导致未定义的行为,这可能会或可能不会崩溃。让我试着解释一下原因:

  • str1 是一个悬空指针。这意味着str1 指向您记忆中的某处,写入str1 可能会产生任意后果。例如崩溃或覆盖内存中的某些数据(例如其他局部变量、其他函数中的变量,一切皆有可能)
  • *str1 = "Hello"; 行也是错误的(即使str1 是有效指针),因为*str1 的类型为char不是 char *)并且是str1 悬空。但是,您为其分配了一个指针("Hello",类型为 char *),这是编译器会告诉您的类型错误
  • str2 是一个有效的指针,但可能指向只读内存(因此崩溃)。通常,常量字符串存储在二进制的只读数据中,您不能写入它们,但这正是您在strcpy(str1,str2); 中所做的。

您想要实现的更正确的示例可能是(堆栈上有一个数组):

#define STR1_LEN 128
char str1[STR1_LEN] = "Hello"; /* array with space for 128 characters */
char *str2 = "World";
strncpy(str1, str2, STR1_LEN);
str1[STR1_LEN - 1] = 0; /* be sure to terminate str1 */

其他选项(动态管理内存):

#define STR1_LEN 128
char *str1 = malloc(STR1_LEN); /* allocate dynamic memory for str1 */
char *str2 = "World";
/* we should check here that str1 is not NULL, which would mean 'out of memory' */
strncpy(str1, str2, STR1_LEN);
str1[STR1_LEN - 1] = 0; /* be sure to terminate str1 */
free(str1); /* free the memory for str1 */
str1 = NULL;

编辑: @chqrlie 在 cmets 中要求将 #define 命名为 STR1_SIZE 而不是 STR1_LEN。大概是为了减少混淆,因为它不是“字符串”的字符长度,而是分配的缓冲区的长度/大小。此外,@chqrlie 要求不要使用strncpy 函数提供示例。这不是我的选择,因为 OP 使用了 strcpy,这非常危险,所以我选择了可​​以正确使用的最接近的函数。但是是的,我可能应该补充一点,不建议使用strcpystrncpy 和类似的功能。

【讨论】:

  • 将大小命名为STR1_LEN 容易出错,您应该调用此STR1_SIZE 或分配STR1_LEN+1 字节。鼓励使用braindead strncpy() 是个坏建议。它不会像大多数程序员所想的那样做,而且往往会导致错误和效率低下。
  • @chqrlie STR1_SIZE 可能确实是更好的名字。但是,LEN 并没有错,它只是不是“字符串长度”而是“缓冲区长度”。另外:我没有选择使用strcpy,那是在 OPs 问题中。然后我选择了strncpy,因为它至少是正确的。显然你还是应该避免它。
  • OP 可能不知道strncpy(),他应该学会避免它。它只正确了一半,并且您知道如何在缓冲区太短的情况下纠正令人困惑的错误特征。但是你知道strncpy() 将用NUL 填充str1 字节到指定的大小(如果大于源字符串长度)? snprintf(str1, STR1_SIZE, "%s", str2); 也效率低下,但至少可以完成这项工作。我通常在我的项目中定义一个实用函数来做这件事,类似于 BSD 的strlcpy(),但参数顺序不同。
  • @chqrlie,当然,strncpy 总是复制指定的字节数,如果输入不足,它将添加 \0 字节。有什么问题?这个问题根本与效率无关!而且它总是在相同的时间内运行,这并不是真正的低效。对于像 OP 想要的工作,您通常会使用 char *str1 = strdup(str2); ...; free(str1);。但我想尽可能接近这个问题(没有明显错误并使用strcpy)。
  • 好吧,我对这个最初设计用于在二进制文件中填充用户数据库条目的愚蠢函数有点不满。我认为我们的工作是向 OP 传授良好实践和有用的习惯用法,并警告他们注意容易出错的 API。新手滥用指针,scanf()feof() 等每天都会引起很多混乱。
【解决方案3】:

这里似乎有些混乱。两个片段都调用未定义的行为。让我解释一下原因:

  • char *str1; 定义了一个指向字符的指针,但它未初始化。如果此定义出现在函数体中,则其值无效。如果此定义发生在全局级别,则将其初始化为NULL

  • *str1 = "Hello"; 是一个错误:您将字符串指针分配给str1 指向的字符。 str1 未初始化,因此它不指向任何有效的东西,并且您不能将指针分配给字符。你应该写str1 = "Hello";。而且"Hello"这个字符串是常量,所以str1的定义真的应该是const char *str1;

  • char *str2 = "World"; 在这里定义一个指向常量字符串"World" 的指针。这种说法是正确的,但出于与上述相同的原因,最好将str2 定义为const char *str2 = "World";
  • strcpy(str1,str2); //Works as expected 不,它根本不起作用! str1 没有指向一个足够大的 char 数组来保存字符串“World”的副本,包括最后的 '\0'。鉴于这种情况,此代码会调用未定义的行为,这可能会也可能不会导致崩溃。

您提到代码按预期工作:它只是在外观上没有:真正发生的是:str1 未初始化,如果它指向无法写入的内存区域,写入它可能会使程序因分段错误而崩溃;但是如果它恰好指向一个可以写入的内存区域,并且下一条语句*str1 = "Hello"; 将修改该区域的第一个字节,那么strcpy(str1, "World"); 将修改该位置的前 6 个字节。正如预期的那样,str1 指向的字符串将是“World”,但是您已经覆盖了一些可能用于其他目的的内存区域,您的程序可能随后会以意想不到的方式崩溃,这是一个很难找到的错误!这绝对是未定义的行为

第二个片段出于不同的原因调用未定义的行为:

  • char *str1 = "Hello"; 没问题,但应该是const
  • char *str2 = "World"; 也可以,但也应该是 const
  • strcpy(str1,str2); //SEGMENTATION FAULT 当然无效:您正试图用字符串"World" 中的字符覆盖常量字符串"Hello"。如果将字符串常量存储在可修改的内存中,它会起作用,并且随着字符串常量的值发生更改,稍后会在程序中引起更大的混乱。幸运的是,大多数现代环境通过将字符串常量存储在只读存储器中来防止这种情况。尝试修改所述内存会导致段冲突,即:您正在以错误的方式访问内存的数据段。

您应该只使用strcpy() 将字符串复制到您定义为char buffer[SOME_SIZE]; 的字符数组或分配为char *buffer = malloc(SOME_SIZE);SOME_SIZE 大到足以容纳您要复制的内容加上最后的'\0'

【讨论】:

  • 我同意你的看法。但不知何故,在 Cygwin 上,代码的工作原理与 OP 中提到的一样。它似乎更特定于编译器。感谢所有教过以正确方式初始化字符串的人。
  • @Megatron:未定义行为的后果是编译器和环境特定的,以及从一个调用到另一个不同的其他情况。我修改了我的答案,以提示您为什么它似乎表现如预期
【解决方案4】:

这两个代码都是错误的,即使在您的第一种情况下“它有效”。希望这只是一个学术问题! :)

首先让我们看看您要修改的*str1

char *str1;

这声明了一个悬空指针,即具有内存中某个未指定地址的值的指针。这里的程序很简单,没有重要的东西,但你可以在这里修改非常关键的数据!

char *str = "Hello";

这声明了一个指针,该指针指向内存的受保护部分,即使程序本身在执行期间也无法更改,这就是分段错误的含义。

要使用 strcpy(),第一个参数应该是一个用 malloc() 动态分配的 char 数组。如果事实上,不要使用 strcpy(),学习使用 strncpy(),因为它更安全。

【讨论】:

  • 我建议 OP 学习 不要使用 strncpy()。编写您自己的函数来复制字符串,但将其剪辑到目标缓冲区大小。
猜你喜欢
  • 1970-01-01
  • 2016-11-12
  • 2021-02-13
  • 2013-04-26
  • 1970-01-01
  • 2016-08-16
  • 2015-03-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多