一个字符串怎么会比它声明的长度长?

How can a string appear longer than its declared length?

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

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

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

首先,正如评论部分已经指出的那样,您永远不应该在现代 C 代码中使用 gets。那个函数is so dangerous that it has been removed from the ISO C standard. A safer alternative is fgets.

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

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

I realized str2 can store more characters than its size?

没有。发生的事情是多余的字符被写入一个数组的末尾,并且覆盖了另一个数组(或其他对象)的内容。 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() 列为有效签名,否则请使用上述两种签名之一(在您的情况下,第一种是合适的)。