C 中的函数引用外部分配的内存

function in c referencing memory externally allocated

提问人:aerijman 提问时间:1/1/2021 更新时间:1/1/2021 访问量:78

问:

我正在拆分这些行,因为它们从输入中是红色的:

char **split (char *in, size_t *n, char *delimiter){
    if (delimiter == NULL) delimiter="\t"; // default
    
    char **tokens = malloc(20 * sizeof(char*));

    char *token = strtok(in, delimiter);
    while (token != NULL) {
        tokens[*n] = malloc(30 * sizeof(char*)); // <- why not *tokens[0]
        strcpy(tokens[*n], token);
        (*n)++;
        token = strtok(NULL, "\t");
    }
    return tokens;
}

我有一个(可能是错的,但请向我解释)的想法,即在每一行上分配(和释放)不同的内存插槽是不有效的。

在拆分函数之外分配内存并将指向字符串数组的指针传递给拆分函数是否安全(并且可能?)?如何(那么我会在下一行之前使用 memset)?

谢谢!

c 函数 malloc

评论

1赞 Andrew Henle 1/1/2021
我有一个(可能是错的,但请向我解释)的想法,即在每一行上分配(和释放)不同的内存插槽是不有效的。那么如果是这样呢?你有什么证据表明这种低效率会带来任何问题吗?您可能出于某种原因购买了 CPU 及其提供的周期。如果这意味着你要以牺牲一定的效率为代价来编写易于阅读、易于维护的代码,那么这就是快速 CPU 的一个很好的理由。

答:

2赞 Chris Dodd 1/1/2021 #1

你遇到的主要问题是,你在循环中为30个指针分配了空间,然后将一个串线复制到该空间中。如果该字符串恰好超过 30 个指针(包括终止 NUL),则会溢出错位的空间并损坏内存。如果它更短,你只是在浪费一些空间。

更好的做法是为字符串分配足够的空间:

tokens[*n] = malloc(strlen(token) + 1);

或者如果你有可用的 (POSIX),你可以用它来同时执行 malloc 和 strcpystrdup

tokens[*n] = strdup(token);

你还有一个问题,即在初始分配中你只为 20 个令牌(指针)分配空间——如果你有超过 20 个,你也会超出这个空间。最好根据需要使用指针数组以使其更大。realloc

1赞 Schwern 1/1/2021 #2

在拆分函数之外分配内存并将指向字符串数组的指针传递给拆分函数是否安全(并且可能?)?

不,因为在您阅读之前,您不知道每个令牌的大小,也不知道有多少个令牌。因此,您编写的代码是不安全的。它假设每个令牌不超过 29 个字符,并且不超过 20 个令牌。


为每个令牌分配恰到好处的内存量可以用 来解决,正如 Chris Dodd 所解释的那样。但这仍然会让你分配一堆小内存。strdup

为了最大程度地减少分配,您可以将所有令牌存储在单个内存块中。就是这样;它修改输入字符串,将分隔符替换为 null。与其复制每个标记,不如仅指向原始字符串中每个标记的开头。strtoktokens

这意味着指向并且您的函数正在修改。我会将是否复制输入字符串的选择权留给调用者。tokensinin

// Don't modify the original, tokens points at a copy.
char **tokens = split(strdup(input), &num_tokens, "\t");

// Modify the original, tokens points at input.
char **tokens = split(input, &num_tokens, "\t");

这就留下了分配的问题。要做到这一点,你要么需要知道有多少代币,要么即时重新分配。tokens

为了最小化 CPU,您可以根据需要增长。realloctokens

    size_t tokens_size = 1;
    char **tokens = malloc(tokens_size * sizeof(char*));

    *num_tokens = 0;
    for(
        char *token = strtok(in, delimiter);
        token;
        token = strtok(NULL, delimiter)
    ) {
        if( *num_tokens >= tokens_size ) {
            tokens_size *= 2;
            tokens = realloc(tokens, tokens_size * sizeof(char*));
        }

        tokens[*num_tokens] = token;

        (*num_tokens)++;
    }

作为效率和内存使用之间的折衷方案,我没有为每个代币重新分配,而是将每个 realloc 的代币大小增加了一倍。这将使用额外的内存,但可以最大程度地减少重新分配。


为了最大程度地减少分配,您需要对字符串进行标记化,并记住有多少个标记。然后分配足够的空间。然后再次读取字符串,存储指向令牌的指针。

    // tokenize in and remember how many there are.
    *num_tokens = 0;
    for(
        char *token = strtok(in, delimiter);
        token;
        token = strtok(NULL, delimiter)
    ) {
        (*num_tokens)++;
    }

    // Allocate exactly enough space.
    char **tokens = malloc(*num_tokens * sizeof(char*));
    
    // Iterate through the tokens and store them.
    char *token = in;
    for(size_t i = 0; i < *num_tokens; i++) {
        // Store a pointer to the token
        tokens[i] = token;
        // Jump ahead the length of the token plus the null.
        token += strlen(token) + 1;
    }

对于扫描两次输入的成本,您可以只执行一次 malloc。


注意:使用包含所有信息的结构,此代码会更容易、更安全。

typedef struct {
    char *original;
    char **tokens;
    size_t num_tokens;
    size_t tokens_size;
} Tokens;

这样可以把所有东西放在一起,你可以把它分解成像 和 这样的函数。Tokens_initTokens_free

评论

0赞 aerijman 1/2/2021
多谢!在第二种选择中,给定了确切数量的代币和每个代币的分配,不需要 memset,对吧?
0赞 Schwern 1/2/2021
@aerijman正确。你没有复制任何东西,只是对指针列表进行排序。