即时调整char*大小-为什么这段代码“工作”?

4
我正在尝试使用malloc和realloc,并为以下问题编写了一些代码:
我想创建一个未知大小的字符串,而不设置任何限制。我可以要求用户输入字符数,但我更愿意在用户输入每个字符时调整字符串大小。
因此,我尝试使用malloc + realloc来实现这个目标,我的想法是每次用户输入新字符时,我使用realloc请求+1个内存块以容纳该字符。
在尝试实现此过程时,我犯了一个错误,最终执行了以下操作:
int main () {
    /* this simulates the source of the chars... */
    /* in reality I would do getch or getchar in the while loop below... */

    char source[10];
    int i, j;
    for (i=0, j=65; i<10; i++, j++) { 
            source[i] = j;
    }

    /* relevant code starts here */

    char *str = malloc(2 * sizeof(char)); /* space for 1 char + '\0' */
    int current_size = 1;

    i = 0;
    while(i<10) {
            char temp = source[i];
            str[current_size-1] = temp;
            str[current_size] = '\0';
            current_size++;
            printf("new str = '%s' | len = %d\n", str, strlen(str));
            i++;
    }

    printf("\nstr final = %s\n", str);

    return 0;

} 

请注意,realloc部分尚未实现。 我编译并执行了此代码,并得到以下输出。
new str = 'A' | len = 1
new str = 'AB' | len = 2
new str = 'ABC' | len = 3
new str = 'ABCD' | len = 4
new str = 'ABCDE' | len = 5
new str = 'ABCDEF' | len = 6
new str = 'ABCDEFG' | len = 7
new str = 'ABCDEFGH' | len = 8
new str = 'ABCDEFGHI' | len = 9
new str = 'ABCDEFGHIJ' | len = 10

我发现这些结果很奇怪,因为我预期程序会崩溃:str有2个字符的空间,但代码添加的字符数超过了2个,并没有请求更多的内存。据我的理解,这意味着我正在写入我不拥有的内存,所以应该会导致运行时错误。
那么... 为什么这个程序能够正常工作?
(编译器是GCC 4.3.4。)
提前感谢您的帮助。
编辑: 其中一位评论者建议调用free()可能会导致错误被标记。我尝试使用上述代码调用free(),执行代码时并没有出现错误。然而,在向源数组添加更多项并调用free后,出现了以下错误:
* glibc detected ./prog: free(): invalid next size (fast): 0x09d67008 **

2
sizeof(char) 是 1。你不需要拼写出来。 - Kerrek SB
3
顺便提一下,当实现这样的东西时,通常最好从一个相当大的缓冲区开始(比如说256个字节左右),因为对于大多数分配器来说,malloc(1)的效率不是很高。此外,经常值得避免过于频繁地调用realloc(),所以通常采用每次重新分配时将缓冲区大小*翻倍的策略。当然,你的程序可能不会受到性能的关键影响,但我想指出这一点。 - unwind
5个回答

7

由于你写入了超过分配内存的内容,你的代码存在未定义行为

事实上,代码没有崩溃一次(甚至多次)也无法改变这个事实。

未定义行为并不意味着代码必须崩溃。在你的情况下,恰好有一些紧随str之后的内存,你正在覆盖它。覆盖该内存的实际影响是未知的(你可能会更改其他变量的值,破坏堆,发射核打击等)。


+1 感谢提供的“未定义行为”链接。 - MyName
我将此回复标记为正确答案,因为它首先提到了行为是未定义的及其后果。 - MyName

4

从glibc-2.14开始,内存分配将按照以下大小进行分配,并设置边框,因此当您分配2字节大小时 " char *str = malloc(2 * sizeof(char))",似乎分配的内存不少于16字节,因此您可能会添加更多项目,从而导致程序错误。

struct _bucket_dir bucket_dir[] = {

    { 16,   (struct bucket_desc *) 0},

    { 32,   (struct bucket_desc *) 0},

    { 64,   (struct bucket_desc *) 0},

    { 128,  (struct bucket_desc *) 0},

    { 256,  (struct bucket_desc *) 0},

    { 512,  (struct bucket_desc *) 0},

    { 1024, (struct bucket_desc *) 0},

    { 2048, (struct bucket_desc *) 0},

    { 4096, (struct bucket_desc *) 0},

    { 0,    (struct bucket_desc *) 0}};   /* End of list marker */


void *malloc(unsigned int len)
{
    struct _bucket_dir  *bdir;
    struct bucket_desc  *bdesc;
    void            *retval;

    /*
     * First we search the bucket_dir to find the right bucket change
     * for this request.
     */
    for (bdir = bucket_dir; bdir->size; bdir++)
        if (bdir->size >= len)
            break;
    if (!bdir->size) {
        printk("malloc called with impossibly large argument (%d)\n",
            len);
        panic("malloc: bad arg");
    }
    /*
     * Now we search for a bucket descriptor which has free space
     */
    cli();  /* Avoid race conditions */
    for (bdesc = bdir->chain; bdesc; bdesc = bdesc->next) 
        if (bdesc->freeptr)
            break;
    /*
     * If we didn't find a bucket with free space, then we'll 
     * allocate a new one.
     */
    if (!bdesc) {
        char        *cp;
        int     i;

        if (!free_bucket_desc)  
            init_bucket_desc();
        bdesc = free_bucket_desc;
        free_bucket_desc = bdesc->next;
        bdesc->refcnt = 0;
        bdesc->bucket_size = bdir->size;
        bdesc->page = bdesc->freeptr = (void *) cp = get_free_page();
        if (!cp)
            panic("Out of memory in kernel malloc()");
        /* Set up the chain of free objects */
        for (i=PAGE_SIZE/bdir->size; i > 1; i--) {
            *((char **) cp) = cp + bdir->size;
            cp += bdir->size;
        }
        *((char **) cp) = 0;
        bdesc->next = bdir->chain; /* OK, link it in! */
        bdir->chain = bdesc;
    }
    retval = (void *) bdesc->freeptr;
    bdesc->freeptr = *((void **) retval);
    bdesc->refcnt++;
    sti();  /* OK, we're safe again */
    return(retval);
}

+1,非常有趣的信息。 - MyName
我总是实现类似这样的东西来避免频繁调用relloc。 - nerkn

2
除了您触发未定义行为的事实,其中包括但不限于“崩溃”,我认为您的应用程序实际上确实拥有您要写入的内存。

在现代操作系统中,内存以页面的形式处理,这些内存块远大于两个字节。据我所知,malloc会向操作系统请求完整的页面,并在需要时进行内部划分。(注意:这取决于实现,但我认为至少glibc是这样运作的。)因此,操作系统允许您写入内存,因为从技术上讲,它是您的。在内部,malloc通常会划分页面并在每次请求时提供其中的一部分。因此,您可能会覆盖堆上的另一个变量。或者根据malloc的视图,在超出边界的情况下编写仍在等待请求的内存。

我只期望在尝试写入尚未从操作系统分配或标记为只读的页面时才会崩溃。

1

[忽略此操作行为未定义的事实]

在调用realloc或free时通常会检查堆的完整性,而不是在每次写入时进行检查,您可能没有覆盖足够的内容以导致崩溃。

请注意,您没有在最后调用free,如果您这样做,您很可能会遇到崩溃。


+1,有趣。我尝试使用上述代码调用free函数。没有错误。我将源数组大小从10更改为20,并再次调用free函数。出现错误!编辑原帖以反映这一点。 - MyName

0
补充上一个答案,实际上并没有为2个字符保留空间,只是存储在内存中的指针。malloc确实记得它分配了2个字符的空间,但这是malloc的内部工作。
您可以尝试以下小实验来了解其工作原理:
在第一个指针后面创建另一个指针。
char *str2 = str + 5;

/* or you could simply malloc another */

char *str2 = malloc(2);

printf("str=%d, str2=%d\n",str,str2);

/* to eyeball the pointers actually received
and note the difference in the two pointers. 
You will need to raise source length to at least
that much to see the results below
*/

在第一个printf之后,在循环中引入另一个printf:
printf("new str2 = '%s' | len = %d\n", str2, strlen(str2));

迟早,str2也会开始显示相同的字母。

希望对你有所帮助。


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