【问题标题】:How can a string appear longer than its declared length?一个字符串怎么会比它声明的长度长?
【发布时间】:2021-07-31 07:46:03
【问题描述】:

我已经声明了两个 char 字符串(str1 和 str2)的大小相同。之后,我通过gets()函数读取一个字符串并将其存储在str1上,然后将str1复制到str2。当它们显示时,我意识到 str2 可以存储比它的大小更多的字符?

这是我的代码:

#include<stdio.h>
#include<string.h>
void main()
{
    char str1[20], str2[20];
    printf("Enter the first string:");
    gets(str1);
    strcpy(str2,str1);
    printf("First string is:%s\tSecond string is:%s\n",str1,str2);
}

这里的输出:

Enter the first string: Why can str2 store more characters than str1?
First string is:ore characters than str1?       Second string is:Why can str2 store more characters than str1?

提前谢谢大家

【问题讨论】:

  • 它不能——你在 str2 结束后覆盖内存并调用未定义的行为。它似乎起作用的事实是未定义行为的更有趣的方面之一。任何事情都可能发生,包括无法观察到明显错误的情况(例如,因为在 str2 结束后被覆盖的字节中没有发生“重要”的事情)。不过,你不能依赖它工作
  • 这是一个很好的例子,说明为什么在任何情况下您都应该从不使用gets。你的编译器应该对你大喊大叫。 gets 函数允许将任意信息写入内存。
  • 考虑查找缓冲区溢出、未定义的行为以及为什么gets 很危险。
  • scanf(str1); - 最好阅读 scanf 的手册页。您正在寻找scanf("%19s", str1);
  • 我建议用有意义的东西替换标题,例如“字符串怎么会比它声明的长度长?”

标签: c strcpy


【解决方案1】:

首先,正如 cmets 部分已经指出的那样,您永远不应该在现代 C 代码中使用 gets。那个函数is so dangerous that it has been removed from the ISO C standard。更安全的选择是fgets

当您使用%s 格式说明符打印str2 时,printf 将不仅仅打印str2 数组的内容。它将打印在内存中找到的所有内容,直到找到一个空终止字符。

由于数组str2 不包含这样的空字符,它将继续打印它在内存中找到的所有内容,超过str2 的边界,直到找到空字符(除非它事先崩溃)。由于您之前似乎已经将字符串写入了str2 的边界(这是缓冲区溢出),因此它将打印该字符串,除非内存同时被其他东西覆盖。

【讨论】:

  • @EdHeal:感谢您指出这一点。我已更新我的答案以指出 scanf 的错误用法,并参考您的答案以了解正确用法。
  • @EdHeal 你是对的。实际上,我使用了gets 而不是scanf 然后我得到了上面发布的输出。
  • @JohnTran:如果问题不能用scanf重现,而只能用gets,那么你应该恢复你的问题以使用gets,一般来说,你应该确保你发布的代码实际上重现了问题。
  • @AndreasWenzel 谢谢。我用gets() 替换了scanf()。第一次所以我有很多缺点。
  • @JohnTran:是的,一开始犯错是正常的。不要担心他们。只要你从错误中吸取教训,那么犯错其实是一件好事。
【解决方案2】:

用cmets查看更新后的代码将确保str1中实际存储了一些东西并且内容不会溢出

#include <stdio.h>
#include <string.h>
// For EXIT_...
#include <stdlib.h>
int main() // Should be returning int
{
    char str1[20], str2[20];
    printf("Enter the first string:");
    // Incorrect - see manual page - scanf(str1);
    if (scanf("%19s", str1) == 1) { // Please read the manual page - this prevents buffer over runs and checks that something is stored in str1  
    
      strcpy(str2,str1);
      printf("First string is:%s\tSecond string is:%s\n",str1,str2);
      return EXIT_SUCCESS;
    } else {
      fprintf("Unable to read string\n");
      return EXIT_FAILURE;
    }  
}

【讨论】:

  • scanf("%19s", str1) 表示读取前19个字符,返回值始终为1,为什么需要else部分?
  • @JohnTran:如果scanf 返回的值不是1,则将执行else 块。例如,它会在输入失败时返回EOF(通常定义为-1)(不太可能发生,但可能发生)。
【解决方案3】:

我意识到 str2 可以存储比它的大小更多的字符?

没有。发生的事情是多余的字符被写入一个数组的末尾,这会覆盖另一个数组(或其他对象)的内容。 C 不强制对数组访问进行边界检查 - 如果您写到数组末尾之后,您将不会收到“IndexOutOfBounds”异常或类似的情况。

根据您的输出,这是发生了什么 - str2 分配在比 str1 更低的地址,就像这样(地址值仅用于说明):

              +---+
0x1000  str2: |   | str2[0]
              +---+ 
0x1001        |   | str2[1]
              +---+
0x1002        |   | str2[2]
              +---+
               ...
              +---+
0x1013        |   | str2[19]
              +---+
0x1014  str1: |   | str1[0]
              +---+ 
0x1015        |   | str1[1]
              +---+
0x1016        |   | str1[2]
              +---+
               ...
              +---+
0x1027        |   | str1[19]
              +---+

所以你要做的第一件事是

gets( str1 );

并输入字符串"Why can str2 store more characters than str1?",长度为 45 个字符。不幸的是,gets 只接收缓冲区的起始地址——它无法知道缓冲区的长度。所以它很乐意将字符串的"ore characters than str1?" 部分存储到紧跟str1 结尾的内存中:

              +---+
0x1000  str2: |   | str2[0]
              +---+ 
0x1001        |   | str2[1]
              +---+
0x1002        |   | str2[2]
              +---+
               ...
              +---+
0x1013        |   | str2[19]
              +---+
0x1014  str1: |'W'| str1[0]
              +---+ 
0x1015        |'h'| str1[1]
              +---+
0x1016        |'y'| str1[2]
              +---+
               ...
              +---+
0x1027        |'m'| str1[19]
              +---+
0x1028        |'o'| ???
              +---+
0x1029        |'r'| ???
              +---+
0x102a        |'e'| ???
              +---+
               ...
              +---+
0x103f        |'1'| ???
              +---+
0x1040        |'?'| ???
              +---+
0x1041        | 0 | ???
              +---+

gets 还写了一个 0 终止符来标记字符串的结尾。

接下来你调用strcpystr1 的内容复制到str2。和gets 一样,strcpy 只获取源缓冲区和目标缓冲区的起始地址——它不知道每个缓冲区的长度。它依赖于源字符串中 0 终止符的存在来告诉它何时停止复制。因此,str1 的前 20 个字符被复制到 str2,其余字符“溢出”回 str1,覆盖原来的内容。在strcpy 调用之后,您会得到以下信息:

              +---+
0x1000  str2: |'W'| str2[0]
              +---+ 
0x1001        |'h'| str2[1]
              +---+
0x1002        |'y'| str2[2]
              +---+
               ...
              +---+
0x1013        |' '| str2[19]
              +---+
0x1014  str1: |'m'| str1[0]
              +---+ 
0x1015        |'o'| str1[1]
              +---+
0x1016        |'r'| str1[2]
              +---+
0x1017        |'e'| str1[3]
              +---+
               ...
              +---+
0x1027        |' '| str1[19]
              +---+
0x1028        |'s'| ???
              +---+
0x1029        |'t'| ???
              +---+
0x102a        |'r'| ???
              +---+
0x102b        |'1'| ???
              +---+
0x102c        |'?'| ???
              +---+
0x102d        | 0 | ???
              +---+
               ...
              +---+
0x103f        |'1'| ???
              +---+
0x1040        |'?'| ???
              +---+
0x1041        | 0 | ???
              +---+

读取或写入数组末尾的行为是未定义 - 语言标准对编译器或运行时环境没有要求以任何特定方式处理这种情况。一个实现可能在数组访问中添加边界检查代码,但我不知道有什么这样做的。 只要您不覆盖任何“重要”内容或尝试访问受保护的内存,您的代码就会显示正常运行。但是,看似正常运行与实际运行正常并不相同。实际上,您正在破坏程序中的其他对象。您还可以覆盖堆栈帧的重要部分,这就是为什么像这样的缓冲区溢出是常见的恶意软件利用的原因。

具体问题:

  • 永远不要出于任何原因使用gets - 它会在您的代码中引入一个故障点,如上所示。它在 C99 标准之后被弃用,并从 2011 标准起从标准库中删除。请改用fgets
    if ( fgets(str1, sizeof str1, stdin) )
    {
      // do stuff with str1
    }
  • main 的标准签名是
    • int main( void )
    • int main( int argc, char **argv ) // or equivalent
    除非您的实现明确将 void main() 列为有效签名,否则请使用上述两个之一(在您的情况下,第一个是合适的)。

【讨论】:

    【解决方案4】:

    您还可以使用 strncpy,它提供了一个长度参数作为第三个参数。 这有助于避免写越界。示例:

     strncpy (str2, str1, (size_t) 20); //fixed size 20
    

    【讨论】:

    • 你不需要演员表
    • 正确 - 忽略该演员表。 strncpy (str2, str1, 20);很好
    • 请注意,即使strncpy 也没有将 NUL 终止符放在末尾,所以它仍然可能不安全。要么手动放置它,要么做类似snprintf(str1, sizeof str1, "%s", str2) 之类的操作,因为snprintf 放置NUL 终止符,不像strncpy。但是,使用 snprintf 可能会产生更多开销。
    猜你喜欢
    • 1970-01-01
    • 2015-01-14
    • 2012-05-28
    • 1970-01-01
    • 2011-01-19
    • 2016-07-06
    • 1970-01-01
    • 2022-11-14
    • 1970-01-01
    相关资源
    最近更新 更多