就地修改数组并了解其内存分配

Posted

技术标签:

【中文标题】就地修改数组并了解其内存分配【英文标题】:Modifying arrays in-place and understanding memory allocation for it 【发布时间】:2019-09-27 03:30:49 【问题描述】:

我有以下两个函数,它们接受一个字符串数组并将它们变成小写(就地)-

#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

void to_lower(char ** strings) 

    char * original_string;
    char * lower_string;

    for (int i=0; (original_string=strings[i]) != NULL; i++) 
        lower_string = malloc(strlen(original_string + 1) * sizeof(char));
        for (int j=0; j<=strlen(original_string); j++) 
            lower_string[j] = tolower(original_string[j]);
        
        strings[i]=lower_string;
    



int main(void) 
    char * strings[] = "Hello", "Zerotom", "new", NULL ;
    to_lower(strings);
    to_lower(strings); // this and successive calls won't change
    to_lower(strings); // anything but are here to try and understand malloc
    to_lower(strings);
    to_lower(strings);
    return 0;

在调用to_lower之前的main函数开头,消耗了多少内存?我的猜测是字符数组中的 16 个字节(最后 15 个字符 + 1 个空字节)。

to_lower 运行 5 次后,函数返回前,消耗了多少内存?我应该在哪里“释放”正在传递给函数的字符串(因为我的想法是在每次复制/小写字符串时调用malloc,它会创建那么多额外的内存但从不释放任何东西。

to_lower 函数看起来是否正常,或者如何对其进行修改以使其当前不会泄漏内存?

【问题讨论】:

strlen(original_string + 1) --> 也许是(strlen(original_string) + 1)? main() 中存在内存泄漏。您将动态分配的内存放在字符串 [] 中。所以在使用 strings[] 之后,你应该释放它的每个成员并用 NULL 代替它。 @KamalPancholi 你能在答案中说明你的意思吗? sizeof(char) 始终为 1,应省略。 恕我直言 malloc 和“就地”不能很好地结合在一起。 【参考方案1】:

通过将分配和转换组合到一个单一的void to_lower(char ** strings) 函数中,您将自己包裹在轴上(让自己感到困惑)。正如您所发现的,如果您想在同一个对象上调用该函数两次,您必须 free 在两次调用之间分配用于保存小写字符串的内存 - 但是您丢失了指向原始字符串的指针..

虽然在一个函数中组合多个操作并没有错,但您必须退后一步,确保您所做的事情是有意义的,并且不会导致比它解决的问题更多的问题。

在修改strings 中包含的字符串之前,需要分配和复制strings,因为您将strings 初始化为指向字符串文字的指针数组。字符串文字是在只读内存(通常是可执行文件的.rodata 部分)中创建的不可变的(在除少数系统之外的所有系统上)。尝试修改字符串文字几乎可以保证 SegFault(在除少数古怪系统之外的所有系统上)

此外,如果您已经用指向保存小写结果的已分配内存的指针覆盖了指向原始字符串的指针地址,您将如何取回原始字符串? (更不用说你通过用指向已分配内存的指针替换指向文字的指针所造成的跟踪头痛,只要free 这些指针是可以的)。

这里最好保留原始strings 原样并简单地分配原始副本(通过分配指针,包括一个用于标记值的指针,并为每个原始字符串分配存储空间,然后复制原始转换为小写之前的字符串。这解决了您的内存泄漏问题以及丢失指向字符串文字的原始指针的问题。您可以根据需要释放小写字符串,并且您可以随时制作另一个原件副本发送到您的再次转换函数。

那么您将如何实施呢?最简单的方法就是声明一个 pointer to pointer to char(例如双指针),你可以为你喜欢的任意数量的指针分配一块内存。在这种情况下,只需分配与 strings 数组中相同数量的指针,例如:

    char *strings[] = "Hello", "Zerotom", "new", NULL ;
    size_t nelem = *(&strings + 1) - strings;   /* number of pointers */
    /* declare & allocate nelem pointers */
    char **modstrings = malloc (nelem * sizeof *modstrings);

    if (!modstrings)   /* validate EVERY allocation */
        perror ("malloc-modstrings");
    

注意:也可以使用sizeof strings / sizeof *strings获取元素个数)

现在您已经为modstrings 分配了一个内存块,其中包含与strings 中相同数量的指针,您可以简单地分配足以容纳每个字符串文字的内存块并分配起始地址每个块指向modstrings 中的连续指针,将最后一个指针NULL 设置为您的标记,例如

void copy_strings (char **dest, char * const *src)

    while (*src)                           /* loop over each string */
        size_t len = strlen (*src);         /* get length */
        if (!(*dest = malloc (len + 1)))   /* allocate/validate */
            perror ("malloc-dest");         /* handle error */
            exit (EXIT_FAILURE);
        
        memcpy (*dest++, *src++, len + 1);  /* copy *src to *dest (advance) */
    
    *dest = NULL;       /* set sentinel NULL */

(注意:通过将src 参数传递为char * const *src 而不仅仅是char **src,您可以向编译器指示src 不会被更改,从而允许进一步优化编译器。restrict 会类似,但这个讨论留到另一天)

您的 to_lower 函数然后简化为:

void to_lower (char **strings) 

    while (*strings)                        /* loop over each string */
        for (char *p = *strings++; *p; p++) /* loop over each char */
            *p = tolower (*p);              /* convert to lower */

为方便起见,由于您知道在每次调用 to_lower 之前都希望将 strings 复制到 modstrings,因此您可以将这两个函数组合到一个包装器中(组合起来确实有意义),例如

void copy_to_lower (char **dest, char * const *src)

    copy_strings (dest, src);   /* just combine functions above into single */
    to_lower (dest);

(如果您也总是想在一次调用中执行这些操作,您甚至可以添加上面的 print_arrayfree_strings -- 稍后会更多)

copy_to_lowermodstrings 的每个copy_to_lowerprint_array 之间,您需要释放分配给每个指针的存储空间,这样当您再次调用copy_to_lower 时就不会泄漏内存。一个简单的free_strings 函数可以是:

void free_strings (char **strings)

    while (*strings)           /* loop over each string */
        free (*strings);        /* free it */
        *strings++ = NULL;      /* set pointer NULL (advance to next) */
    

您现在可以在main() 中任意多次分配、复制、转换为较低、打印和免费。您只需反复调用:

    copy_to_lower (modstrings, strings);    /* copy_to_lower to modstrings */
    print_array (modstrings);               /* print modstrings */
    free_strings (modstrings);              /* free strings (not pointers) */

    copy_to_lower (modstrings, strings);    /* ditto */
    print_array (modstrings);
    free_strings (modstrings);
    ...

现在回想一下,当您调用free_strings 时,您正在为每个字符串 释放存储空间,但是您正在离开包含分配给modstrings 的指针的内存块。所以要完成你分配的所有内存的释放,不要忘记释放指针,例如

    free (modstrings);                      /* now free pointers */

将其完全放入示例中,您可以执行以下操作:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

void print_array (char **strings)

    while (*strings)
        printf ("%s, ", *strings++);
    putchar ('\n');


void free_strings (char **strings)

    while (*strings)           /* loop over each string */
        free (*strings);        /* free it */
        *strings++ = NULL;      /* set pointer NULL (advance to next) */
    


void copy_strings (char **dest, char * const *src)

    while (*src)                           /* loop over each string */
        size_t len = strlen (*src);         /* get length */
        if (!(*dest = malloc (len + 1)))   /* allocate/validate */
            perror ("malloc-dest");         /* handle error */
            exit (EXIT_FAILURE);
        
        memcpy (*dest++, *src++, len + 1);  /* copy *src to *dest (advance) */
    
    *dest = NULL;       /* set sentinel NULL */


void to_lower (char **strings) 

    while (*strings)                        /* loop over each string */
        for (char *p = *strings++; *p; p++) /* loop over each char */
            *p = tolower (*p);              /* convert to lower */


void copy_to_lower (char **dest, char * const *src)

    copy_strings (dest, src);   /* just combine functions above into single */
    to_lower (dest);


int main(void) 

    char *strings[] = "Hello", "Zerotom", "new", NULL ;
    size_t nelem = *(&strings + 1) - strings;   /* number of pointers */
    /* declare & allocate nelem pointers */
    char **modstrings = malloc (nelem * sizeof *modstrings);

    if (!modstrings)   /* validate EVERY allocation */
        perror ("malloc-modstrings");
    

    copy_to_lower (modstrings, strings);    /* copy_to_lower to modstrings */
    print_array (modstrings);               /* print modstrings */
    free_strings (modstrings);              /* free strings (not pointers) */

    copy_to_lower (modstrings, strings);    /* ditto */
    print_array (modstrings);
    free_strings (modstrings);

    copy_to_lower (modstrings, strings);    /* ditto */
    print_array (modstrings);
    free_strings (modstrings);

    copy_to_lower (modstrings, strings);    /* ditto */
    print_array (modstrings);
    free_strings (modstrings);

    free (modstrings);                      /* now free pointers */

使用/输出示例

$ ./bin/tolower_strings
hello, zerotom, new,
hello, zerotom, new,
hello, zerotom, new,
hello, zerotom, new,

内存使用/错误检查

在您编写的任何动态分配内存的代码中,对于分配的任何内存块,您都有 2 个职责:(1)始终保留指向起始地址的指针内存块,因此,(2) 当不再需要它时可以释放

您必须使用内存错误检查程序来确保您不会尝试访问内存或写入超出/超出分配块的边界,尝试读取或基于未初始化的值进行条件跳转,最后,以确认您释放了已分配的所有内存。

对于 Linux,valgrind 是正常选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。

$ valgrind ./bin/tolower_strings
==5182== Memcheck, a memory error detector
==5182== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==5182== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==5182== Command: ./bin/tolower_strings
==5182==
hello, zerotom, new,
hello, zerotom, new,
hello, zerotom, new,
hello, zerotom, new,
==5182==
==5182== HEAP SUMMARY:
==5182==     in use at exit: 0 bytes in 0 blocks
==5182==   total heap usage: 13 allocs, 13 frees, 104 bytes allocated
==5182==
==5182== All heap blocks were freed -- no leaks are possible
==5182==
==5182== For counts of detected and suppressed errors, rerun with: -v
==5182== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

始终确认您已释放已分配的所有内存并且没有内存错误。

这是一篇很长的帖子,但您在学习动态分配方面取得了进展。您需要一些时间来消化所有内容,但如果您还有其他问题,请告诉我。

【讨论】:

【参考方案2】:

to_lower 函数看起来是否正常,或者如何修改它以便 如果当前是,它不会泄漏内存吗?

正如@chux 在 cmets 中指出的那样,您需要将 1 添加到 orginal_string 的 len,而不是指针本身。

关于您的问题,是的,它泄漏了,每次调用malloc 都希望调用free。这里的问题是:你不能在初始值上调用free,因为它们是用string-literals填充的。

一个可能的解决方案是:

extern char *strdup(const char *);

static char *dup(const char *str)

    char *res = strdup(str);

    if (res == NULL) 
        perror("strdup");
        exit(EXIT_FAILURE);
    
    return res;


int main(void)

    char *strings[] = 
        dup("Hello"),
        dup("Zerotom"),
        dup("new"),
        NULL
    ;

    ...

现在您可以调用to_lower,而无需在函数内部调用malloc,只需在不再需要数组时在最后为每个元素调用free

请注意,strdup 不是标准的一部分(但在许多实现中可用)

【讨论】:

感谢您的回答。是否可以在不必调用 malloc 的情况下执行 to_lower 函数?比如修改指针的值?如果是这样,那怎么做? UV +1 用于验证返回 strdupstrdup 分配,所以你必须validate——干得好。 99% 的时间都被忽视了。 (另外,请注意 strdup 不是 libc 的一部分,而是 POSIX,并且不保证可用也很好) @Shares,可以避免在to_lower 中调用malloc,因为你的函数不会改变位置和大小,只会改变他的内容。【参考方案3】:

每次调用to_lower() 时,您都将所有字符串文字替换为动态内存指针。 如果您在没有释放现有内存的情况下再次调用 to_lower(),则存在内存泄漏。

lower_string = malloc(strlen(original_string + 1) * sizeof(char));
        for (int j=0; j<=strlen(original_string); j++) 
            lower_string[j] = tolower(original_string[j]);
        
        strings[i]=lower_string;

因此,当不再需要 strings[] 数组时,您应该释放其所有内存。 例如

for (int i=0; strings[i] != NULL; i++) 
    free(strings[i]);
    strings[i] = NULL;

【讨论】:

【参考方案4】:

作为"Hello", "Zerotom", "new", NULL ;malloc() 和朋友的替代方案,形成指针数组char * strings[] 以使用指向可修改数据的指针进行初始化。

从 C99 开始,使用复合文字

void inplace_strtolower(char * s) 
  while (*s) 
    *s = (char) tolower((unsigned char ) *s);
    s++;
  


// "Non-const string literal"
// Compound literal from string literal"
#define NCSL(sl) ((char[sizeof(sl)])  sl )

int main(void) 
  char * strings[] = NCSL("Hello"), NCSL("Zerotom"), NCSL("new"), NULL;
  inplace_strtolower(strings[0]);
  inplace_strtolower(strings[1]);
  inplace_strtolower(strings[2]);
  puts(strings[0]);
  puts(strings[1]);
  puts(strings[2]);
  return 0;

输出

hello
zerotom
new

【讨论】:

以上是关于就地修改数组并了解其内存分配的主要内容,如果未能解决你的问题,请参考以下文章

C# 就地将 `int[]` 数组转换为 `byte[]` 数组

JAVA堆内存和栈内存初步了解

c++中二维数组的内存分配

了解自己主动内存管理

动态开辟内存的这些知识你知道了吗?了解柔性数组吗?超详细画图以及文字讲解,干货满满

从 C 扩展对 Numpy 数组进行操作,无需内存复制