Objective-c 内存管理学习笔记之一(c篇)

 

Objective-c的内存管理机制是非常灵活的,即支持最原始的c内存管理方式,到较早版本的oc的手工分配内存(alloc与releae手工匹配次数),再到使用AutoreleasePool的半手工处理,再到最新的ios5以上版本支持的ARC(Automatic Reference Counting),以及非移动设备支持的垃圾回收,看似纷乱,但其实脉络是非常清晰的。虽然目前由于对arc的支持,写程序的时候基本已经不再需要去手工分配释放内存,但是学习c与早期oc内存管理的原理,对于理解arc以及优化应用程序性能,还是非常必要的,所以,我想按照这个发展的过程,分为四部分说一下,下面先从c的内存管理相关内容说起。

 

 

Oc内存管理发展过程

 

 

  1. c语言的数据类型

在C语言中,数据类型可分为:基本数据类型,构造数据类型,指针类型,空类型四大类,如果不使用malloc等函数特殊创建,那么可以认为,这些数据类型都是创建在栈上,并且作用域维持到同级别的下一个花括号}为止,不必特别做释放处理,而使用malloc创建在堆上的数据,则需要使用free函数释放,这点很重要,以至在oc中,搞清楚那些类型的数据需要手工释放,哪些属于自动释放,根源都来自于c的这个特性。

1)    基本数据类型

基本数据类型最主要的特点是,其值不可以再分解为其它类型。也就是说,基本数据类型是自我说明的。

 

2)    构造数据类型

构造数据类型是根据已定义的一个或多个数据类型用构造的方法来定义的。也就是说,一个构造类型的值可以分解成若干个“成员”或“元素”。每个“成员”都是一个基本数据类型或又是一个构造类型。在C语言中,构造类型有以下几种:
·数组类型
、结构类型、联合类型。

 

3)    指针类型

指针是一种特殊的,同时又是具有重要作用的数据类型。其值用来表示某个量在内存储器中的地址。虽然指针变量的取值类似于整型量,但这是两个类型完全不同的量,因此不能混为一谈。

 

4)    空类型

在调用函数值时,通常应向调用者返回一个函数值。这个返回的函数值是具有一定的数据类型的,应在函数定义及函数说明中给以说明,例如在例题中给出的max函数定义中,函数头为: int max(int a,int b);其中“int ”类型说明符即表示该函数的返回值为整型量。又如在例题中,使用了库函数 sin,由于系统规定其函数返回值为双精度浮点型,因此在赋值语句s=sin (x);中,s 也必须是双精度浮点型,以便与sin函数的返回值一致。所以在说明部分,把s说明为双精度浮点型。但是,也有一类函数,调用后并不需要向调用者返回函数值, 这种函数可以定义为“空类型”。其类型说明符为void。

 

  1. c语言的指针

在c与oc中,指针无处不在,理解透彻指针的概念和用处是基础中的基础,下面简单的较少一下c语言中指针的概念,详细的还是需要看资料。

首先,指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。 要搞清一个指针需要搞清指针的四方面的内容:指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区。现在分别说明。  

  先声明几个指针做例子:  

  例一:  

int * ptr;  

char * ptr;  

int ** ptr;  

int (*ptr)[3];  

int * (*ptr)[4];  

  

1)    指针的类型

从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:  

    int *ptr;//指针的类型是int*  

    char *ptr;//指针的类型是char*  

    int **ptr;//指针的类型是int**  

    int (*ptr)[3];//指针的类型是int(*)[3]  

    Int *(*ptr)[4];//指针的类型是int*(*)[4]  

 

2)    指针所指向的类型

当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。  

从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:  

  int *ptr;//指针所指向的类型是int  

  char *ptr;//指针所指向的的类型是char  

  int **ptr;//指针所指向的的类型是int*  

  int (*ptr)[3];//指针所指向的的类型是int()[3]  

  int *(*ptr)[4];//指针所指向的的类型是int*()[4]  

在指针的算术运算中,指针所指向的类型有很大的作用。  

指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越来越熟悉时,你会发现,把与指针搅和在一起的 "类型 "这个概念分成 "指针的类型 "和 "指针所指向的类型 "两个概念,是精通指针的关键点之一。

3)    指针的值,或者叫指针所指向的内存区或地址

指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。   指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。  

指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。  

4)   指针本身所占据的内存区

指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度。  

指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。  

5)   运算符&和*  

&取地址,*取值,例如:

int *p, *a = 20; \\声明p,a为int型指针

int j, i = 10; \\声明i,j 为int

j = i;

p = &i; \\将i的地址传递给p

j = *a; \\将指针a指向的数值付给j

 

*在声明变量是,意味着该变量为指针类型,在=后边意味着从一个指针中获得该指针的数值。

 

下面举一个简单的例子,演示一下关于指针的算术运算  

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的。例如:  

  例一:  

char a[20];  

int * ptr=a;

ptr++;  

  在上例中,指针ptr的类型是int*,它指向的类型是int,它被初始化为指向整形变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr的值加上了sizeof(int),在32位程序中,是被加上了4。由于地址是用字节做单位的,故ptr所指向的地址由原来的变量a的地址向高地址方向增加了4个字节。  

由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的四个字节,此时指向了数组a中从第4号单元开始的四个字节。  

  我们可以用一个指针和一个循环来遍历一个数组,看例子:

  例二:  

int array[20];  

int* ptr=array;  

...  

//此处略去为整型数组赋值的代码。  

...  

for(i=0;i <20;i++)  

{  

 (*ptr)++;  

 ptr++;  

}

  这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1,所以每次循环都能访问数组的下一个单元。  

 

  1. c语言的内存分配方式与作用域

内存管理,最重要的是两个方面,分配方式和作用域,内存分配方式有三种:

  

1)    从静态存储区域分配。

内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

  

2)    在栈上创建。

在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。



3)     从堆上分配,亦称动态内存分配。

程序在运行的时候用malloc申请任意多少的内存,程序员自己负责在何时用free释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

 

彻底理解数据的内存分配方式和作用域对于以后学些oc的内存管理非常非常重要,分配在栈的数据不存在内存泄露问题,就像前面说过,分配在栈上的数据,在该数据声明的配对}结束时就自动释放,只可能存在野指针的问题,而oc的内存管理,主要是针对分配在堆上的数据。下面单开一章介绍一下c在堆上实现动态存储分配的方法。

  1. malloc/free

首先需要重点介绍一下malloc/free函数,Malloc 向系统申请分配指定size个字节的内存空间。返回类型是 void* 类型。void* 表示未确定类型的指针。C,C++规定,void* 类型可以强制转换为任何其它类型的指针。

1)    函数简介

  原型:extern void *malloc(unsigned int num_bytes);

 

  头文件:在TC2.0中可以用malloc.h或 alloc.h (注意:alloc.h 与 malloc.h 的内容是完全一致的),而在Visual C++6.0中可以用malloc.h或者stdlib.h

 

  功能:分配长度为num_bytes字节的内存块

 

  返回值:如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。函数返回的指针一定要适当对齐,使其可以用于任何数据对象

 

  说明:关于该函数的原型,在旧的版本中malloc返回的是char型指针,新的ANSIC标准规定,该函数返回为void型指针,因此必要时要进行类型转换。

 

  名称解释:malloc的全称是memory allocation,中文叫动态内存分配,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存。

 

2)   函数的工作机制

  malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。

malloc()是C语言中动态存储管理的一组标准库函数之一。其作用是在内存的动态存储区中分配一个长度为size的连续空间。其参数是一个无符号整形数,返回值是一个指向所分配的连续存储域的起始地址的指针。 当函数未能成功分配存储空间(如内存不足)就会返回一个NULL指针。所以在调用该函数时应该检测返回值是否为NULL并执行相应的操作。

3)   举例说明

  正常片段:

 

#include <stdio.h>

#include <stdlib.h>

typedef struct data_type{

    int age;

    char name[20];

} data;

 

 

data *bob;

 

bob = (data *) malloc( sizeof(data) );

if( bob != NULL ) {

    bob->age = 22;

    strcpy( bob->name, "Robert" );

    printf( "%s is %d years old\n", bob->name, bob->age );

}else{

    printf("malloc error!\n");

    exit(1);

}

free(bob );

  内存泄漏实例,malloc分配了内存,却没有使用free释放:

 

#include <stdio.h>

#include <stdlib.h>

#define MAX 100000000

 

int main(void)

{

    int *a[MAX];

    int i;

    for( i=0; i<MAX; i++ ) {

        a[i] = (int *)malloc( MAX );

        }

    return 0;

}

关于c的指针高级应用以及堆内存的分配和释放,内容很多,这里只是简单的介绍一下,这个概念非常重要,oc创建对象,本质上都是在堆上alloc,并在合适的时机free。如果对指向堆的指针使用不当,则会造成野指针与内存泄露问题,内存管理,除了优化外,主要就是为了防止这两个问题,而这两个问题,往往是同时可能存在的。

 

  1. 指针在函数之间的传递方式

为什么讨论这个问题呢?我认为,大量野指针正是指针在函数之间传递时,由于指向的堆内存失效导致,而内存泄露,则是为了解决这种失效问题是,出现的次生灾难(当然,如果在同一段程序里,非要不写free那是犯2,不在讨论范围)。

下面看两个著名的c的标准库函数,strcmp,strcpy和他们的源码:

 

int __cdecl strcmp (const char * src,const char * dst)

{

   

      int ret = 0 ;

   

      while( ! (ret = *(unsigned char *)src - *(unsigned char *)dst) && *dst)

       

          ++src, ++dst;

   

      if ( ret < 0 )

       

          ret = -1 ;

   

      else if ( ret > 0 )

       

          ret = 1 ;

   

      return( ret );

   

      }

// strcmp.c

 

#include <syslib.h>

#include <string.h>

 

main()

{

    char *s1="Hello, Programmers!";

    char *s2="Hello, programmers!";

    int r;

   

    clrscr();

   

    r=strcmp(s1,s2);

    if(!r)

        printf("s1 and s2 are identical");

    else

        if(r<0)

            printf("s1 less than s2");

        else

            printf("s1 greater than s2");

   

   

    getchar();

    return 0;

}

 

 

由于strcmp返回一个int值作为判断两个字符串大小的标志,当int作为返回类型的时候,属于值复制,故不存在上述两种内存问题。

#include <stdio.h>

char * strcpy(char *to, const char *from)

{

    char *save = to;

   

    for (; (*to = *from) != '/0'; ++from, ++to);

    return(save);

}

 

// strcpy.c

 

#include <syslib.h>

#include <string.h>

 

main()

{

    char *s="Golden Global View";

    char d[20];

   

    clrscr();

   

    strcpy(d,s);

    printf("%s",d);

   

    getchar();

    return 0;

}

 

strcpy返回的是一个char*,所以调用方式会需要在调用函数之间,创建好char数组并分配好内存,并将该数组当做参数传递给strcpy,这样在退出strcpy函数时,该内存仍然未失效,由于char数组属于基本数据类型,所以该变量分配在栈上,并在main函数结束时,失效。

 

现在分析一下以上两个例子,当返回值是一个基本类型的时候,比如int,不存在任何问题,在strcmp中创建一个int,并在return的时候,复制一份传递出来,而当返回值是一个指针类型的时候,比如char*,则需要在调用之前创建好对象并传递给函数,如果不这样做,只是在strcpy中创建一个char*并reture,好了,那么在strcpy函数结束时,该内存失效,虽然成功的传递出一个char*,但指向的是失效的内存区域,野指针出现了,但我做例子的时候,有个有趣的现象,虽然该内存失效,但是仍然可以printf出该字符串,大惑不解,小强一句话点醒梦中人,该内存只是可以被使用,在没有被其他变量使用前,并没有被清空。

 

Ok现在遇到问题了,如果我想返回一个字符串数组,但有不想事先创建怎么办?也有办法解决,就是利用struct。这种形式相对较安全,可以避免忘记释放指针而造成内存泄露,也可以避免访问悬挂指针造成的错误。但缺点是由于结构是先拷贝再返回,因此如果结构较大时,会影响效率和占用较大内存。

 例子如下:

 

#include <stdio.h>

 

struct tag

{

    int a[10];

}x,y;

 

struct tag retArray()

{

    int i=0;

    for(i=0;i<10;i++)

        x.a[i]=i;

    return x;

}

 

int main()

{

    struct tag y=retArray();

    printf("%d/n",y.a[3]);

    return 0;

}

这种方法,看似完美的解决了问题,除了效率问题,但是这种方法是非常不值得推荐的,如果return一个10000字节的字符串,这种方法在内存中创建20000byte,而传递指针的方式只有10004个字节,效率不可同日而语。

 

下面是使用malloc解决返回指针的例子:

 

int * returnIntPoint()

{

    int * i = malloc(sizeof(int));

    *i = 10;

    return i;

}

 

int main(int argc, const char * argv[])

{

    int *j = returnIntPoint();

    printf("%d\n",*j);

    free(j);

    return 0;

}

 

虽然只是个小的例子,但我认为这是所有objective-c内存对象管理的本质,即需要的时候,在堆上创建对象,通过指向该对象的指针调用,并在所有指向该内存的指针失效时,立刻使该段内存释放,而这一切,是跨作用域的。

 

 

posted on 2012-10-18 10:01  jinshoucai  阅读(421)  评论(0)    收藏  举报