字符串如何出现比其声明的长度更长?

问题描述

我已经声明了两个相同大小的字符字符串(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?

先谢谢大家

解决方法

查看带有注释的更新代码将确保某些内容实际存储在 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;
    }  
}
,

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

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

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

,

我意识到 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 | ???
              +---+

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

具体问题:

  • NEVER NEVER NEVER 出于任何原因使用 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() 列为有效签名,否则请使用上述两个之一(在您的情况下,第一个是合适的)。
,

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

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