目录
1. 动态存储分配
1.1 malloc函数
1.2 calloc函数
1.3 realloc函数
1.4 free函数
2. 空指针NULL解析
2.1 NULL的定义形式
2.2 程序如何知道发生了"空指针赋值"
3. 不完整类型在链表中的使用
4. 指向指针的指针
5. 指向函数的指针
5.1 函数名与函数指针
5.2 函数指针解析
5.3 函数指针应用场景
5.3.1 作为函数参数(回调函数)
5.3.2 作为函数的返回值
5.3.3 作为结构体/联合的成员或数组的元素
5.4 经典示例:qsort函数
6. 受限指针(C99)
6.1 关键字
6.2 含义
6.3 示例
6.4 restrict关键字生效范围
6.5 C99中使用restrict的函数
6.6 补充说明
7. 灵活数组成员(C99)
7.1 struct hack
7.2 更狠的struct hack
7.3 灵活数组成员(flexible array member)的提出
1. 动态存储分配
1.1 malloc函数
void *malloc(size_t size);
功能:分配size个字节的内存块,不对内存块清零
返回值:指向内存块开始处的指针;如果无法分配要求尺寸的内存块,返回空指针
说明1:malloc函数不对分配的内存块清零,效率更高
说明2:返回值为void * 类型,表示"通用"指针,本质上他只是内存地址值
说明3:void * 是一种不完整类型,所以不能对void *类型指针解引用
说明4:malloc(0) 会返回什么 ???
说明5:对malloc 函数的返回值进行强制类型转换是否必要 ?
C89中无此必要,因为void * 类型的指针会在赋值操作时自动转换为任何指针类型
C语言中保留强制类型转换的原因是K&R C 中,malloc函数的返回值为char * 类型(那个年代还没有void *类型指针)
说明6:用malloc函数给字符串分配内存空间时,不要忘记包含空字符的空间
char *p = (char *)malloc(n + 1);
说明7:程序可能分配了内存块,然后丢失了对这些块的记录,因而浪费了空间。如,
p = malloc(...);q = malloc(..);p = q;
1.2 calloc函数
void *calloc(size_t nmemb, size_t size);
功能:为带有nmemb个元素的数组分配内存块,其中每个数组元素占size个字节。通过设置所有位为零来清零内存块
返回值:指向内存块开始处的指针;如果不能分配所要求大小的内存块,返回空指针
1.3 realloc函数
void *realloc(void *ptr, size_t size);
功能:假设ptr指向先前由calloc、malloc或realloc函数分配的内存块,realloc 函数分配size个字节的内存块,如果需要可以复制旧内存块的内容。
返回值:指向新内存块开始处的指针;如果无法分配要求尺寸的内存块,返回空指针
说明1:要确定传递给reallo函数的指针来自于先前malloc、calloc或realloc的调用。如果不是这样,程序可能会行为异常
说明2:当扩展内存块时,realloc函数不会对添加进内存的字节进行初始化
说明3:如果realloc函数不能按要求扩大内存块,那么他会返回空指针,并且在原有的内存块中的数据不会发生改变
说明4:如果realloc函数被调用时以空指针作为第一个实际参数,那么他的行为就像malloc 函数一样(不推荐用法)
说明5:如果realloc函数被调用时以0作为第二个实际参数,那么他会释放掉内存块(不推荐用法)
说明6:一旦realloc 函数返回,需要及时对指向内存块的所有指针进行更新。因为realloc函数可能将内存块移动到了其他地方
1.4 free函数
void free(void *ptr);
功能:释放ptr指向的内存块
说明1:ptr为空指针时调用无效
说明2:free函数的实际参数必须是先前由内存分配函数返回的指针。如果实际参数是指向其他对象(如变量或数组元素)的指针,可能导致未定义行为
说明3:"悬空指针"问题
因为几个指针可能指向相同的内存块,在用free函数释放内存后,全部指针都悬空了。而试图访问或修改已被释放掉的内存块会导致未定义行为,通常会导致程序崩溃
2. 空指针NULL解析
2.1 NULL的定义形式
#define NULL 0#define NULL (void *)0
说明:把NULL 定义成指针的好处
① 帮助编译器检查空指针的不正确使用,如
int i;i = NULL;
② 调用带有可变长度实际参数列表的函数,并且将NULL作为一个实际参数
此时编译器可以向函数传递空指针,而不是整数0
2.2 程序如何知道发生了"空指针赋值"
举例一种实现方法:
① 编译器在数据段的开始处留出"空洞",即初始化为0但未被使用的一段内存
② 当程序终止时,检查该"空洞"中的数据是否仍为0
3. 不完整类型在链表中的使用
声明链表结点类型时,用到了不完整类型的知识
struct node{int value;struct node *next;};
说明1:由于node结构体中包含指向node类型结构体的指针,所以此处的结构标记(node)是必须的
说明2:不完整类型进阶示例
struct s1; // 此处的不完整类型声明,使得struct s2中可以包含指向s1的指针struct s2{struct s1 *p;};struct s1{struct s2 *q;};
4. 指向指针的指针
① 应用场景
希望在函数中修改指针变量的指向,需要向函数传递指向该指针的指针
② 本质原因
指针也是按值传递的
③ 典型示例
元素类型为chat *的数组,指向数组元素的指针类型为char **
5. 指向函数的指针
5.1 函数名与函数指针
① 如果f 是函数名,C语言将f(x)作为函数调用处理,而f 本身被视为指向函数的指针
② 如果pf 是指向函数的指针,那么*pf 表示pf 所指向的函数,所以通过(*pf)(x) 可以调用pf 指向的函数(注意优先级 ( ) > *)
C语言允许用pf(x) 的方式调用pf 指向的函数
5.2 函数指针解析
函数指针反汇编示例
程序源代码如下(gcc -o add -g main.c):
程序反汇编代码如下(objdump -dS add > add.dis):
说明1:局部函数指针变量的赋值的汇编语句为movl $0x80483e4, 0x18(%esp),实际就是将函数add的起始地址保存在栈中
说明2:函数调用时值传递分析
主调函数:将参数压入自己栈中,而且是从右向左压栈
被调函数:从主调函数栈中取得局部变量的值
说明3:分析栈的动态结构时要注意,call指令会将函数返回地址压栈
说明4:指向函数的指针变量(设为p)只能指向函数的入口处,而不能指向函数中间的某一条指令处,因此不能用*(p + 1)来表示函数的下一条指令
个人:此时指针算术运算的类型也不对
5.3 函数指针应用场景
5.3.1 作为函数参数(回调函数)
函数指针的真正意义,并不是为了调用函数的便利,而是为了用作函数参数,在函数间传递函数
函数,意味着一系列动作指令(如对数据进行处理),而在函数间传递函数,意味着传入的函数可以对被传入的函数的动作指令进行一定程度的自定义。也就是根据传入的函数指针参数不同,函数具有不同的行为
通常,我们要表达一个通用(可以应对各种情况,具有多种算法规则)算法的时候,用某个函数描述描述算法的基本框架,而在算法的一些关键步骤中,通过传入的函数指针参数的不同,调用不同的算法规则函数,以实现对算法的自定义,进而达到通用的目的
5.3.2 作为函数的返回值
典型示例:signal 函数
void (*signal(int signo, void (*func)(int)))(int);
5.3.3 作为结构体/联合的成员或数组的元素
示例:一种通过字符串调用函数的方法
struct{char *cmd_name;void (*cmd_pointer)(void);} file_cmd[] ={{"new", new_cmd},{"open", open_cmd},{"close", close_cmd},{"exit", exit_cmd},};
小缺点:此时所有命令的处理函数类型必须一致
5.4 经典示例:qsort函数
void qsort(void *base, size_t nmemb, size_t size,int (*compare)(const void*, const void*));
功能:qsort可以给任意数组排序(按升序排序)
参数说明:
base:指向数组中的第一个元素(如果只是对数组的一段区域进行排序,base则指向这段区域的第一个元素)。
nmemb:要排序的元素数量(不一定是数组的元素个数)
size:每个数组元素的大小,以字节位单位
compare:指向比较函数的指针
比较函数说明:因为要排序的数组元素可以是任何类型,包括结构或联合,所以必须告诉qsort如何确定两个数组元素的大小关系(即确定排序的规则)。比较函数compare就是向qsort函数提供这些信息
比较函数原型为:
int compare(const void *p, const void *q);
比较函数示例:
① 比较结构中整型成员number的大小
int compare_parts(const void *p, const void *q){return ((struct part *)p)->number -((struct part *)q)->number;}
② 比较字符指针数组中的元素
int compare_strings(const void *p, const void *q){return strcmp(*(char **)p, *(char **)q);}
说明:由于比较函数的形参为void * 指针,无法直接用于数组元素的访问,所以首先要将其强制转换为指向数组元素的指针
6. 受限指针(C99)
6.1 关键字
关键字:restrict
int *restrict p;
6.2 含义
如果指针p指向的对象在之后需要修改,则该对象不允许通过除指针p之外的任何方式访问
补充:what is 别名 ?
如果一个对象有多种访问方式,通常将这些方式互称为别名
6.3 示例
int *restrict p;int *restrict q;p = malloc(sizeof(int));q = p;*q = 0; // 由于p为受限指针,无法通过p的别名q来访问同一个对象
6.4 restrict关键字生效范围
局部变量:声明变量的代码块中
函数参数:仅在函数执行时生效
全局变量:整个程序
6.5 C99中使用restrict的函数
void *memcpy(void *restrict s1, const void *restrict s2, size_t n);void *memmove(void *s1, const void *s2, size_t n);
说明:memcpy函数原型中使用restrict修饰,说明复制的源地址和目的地址不应相互重叠(但不能保证不重叠),否则不能保证函数能执行
而memmove可以保证当源和目的重叠时依然能执行复制过程。例如,可以用memmove将数组中的元素偏移一个位置
int a[10];memmove(a, a + 1, sizeof(int) * 9);
6.6 补充说明
restrict关键字与register关键字类似,都是提示编译器可以进行优化。当程序员禁用优化功能或使用的编译器不支持该优化时,C99标准保证restrict不会对遵循标准的程序产生任何影响
7. 灵活数组成员(C99)
7.1 struct hack
目的:声明长度为1 的char 类型数组,然后动态分配每一个字符串
struct vstring{int len;char chars[1];};struct vstring *str = malloc(sizeof(struct vstring) + n - 1);str->len = n;
7.2 更狠的struct hack
特点:声明长度为0 的char 类型数组
struct vstring{int len;char chars[0];};struct vstring *str = malloc(sizeof(struct vstring) + n);str->len = n;
问题:C89不能保证struct hack工作,也不允许数组长度为0(GCC一般均支持零长度数组,Linux内核中有应用)
7.3 灵活数组成员(flexible array member)的提出
关键:当结构的最后一个成员是数组时,其长度可以忽略
struct vstring{int len;char chars[ ]; // 该灵活数组成员在结构中不占空间,sizeof计算时忽略其大小};struct vstring *str = malloc(sizeof(struct vstring) + n );str->len = n;
灵活数组成员规则:
① 灵活数组成员必须出现在结构的最后,而且结构必须至少包含一个其他成员。
② 复制包含灵活数组成员的结构时,不会复制灵活数组本身(很显然,灵活数组通过malloc函数单独在堆中分配)
说明:包含灵活数组成员的结构是不完整类型
如果觉得《C程序设计语言现代方法17:指针的高级应用》对你有帮助,请点赞、收藏,并留下你的观点哦!