10.C语言之动态内存分配

一.动态内存分配和函数指针指针

1. 动态内存分配

先来介绍三个动态内存分配的函数:malloc,calloc和realloc。说来惭愧,以前只知道malloc。现在来看下他们的区别:

malloc:最常用的分配内存块,但是不对内存进行初始化。

calloc:分配内存块,但是对内存块进行清零操作,这就造成此函数的效率要比malloc要低。

realloc:调整(增加或者减少)之前分配内存块的大小。

由于上面的函数只是开辟了一段内存,因此无法知道你要利用这段内存来存储什么类型的数据,因此只是返回一个void *类型的值,当然,void *可以和任何指针类型互相转换。

当分配内存失败时(可能是内存不足或者其他原因),以上的函数都会返回一个空指针,那么我们安全期间,在我们使用这块分配的内存前,都应该进行一次空指针的验证。

int main (void)
{
    int *p;
    p=(int *)malloc(sizeof(*p)*1000);
    if(p==NULL)
    {
        exit(EXIT_FAILURE);
    }
    printf("success");
    return 0;
}

注意上面的两点,一个是空指针的验证,另外一个是分配内存的大小,这里我们常常是用类型的大小乘以存储成员的数量来计算分配。如果是字符串,我们就不要忘记了加1,来存储\0。

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

int main (void)
{
    char *p;
    p=(char *)malloc(sizeof(*p)*1000+1);
    if(p==NULL)
    {
        exit(EXIT_FAILURE);
    }
    printf("success");
    return 0;
}

接下来我们看一下calloc元素的原型:

void * __cdecl calloc(_In_ size_t _Count, _In_ size_t _Size);

从上面我们可以看到calloc函数有两个参数,分配是,数量和大小。由此可以说明,calloc是C语言用来分配数组空间最好的选择。那么我们就把第一段代码改一下:

int main (void)
{
    int *p;
    p=(int *)calloc(1000,sizeof(int));
    if(p==NULL)
    {
        exit(EXIT_FAILURE);
    }
    printf("success");
    return 0;
}

这样就更合适了。

最后是realloc,我们也来看一下realloc元素的原型:

void * __cdecl realloc(_Post_ptr_invalid_ void * _Memory, _In_ size_t _NewSize);

当我们之前分配了一个数组的大小,但是后来我们却发现这个大小不够用了,或者是太大了,那么我们就可以利用realloc来调整我们的占用内存大小:

int main (void)
{
    int *p;
    p=(int *)calloc(1000,sizeof(int));
    if(p==NULL)
    {
        exit(EXIT_FAILURE);
    }
    realloc(p,sizeof(int)*100);
    if(p==NULL)
    {
        exit(EXIT_FAILURE);
    }
    printf("success");
    return 0;
}

也别忘了检验p是否为空指针的情况。

在C标准中,并没有对realloc的实现做以规定,但是对于大部分编译器来说,如果是把原地址空间缩小,他会尽量地不去移动原来的数据。如果是把空间增大,那么他会尽量首先在原地址的末尾去分配内存,如果不足以分配,那么编译器才会去寻找新的地址块,并且把原地址空间内的数据转移到新的地址上。

2. 释放空间

习惯了Java/C#的我们,似乎已经忘记了要回收垃圾的习惯,在C/C++中,是没有GC的,因此我们要记得,当我们在堆上分配了一块内存,并且不在使用时,我们要使用free函数来释放掉空间。看下free的原型:

void   __cdecl free(_Post_ptr_invalid_ void * _Memory);

很简单,不再赘述。

3. 指向指针的指针

在读大学时,我一直对这个概念不是很理解,现在我更愿意这样去理解指针。

当我们声明了int *p=malloc(1000)的时候,我们可以这样来理解:

image

其实我更愿意把p就理解成一个地址的值,p=0x1111(0x1111是分配的1000字节内存的首地址)。那么什么是指向指针的指针呢?

image

这里的q就是指向指针的指针,q的值就是0x0004,也就是p所在的地址。

以此类推,我们还可以知道指向指针的指针的指针。

4. 函数指针

我们来看C语言里提供了qsort函数:

_CRTIMP void __cdecl qsort(_Inout_bytecap_x_(_NumOfElements * _SizeOfElements) void * _Base, 
    _In_ size_t _NumOfElements, _In_ size_t _SizeOfElements, 
        _In_ int (__cdecl * _PtFuncCompare)(const void *, const void *));

最后一个参数就是一个函数指针,其实不用这么麻烦,我们来看个简单的函数指针的原型:

double (*function)(int);

这个就是最简单的函数指针的原型,与返回指针类型的函数相比,他们相差的只是*和函数名之间要用括号括起来。

当传进来一个函数指针时,我们便可以在函数中适用这个传进来的参数(函数指针)了。例如在qsort里,我们便可以自己制定比较规则,不再多说。

 

 

二.位域结构体和volatile限定符

 

1. 位域

我们来看一个表示日期的结构体:

typedef struct
{
    unsigned int year;
    unsigned int month;
    unsigned int day;
}MyDate;

但是我们可以发现,其实year最大也不会超过四位数,month也就是12,而day最大也就是31。但是我们在上述的结构体中,却为其分配了4*3=12字节的内存,是不是很浪费呢?

C语言为了解决这个问题,提出了一个概念,叫位域。看段代码:

typedef struct
{
    unsigned int year:7;
    unsigned int month:4;
    unsigned int day:5;
}MyDate;

这个的意思是,我为结构体中的每一个字段分配指定的位数,比如,我为year分配了7位。这样就有效地节省了内存。

网上有着有限的几篇关于位域的文章,我觉得在CSDN的一个帖子中,对于关于位域分配内存的情况说的最为详细,原封不动地拿下来了:

C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,
允许其它类型类型的存在。
使用位域的主要目的是压缩存储,其大致规则为:
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字
段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字
段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方
式,Dev-C++采取压缩方式;
4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。

补充:

1)位域不允许跨越两个字节,因此位域的长度不能大于一个字节, 不能超过八位二进制;

2)位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的.

 

2. 再谈联合体

在之前,我们谈到过使用联合体来节约内存空间,但是联合体也经常被用于一个其他的目的:从两个或更多的角度去看待内存块。

例如:

union int_Date
{
    int i;
    MyDate dt;
};

在上面的程序中,我们就可以用两种视角(整数视角和日期视角)来看待一个数据。

我们知道,在X86的机器中,我们常常会用到4个16位寄存器,这四个16位寄存器又可以分成8个8位的寄存器。例如,当我们改变ax的时候,其实al和ah也同时发生了改变,那么我们可以把这个关系用联合体来表示。

typedef union 
{
    struct 
    {
        short ax,bx,cx,dx;
    }word;
    struct
    {
        char al,ah,bl,bh,cl,ch,dl,dh;
    }byte;
}Regs;

int main (void)
{
    Regs regs;
    regs.byte.ah=0x12;
    regs.byte.al=0x34;
    printf("%x",regs.word.ax);
    return 0;
}

3. volatile限定符

volatile告诉编译器,这段内存空间所存储的值是易变的。volatile通常用于指向易变内存空间的指针。有个例子我觉得非常恰当。

我们假设*p指向的内存空间用于存放用户通过键盘所输入的字符,然后我们读取到这个字符,再将其放入到一个数组中。

但是一些优秀的编译器会发现,在这个过程中,p和*p都未被程序显式地改变,这样编译器就会对其作出优化,使*p只被取一次,这样就读入了数组中一些重复的数据,这明显不是我们想要的。

但是当我们在*p前加上volatile限定符,其实就是在告诉编译器,不要对该段程序进行优化,因为这段程序是易变的,每次的读取都要从内存中去重新取得。

在C#中也有volatile关键字,目的也是一样,volatile关键字代表某个变量可能会被多个并发的线程进行修改,所以通知编译器不要对其进行优化,这样就能保证每次取出来的值不是被缓存的,而是最新的值。

 

volatile的作用: 作为指令 关键字,确保本条指令不会因 编译器的优化而省略,且要求每次直接读值.
简单地说就是防止 编译器对代码进行优化.比如如下程序:
1
2
3
4
XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;
对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是 编译器就不能像对待纯粹的程序那样对上述四条语句进行优化,只认为XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。如果键入 volatile,则编译器会逐一的进行编译并产生相应的机器代码(四条).

优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份

http://baike.baidu.com/link?url=jsgE7JkbU0wb1svb8JAhSTBhRQ0UH-AmfUKy1AiGIpH3VFpvkdg9l-_1ck6sjimH

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值