动态存储分配
至今我们都是通过声明变量的方式获取内存空间的,这样程序编译完后就无法对数据结构的存储空间进行伸缩了,就无法设计需要伸缩的数据结构(如:链表),可能有人会说那一开始就声明一个很大的数组不就好了吗,那样会造成很大的资源浪费,而且再大的数组也会有不够的时候
内存分配函数
我们可以通过内存分配函数做到在程序运行过程中申请并获得内存空间使用的权力
-
malloc 分配内存块,不初始化 (常用! 因为不初始化所以效率更高)
-
calloc 分配内存块,对内存块初始化清零
-
realloc 调整分配的内存块大小
这三个函数的返回类型都是 void *
我们称之为“通用”指针,void *
可以赋值给任何指针类型,反之亦然
malloc 函数
void *malloc(size_t size);
size_t
是 C 语言中定义的无符号整数类型,觉得困惑的话可以把它当成 int
型。传给函数的参数 size
就是想要获取的内存大小
其实不是使用了 malloc
函数就会给你分配你想要的内存的,还是存在分配不到内存的情况的,比如你的内存剩余空间比你想获取的空间小的时候,系统无法为你分配足够的空间,就会分配失败,函数就会返回 NULL
空指针
NULL
在 <locale.h> <stddef.h> <stdio.h> <stdlib.h> <string.h> <time.h>
6 个头文件中都有定义
calloc 函数
void *calloc(size_t nmemb, size_t size);
nmemb
表示元素的个数;size
表示每个元素的长度为 size 个字节
这个函数会对分配到的内存块初始化清零,如果分配失败将会和 malloc
函数一样返回 NULL
分配一个可以存四个整数的内存空间:
int *a = calloc(4, sizeof(int));
然后你可以把指针 a
看成一个有四个元素的整型数组,数组的每个元素都被初始化为 0
元素的长度最好通过 sizeof()
进行计算,因为不同计算机同个变量类型的大小可能是不一样的,比如有些计算机 int
类型只占 2 个字节,尽管现在大部分计算机都是 4 个字节,但最好还是通过 sizeof()
计算比较好
realloc 函数
void *realloc(void *ptr, size_t size);
函数的第一个参数 ptr
必须是指向由 malloc
, calloc
或 realloc
函数分配的内存块或者是空指针 NULL
第二个参数是新的内存块的大小,可以比原大小大也可以比原大小小
如果传给
ptr
的是空指针,则相当于malloc
函数
如果size
为 0 ,将释放内存块,相当于free
函数(后面会介绍)
释放存储空间
...
int *p, *q;
p = malloc(sizeof(int)); //内存块 A
q = malloc(sizeof(int)); //内存块 B
p = q;
...
这段代码最后 p
和 q
都会指向内存块 B,你会发现我们没有办法重新访问内存块 A 了,但它还是属于这个程序的,其他程序也使用不了这块内存,这种大家都没办法访问的内存块称为垃圾 (garbage) ,这样的程序存在内存泄漏 (memory leak)
解决这种问题的办法就是记得释放掉不需要的内存
free 函数
void free(void *ptr);
例子:
...
int *p, *q;
p = malloc(sizeof(int));
q = malloc(sizeof(int));
free(p); //释放 p 指向的内存块
p = q;
...
悬空指针
...
char *s = malloc(6);
char *p = s;
free(s);
strcpy(p, “piggy”); /***wrong***/
...
永远不要访问或修改释放掉了的内存块,会导致未定义行为,可能会引起程序崩溃
悬空指针有时候很难被发现,比如当同时有几个指针指向同一个内存块时,释放了内存块,这几个指针就都悬空了,但你可能只记得传给 free
的那个指针是悬空的
指向指针的指针
这一概念频繁出现在链式数据结构中,当希望通过函数将指针指向其他对象时就需要指向指针的指针
如在链表中:
...
void add(struct node **tail, int n) {
struct node *new_node = malloc(sizeof(struct node));
if (new_node == NULL) {
printf(“Not Enough Memory!\n”);
exit(1);
}
new_node->element = n;
(*tail)->next = new_node;
*tail = new_node;
}
...
这样就可以在函数中完成为链表添加元素并移动指向链表尾部的指针tail
,如果不用指向指针的指针就需要返回新添加的node
的地址,然后在函数外移动tail
指向指针的指针还有很多的用途,下面我再例举一个用途:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void) {
char **shopping_list = malloc(3 * sizeof(char *));
if (shopping_list ==NULL) {
printf(“Not Enough Memory!\n”);
exit(1);
}
char *s1 = “iPhone”;
char *s2 = “iPad”;
char *s3 = “MacBook”;
shopping_list[0] = s1;
shopping_list[1] = s2;
shopping_list[2] = s3;
for (int i = 0; i < 3; i++)
printf(“%s\n”, shopping_list[i]);
return 0;
}
输出结果为:
iPhone
iPad
MacBook
指向函数的指针
指针可以指向变量、数组、结构体,甚至还可以指向函数,函数在编译时会被分配一个入口地址,可以用指针变量指向这个函数的入口地址,然后通过该指针变量调用此函数
C 语言指向函数的指针和其他的指针是一样的,可以存储在变量中、数组元素中或结构的成员中,还可以写返回函数指针的函数
函数指针的声明
void (*pf)(int);
这声明了一个名为 pf
的函数指针,它可以指向返回 void
接收一个 int
参数的函数
调用方法:(*pf)(x)
或 pf(x)
为了突显是个函数指针,推荐使用第一种方法
例子:
...
void printx(int x) {
printf("%d\n", x);
}
int main(void) {
...
void (*pf)(int);
pf = printx; //函数名就是个指向此函数的指针,就像数组名一样
(*pf)(4869);
...
}
编译运行后就会输出 4869
了
函数指针作为参数
C 函数库中有一个强大的函数 qsort
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
其中第四个参数就是函数指针,*compar
的两边的括号说明它是个指向函数的指针,而不是返回值为指针的函数,int (*compar)(const void *, const void *)
可以接收任何一个返回 int
接收两个 void *
参数的函数
qsort
中第四个元素指向一个比较函数,调用 qsort
时,会对传入的数组进行排序,在排序中任何需要用到比较数组元素的地方都会调用这个比较函数,这样就可以定制自己的排序规则
函数指针的其他用途
元素是函数指针的数组拥有广泛的应用,例如编写的程序具有命令菜单时,可以编写函数实现各个命令,然后把指向这些函数的指针存在一个数组中:
void (*cmd[])(void) = {open,
close,
save,
print,
exit};
然后就可以通过对这个数组取下标来调用对应的函数了
(*cmd[n])();