GNOME-内存管理

以下内容译自:https://developer.gnome.org/documentation/guidelines/programming/memory-management.html 加入了个人的一些理解。

GNOME是的堆栈主是是用C编写的,所以动态的内存分配都要手动管理。可以使用Glib中提供的API来实现,但在编代码时要时刻记得内存。

这里假定读者都知道内存使用、堆分配,Glib中与malloc(), free(),相类似的是 g_malloc(), g_free().

前提知识-程序内存分配

摘自 https://blog.csdn.net/yingms/article/details/53188974

一个由C/C++编译的程序占用的内存分为以下几个部分

1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放

4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放

5、程序代码区—存放函数体的二进制代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//main.c
int a = 0; //全局初始化区
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main() {
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区,p3在栈上。
static int c = 0; //全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得来的10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

栈 stack:

由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间

堆 heap:

需要程序员自己申请,并指明大小,在C中malloc函数

如p1 = (char *)malloc(10);

在C++中用new运算符

如p2 = (char *)malloc(10);

但是注意p1、p2本身是在栈中的。

内存管理原则

这部分原文写的比较绕,简单的说,就是GNOME提供了一种内存所有权的机制来管理内存。每块分配的内存,只有一个所有者,所有权可以转移,只有所有者有权释放内存。

这里有一个重要的限制:变量绝不能从 owned 更改 更改为 unowned(反之亦然)。此限制是简化的关键。

1
2
3
char *  generate_string (const char *template);
/*这里定义的参数是用const修饰的,表示函数在调用的时候,不会把参数的所有权转进来*/
void print_string (const char *str);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//从修饰符可以看出变量具不具有内存所有权
char *my_str = NULL; /* owned 表示在栈里,退出时需要释放 char*修饰的 */
const char *template; /* unowned 表示不在栈里,因为这是const修饰的*/
GValue value = G_VALUE_INIT; /* owned GValue修饰的 */
g_value_init (&value, G_TYPE_STRING);

/* Transfers ownership of a string from the function to the variable. */
template = "XXXXXX";
my_str = generate_string (template); //template的所有权从系统管理到my_str了,因为template原本就没有所有权
/* template是用const修饰的,generate_string的参数也是const修饰的,他们本身都没有内存的所有权 */

/* No ownership transfer. */
print_string (my_str); //没有发生所有权转移

/* Transfer ownership. We no longer have to free @my_str. */
//这里发生所有权转移,猜测my_str的形参类型定义的是gchar,没有const修饰
g_value_take_string (&value, my_str);

/* We still have ownership of @value, so free it before it goes out of scope. */
g_value_unset (&value);

有一些库中用一些词来表示内存所有权的转移,比如函数名中用take表示完全转移,例如g_value_take_string()

理想情况是,所有的函数都注释了所有相关的参数和返回值是否有转移所有权,下面有几个方法可以确定是否有所有权转移。

  1. 如果这个类型有注释,认真看注释里怎么说的
  2. 否则,如果是const修饰的类型,没有转移
  3. 否则,如果函数文档明确的说明了返回值必须要释放,这里有或全部转移了
  4. 否则,如果函数名中有dup,take,steal,这里有或全部转移了
  5. 否则,如果函数名中有peek,没有转移
  6. 否则,你需要看一下函数的代码来确认是否发生转移

有了这些方法,就可以方便的判断内存所有权和转移情况了。注意,copy()函数必须使用正确的数据类型,比如g_strdup()是针对string的,g_object_ref()是针对GObject的。

论起内存所有权转移,malloc()/free() 与 引用计数是等价的。前者,一个新分配的堆内存转移,后者,一个新的引用(转移)。

函数文档

正确的在文档中记录每个函数和参数以及返回值的所有权转移是很重要的。最好的记录所有权转移的方法,是用(transfer) 记录,这是在gobject-introspection里介绍的方法。在API文档中包含了这个注释后,自检工具就可以读到了。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* g_value_take_string:
* @value: (transfer none): an initialized #GValue
* @str: (transfer full): string to set it to
*
* Function documentation goes here.
*/

/**
* generate_string:
* @template: (transfer none): a template to follow when generating the string
*
* Function documentation goes here.
*
* Returns: (transfer full): a newly generated string
*/

变量的内存所有权可以在代码中通过行内注释来记录。这个不是标准化的,任何工具也都读不到, 用好了,形成一个规范,也很方便。

1
2
GObject *some_owned_object = NULL;  /* owned */
GObject *some_unowned_object; /* unowned */

在文档中记录容器类型也是一种规范,它包括了所包含元素的类型。如

1
2
3
4
5
6
7
8
/* PtrArray<owned char*> */
GPtrArray *some_unowned_string_array; /* unowned */

/* PtrArray<owned char*> */
GPtrArray *some_owned_string_array = NULL; /* owned */

/* PtrArray<owned GObject*> */
GPtrArray *some_owned_object_array = NULL; /* owned */

需要注意,有所有权的变量,都应该被初始化,这样释放的时候会更方便。

引用计数

GLib库除了提供类似malloc()/free()这样的常规内存管理类型外,还包含多种引用计数类型的实现,其中GObject是一个典型的例子。所有权和转让的概念同样适用于引用计数类型,正如它们适用于已分配的类型一样。当一个作用域通过调用g_object_ref()持有对实例的强引用时,即表示该作用域拥有这个引用计数类型。通过再次调用g_object_ref()可以实现实例的“复制”(增加引用计数)。而释放所有权则是通过调用g_object_unref()来完成的——尽管这一步可能并不会立即导致实例被销毁,但它确实释放了当前作用域对该实例的所有权。

在GLib中,还有其他支持引用计数的数据类型,例如 GHashTable(使用 g_hash_table_ref() 和 g_hash_table_unref())或 GVariant(使用 g_variant_ref() 和 g_variant_unref())。由于历史原因,一些类型,如 GHashTable,既支持引用计数也支持显式终结。应始终优先使用引用计数,因为它允许实例在多个作用域之间轻松共享(每个作用域持有自己的引用),而无需为实例分配多个副本。这样可以节省内存。

浮动引用