如何区分堆中的字符串和字面量字符串?

27

我有一个使用情况,可以获取在内存中分配或字面上的字符串指针。现在后者不能被释放,所以如果我传递了错误的指针就会出现问题。有没有办法知道哪一个是已分配的,哪一个不是?

char *b = "dont free me!";
if(!IS_LITERAL(b)) {
    free(b);
}

我想象那样的事情。

我的例子:

情景1:字面意义

char *b = "dont free me!";
scruct elem* my_element = mylib_create_element(b);
// do smth
int result = mylib_destroy_element(my_element); // free literal, very bad

情景 2:在堆中

char *b = malloc(sizeof(char)*17); // example
strncpy(b, "you can free me!",17);

scruct elem* my_element = mylib_create_element(b);
// do smth
int result = mylib_destroy_element(my_element); // free heap, nice

用户如何调用 mylib_create_element(b); 不在我的控制范围内。如果他在 mylib_destroy_element 之前释放它,可能会导致崩溃。因此必须由 mylib_destroy_element 来进行清理。


5
在同一代码部分进行内存释放操作,可以避免这个问题。阅读 RAII 相关资料,了解其中一种方法。 - Sander De Dycker
2
仅仅因为它不是字面量并不意味着你需要调用 free。例如 char buffer[20]; b=buffer;,buffer 不是字面量,但它也没有在堆上分配。 - Sean
2
仅有一个 char*,无法确定它指向哪里。它只是一个 char 指针,不管你如何设置它,它都没有任何附加信息。如果您进行动态分配,则必须自行跟踪它。 - P.P
12
@Aldi:你应该让你的库同时进行分配和释放,或者都不进行。不要在库中释放一个在库外部分配的对象。换句话说:如果你控制创建和销毁函数,那么你就控制了进入库中的内容。 - Sander De Dycker
2
当你没有析构函数帮助你时,如何在裸C中执行RAII? - pjc50
显示剩余7条评论
8个回答

26

我最近也遇到了类似的情况。这是我的做法:

如果你正在创建一个接受字符串指针并使用它来创建对象(mylib_create_element)的 API,一个好主意是将字符串复制到单独的堆缓冲区中,然后在适当的时候释放它。这样,用户有责任释放他在调用你的 API 时使用的字符串,这是有道理的。毕竟,那是他的字符串。

请注意,如果你的 API 依赖于用户在创建对象后更改字符串,则此方法不起作用!


我想我会选择这个。 - alknows
另一种可能性是,如果API不进行复制,并且用户负责从API中删除字符串,然后释放它。 POSIX的putenv API就是这样工作的。 - Random832
@Random832:这很好,但它取决于使用情况。例如,我的API处理在回调中发送的对象,因此它无法承受这种情况:如果传递的字符串是基于堆栈的char数组,则会超出范围,给用户带来不愉快的惊喜。不知道这里的使用情况,我试图提供一个适合大多数用例的解决方案。在某些情况下,不进行任何复制可能实际上更好:一些用户可能会修改传递给期望对象更改的函数的字符串。但无论哪种方式,都必须进行适当的文档记录 =) - user3079266
@Aldi 这是一种非常常见的技术,有一个专门将字符串复制到堆上的函数:strdup - Mark Ransom
@MarkRansom的问题是,我们能比那更聪明吗?显然没有现成的标准解决方案。最佳实践是像Mints97建议的那样。谢谢大家。 - alknows

23

在大多数 Unix 系统中,存在 'etext' 和 'edata' 值。如果您的指针位于 'etext' 和 'edata' 之间,则它将被静态初始化。这些值没有出现在任何标准中,因此使用是不可移植的,存在风险。

例如:

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

extern char edata;
extern char etext;

#define IS_LITERAL(b) ((b) >= &etext && (b) < &edata)

int main() {
    char *p1 = "static";
    char *p2 = malloc(10);
    printf("%d, %d\n", IS_LITERAL(p1), IS_LITERAL(p2));
}

这很有趣。实际上,这回答了我的问题,尽管如果不是标准功能和可移植的话,我可能无法使用它。谢谢。 - alknows

12

您只能要求用户显式地将其输入标记为字面量或分配的字符串。

但是,正如@Mints97在他的回答中提到的那样,基本上这种方法在架构上是不正确的:您强制库的用户执行某些显式操作,如果他忘记了,最有可能导致内存泄漏(甚至应用程序崩溃)。因此,只有在以下情况下才使用它:

  1. 您想大大减少分配的数量。在我的情况下,这是JSON节点名称,在程序生命周期中从不更改。
  2. 您可以对您的库的消费者代码有很好的控制。在我的情况下,库与二进制文件一起提供,并且与其紧密绑定。

实现示例

#define AAS_DYNAMIC             'D'
#define AAS_STATIC              'S'

#define AAS_STATIC_PREFIX       "S"
#define AAS_CONST_STR(str)      ((AAS_STATIC_PREFIX str) + 1)

char* aas_allocate(size_t count) {
    char* buffer = malloc(count + 2);
    if(buffer == NULL)
        return NULL;

    *buffer = AAS_DYNAMIC;

    return buffer + 1;
}

void aas_free(char* aas) {
    if(aas != NULL) {
        if(*(aas - 1) == AAS_DYNAMIC) {
            free(aas - 1);
        }
    }
}

...

char* s1 = AAS_CONST_STR("test1");
char* s2 = aas_allocate(10);

strcpy(s2, "test2");

aas_free(s1);
aas_free(s2);

测试性能(注1)

我使用以下代码(800k次迭代)对我的libtsjson库进行基准测试:

    node = json_new_node(NULL);
    json_add_integer(node, NODE_NAME("i"), 10);
    json_add_string(node, NODE_NAME("s1"), json_str_create("test1"));
    json_add_string(node, NODE_NAME("s2"), json_str_create("test2"));
    json_node_destroy(node);

我的CPU是Intel Core i7 860。 如果NODE_NAME只是一个宏,每次迭代的时间为479ns。 如果NODE_NAME是内存分配,则每次迭代的时间为609ns

提示用户或编译器(注意#2)

  • 给所有这些指针添加提示,即Linux静态源代码分析器Sparse可能会捕获此类问题。

char __autostring* s1 = aas_copy("test"); /* OK */
char __autostring* s2 = strdup("test");   /* Should be fail? */
char* s3 = s1;                            /* Confuses sparse */
char* s4 = (char*) s1;                    /* Explicit conversion - OK */

(不完全确定 Sparse 的输出)

  • 使用简单的 typedef,使编译器在您做错事情时发出警告:

#ifdef AAS_STRICT
typedef struct { char a; } *aas_t;
#else
typedef char *aas_t;
#endif

这种方法是迈向一个充斥着肮脏C语言黑客技巧的世界的又一步,即sizeof(*aas_t)现在大于1。

完整的更改源代码可以在此处找到。如果使用-DAAS_STRICT编译它,将会出现大量错误:https://ideone.com/xxmSat 即使对于正确的代码,它也可能抱怨strcpy()(在ideone上未重现)。


这是一个相当不错的解决方法。问题在于我无法强制这个库的用户像那样创建他们的字符串。感谢分享。 - alknows
3
这个功能是可行的,但API设计不好。C语言中的所有其他函数都只接受普通字符串,为什么你的不接受?当你看到像“mylib_create_elem(char*)”这样的函数签名时,这不是你想要的!如果你使用不当,你将悄悄泄漏内存并且没有任何警告。Mints97的回答在所有方面都更好。 - Christian Aichinger
@ChristianAichinger,是的,你和Mints97都是正确的。在我回答的那个时刻,Aldi没有提到他将会把接口暴露为库调用。当我设计我的API时,关键的想法是提高性能。我已经添加了有关我的方法性能以及如何克服它所引起的问题的注释。 - myaut

6
简单来说,由于C语言没有划分堆栈、堆和数据段,因此您不能这样做。 如果你想猜测的话,你可以收集堆栈上第一个变量的地址、调用函数的地址和分配给堆的字节的内存地址;然后将它与你的指针进行比较——这是一种非常糟糕的做法,没有任何保证。 最好通过修改代码的方式解决这个问题。

2
有趣的事实:在C99的文本中,单词“stack”并没有出现过。(我没有检查C11;随着线程的引入,这个情况可能已经改变。)虽然需要支持递归函数调用,但是为了满足这个要求,并不一定需要传统的堆栈,甚至不需要LIFO数据结构;例如,“Cheney on the M.T.A.”。 - zwol

6
这里有一种实用的方法:
虽然C语言标准没有规定这一点,但对于代码中所有相同的字面字符串,在可执行映像的RO-data部分中编译器只会生成单个副本。
换句话说,你代码中每个"don't free me!"字面字符串的出现都被翻译成相同的内存地址。
因此,在你想要释放该字符串的时候,你可以简单地将其地址与字面字符串"don't free me!"的地址进行比较。
if (b != "dont free me!") // address comparison
    free(b);

再次强调,这不是C语言标准规定的,但几乎所有像样的该语言编译器都会实现。


上述仅仅是对问题的直接应对之法,而非问题背后的动机。

严格来讲,如果你的实现已经到了需要区分静态分配字符串和动态分配字符串的阶段,那么我会猜测你的初始设计可能存在缺陷。


4
你可以执行以下操作:
  typedef struct 
{
 int is_literal;
 char * array;
} elem;

每次在堆上分配elem.array时,只需将is_literal设置为0。当您将数组设置为文字时,请将标志设置为非零值,例如:

elem foo;
foo.array = "literal";
foo.is_literal = 1 ;

或者

elem bar;
bar.array = (char*) (malloc(sizeof(char) * 10)) ;
bar.is_literal = 0;

然后在客户端:

if(!bar.is_literal) {
free(bar.array);
}

就是这么简单。

1
把它包装起来其实听起来是个好主意……但我无法控制字符串的创建方式。 - alknows
7
如果您无法控制字符串的创建,那么您不需要释放该字符串! - Evdzhan Mustafa

4

这就是为什么规则是只有创建字符串的代码或模块才能释放它。换句话说,每个字符串或数据片段都由创建它的代码单元“拥有”。只有所有者才能释放它。函数不应该释放作为参数接收到的数据结构。


2
在早期,当80386最多只能拥有8兆字节的RAM,并且每篇杂志文章都在解释制作对象的思想时,我不喜欢将完全好的文字复制到字符串对象中(分配和释放内部副本),因为粗略的字符串类是C++花哨东西的示例之一,所以我问了Bjarne。他说不要担心。
这是否与文字与其他char*指针有关?您始终可以拥有该内存。从您寻找不同内存段的想法来看,我认为是这样的。
还是更普遍地说,所有权可能会给出,也可能不会给出,没有办法判断,需要存储一个标志:“嘿,这是一个堆对象,但是其他人仍在使用它,并且稍后会处理它,好吗?”
对于可处理的情况,如“在堆上”或“不在堆上”(文字,全局变量,基于堆栈),您可以让free函数知道。如果您提供了一组匹配的allocate/maybe-free,它就可以被编写为“知道”其控制下的内存。

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