字符串没有以NULL结尾,但仍然表现正常,为什么?

6
在以下代码中,我使用strncpy()将一个字符串复制到长度为10个字符的char* str中。
现根据strncpy()手册,“警告:如果在src的前n个字节中没有空字节,则放置在dest中的字符串将不以空字符结尾。”这正是这里发生的情况。
源字符串长度为26个字符,我复制了10个字符,因此在str末尾没有放置空字符。
但是当我打印从0开始到获取'\0'的str内容时,它表现正常。
为什么?当没有在末尾放置'\0'时,为什么循环会停在正确的位置?
我的理解是,它应该给出“分段错误”或至少不应该在那里停止并继续打印一些垃圾值。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 10

int main()
{
    char *str ;
    str = malloc( sizeof( char ) * SIZE );
    if( str == NULL ) 
        exit( 1 );
    memset( str, 0, sizeof( char ) * SIZE );

    strncpy( str, "abcdefghijklmnopqrstuvwxyz", sizeof( char ) * SIZE );

    unsigned int index;
    for( index = 0; str[ index ] != '\0' ; index++ ) {
        printf( "str[ %u ] has got : %c \n ", index, str[ index ] );
    }

    return 0;
}

以下是输出结果:

str[0]的值为:a
str[1]的值为:b
str[2]的值为:c
str[3]的值为:d
str[4]的值为:e
str[5]的值为:f
str[6]的值为:g
str[7]的值为:h
str[8]的值为:i
str[9]的值为:j

感谢您的帮助。

编辑

有没有一种正确的方法来检查一个字符串是否以'\0'结尾?我一直认为上面的循环是最终的测试方法,但现在似乎不是。

比如说我们从其他程序员编写的函数中获取一个字符串。那么怎么知道它是否以正确的位置和'\0'结尾呢?也许没有,然后它会超出实际大小,直到我们得到一些'\0'。我们永远无法知道字符串的实际大小。

那么我们该如何应对这种情况呢?

有什么建议吗?


2
sizeof(char) 永远 等于1!! - paxdiablo
1
@Pax:没错,但这里有一个相关讨论:https://dev59.com/pnNA5IYBdhLWcg3wUMDn - sharptooth
strncmp接受字符数,而不是字符串大小,因此在那里使用它是错误的。 - Pete Kirkham
@Pete Kirkham:从手册上我理解的是strncpy()会将src的n个字节复制到dest中。我并不想给它字符串的大小,这只是我想到的一种覆盖空终止符的方法。 - Andrew-Dufresne
6个回答

15

刚好在分配的内存块末尾后面出现了一个空字节。

很可能malloc()会分配更多的内存并放置所谓的守卫值,这些值包含空字节,或者它会放置一些元数据以供稍后由free()使用,并且该元数据恰好在该位置上包含一个空字节。

无论如何,您都不应依赖这种行为。您必须请求(malloc())一个额外的字节用于空字符,以便空字符位置也被合法地分配给您。

没有可移植的方法来测试字符串是否正确以null结尾。一旦超出分配的块结束,你的程序就可能崩溃。或者当操作错误解释的字符串时,可能会发生在块结束之外某个位置存在空字符并覆盖块结束后的内存的情况。

理想情况下,您需要某个函数来检查给定地址是否被分配给您,并且属于另一个给定地址(可能是块的开头)所属的分配。这将很慢且不值得,在标准方式中没有进行此操作的方法。

换句话说,如果遇到一个应该以null结尾但实际上不是的字符串,您将遭到重大破坏 - 您的程序将遇到未定义的行为。


是的,字符串末尾恰好是一个空字节。如果您尝试不同的大小,将得到错误的输出。 - Nick Dandoulakis
所以没有标准的方法来检查一个字符串是否已经以空字符结尾。这是个坏消息。我认为所有在应用程序上工作的程序员都必须达成一些标准,比如指针的前三个字符将告诉它的大小,从第四个字符开始才是实际的字符串。 - Andrew-Dufresne
3
如果字符串开头的那三个字节错了会发生什么?同样,你的程序将会崩溃。关键在于,如果数据结构不一致,就会出现问题,解决这个问题非常困难(读作“非常困难”即为“逻辑上不可能”)。 - Thomas Padron-McCarthy
7
@Andrew:有一份协议。它说明字符串包含一个额外的字节,用来保存空终止符。 - sharptooth

6
关于您的编辑,我认为严谨会有助于阐明一些问题。
在 C 中没有字符串这样的东西。有一个“C 字符串”的概念,这是 C 标准库使用的,它被定义为仅仅是以 NUL 结尾的字符序列,因此在 C 中没有“非以空字符结尾的字符串”这样的东西。所以您的问题最好表述为“如何确定任意字符缓冲区是否是有效的 C 字符串?”或“如何确定找到的字符串是否是预期的字符串”。
不幸的是,对于第一个问题,答案就是像您正在做的那样线性扫描缓冲区,直到遇到 NUL 字节。这将给出 C 字符串的长度。
第二个问题没有简单的答案。由于 C 没有实际的带有长度元数据的字符串类型(或者可以在函数调用之间携带数组大小的能力),因此没有真正的方法来确定我们上面确定的字符串长度是否是预期字符串的长度。如果程序开始出现 segfaults 或输出中出现“垃圾”,那么可能很明显,但通常我们必须通过扫描直到第一个 NUL 字节(通常是带有字符串长度上限,以避免混乱的缓冲区溢出错误)来执行字符串操作。

4
为什么它有效?
您分配的内存恰好在正确的位置有一个'\0'字节。(例如,如果您正在使用Visual C++的调试模式,则堆管理器将在将分配的内存交给程序之前清零。但它也可能纯粹是运气。)
有没有一种合适的方法来检查字符串是否以'\0'结尾?
没有。您需要使字符串要么以零终止(这是C标准库字符串处理函数所期望的内容),要么需要在额外的变量中携带其长度。如果两者都没有,那么就会出现错误。
现在我们如何知道其他程序员开发的某些函数的某个字符串是否以正确的位置和'\0'结束。或许不是,然后它将超出实际大小,直到我们得到一些'\0'。我们永远无法知道字符串的实际大小。
那么我们该如何应对这种情况呢?
你不能。如果其他函数犯了如此严重的错误,那么你也会受到同样严重的影响。

关于堆管理器清零内存:微软编译器不会清零内存(无论是调试版还是发布版)。当使用调试堆时,MSVC运行时会用0xCD字节填充分配的内存,而不是零。用“垃圾”填充而不是清除内存通常更有效地发现问题。此外,分配之前和之后的一些内存部分将被填充为0xFD值。请参见https://dev59.com/ZXRC5IYBdhLWcg3wOeWB#370362。 - Michael Burr
@Micheal:据我所知,你可能是对的。但是,我还记得一遍又一遍地阅读过变量未被清零是导致发布版本在VC中崩溃而调试版本正常工作的典型原因。 <挠头> - sbi

0

我认为Sharptooth的答案是正确的。有更多的空间被分配。我将程序修改如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 10

int main()
{
    char *str ;
    int *p;
    int actual_length;
    str = malloc( sizeof( char ) * SIZE );
    if( str == NULL ) 
        exit( 1 );

    actual_length = (int)*(str - 4) - 1 - 4;
    printf("actual length of str is %d\n", actual_length);
    p = (int*) malloc(sizeof(int));
    if (p == NULL) exit(1);
    *p = -1;
    char* pc = (char*)(p - 1);
    pc [0] = 'z';
    pc [1] = 'z';
    pc [2] = 'z';
    pc [3] = 'z';

    memset( str, 0, sizeof( char ) * SIZE );

    memcpy( str, "abcdefghijklmnopqrstuvwxyz", sizeof( char ) * SIZE );

    int i;
    for (i = SIZE; i < actual_length; i++)
     str[i] = 'y';

    unsigned int index;
    for( index = 0; str[ index ] != '\0' ; index++ ) {
        printf( "str[ %u ] has got : %c \n ", index, str[ index ] );
    }

    return 0;
}

输出结果为:

actual length of str is 12
str[ 0 ] has got : a 
 str[ 1 ] has got : b 
 str[ 2 ] has got : c 
 str[ 3 ] has got : d 
 str[ 4 ] has got : e 
 str[ 5 ] has got : f 
 str[ 6 ] has got : g 
 str[ 7 ] has got : h 
 str[ 8 ] has got : i 
 str[ 9 ] has got : j 
 str[ 10 ] has got : y 
 str[ 11 ] has got : y 
 str[ 12 ] has got : z 
 str[ 13 ] has got : z 
 str[ 14 ] has got : z 
 str[ 15 ] has got : z 
 str[ 16 ] has got : \377 
 str[ 17 ] has got : \377 
 str[ 18 ] has got : \377 
 str[ 19 ] has got : \377 

我的操作系统是Debian Squeeze/sid。


0

Sharptooth已经解释了这种行为的可能原因,所以我不会重复他的话。

在分配缓冲区时,我总是多分配一个字节,就像这样:

#define SIZE 10
char* buf = malloc(sizeof(char)*(SIZE+1));
/* error-check the malloc call here */
buf[SIZE] = '\0';

哎,"sizeof(char)-(SIZE+1)"?减号? - Thomas Padron-McCarthy
我们也可以这样做: memset(dest, 0, SIZE); strncpy(dest, source, SIZE -1);这样最后一个字节将会是零。 - Andrew-Dufresne
那应该是* - 次。新键盘 :) - gnud
是的,我经常使用memset。但我发现直接赋值更清晰。而且如果您使用strncpy(),它会在任何未使用的空间中填充0。 - gnud
虽然这并不是一个坏主意,但更简单的解决方案是永远不要使用字符串函数的无界版本。而是只使用那些带有上限参数并保证返回有效C字符串的函数(有点遗憾的是strncpy没有提供这种保证)。通常有更安全的strncpy版本,例如BSD上的strlcpy和Windows上的strncpy_s。 - Falaina
结合过度分配一个字符并将其设置为NULL,strncopy确保了这一点。这就是为什么我这样做的原因。 - gnud

0

你很幸运,超出分配空间的部分是零。

在其他平台上尝试这段代码,你会发现它可能不会表现出相同的行为。


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