C语言总结之指针

C语言总结之指针



前言

关于指针的一些基础知识,旨在比较深入全面的理解指针(这个在C语言中扮演重要角色的变量),是一些基础知识的总结。


一、指针

  • 定义:指针(pointer)是一个值为内存地址的变量
    就像是int类型变量的值是整数,char类型变量的值是字符一样,指针型变量的值是地址
  • 理解:内存中的每一个位置都是由一个独一无二的地址标识,并且每个位置都包含一个值,如果我们记住了一个值的存储地址,那就可以根据这个地址取得这个值了。但是记住所有的地址是不实际的,所以C语言的特性之一就是通过名字而不是地址来访问内存的位置,这些名字就是所谓的变量,其中名字与内存位置之间的关联,也就是变量名与地址之间的关联并不是硬件提供的,而是由编译器为我们实现的。硬件仍然通过地址访问内存位置。
  • 声明指针int *ptr;,即将ptr声明为一个指向整型的指针。
  • 初始化:如上所示,在声明指针的时候对其进行初始化是一种好的做法,下面代码说明了一个比较常见的错误
int *p;
*p = 24;

此声明创建了一个名叫p的指针变量,并将24存储在p所指向的内存位置。但是需要注意的是,指针p究竟指向的是哪里?代码声明的时候并未对指针p进行初始化,所以没有办法预测24这个值将会存储于什么地方。p的值有可能是个非法地址(引发错误segmentation violation 或 memory dault 或 general protection exception),也有可能是个合法地址(不会引发错误,反而修改了本意不想修改的值);所以在对指针进行间接访问之前,必须确保其被初始化

  • NULL指针:NULL是一个特殊的指针变量,表示不指向任何东西,可以给一个指针变量赋为零值使其变为NULL;选择零值是一种源代码约定,再机器内部,NULL指针的实际值可能与此不同,此时,编译器将负责零值与内部值之间的转换翻译。注意对一个NULL指针进行解引用操作是非法的,在对指针进行解引用操作之前,必须确保它不是NULL指针。
  • 间接访问操作符:通过一个指针访问它所指向的地址的过程称为间接访问或解引用指针。用于解引用的操作符是单目操作符 *, 如图1所示,d的值就是100,当对d使用间接访问操作符时,*d表示:访问内存位置100并查看该位置的值;指针变量并不存在内建的间接访问属性,除非表达式中存在间接访问操作符,如果不对指针变量进行间接访问操作时,它的值就是一个数字(内存某个位置的地址)。
  • 指针变量的内容:指针变量的内容是地址而不是整型或者浮点型的数值。假设内存中存储了如下图1所示的5个整数,对应的有其地址和变量名
    在这里插入图片描述
图1

并对上述变量进行如下的声明:

//codeOne
1> int a = 112, b = -1;
2> float c = 3.14;
3> int *d = &a;
4> float *e = &c;

d和e被声明为指针,并用变量a和c的地址进行了相应初始化,从图中可以看到,指针d和e的内容是变量a和变量c的地址(100, 108),对于指针变量d而言,112是它的地址,其指针变量的值是100(注意变量的值就是分配给该变量的内存位置所存储的数值),而这个100是用来标识其它位置(变量a)的,是变量a的地址。

  • 指针运算:① 算术运算:当一个指针和一个整数量执行加法运算时,整数在执行加法运算前始终会根据合适的大小进行调整。”合适的大小“即指针所指向类型的大小,”调整“即把整数值和合适的大小相乘。例:把3与指针(指向float类型)相加使指针的值增加3个float大小(12个字节),而不是3个字节。
1> #include <stdio.h>
2> int main(void){
3>		float *ptr = NULL;
4>		int num = 3;
5>		printf("the ptr value is %d\n", ptr);
6>		ptr += 3;
7>		printf("the ptr value is %d\n", ptr);
8>		return 0;
9>}

在这里插入图片描述

编译结果

指针的算数运算最主要的还是体现在数组的应用上,前篇文章C语言总结之数组中说过,数组中的元素存储于连续的内存位置中,后面元素的地址大于前面元素的地址,很显然,对一个指针加1能够使它指向数组中下一个元素位置,加3使它向右移动3个元素的位置,依次类推。但是重点需要注意的是:对指针执行加减法之后如果结果指针所指的位置在数组第1个元素之前或者最后一个元素的后面,那其结果将是未定义的。

codeFour
#include<stdio.h>
#define NUM  5
int main(void){
	float var = 3.0;
	float values[NUM];
	float *vp;
	printf("the var       value   is %f\n", var);
	for(vp = &values[0]; vp < &values[NUM]; ){
		*vp++ = 2.0;
	}
	*vp = 2.0;
	/* 打印数组元素的值 */
	printf("the values[0] value   is %f\n", values[0]);
	printf("the values[1] value   is %f\n", values[1]);
	printf("the values[2] value   is %f\n", values[2]);
	printf("the values[3] value   is %f\n", values[3]);
	printf("the values[4] value   is %f\n", values[4]);
	/* 打印数组最后一个元素后面的位置的地址 */ 
	printf("the values[5] address is %p\n", &values[5]);
	/* 打印变量var的地址 */
	printf("the var       address is %p\n", &var); 
	printf("the var       value   is %f\n", var);
	printf("the vp        value   is %016X\n", vp);
	return 0;
}

代码codeFour中对数组values的元素进行了赋值(利用指针vp自加的方式),在for循环后vp所指向的是数组最后一个元素后面的那个内存位置,通过查看变量地址发现,变量var的地址正好是数组values最后一个元素后面的那个位置,也就是说vp所指向的变量正好是var(这里只是为了说明问题,不同编译器可能有区别),此时对vp进行间接访问操作,会访问原先存储于这个位置的变量(即访问变量var),这样就会意外改变var的值,这是我们不允许,也是程序设计预料之外的。

在这里插入图片描述

编译结果

总的来说绝大多数编译器都不会检查指针表达式的结果是否位于合法的边界之内,类似的,编译器也不会阻止取一个标量变量的地址并对它执行指针运算,即使它无法预测运算结果所产生的指针将指向哪个变量。越界指针和指向未知值的指针是两个常见的错误,所以在使用指针运算时,要确保运算的结果将指向有意义的东西。

  • 指针表达式:当理解了指针的含义,对于指针表达式的理解就是水到渠成的事情,我们列举几个指针表达式,同样做一些简单的说明,看看当这些表达式分别作为左值和右值时是如何进行求值计的:初始化如下char ch = 'a'; char *cp = &ch;
表达式右值左值
&ch表达式的值是变量ch的地址,也是cp中存储的值非法(因为当对&ch求值时,它的结果不知道存储于计算机的什么地方,也就是并未标识任何机器内存的特定位置)
cp就是cp的值cp所处的内存位置
&cp取的是指针变量的地址,表达式的结果的类型就是指向字符的指针的指针非法表达式结果的存储位置并未清晰定义
*cp指针cp所指向的位置的值cp所指向变量ch的位置
*cp+1cp所指向的位置的值加1,即字符’b’非法表达式的结果的存储位置并未清晰定义
*(cp+1)紧随ch之后的内存位置的值紧随ch之后的内存位置本身
++cp指针cp自加后的值非法表达式的结果的存储位置并未清晰定义
cp++指针cp的值非法
*cp++变量ch的值。该表达式的求值可以分为3步:① ++操作符产生cp的一份拷贝;② ++操作符增加cp的值,③ 在cp的拷贝上执行间接访问操作。常见于访问数组元素的时候,首先用数组地址初始化指针,然后使用这种表达式就可以依次访问数组的内容了。变量ch的内存位置
*++cp间接访问操作符作用于增值后的指针上,所以表达式的结果就是ch后面那个内存地址的值作为左值,指的就是那个内存本省
++*cpcp所指向的位置的值加1非法 :并不知道表达式的结果的值存储于什么地方
(*cp) ++ch增值前的原先值非法:同上
  • 指针的指针:也就是指向指针类型的指针,指针的指针并不难理解,只要理解了指针,指针的指针只是向下延申的理解,举个例子,代码如示:codeThree
//codeThree
1> #include <stdio.h>
2> int main(void){
3>		int a = 12;
4>		int *b = &a;
5>		int **c = &b;
6>		printf("the  a address is %p\n", &a);
7>		printf("the  b value   is %016X\n", b);
8>		printf("the  b address is %p\n", &b);
9>		printf("the  c value   is %016X\n", c);
10>		printf("the *c value   is %016X\n", *c);
11>		return 0;
12> }

代码中我们定义了3个变量,分别是整型变量a, 指向整型的指针b,指向"指向整型的指针“的指针c(指针的指针) ;字面意思可能不好理解,画个简单的示意图说明一下:
在这里插入图片描述

图2 示意图

首先,b是一个指针变量,它指向整型变量a,所以b的值就是变量a的地址,也就是62FE14;
再看**c,*操作符具有从左向右的结合性;所以从里向外逐层求值 *(*c): *c访问c所指向的位置,是变量b,第二个间接访问操作符访问这个位置所指向的地址,也就是变量a的值。编译结果证实了我们的理解。
在这里插入图片描述

编译结果
  • 指针常量:int *const p 表示指针p的内容是一个常量,不可更改,也就是其指向的内存位置是固定的,该位置存储的值是可以改变的。(有个门牌号的比喻很形象:指针p的内容是一个地址,这个地址就好比是家里的门牌号,指针指向的变量的值就好比是家里住的人,所谓指针常量,就是不关心屋里住的谁,爱谁谁,只要这个屋的门牌号不变就可以)。
    顺便提一下常量指针(中文有点子绕口) int const *p 表示指针p指向的内容是一个常量,不可更改,但是指针的值是可以改变的(也就是门牌号可以换,但是里面住的人不能变)。举个例子理解一下:
//codeTwo
1> #include <stdio.h>
2> int main(void){
3> 		int a = 3, b = 4, c = 5, d = 6;
4>		int const *ptr1 = &a;//const pointer
5>		int *const ptr2 = &b;//pointer to const
6>  	ptr1 = &c;   // true
7>		*ptr1 = c;   // [Error] assignment of read-only location *ptr1	
8>		*ptr2 = d;   //true
9>		ptr2 = &d;   // [Error] assignment of read-only location ptr1
10>		return 0;
11> }

值得注意的一点是:常量指针(ptr1)中指针p指向的值是一个不可更改的常量,不代表ptr1(指针指向的值)是同一个数,当改变ptr1(指针本身的值)的值的时候,其指向的变量的值也会发生变化。如codeTwo所示,codeTwo_4>将变量a的地址初始化赋值给ptr1,此时ptr1 = 3,codeTwo_6>将变量c的地址赋值给ptr1,此时*ptr1 = 5。

二、总结

写着写着就有点儿多了,也在书籍上面查了很多之前不是理解的很透彻的知识点,总归是有收获的,静下心来总结归纳自己的知识结构,对自己获益匪浅,也期望能够对开始学或者正在学的伙伴有所帮助,所学有限不当之处还请见谅指正;
后面即将开展指针数组和数组指针的相关内容,敬请期待!!!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yaoji1234

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值