C语言中威力最大的指针底层原理和使用技巧讲解
四、指向不同数据类型的指针
1. 数值型指针
通过上面的介绍,指向数值型变量的指针已经很明白了,需要注意的就是指针所指向的数据类型。
2. 字符串指针
字符串在内存中的表示有2种:
用一个数组来表示,例如:char name1[8] = "zhangsan";用一个char *指针来表示,例如:char *name2 = "zhangsan";
name1在内存中占据8个字节,其中存储了8个字符的ASCII码值;name2在内存中占据9个字节,因为除了存储8个字符的ASCII码值,在最后一个字符'n'的后面还额外存储了一个'',用来标识字符串结束。
对于字符串来说,使用指针来操作是非常方便的,例如:变量字符串name2:
char *name2 = "zhangsan";
char *p = name2;
while (*p != '')
{
printf("%c ", *p);
p = p + 1;
}
在while的判断条件中,检查p指针指向的字符是否为结束符''。在循环体重,打印出当前指向的字符之后,对指针比那里进行自增操作,因为指针p所指向的数据类型是char,每个char在内存中占据一个字节,因此指针p在自增1之后,就指向下一个存储空间。
也可以把循环体中的2条语句写成1条语句:
printf("%c ", *p++);
假如一个指针指向的数据类型为int型,那么执行p = p + 1;之后,指针p中存储的地址值将会增加4,因为一个int型数据在内存中占据4个字节的空间,如下所示:
思考一个问题:void*型指针能够递增吗?如下测试代码:
int a[3] = {1, 2, 3};
void *p = a;
printf("1: p = 0x%x ", p);
p = p + 1;
printf("2: p = 0x%x ", p);
打印结果如下:
1: p = 0x733748c0
2: p = 0x733748c1
说明void*型指针在自增时,是按照一个字节的跨度来计算的。
3. 指针数组与数组指针
这2个说法经常会混淆,至少我是如此,先看下这2条语句:
int *p1[3]; // 指针数组
int (*p2)[3]; // 数组指针
3.1 指针数组
第1条语句中:中括号[]的优先级高,因此与p1先结合,表示一个数组,这个数组中有3个元素,这3个元素都是指针,它们指向的是int型数据。可以这样来理解:如果有这个定义char p[3],很容易理解这是一个有3个char型元素的数组,那么把char换成int*,意味着数组里的元素类型是int*型(指向int型数据的指针)。内存模型如下(注意:三个指针指向的地址并不一定是连续的):
如果向指针数组中的元素赋值,需要逐个把变量的地址赋值给指针元素:
int a = 1, b = 2, c = 3;
char *p1[3];
p1[0] = &a;
p1[1] = &b;
p1[2] = &c;
3.2 数组指针
第2条语句中:小括号让p2与*结合,表示p2是一个指针,这个指针指向了一个数组,数组中有3个元素,每一个元素的类型是int型。可以这样来理解:如果有这个定义int p[3],很容易理解这是一个有3个int型元素的数组,那么把数组名p换成是*p2,也就是p2是一个指针,指向了这个数组。内存模型如下(注意:指针指向的地址是一个数组,其中的3个元素是连续放在内存中的):
在前面我们说到取地址操作符&,用来获得一个变量的地址。凡事都有特殊情况,对于获取地址来说,下面几种情况不需要使用&操作符:
字符串字面量作为右值时,就代表这个字符串在内存中的首地址;数组名就代表这个数组的地址,也等于这个数组的第一个元素的地址;函数名就代表这个函数的地址。
因此,对于一下代码,三个printf语句的打印结果是相同的:
int a[3] = {1, 2, 3};
int (*p2)[3] = a;
printf("0x%x ", a);
printf("0x%x ", &a);
printf("0x%x ", p2);
思考一下,如果对这里的p2指针执行p2 = p2 + 1;操作,p2中的值将会增加多少?
答案是12个字节。因为p2指向的是一个数组,这个数组中包含3个元素,每个元素占据4个字节,那么这个数组在内存中一共占据12个字节,因此p2在加1之后,就跳过12个字节。
4. 二维数组和指针
一维数组在内存中是连续分布的多个内存单元组成的,而二维数组在内存中也是连续分布的多个内存单元组成的,从内存角度来看,一维数组和二维数组没有本质差别。
和一维数组类似,二维数组的数组名表示二维数组的第一维数组中首元素的首地址,用代码来说明:
int a[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; // 二维数组
int (*p0)[3] = NULL; // p0是一个指针,指向一个数组
int (*p1)[3] = NULL; // p1是一个指针,指向一个数组
int (*p2)[3] = NULL; // p2是一个指针,指向一个数组
p0 = a[0];
p1 = a[1];
p2 = a[2];
printf("0: %d %d %d ", *(*p0 + 0), *(*p0 + 1), *(*p0 + 2));
printf("1: %d %d %d ", *(*p1 + 0), *(*p1 + 1), *(*p1 + 2));
printf("2: %d %d %d ", *(*p2 + 0), *(*p2 + 1), *(*p2 + 2));
打印结果是:
0: 1 2 3
1: 4 5 6
2: 7 8 9
我们拿第一个printf语句来分析:p0是一个指针,指向一个数组,数组中包含3个元素,每个元素在内存中占据4个字节。现在我们想获取这个数组中的数据,如果直接对p0执行加1操作,那么p0将会跨过12个字节(就等于p1中的值了),因此需要使用解引用操作符*,把p0转为指向int型的指针,然后再执行加1操作,就可以得到数组中的int型数据了。
5. 结构体指针
C语言中的基本数据类型是预定义的,结构体是用户定义的,在指针的使用上可以进行类比,唯一有区别的就是在结构体指针中,需要使用->箭头操作符来获取结构体中的成员变量,例如:
typedef struct
{
int age;
char name[8];
} Student;
Student s;
s.age = 20;
strcpy(s.name, "lisi");
Student *p = &s;
printf("age = %d, name = %s ", p->age, p->name);
看起来似乎没有什么技术含量,如果是结构体数组呢?例如:
Student s[3];
Student *p = &s;
printf("size of Student = %d ", sizeof(Student));
printf("1: 0x%x, 0x%x ", s, p);
p++;
printf("2: 0x%x ", p);
打印结果是:
size of Student = 12
1: 0x4c02ac00, 0x4c02ac00
2: 0x4c02ac0c
在执行p++操作后,p需要跨过的空间是一个结构体变量在内存中占据的大小(12个字节),所以此时p就指向了数组中第2个元素的首地址,内存模型如下:
6. 函数指针
每一个函数在经过编译之后,都变成一个包含多条指令的集合,在程序被加载到内存之后,这个指令集合被放在代码区,我们在程序中使用函数名就代表了这个指令集合的开始地址。
函数指针,本质上仍然是一个指针,只不过这个指针变量中存储的是一个函数的地址。函数最重要特性是什么?可以被调用!因此,当定义了一个函数指针并把一个函数地址赋值给这个指针时,就可以通过这个函数指针来调用函数。
如下示例代码:
int add(int x,int y)
{
return x+y;
}
int main()
{
int a = 1, b = 2;
int (*p)(int, int);
p = add;
printf("%d + %d = %d", a, b, p(a, b));
}
前文已经说过,函数的名字就代表函数的地址,所以函数名add就代表了这个加法函数在内存中的地址。int (*p)(int, int);这条语句就是用来定义一个函数指针,它指向一个函数,这个函数必须符合下面这2点(学名叫:函数签名):
有2个int型的参数;有一个int型的返回值。
代码中的add函数正好满足这个要求,因此,可以把add赋值给函数指针p,此时p就指向了内存中这个函数存储的地址,后面就可以用函数指针p来调用这个函数了。
在示例代码中,函数指针p是直接定义的,那如果想定义2个函数指针,难道需要像下面这样定义吗?
int (*p)(int, int);
int (*p2)(int, int);
这里的参数比较简单,如果函数很复杂,这样的定义方式岂不是要烦死?可以用typedef关键字来定义一个函数指针类型:
typedef int (*pFunc)(int, int);
然后用这样的方式pFunc p1, p2;来定义多个函数指针就方便多了。注意:只能把与函数指针类型具有相同签名的函数赋值给p1和p2,也就是参数的个数、类型要相同,返回值也要相同。
注意:这里有几个小细节稍微了解一下:
在赋值函数指针时,使用p = &a;也是可以的;使用函数指针调用时,使用(*p)(a, b);也是可以的。
这里没有什么特殊的原理需要讲解,最终都是编译器帮我们处理了这里的细节,直接记住即可。
函数指针整明白之后,再和数组结合在一起:函数指针数组。示例代码如下:
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
int main()
{
int a = 4, b = 2;
int (*p[4])(int, int);
p[0] = add;
p[1] = sub;
p[2] = mul;
p[3] = divide;
printf("%d + %d = %d ", a, b, p[0](a, b));
printf("%d - %d = %d ", a, b, p[1](a, b));
printf("%d * %d = %d ", a, b, p[2](a, b));
printf("%d / %d = %d ", a, b, p[3](a, b));
}
这条语句不太好理解:int (*p[4])(int, int);,先分析中间部分,标识符p与中括号[]结合(优先级高),所以p是一个数组,数组中有4个元素;然后剩下的内容表示一个函数指针,那么就说明数组中的元素类型是函数指针,也就是其他函数的地址,内存模型如下:
如果还是难以理解,那就回到指针的本质概念上:指针就是一个地址!这个地址中存储的内容是什么根本不重要,重要的是你告诉计算机这个内容是什么。如果你告诉它:这个地址里存放的内容是一个函数,那么计算机就去调用这个函数。那么你是如何告诉计算机的呢,就是在定义指针变量的时候,仅此而已!
五、总结
我已经把自己知道的所有指针相关的概念、语法、使用场景都作了讲解,就像一个小酒馆的掌柜,把自己的美酒佳肴都呈现给你,但愿你已经酒足饭饱!
如果以上的内容太多,一时无法消化,那么下面的这两句话就作为饭后甜点为您奉上,在以后的编程中,如果遇到指针相关的困惑,就想一想这两句话,也许能让你茅塞顿开。
指针就是地址,地址就是指针。指针就是指向内存中的一块空间,至于如何来解释/操作这块空间,由这个指针的类型来决定。
另外还有一点嘱咐,那就是学习任何一门编程语言,一定要弄清楚内存模型,内存模型,内存模型!
祝您好运!
最新活动更多
-
11月28日立即报名>>> 2024工程师系列—工业电子技术在线会议
-
12月19日立即报名>> 【线下会议】OFweek 2024(第九届)物联网产业大会
-
即日-12.26火热报名中>> OFweek2024中国智造CIO在线峰会
-
即日-2025.8.1立即下载>> 《2024智能制造产业高端化、智能化、绿色化发展蓝皮书》
-
精彩回顾立即查看>> 2024 智能家居出海论坛
-
精彩回顾立即查看>> 【在线会议】多物理场仿真助跑新能源汽车
推荐专题
发表评论
请输入评论内容...
请输入评论/评论长度6~500个字
暂无评论
暂无评论