我们已经掌握的内存开辟方式有:
int a = 0; //在栈空间开辟四个字节的空间
char arr[10] = {0,}; //在栈空间开辟十个字节连续的空间
但是上述的开辟空间的方式有两个特点:
但是有时候我们所需要的空间要到程序运行后才知道,比如说数组版本的通讯录系统中实时添加一个联系人的信息,如果此时数组已经满了,就不能添加了.如果在程序运行的时候实时扩大空间就可以了.
C语言提供了动态开辟的概念以及相关实现,以供在运行时实现对空间的实时开辟.
malloc
并不是从一个在编译时就确定的固定大小的数组中分配存储空间,而是在需要时向操作系统申请空间.
void* malloc(size_t size);
因为程序中的某些地方可能不通过malloc
调用申请空间,所以,malloc
管理的空间不一定是连续的.
这样,空闲存储空间以空闲块链表的方式组织,每个块包含一个长度,一个指向下一块的指针以及一个指向自身空间的指针.这些块按照存储地址的升序组织,最后一块(最高地址)指向第一块.
当有申请请求时,malloc
将扫描空闲块链表,直到找到一个足够大的块为止,这种方法叫做首次适应(first fit);与之相对的是最佳适应(best fit),它寻找满足条件的最小块.
在malloc
函数中,为了内存对齐,请求的长度(以字符为单位)将被舍入,以保证他是头部大小的整数倍.
实际分配的块将多包含一个单元,用于头部本身.实际分配到的块的大小将被记录到size
块内.
malloc
返回的指针指向空闲空间的起始地址,用户只能在分配的空间内进行操作.如果在分配空间外写入数据,则可能会破坏块链表.
malloc
是在堆空间内开辟空间的,同时一般在栈空间创建指针类型的自动变量指向malloc
开辟的空闲空间首地址.
malloc
开辟空间后不会主动还给操作系统,只有程序运行结束或者主动使用free
函数才可以将空间还给操作系统.
void free(void* ptr);
释放过程也是首先搜索空闲块链表,以找到可以插入被释放块的合适位置.如果被释放块的任一一边也是个空闲块,则将这两个空闲块直接合并.这样就不会有太多的碎片.因为空闲块链表是以地址的递增顺序链接在一块的,所以很容易判断释放块是否有相邻空闲块.
使用实例
#include
#include
#define N 10
int main(void)
{
//动态开辟一块能存放10个int类型的空间
int* p = (int*)malloc(sizeof(int) * N);
//使用这块空间前一定要判断是否开辟成功
if (p == NULL)
{
perror("malloc:");
return 1;
}
//开辟成功,打印未初始化的空间的值
int i = 0;
for (i = 0; i < N; i++)
{
printf("%d\n", *(p + i));
}
//释放空间
free(p);
//p置为NULL,防止p成为野指针
p = NULL;
return 0;
}
malloc
函数
NULL
指针,因此动态开辟空间后一定要检查是否开辟成功.void*
,所以malloc
函数并不知道开辟空间的类型,具体使用的时候按照需求进行强制类型转换.size
是0
,则malloc
函数的行为是未定义的,取决于编译器malloc
对空间不进行初始化,仅仅是像操作系统申请指定大小空间,并返回空间的起始地址.malloc
不会主动归还空间,只有程序运行结束或者主动使用free
函数归还空间.free
函数
ptr
指向的空间不是动态开辟的,那么free
函数的行为则是未定义的.ptr
是NULL
指针,则函数什么也不做.free
函数会根据ptr
前一块空间,那一块空间记录了申请了size
大小的空间,以便释放空间正确.malloc
和free
函数都是在
头文件中.
calloc
也用来动态内存分配,同时将空间初始化为0
void* calloc(size_t num, size_t size);
calloc
函数的功能是为num
个大小为size
的元素开辟一块空间,并且把空间内的每个字节初始化为0
.malloc
函数的区别只在于calloc
会在返回地址之前把申请的空间的每个字节初始化为0
.使用实例:
#include
#include
int main(void)
{
char* p = (char*)calloc(10, sizeof(char));
if (p == NULL)
{
perror("calloc:");
return 1;
}
free(p);
p = NULL;
return 0;
}
通过调试,观察到p
指向的10
字节空间都被初始化为0
realloc
的出现让动态内存管理更加灵活.
realloc
改变ptr
指向的已经分配的空间大小
void* realloc(void* ptr, size_t size);
参数
ptr
- 指向要调整的内存地址size
- 调整后的新大小返回值
NULL
注意事项
ptr
是NULL
指针,则realloc
实现的功能和malloc
是一样的realloc
在调整内存空间存在两种情况:
注意:
情况2如果申请失败,则原来的空间不会被释放.所以不能直接用原来的ptr
来接受realloc
的返回值,这样如果申请失败,接受NULL
,原来的空间也使用不了同时也是释放不了了,造成内存泄漏.
需要一个临时变量存储realloc
返回值,如果内存开辟成功,再将临时变量赋值给原来的ptr
,这样就能避免内存泄漏.
使用实例:
#include
#include
int main(void)
{
int* ptr = (int*)malloc(sizeof(int) * 10);
int* p = NULL;
if (ptr == NULL)
{
perror("malloc");
return 1;
}
//业务处理
//...
//扩展容量
p = (int*)realloc(ptr, sizeof(int) * 10);
if (p != NULL)
{
ptr = p;
p = NULL;
}
else
{
perror("realloc:");
return 1;
}
//扩展容量
p = (int*)realloc(ptr, sizeof(int) * 1000);
if (p != NULL)
{
ptr = p;
p = NULL;
}
else
{
perror("realloc:");
return 1;
}
free(ptr);
ptr = NULL;
return 0;
}
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20; //如果p的值是NULL,就会有问题
free(p);
}
void test()
{
int i = 0;
int *p = (int*)malloc(10 * sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i = 0; i <= 10; i++)
{
*(p + i) = i; //当i是10的时候越界访问
}
free(p);
}
void test()
{
int a = 10;
int* p = &a;
free(p); //程序直接崩溃
}
void test()
{
int* p = (int*)malloc(100);
p++;
free(p); //p不再指向动态内存的起始位置
}
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p); //重复释放
}
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main(void)
{
test();
while(1);
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏
切记:
动态开辟的空间一定要释放,并且正确释放.
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
运行Test
函数会有什么样的结果?
GetMemory
的p
是在栈空间临时创建的,接受实参NULL
,随后开辟空间,并用p
存放空间起始地址.但是形参的改变对实参没有影响.str
在调用GetMemory
后的值仍然是NULL
,调用strcpy
对空指针解引用,出错GetMemory
函数内开辟的空间起始地址存放到了p
中,随着函数调用结束,p
的空间被释放,这块开辟后的空间找不到了,未能进行释放空间,造成了内存泄漏正确应该使用传值调用,这样才能改变实参的值
void GetMemory(char** p)
{
*p = (char*)malloc(100);
if (*p == NULL)
{
perror("GetMemory");
}
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
运行Test
函数会有什么样的结果?
GetMemory
中,创建了一个字符数组,并将首元素地址返回.这里犯了将局部变量返回的错误.函数在栈上创建了变量,函数调用完毕后,空间还给操作系统,返回值指向的空间其实是一块未知的空间.str = GetMemory();
中,将返回值赋值给str
,str
成为了野指针,使用str
访问这片已经归还给操作系统的空间,造成了非法访问,实际会打印出随机值.void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
运行Test
函数会有什么样的结果?
GetMemory
传址调用,可以修改实参的值.调用GetMemory
后,str
指向了开辟的大小为100
字节的空间.hello
,但是开辟后的空间没有被释放,造成了内存泄漏.修改后的代码:
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
运行Test
函数会有什么样的结果?
str
指向的开辟的空间被释放后,没有将str
置为NULL
.str
仍然存放原来的值,成为了一个野指针.调用strcpy
对野指针进行了访问,出现非法访问修改后代码如下:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
C/C++程序内存分配的几个区域:
- 程序代码和数据:对所有的进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置.
- 栈区(stack):位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用.用户栈在程序执行期间可以动态地扩展和收缩.特别地,每次我们调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩.
- 共享库:大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域.
- 堆(heap):当调用像
malloc
和free
这样的C标准库函数时,堆可以在运行时动态地扩展和收缩.- 内核虚拟内存:地址空间顶部的区域是为内核保留的.不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数.相反,它们必须调用内核来执行这些操作.
这样就能更好的理解在初识C语言中static
关键字修饰局部变量的例子了
实际上普通的局部变量(自动变量)是在栈区上分配空间的,栈区的特点就是上面创建的变量出了作用域就被销毁.
但是被
static
修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长.即使出了作用域也仍然存在.
柔性数组(Flexible Array)是一种特殊的C语言数组,它允许在结构体中声明一个长度未知的数组.这个数组位于结构体的末尾,它的长度可以根据实际情况动态分配和调整.
其最后一个数组成员有一些特性.
- 该数组不会立即存在
- 就好像确实存在被具有所需数目的数组一样
C99新增特性,柔性数组结构有如下规则
- 数组成员必须时结构的最后一个成员
- 结构中至少有一个成员
- 柔性数组的声明类似于普通数组,只是它的方括号是空的
例如:
struct S
{
int i;
int a[0]; //柔性数组成员
};
有些编译器会报错无法编译可以改成
struct S
{
int i;
int a[]; //柔性数组成员
};
声明一个struct S
类型的结构变量时,不能用a
做任何事,因为没有给这个数组预留存储空间.
C99
的意图不是让你声明一个struct S
的变量,而是要你声明一个指向struct S
类型的指针,然后用malloc
来分配足够的空间,以存储struct S
类型结构的常规内容和柔性数组成员所需要的额外空间.
假如,用a
表示一个内含5
个int
类型的数组,可以这样做:
//声明一个柔性数组结构类型的指针
//并且为一个结构和一个数组分配空间
struct S* pf = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
现在有足够的空间可以存储i
和一个内含5
个int
类型的数组.可以用指针pf
来访问它们.
pf->i = 5; //设置 i 成员
pf->a[0] = 1; //访问数组元素
下面展示了使用柔性数组的例子:
#include
#include
struct flex
{
size_t count;
double average;
double scores[]; //柔性数组成员
};
void showFlex(const struct flex* p)
{
int i = 0;
printf("Scores : ");
for (i = 0; i < p->count; i++)
{
printf("%g ", p->scores[i]);
}
printf("\nAverage: %g\n", p->average);
}
int main(void)
{
struct flex* pf1, * pf2;
int i = 0;
int sum = 0;
int n = 5;
//为结构和数组分配存储空间
pf1 = (struct flex*)malloc(sizeof(struct flex) + n * sizeof(double));
if (pf1 == NULL)
{
perror("malloc:");
return 1;
}
pf1->count = n;
for (i = 0; i < pf1->count; i++)
{
pf1->scores[i] = 20.0 - i;
sum += pf1->scores[i];
}
pf1->average = sum / n;
showFlex(pf1);
n = 9;
sum = 0;
pf2 = (struct flex*)malloc(sizeof(struct flex) + n * sizeof(double));
if (pf2 == NULL)
{
perror("malloc:");
return 1;
}
pf2->count = n;
for (i = 0; i < pf2->count; i++)
{
pf2->scores[i] = 20.0 - i / 2.0;
sum += pf2->scores[i];
}
pf2->average = sum / n;
showFlex(pf2);
return 0;
}
程序运行结果如下:
柔性数组成员的的结构确实有一些特殊的处理要求:
struct flex *pf1, *pf2; //*pf1和*pf2 都是结构
*pf1 = *pf2; //不要这样做
这样做只能拷贝除柔性数组成员以外的成员.确实要进行拷贝,可以使用memcpy
函数进行拷贝.柔性数组结构中的柔性数组成员,实际上是数组的首元素地址,如果直接改为放个指针可不可以呢?
例如设计成这样的格式:
#include
#include
struct flex
{
size_t count;
double average;
double* scores;
};
int main(void)
{
struct flex* pf = NULL;
int n = 5;
//为结构申请空间
pf = (struct flex*)malloc(sizeof(struct flex));
//为结构中的成员指向了又一次申请的空间
pf->scores = (double*)malloc(n * sizeof(double));
pf->count = n;
//释放空间
free(pf->scores);
pf->scores = NULL;
free(pf);
pf = NULL;
return 0;
}
这段代码也可以完成柔性数组的功能,但在底层还是稍有区别的.
归根结底,还是指针和数组的区别.
指针保存的是空间的地址,而数组原地就是空间.
柔性数组相比还有两个好处:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户.用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以不能指望用户来发现这个事.如果是柔性数组结构,一次性就可以分配好所需要的空间,同时用户free一次也可以把所有的内存释放掉.
第二个好处是:有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片.
malloc
无论使用最佳适应还是首次适应来申请空间,不免都会产生内存碎片,内存碎片会减慢访问速度.
扩展阅读:陈皓大佬的C语言结构体里的成员数组和指针
本章完.