【C高级专题】指针才是C语言的精髓

目录

章节介绍

1.指针到底是什么?
本节深入剖析指针的本质,指出指针全称指针变量,强调指针其实也是变量的一种,和普通变量并没有本质分别。这个是大家理解关于指针的更高深问题的基础。
2.指针带来的一些符号的理解
本节重点讲解指针符号*和取地址符&,及其在指针定义和解引用时的理解。重点是指针符号在定义同时初始化和定义后再次赋值、解引用时各种符号的理解。
3.深入学习一下数组
本节输入讲解数组,首先从内存和编译器两个角度来理解数组,然后重点讲解了数组相关的几个符号在做左值和右值时的意义区别,这些含义是深入理解C语言数组的关键
4.指针与数组的天生姻缘
本节将数组和指针结合起来,讲述指针指向数组时的一些运算,分析其运算时的结果。通过实例让大家明白指针方式访问数组,以及指针运算访问数组元素的细节。
5.指针与强制类型转换
本节首先讲解普通变量数据类型的意义,然后讲解指针变量数据类型的含义,最后讲了指针变量在进行加减运算时数据类型的作用和影响。

一、指针

1、指针全称是指针变量,其实质是一种变量,通常它的值会被赋值为某个变量的地址值(p = &a),然后我们可以使用*p这样的方式去间接访问p所指向的那个变量。

2、指针的实质就是个变量,它跟普通变量没有任何本质区别,它们用途和普通变量不同,指针变量存储的应该是另外一个变量的地址。

3、为什么需要指针?
(1)指针的出现是为了实现间接访问。在汇编中都有间接访问,其实就是CPU的寻址方式中的间接寻址。在STM32中,CPU以结构体的方式访问寄存器来间接性的访问内存。

(2)间接访问(CPU的间接寻址)是CPU设计时决定的,这个决定了汇编语言必须能够实现间接寻址,又决定了汇编之上的C语言也必须实现间接寻址。

4、指针使用三部曲:定义指针变量、关联指针变量、解引用

	// 指针使用分3步:定义指针变量、给指针变量赋值(绑定指针)、解引用
	// 第一步,定义指针变量
	int *p;
	// 第二步,绑定指针,其实就是给指针变量赋值,也就是让这个指针指向另外一个变量
	// 当我们没有绑定指针变量之前,这个指针不能被解引用。
	p = &a;				// 实现指针绑定,让p指向变量a
	p = (int *)4;		// 实现指针绑定,让p指向内存地址为4的那个变量
	
	// 第三步,解引用:指针指向的那个空间。
	// 如果没有绑定指针到某个变量就去解引用,几乎一定会出错。
	*p = 555;			// 把555放入p指向的变量中

(1)*p定义一个指针变量p时,因为p是局部变量,所以也遵循C语言局部变量的一般规律(定义局部变量并且未初始化,则值是随机的),所以此时p变量中存储的是一个随机的数字。
(2)此时如果我们解引用p,则相当于我们访问了这个随机数字为地址的内存空间。那这个空间到底能不能访问不知道(也许行也许不行),所以如果直接定义指针变量未绑定有效地址就去解引用几乎必死无疑。
(3)指针绑定的意义就在于:让指针指向一个可以访问、应该访问的地方
指针的解引用是为了间接访问目标变量

5、指针定义的二种理解思路:
int p;
第一种:首先看到p,这个是变量名;其次,p前面有个
,说明这个变量p是一个指针变量;最后,*p前面有一个int,说明这个指针变量p所指向的是一个int型数据。
第二种:首先看到p,这个是变量名;其次,看到p前面的int *,把int *作为一个整体来理解,int *是一种类型(复合类型),该类型表示一种指向int型数据的指针。

二、 指针带来的一些符号的理解

1、星号*
(1)C语言中*可以表示乘号,也可以表示指针符号。这两个用法是毫无关联的,只是恰好用了同一个符号而已。
(2)星号在用于指针相关功能的时候有2种用法:第一种是指针定义时,结合前面的类型用于表明要定义的指针的类型;第二种功能是指针解引用,解引用时p表示p指向的变量本身

2、取地址符&
(1)取地址符使用时直接加在一个变量的前面,然后取地址符和变量加起来构成一个新的符号,这个符号表示这个变量的地址。

3、指针定义并初始化、与指针定义然后赋值的区别
(1)指针定义时可以初始化,指针的初始化其实就是给指针变量初值(跟普通变量的初始化没有任何本质区别)。
(2)指针变量定义同时初始化的格式是:int a = 32; int *p = &a;
(2)不初始化时指针变量先定义再赋值:int a = 32; int *p; p = &a; 正确的 *p = &a; 错误的

4、演示指针变量定义

	// 演示指针变量定义
	// *和int结合,表明p的类型是int *,也就是p是指向int类型变量的指针
	int *p;
	// 把*和指针变量放在一起,而不是和int挨着,是为了一行定义多个变量时好理解
	int *p5, *p6;		// 这样才是定义了2个int *指针变量p5、p6
	int *p5, p6;		// p5是int *指针,p6是int的普通变量
	int* p5, p6;		// p5是int *指针,p6是int的普通变量

5、左值与右值
(1)放在赋值运算符左边的就叫左值,右边的就叫右值。所以赋值操作其实就是:左值 = 右值;
(2)当一个变量做左值时,编译器认为这个变量符号的真实含义是这个变量所对应的那个内存空间;当一个变量做右值时,编译器认为这个变量符号的真实含义是这个变量的值,也就是这个变量所对应的内存空间中存储的那个数。
(3)变量的二义性:一重含义指的是这个变量的内存空间,第二重含义是这个变量的内存空间所存储的值

int a = 3, b = 5;
a = b; // 当 a 做左值时,a是被编译器分配在内存中对应那个存储空间
b = a; // 当 a 做右值时,a是对应存储空间存储中的值
为 b 了

三、深入学习一下数组

1、从内存角度来理解数组
(1)从内存角度讲,数组变量就是一次分配多个变量(地址),而且这多个变量在内存中的存储单元是依次相连接的。
(2)我们分开定义多个变量(譬如int a, b, c, d;)和一次定义一个数组(int a[4]);这两种定义方法相同点是都定义了4个int型变量,而且这4个变量都是独立的单个使用的;不同点是单独定义时a、b、c、d在内存中的地址不一定相连,但是定义成数组后,数组中的4个元素地址肯定是依次相连的。
(3)数组中多个变量虽然必须单独访问,但是因为他们的地址彼此相连,因此很适合用指针来操作,因此数组和指针天生就叫纠结在一起。

2、从编译器角度来理解数组
(1)从编译器角度来讲,数组变量也是变量,和普通变量和指针变量并没有本质不同。变量的本质就是一个地址,这个地址在编译器中决定具体数值,具体数值和变量名绑定,变量类型决定这个地址的延续长度
(2)搞清楚:变量、变量名、变量类型这三个概念的具体含义,很多问题都清楚了。
变量:代表内存一个具体的内存值(0x0000008)占一个字节
变量名:变量的符号与变量进行绑定
变量类型:在内存中代表定义的变量占内存的大小

3、数组中几个关键符号(a a[0] &a &a[0])的理解(前提是 int a[10])
(1)这4个符号搞清楚了,数组相关的很多问题都有答案了。理解这些符号的时候要和左值右值结合起来,也就是搞清楚每个符号分别做左值和右值时的不同含义。
(2)a就是数组名。a做左值时表示整个数组的所有空间(10×4=40字节),又因为C语言规定数组操作时要独立单个操作,不能整体操作数组,所以a不能做左值;a做右值表示数组首元素(数组的第0个元素,也就是a[0])的首地址(首地址就是起始地址,就是4个字节中最开始第一个字节的地址)。a做右值等同于&a[0];
(2)a[0]表示数组的首元素,也就是数组的第0个元素。做左值时表示数组第0个元素对应的内存空间(连续4字节);做右值时表示数组第0个元素的值(也就是数组第0个元素对应的内存空间中存储的那个数)
(3)&a就是数组名a取地址,字面意思来看就应该是数组的地址。&a不能做左值(&a实质是一个常量,不是变量因此不能赋值,所以自然不能做左值。);&a做右值时表示整个数组的首地址。
(4)&a[0]字面意思就是数组第0个元素的首地址&a[0]不能做左值(&a[0]实质是一个常量,不是变量因此不能赋值),做右值时表示数组首元素的首地址。做右值时&a[0]等同于a。

解释:为什么数组的地址是常量?因为数组是编译器在内存中自动分配的。当我们每次执行程序时,运行时都会帮我们分配一块内存给这个数组,只要完成了分配,这个数组的地址就定好了,本次程序运行直到终止都无法再改了。那么我们在程序中只能通过&a来获取这个分配的地址,却不能去用赋值运算符修改它。

总结:
1:&a和a做右值时的区别:&a是整个数组的首地址,而a是数组首元素的首地址。这两个在数字上是相等的,但是意义不相同。意义不相同会导致他们在参与运算的时候有不同的表现。
2:a和&a[0]做右值时意义和数值完全相同,完全可以互相替代。
3:&a和&a[0]是常量,不能做左值。
4:a做左值代表整个数组所有空间,所以a不能做左值。

四、 指针与数组的天生姻缘

1、以指针方式来访问数组元素
(1)数组元素使用时不能整体访问,只能单个访问。访问方式有2种:数组形式和指针形式。
(2)数组格式访问数组元素是:数组名[下标]; (注意下标从0开始)
(3)指针格式访问数组元素是:*(指针+偏移量); 如果指针是数组首元素地址(a或者&a[0]),那么偏移量就是下标;指针也可以不是首元素地址而是其他哪个元素的地址,这时候偏移量就要考虑叠加了
(4)数组下标方式和指针方式均可以访问数组元素,两者的实质其实是一样的。在编译器内部都是用指针方式来访问数组元素的,数组下标方式只是编译器提供给编程者一种壳(语法糖)而已。所以用指针方式来访问数组才是本质的做法。

	int a[5] = {1, 2, 3, 4, 5};
	
	printf("a[3] = %d.\n", a[3]);
	printf("*(a+3) = %d.\n", *(a+3));
	//	等效于:int b = *(a+3); printf("*(a+3) = %d.\n", b);
	
	int *p;
	p = a;		// a做右值表示数组首元素首地址,等同于&a[0]
	printf("*(p+3) = %d.\n", *(p+3));		// 等同于a[3]
	printf("*(p-1) = %d.\n", *(p-1));		// 等同于a[-1]

2、从内存角度理解指针访问数组的实质
(1)数组的特点就是:数组中各个元素的地址是依次相连的,而且数组还有一个很大的特点(其实也是数组的一个限制)就是数组中各个元素的类型比较相同。类型相同就决定了每个数组元素占几个字节是相同的(譬如int数组每个元素都占4字节,没有例外)。
(2)数组中的元素其实就是地址相连接、占地大小相同的一串内存空间。这两个特点就决定了只要知道数组中一个元素的地址,就可以很容易推算出其他元素的地址。

3、指针和数组类型的匹配问题
(1)int *p; int a[5]; p = a; // 类型匹配
(1)int *p; int a[5]; p = &a; // 类型不匹配。p是int *,&a是整个数组的指针,也就是一个数组指针类型,不是int指针类型,所以不匹配
(2)&a、a、&a[0]从数值上来看是完全相等的,但是意义来看就不同了。从意义上来看,a和&a[0]是数组首元素首地址,而&a是整个数组的首地址;从类型来看,a和&a[0]是元素的指针,也就是int 类型;而&a是数组指针,是int ()[5];类型。

4、总结:指针类型决定了指针如何参与运算
(1)指针参与运算时,因为指针变量本身存储的数值是表示地址的,所以运算也是地址的运算,在通过*解引用就可以得出值。
(2)指针参与运算的特点是指针变量+1,并不是真的加1,而是加1*sizeof(指针类型);如果是int *指针,则+1就实际表示地址+4,如果是char *指针,则+1就表示地址+1;如果是double *指针,则+1就表示地址+8
(2)指针变量+1时实际不是加1而是加1×sizeof(指针类型),主要原因是希望指针+1后刚好指向下一个元素(而不希望错位)。

	int a[5] = {0x12345, 2, 3, 4, 5};
	int *p;
	p = a;
	
	printf("*(p+1) = %d.\n", *(p+1));			// 2 
	// P是强制类型转换char * 类型,则指针p所指向的那个变量的类型为char,*p解引用时以char类型来解析
	// char 占一个字节 0x12345中0x45 = 0b 0100 0100 刚好8bit位
	printf("*(p) = %x.\n", *((char *)p));	 	// 45
	// p+1是强制类型转换char * 类型,p
	printf("*(p+1) = %x.\n", *((char *)p+1));	// 23
//	printf("*(p+1) = %d.\n", *(int *)((unsigned int)p+1));
	
	char *p2;
	// 虽然将p强制类型转换char * 类型,但数据存储在内存中是不变的,
	// 只是*p解引用时的解析方式由int ->  char的方式
	// char *强制类型转换的目的是让编译器通过 (char *)&p做类型匹配 因为p2 是char *类型
	p2 = (char *)p;
	printf("*(p2) = %x.\n", *(p2));				// 45
	printf("*(p2+1) = %x.\n", *(p2+1));			// 23

五、 指针与强制类型转换

1、变量的数据类型的含义
(1)C语言中数据类型的本质含义是:表示一个内存格子的长度和解析方法。
数据类型决定长度的含义:我们一个内存地址(0x30000000),本来这个地址只代表1个字节的长度,但是实际上我们可以通过给他一个类型(int),让他有了长度(4),这样这个代表内存地址的数字(0x30000000)就能表示从这个数字(0x30000000)开头的连续的n(4)个字节的内存格子了(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)。
数据类型决定解析方法的含义:譬如我有一个内存地址(0x30000000),我们可以通过给这个内存地址不同的类型来指定这个内存单元格子中二进制数的解析方法。譬如我 (int)0x30000000,含义就是(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)这4个字节连起来共同存储的是一个int型数据;那么我(float)0x30000000,含义就是(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)这4个字节连起来共同存储的是一个float型数据;

(2)所有的类型的数据存储在内存中,都是按照二进制格式存储的。所以内存中只知道有0和1,不知道是int的、还是float的还是其他类型。
(3)int、char、short等属于整形,他们的存储方式(数转换成二进制往内存中放的方式)是相同的,只是内存格子大小不同(所以这几种整形就彼此叫二进制兼容格式);而float和double的存储方式彼此不同,和整形更不同。通过给内存地址不同的数据类型来指定这个内存地址中存放二进制数的解析方式

(4)int a = 5;时,编译器给a分配4字节空间,并且将5按照int类型的存储方式转成二进制存到a所对应的内存空间中去(a做左值的);我们printf去打印a的时候(a此时做右值),printf内部的vsprintf函数会按照格式化字符串(就是printf传参的第一个字符串参数中的%d之类的东西)所代表的类型去解析a所对应的内存空间,解析出的值用来输出。也就是说,存进去时是按照这个变量本身的数据类型来存储的(譬如本例中a为int所以按照int格式来存储);但是取出来时是按照printf中%d之类的格式化字符串的格式来提取的。此时虽然a所代表的内存空间中的10101序列并没有变(内存是没被修改的)但是怎么理解(怎么把这些1010转成数字)就不一定了。譬如我们用%d来解析,那么还是按照int格式解析则值自然还是5;但是如果用%f来解析,则printf就以为a对应的内存空间中存储的是一个float类型的数,会按照float类型来解析,值自然是很奇怪的一个数字了。
总结:C语言中的数据类型的本质,就是决定了这个数在内存中怎么存储的问题,也就是决定了这个数如何转成二进制的问题。一定要记住的一点是内存只是存储1010的序列,而不管这些1010怎么解析。所以要求我们平时数据类型不能瞎胡乱搞。
分析几个题目:

  • 按照int类型存却按照float类型取 一定会出错
  • 按照int类型存却按照char类型取 有可能出错也有可能不出错
  • 按照short类型存却按照int类型取 有可能出错也有可能不出错
  • 按照float类型存却按照double取 一定会出错

2、指针的数据类型的含义
(1)指针的本质是:变量,指针就是指针变量
(2)一个指针涉及2个变量:一个是指针变量自己本身,一个是指针变量指向的那个变量
(3)int *p;定义指针变量时,p(指针变量本身)是int *类型,*p(指针指向的那个变量)是int类型的。

(4)int*类型说白了就是指针类型,所有的指针变量都是指针类型,只要是指针类型就都是占4字节,解析方式都是按照地址的方式来解析(意思是里面存的32个二进制加起来表示一个内存地址)的。结论就是:所有的指针类型(不管是int * 还是char * 还是double *)的解析方式是相同的,都是地址(指针存放的是某一个变量的地址,占用4个字节)。
(5)对于指针所指向的那个变量来说,指针的类型就很重要了。指针所指向的那个变量的类型(它所对应的内存空间的解析方法)要取决于指针类型。譬如指针是int *的,那么指针所指向的变量就是int类型的。

3、指针数据类型转换实例分析1(int * -> char *)
(1)int和char类型都是整形,类型兼容的。所以互转的时候有时候错有时候对。
(2)int和char的不同在于char只有1个字节而int有4个字节,所以int的范围比char大。在char所表示的范围之内int和char是可以互转的不会出错;但是超过了char的范围后char转成int不会错(向大方向转就不会错,就好比拿小瓶子的水往大瓶子倒不会漏掉不会丢掉),而从int到char转就会出错(就好象拿大瓶子水往小瓶子倒一样)

5、指针数据类型转换实例分析2(int * -> float *)
(1)之前分析过:int和float的解析方式是不兼容的,所以int *转成float *再去访问绝对会出错。

	int a = 25;
	char *pChar;
	
//	pChar = (char *)&a;		// char *强制类型转换的目的是让编译器通过
							// (char *)&a 做类型匹配 pChar是char *类型
//	pChar = &a;				// pChar所指向的那个变量的类型为char,a是char类型,造成类型不匹配
	 	
	printf("*pChar = %d.\n", *pChar);		// 25

编译器给a分配4字节空间,并且将257按照int类型的存储方式转成二进制存到a所对应的内存空间中去(a做左值的),pChar是char * 类型,则指针所指向的那个变量的类型为char,*pChar解引用时以char类型来解析

	int a[3] = {0x11223344, 0x55667788};
	
	int *p1 = a;
	p1 = (char *)a;		//[Error] cannot convert 'char*' to 'int*' in assignment	
// 指针赋值时类型必须匹配,解引用时依据指针所指向的变量的类型解析(强制类型)
	printf("*p1 = 0x%x\n", *p1);
	
	char *p2 = (char *)a;				// 0x11223344
	printf("*p2 = 0x%x\n", *p2);		// 0x44
	printf("*p2 = 0x%x\n", *(p2+1));	// 0x33
	printf("*p2 = 0x%x\n", *(p2+2));	// 0x22
	printf("*p2 = 0x%x\n", *(p2+3));	// 0x11
	printf("*p2 = 0x%x\n", *(p2+4));
	printf("*p2 = 0x%x\n", *(p2+5));	// 0x88

注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Tyx-☆、、、

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

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

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

打赏作者

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

抵扣说明:

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

余额充值