本文将对C语言的基础易混淆概念,数组指针、指针数组、函数指针、指针函数、结构体指针、结构体内指针函数几方面内容进行代码演示和分析。
数组指针&指针数组
函数指针&指针函数
结构体指针
结构体内函数指针
这里将下文会用到的库导入,将用到的简单数据类型定义并将用到的函数声明,结构体类型将在下文进行定义,其余主要代码均位于主函数内。
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
void *pointerFunction();
int functionPointer(char*, char*);
double divide(double*, double*);
int main(int argc,char* agrv[]){
//code
system("pause");
return 0;
}
数组指针即指向数组的指针,写作Elemtype*array[size],本部分定义了包含五个整型元素的数组类型。
//typedef type(name)[size]
//typedef type[size] name
typedef int(Integer5) [5];
Integer5 c;
这里有另一个易混淆的概念,c和&c。
数组名c可以视作指向数组首地址的指针,通过相对它的偏移量可表示数组内的任何一个元素,起到等同于[index]的效果。
printf("&array:%d\n",&c);
printf("&array[0]:%d\n",&c[0]);
printf("array:%d\n\n",c);
printf("在本系统中Integer的长度为:%d\n\n",sizeof(int));
printf("数组首元素的地址+1为:%d\n",&c[0]+1);
printf("数组名+1为:%d",c+1);
输出:
&array:6356720
&array[0]:6356720
array:6356720
在本系统中Integer的长度为:4
数组首元素的地址+1为:6356724
数组名+1为:6356724
可以发现,实际上c(数组名)本身和&c[0](首元素取址)的意义完全相同,故不再讨论&c[0]。
对数组名取地址&c,并不是c这个指针的二级指针,它代表的是整个数组,对其运算都会以整个数组的大小为度量。
printf("sizeof(c):%d\n",sizeof(c));
printf("sizeof(*c):%d",sizeof(*c));
输出:
sizeof ( c ):20
sizeof ( *c ):4
得到上述结果的原因是,c的类型是Integer5,是int[5]整型数组,而&c则是int(*)[5]类型,也就是数组指针,这里的&c是指向int[5]的指针。可以说明,c这个标识符的类型是int[5],但在使用时被规定为指向数组内首元素首地址的指针。
printf("数组名取地址+1为:%d\n",&c+1);
printf("数组名+1为:%d\n\n",c+1);
输出:
数组名取地址+1为:6356740
数组名+1为:6356724
可以看出数组指针指向一片连续地址,操作时涵盖整个数组。
指针数组,写作Elemtype(*array)[size],即某一同类型指针构成的数组,这里用到了一个由五个字符型指针构成的数组。
char *pArrays[5];
这里先为指针数组的指针元素所指的区域分配空间。
这里插一句题外话,有一个我曾犯过的错误和大家分享。
typedef unsigned int size_t;
void malloc(size_t size); //Linux and windows
malloc函数在使用时曾误写作 (char)malloc(sizeof(char*)),
malloc分配的是物理上离散的堆空间,即使是分配给char元素也可以发现其间隔16字节地址,
分配给void*的地址大多数情况下完全够用,故没能发现错误。
下文讨论结构体指针时就会发现可能导致的错误,也就是指针所指的结构体域发生交叉。
for (int index=0;index<5;index++){
pArrays[index]=(char*)malloc(sizeof(char));
}
for (int index=0;index<5;index++){
*(pArrays[index])=index+'v';
}
printf("\n指针数组本身元素为:\n");
for (int index=0;index<5;index++){
printf("%d ",pArrays[index]);
}
printf("\n指针数组所指元素为:\n");
for (int index=0;index<5;index++){
printf("%c ",*(pArrays[index]));
}
输出:
指针数组本身元素为:
15406464 15406480 15406496 15406512 15406528
指针数组所指元素为:
v w x y z
可以发现这个字符型指针数组存储的确实是和字符元素一一对应的地址,和数组指针的区别一目了然。
反过来先分析较为简单的指针函数。指针函数,写作Elemtype*function(parameter),即返回值为指针的函数。
*是指针数据类型的标识。
void *pointerFunction() { return malloc(sizeof(void)); }
printf("malloc(sizeof(void))=%d,sizeof(malloc(sizeof(void)))=%d,sizeof(void)=%d。\n",
pointerFunction(),sizeof(malloc(sizeof(void))),sizeof(void));
输出:
malloc(sizeof(void))=11867456,sizeof(malloc(sizeof(void)))=4,sizeof(void)=1。
是一种返回指针的普通函数,不再赘述。
函数指针,写作Elemtype(*functionPointer)(parameter),即指向函数的指针。函数指针实际上并不复杂,只是用指针代表函数名从而操作函数,指令和数据都是内存的组成部分,函数也占据内存。
*是对function这个指针取值,是一目运算符。
int functionPointer(char *v1,char *v2) {return strcmp(v1,v2);}//strcmp需要俩const char*类型参数。
int (*pointer) (char*, char*);
int (*pointerTst) (char*, char*);
pointer=functionPointer;
pointerTst=&functionPointer;
printf("用指针代替函数的结果:%d(不对函数名取址),%d(对函数名取址)。\n",
(*pointer)("a","b"),(*pointerTst)("a","b"));
输出:
用指针代替函数的结果:-1(不对函数名取址),-1(对函数名取址)。
发现两种指针函数的调用方法效果相同,通常使用Elemtype(*functionPointer)(parameter)=&function这种方式,以免混淆,Function名本身和取址得到的地址都是函数的入口地址,在汇编层面上,调用函数后从此地址开始运行,到RET为止。
更详细地来解释:
假设将函数指针作为一个参数传入函数,是这样的形式:
aFunction(elemType otherElements, pointerType (*functionPointer))
简化为:
aFunction(elemType otherElements, void (*p))
p是一个指针,void (*p) (values)意味着p这个指针指向有value参数的函数的那块内存地址。
当这段代码被编译的时候,编译器不会理会(value),因此value不能被用户直接传入。编译器在读到p的时候,意识到这是个指针,便自然而然去取(*p),顺之运行,恰好执行了一个函数的功能。
另外,函数指针可以定义在结构体内,有时也会有这样的用法需求,下文也给出了说明。
结构体指针,当然就是指向结构体类型的指针。这里不再详细解释结构体指针的使用,主要分析上文中指针数组部分提到的错误,假如malloc(n* sizeof(type))被写成了malloc(n* sizeof(type*)),结构体类型中出现的域交叉,用到了结构体数组来分析。
typedef struct {
int var1,var2,var3;
double var4;
}structType;
printf("\nstructType类型字节长:%d\n",sizeof(structType));
structType *type[5];
for (int index=0;index<5;index++){
type[index]=(structType*)malloc(sizeof(structType*));//模拟错误
}
for (int index=0;index<5;index++){
printf("第%d块内存的地址为:%d \n",index+1,type[index]);
}
//这里不循环直接赋值,更加清晰直接。
type[2]->var1=1;type[2]->var2=2;type[2]->var3=3;type[2]->var4=4;
type[3]->var1=1;printf("\n%d,%d",type[2]->var4,type[3]->var1);
输出:
structType类型字节长:24
第1块内存的地址为:11998496
第2块内存的地址为:11998528
第3块内存的地址为:11998544
第4块内存的地址为:11998560
第5块内存的地址为:11998576
1,1074790400
选择了使用结构体数组中的第三和第四块,11998544和11998560间隔16字节,结构体长度为20,而我sizeof的是结构体指针长4,系统自动分配的堆空间,间隔16字节不足以容纳错误。
观察前一个元素的最后一个域和下个元素的第一个域果然发生错误,发生了域的交叉。
这里int型的type[3]->var1被当作浮点型解释为大数,而上一个域的type[2]->var4变成了1,这里意识到似乎和浮点数的存储机制有关,根据IEEE标准,8字节的浮点数共64位,大概形式如下:标准浮点格式(浮点由3个字段组成)有以下两个类型:
32位的单精度:s、exp和frac字段分别为1位、8位、23位
64位的单精度:s、exp和frac字段分别为1位、11位、52位
因此改掉的是低四字的,浮点数中32bits的数符数码。但为什么出现最后的结果还没有搞懂,猜测是double型的type[2]->var4被当作int型解释,而type[3]->var1被覆盖,赋值无效,望指教。
函数指针定义在结构体内,也就是所谓的结构体函数,在C语言中没有类,无法将方法的主体和其他属性完全封装为一个整体,这时候结构体就可以将属性和函数指针封装,达到类似的效果。
struct user {
double height;
double weight;
double (*divide)(double height,double weight);
} v2beach = {
.height=183,
.weight=140,
.divide=÷
};
double divide(double height,double weight) {return height/weight;}
printf("Divide user's height by weight = %lf",(*v2beach.divide)(v2beach.height,v2beach.weight));
当然在这种情况下,&和*显得冗余,显然直接使用函数名更加方便。
struct user {
double height;
double weight;
double (*divide)(double height,double weight);
} v2beach = {
.height=183,
.weight=140,
.divide=divide
};
double divide(double height,double weight) {return height/weight;}
printf("Divide user's height by weight = %lf",v2beach.divide(v2beach.height,v2beach.weight));
输出:
Divide user’s height by weight = 1.307143
对函数在结构体内的封装让我体会到写Java的快乐,同时具有代码易读、使用灵活等好处,由于开发经验少,并无再多心得,在此不再赘述。
最后,感谢阅读。