漫谈C语言指针
Posted 易水南风
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了漫谈C语言指针相关的知识,希望对你有一定的参考价值。
更多博文,请看音视频系统学习的浪漫马车之总目录
上一篇漫谈C语言内存管理主要讲解了C语言内存管理相关的内容,今天在上一篇的基础上漫谈下C语言的一大精髓——指针。和上一篇一样,假设大家都是有基础的,谈一些比较本质的,一些偏进阶的内容。
什么是指针
C语言里,指针一直是一个难点,初学者容易混淆的地方,但是指针本身其实很简单,指针就是一个存放整数的变量。
C语言中,变量存放在内存中,而内存其实就是一组有序字节组成的数组,这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存中的一个指定数据类型的数值或字符串,它们都有一个自己的地址,而指针便是保存这个地址的变量。也就是说:指针是一种保存变量起始地址的变量。
如图为4GB 的内存的分布图:
什么叫做指向变量首地址呢?比如:
int a = 10;
int *p = &a;
用图表示就是:
为什么要使用指针
如果是写过类似Java、kotlin等高级语言的程序员,对引用肯定非常熟悉了,其实引用就是个简洁版的指针,因为指针实在太灵活,用得不好容易出事故,所以这些高级语言就简化为引用。
引用就像普通钥匙,我们可以用它去打开某种类型的门,其他类型数据无法打开,并且就算打开了门的具体编号(地址)也不会暴露给我们。引用不能通过移动位置(算数运算)去打开其他门,只能指定去打开某个门。
而指针虽然也有类型,但是却并不是要求一定要指向该类型的数据(至于指针的类型的作用后面会讲),指针有点像万能钥匙,虽然指定打开这某种门,但是其他门也是可以打开的(当然读取数据可能会有错误),更厉害的是可以移动任意位置(算术运算挪动指针指向)去打开其他门,门牌号(内存地址)也是暴露给我们的。这样会灵活很多,开发者的操作权限会很大,当然带来的风险也会高很多。
(上面的比喻可能不是很恰当或者难以理解,简单来说就是我可以使用指针直接操作内存,读也好,写也好,怎么样都好,内存的数据尽在我手中,内存的命运尽在我手中,我想读哪里的数据,我想往哪里写什么数据,都随我意。所以一旦操作不当,就会有程序事故,比如访问到没有访问权限的内存导致程序奔溃,比如常见的数组越界和野指针访问就是这个原因导致的奔溃)
总的来说,使用指针有如下好处:
1)指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效;
2)C语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等;
3)C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。
4)可以通过算术运算灵活操作内存。
1和2比较容易理解,主要谈下3和4.
通过指针在函数内部修改函数外部变量值
我们知道C语言函数是传值的,所以不能在函数里直接修改函数外的变量的值,比如:
int changeValue(int x){
x = 9;
return x;
}
int main(){
int x = 10;
changeValue(x);
printf("x: %d\\n", x);
}
运行结果:
x: 10
任你在函数里怎么修改,就是拿函数外的变量没办法,因为函数传参的时候,是拷贝了一个副本,然后在函数里面操作的,所以再怎么修改改的都是函数内部的一个副本,和外部的变量无关。
这时指针排上用场了,既然传参传的是值,那么如果参数为指针,那传的也是地址的值,同个地址对应的变量也是同一个,那就可以直接通过修改该地址指向的内存数据来修改函数外的变量了:
void changeValue(int* x){
*x = 9;
}
int main(){
int x = 10;
changeValue(&x);
printf("x: %d\\n", x);
}
运行结果:
x: 9
修改成功~~
指针算术运算
了解指针的算数运算,就要先了解指针的类型和指向的类型。
指针的类型
与指针类型相关的主要有2点:
1.指针自身类型
2.指针所指向的类型
指针自身类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。
比如
int * ptr;//指针的类型是int *
但是指针本身的类型意义不大,关键是要拿到指针所指向的类型 。
指针所指向的类型
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。
比如:
int * ptr; //指针所指向的类型是int
为什么说指针所指向的类型很重要呢?
一方面,当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
比如:
int a = 10;
int *p = &a;
printf("*p=%d\\n", *p);
当执行指针取值的时候,cpu知道了p指向的类型是int ,所以接下来会读取接下的size(int)个字节并按照按照int的格式解析得到对应的值。
另一个方面,指针所指向的类型决定了指针的步长。指针的步长是什么呢?就是指针在做算术运算的时候,加减1,在实际内存中的加减地址数。
int main(){
printf("int length: %d\\n", sizeof(int));
int a = 10;
int *p = &a;
printf("p=%#X\\n", p);
p++;
printf("p=%#X\\n", p);
return 0;
}
运算结果:
int length: 4
p=0X62FE14
p=0X62FE18
注意到虽然对指针p加了1,但是打印出来指针p的值实际上是加了4,正好是sizeof(int)的值。如果把a改为其他类型,你会发现p增加的量都是等于a的类型对应的空间大小。
为什么要这样说设计呢?因为指针是一种特殊的变量,它专门存放其他类型变量的地址,如果指针加1也是像其他变量一样在数值(即地址上)加1,指针会指向一个变量的中间,那实际上没有意义。
比如刚才的例子,p原来指向
p++后假如p仅在数值上加1:
这样p取值得到的数值会很诡异,对于程序开发来说没有什么意义。
我们要的效果是p++后,从a的数据移动到下一个同样类型的数据:
指针算术运算的意义是什么呢?比如读取一段内存缓冲区里面的数据,类似遍历一个数组,这时候指针指向的类型将决定读取的数据是怎样的,比如a就是一段内存缓冲区,因为a是一个字符串组,我们一般用char*的指针地遍历读取:
int main(){
char *a = "abcdefgh\\0";
char *ptr=a;
while (*ptr != 0){
printf("char %c\\n", *ptr);
ptr++;
}
return 0;
}
运行结果:
char a
char b
char c
char d
char e
char f
char g
char h
假如把ptr指向的类型改为short:
int main(){
char *a = "abcdefgh\\0";
short *ptr= (short *)a; //强制类型转换并不会改变a 的类型
while (*ptr != 0){
printf("char %c\\n", *ptr);
ptr++;
}
return 0;
}
运行结果:
char a
char c
char e
char g
可以清楚看到,遍历的时候,是每次跳2个字节(当前环境short 2个字节)进行遍历的,所以指针的步长会决定如何读取一段内存的数据。
总结下,指针算术运算中,指针的值每次加1(减1),就是在原来值基础上加(减)sizeof(指针指向的类型)个字节。
所以,每当遇到指针,都要发出灵魂的拷问:每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?想清楚了,很多问题就迎刃而解了~
以上是关于漫谈C语言指针的主要内容,如果未能解决你的问题,请参考以下文章