C语言中的字符数组

3

当我们将字符数组定义为'char name [10]'时,这表示数组'name'可以容纳长度为十个字符的字符串。但在下面所示的程序中,数组名称可以容纳超过十个字符。这是如何可能的呢?

//print the name of a person.  
char name[10];  
scanf("%s",name);  
printf("%s",name);  

如果我输入的名字长度超过十个字符,即使没有运行时错误,程序也会打印出我输入的所有字符。

如果我输入二十个或更多字符的名称,则程序将终止。

注意:我正在Ubuntu9.04上使用gcc编译器运行程序。


1
这被称为溢出。 - Praveen S
编译器和环境都没问题,问题出在代码上。未定义的行为是因为您没有遵守所声明的内容(char a [10])以及输入到存储10个字符的桶(内存)中,其中包括尾随的'\0'。 - Praveen S
1
http://en.wikipedia.org/wiki/Buffer_overflow - David Z
1
这是一个对于名为stackoverflow的网站来说非常好的问题。C语言非常像人类,它可以承受压力,但结果总是给程序员带来更多的压力。并且要随时准备一份valgrind的副本。 - Fanatic23
谢谢Arpan。我肯定会参考它。 - Mohit
1
并确保如果答案对您有用,请打上绿色勾号:-) - eruciform
9个回答

7
因为scanf不知道数组的长度。变量"name"不是类型"array"而是类型"pointer"(或者"address")。它说,从这里开始写,一直写到你完成为止。你可能会幸运地在栈上有其他不太重要的东西被覆盖,但最终,scanf会不断地写入和覆盖一些致命的东西,然后你会得到一个段错误。这就是为什么你必须始终传递数组的大小。
这就像给一个盲人一支铅笔,让他从这里开始写,却看不见纸张的结尾。他们最终会在桌子上写字并损坏某些东西。(注意:这不是针对盲人的攻击,这只是个比喻。)
在上述情况下,我强烈建议使用fgets()从stdin中获取特定数量的内容,然后使用sscanf()从该行中提取任何信息,并根据需要将其放入单独的变量中。Scanf()和fscanf()是邪恶的,我从未发现过它们能解决fgets()+sscanf()不能更安全地解决的问题。
char line[1024]; /* arbitrary size */
if( fgets( line, 1024, stdin ) != NULL )
{
  fprintf( stdout, "Got line: %s", line );
}

或者对于超出字符串的内容:

# cat foo.c
  #include <stdio.h>
  int main( int argc, char **argv )
  {
    int i;
    char line[1024];
    while( fgets( line, 1024, stdin ) != NULL )
    {
      if( sscanf( line, "%d", &i ) == 1 )
      { /* 1 is the number of variables filled successfully */
        fprintf( stdout, "you typed a number: %d\n", i );
      }
    }
  }
# gcc foo.c -o foo
# ./foo
  bar
  2
  you typed a number: 2
  33
  you typed a number: 33
  <CTRL-D>

4

使用一个大小为10的字符数组在C语言中表示一个字符串,你只能使用9个字符和一个空字符。如果使用超过9个字符(加上1个终止符),那么就会出现未定义行为。

你只是在覆盖不应该被覆盖的内存。无论是段错误还是按照你的期望工作,都是随机的。


3

scanf允许使用最大宽度说明符,例如

scanf("%9s", name);

这将读取最多9个字符并添加一个终止的NUL字符,总共10个字符。
如果您不限制scanf可以读取的字符数会发生什么?那么您的字符串就会覆盖其他东西。在这种情况下,我猜您的缓冲区位于堆栈上,因此您会覆盖堆栈上的某些内容。堆栈保存本地变量、返回地址(指向调用此函数的函数)和函数参数。现在,恶意用户可以使用任意代码填充该缓冲区,并使用该代码的地址覆盖返回地址(有许多此类攻击的变体)。恶意用户可以通过该程序执行任意代码。

帮了很大的忙。我不知道%9s这个东西。感谢提供信息。 - Mohit

2

欢迎来到C世界...

  • C语言不会执行数组边界检查;
  • 数组的名称只是指向数组第一个元素的指针;
  • scanf(如Mohit示例程序中使用的)不能处理目标缓冲区大小限制;
  • 如果指针值错误,您可以在内存中任意写入,并且应该预期不可预测的行为,如果幸运的话,会出现分段错误。

1
scanf确实处理缓冲区大小限制;您只需要告诉它限制是什么... - R.. GitHub STOP HELPING ICE
@R..: 多么龟毛啊!显然我指的是在 Mohit 程序中使用的 scanf 。让我编辑一下帖子,否则又有人要决定再次给它点踩了… - Vanni Totaro

1

C语言没有对数组长度进行检查。它允许您溢出数组。

在您的情况下,数组后面恰好有可写内存,因此如果您稍微溢出一点,就不会崩溃(尽管谁知道您是否正在破坏数据)。

尝试运行此代码,并查看当您输入超过10个字符时会发生什么。

char name[10];
char name2[10];  
scanf("%s",name);  
printf("%s",name);  
printf("%s",name2); 

同时,名称数组最多可以容纳9个字符,第10个字符必须是终止空字符'\0'


当我输入超过十个字符的名称时,这会创建一个堆栈崩溃。之前,当我输入二十个字符后,我也收到了相同的“堆栈崩溃”消息。您能否请更清楚地解释一下? - Mohit
@Mohit,你需要明白,你不能在字符串name、name2等中添加超过9个字符。它们被声明为最大长度为9的字符串。否则会导致损坏。有时会立即崩溃。有时会覆盖相邻的内存位置。但当新的内存请求到达时,相邻的内存可能会被分配。因此它会破坏初始内存。 - Praveen S
显然,name和name2在内存中并不相邻。猜测它们会相邻只是一种猜测,没有保证它会起作用。 - shf301
您的name和name2被颠倒了。 - ninjalj
@junjalj - 这取决于编译器选择如何布局字段,但是这可能可行。 - shf301

1
这怎么可能呢?
该数组在堆栈上分配。在其后可能有空白空间或数据,这些数据不太重要(例如,在调用者中实际未使用的被调用者保存的寄存器)。最终,如果您输入的名称足够长,则会覆盖某些重要内容。包括在某些编译器下,返回地址!
valgrind下运行程序将立即检测到溢出错误。

0

您的代码引发了未定义行为。 永远不要使用 scanf()来读取字符串,应改用fgets()

scanf()gets() 都存在内存溢出的问题。您可以轻松地读入比char[]更多的字符。


在scanf中,你可以使用宽度说明符(例如,在这种情况下为"%9s")限制读取的字符。但是,如果我没记错的话。 - Matteo Italia
有关scanf的错误信息是不正确的,因为它具有完全可用的字段宽度说明符。 - R.. GitHub STOP HELPING ICE
@R..:使用scanf确实可以避免缓冲区溢出,但完全避免使用scanf仍然更简单,并且这样做可以避免其他scanf问题。 - jamesdlin
@R:抱歉,我删除了我的评论。 :-) - Prasoon Saurav

0

你正在利用未定义的行为,因此任何事情都可能发生 - 程序可能会崩溃、继续正常运行或开始做一些奇怪的事情。


0
当你声明 char c[10] 时,你为该变量分配了10个字节。然而,你的程序可能也“拥有”后续的字节,这就是为什么你可能不会得到一个段错误。但是你会遇到很多其他问题,你会希望得到一个段错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接