1.前言:
首先,指针作为C语言中最为重要的知识点,它在后期对数据进行管理包括对变量的掌控都起到了关键性作用,但是指针由于其本身存在多阶迭代性,导致其存在让人难以理清思路和难以翻译清楚的问题,所以这里我们就重点分析一下这些问题:
2.问题列举:
1.* 的使用和辨析:
首先让我们开创一个变量:int a=10;当变量开创完毕后,这个变量会在内存的栈区寻找一段空间,空间就如同这个变量的家一样,它会有对应的门牌号,当我们需要这个门牌号的时候,我们就需要一个指针变量:intp=&a.注意,这里我们第一次使用,这里的表示的是指针变量的意思,也就是说,pa变量的类型是int类型。而取得门牌号的目的是让我们可以去这个变量的家串门并交代给我们要交代给它的,所以这里我们再一次pa=20;这里我再一次使用 *此时代表的是解引用,即去访问这个指针对应的变量的内容并改变它,通过这样的改变,我们的a就变成了20。
如图:
int main() { int a=10; int*pa=&a; *pa=20; printf("%d",a); return 0; }
综上:我们可以总结:对于带着变量类型的 *号的意思是指针的意思,而直接 * 加变量名的则是解引用访问具体内容的意思。
2.:指针变量的大小问题:
注意,指针的大小,并不取决于指针变量的类型,只取决于电脑是32位环境还是64位环境,如果是32位环境对应32个比特位,则一个指针对应32个比特位也就是4个字节,同理,64个比特位对应一个指针变量8个字节,所以,别搞错指针变量的大小问题。
3.指针变量的运算:
由上文我们知道:指针变量的大小在一个操作环境下是一样的,那么我们创建不同的指针变量有何用呢?这就涉及到指针变量的运算问题了:
1.指针+=整数:
int main() { int a=10; int*arr1=&a; char*arr2=&a; printf("%p",arr1+1); printf("%p",arr2+1); return 0; }
这次我们同样定义一个变量a,相对应的,在内存中它会开辟一块地址,注意,a为整型变量,然后我们创建了整型指针arr1,字符指针arr2,当我们对arr1和arr2都加一时发现,打印出的地址竟然不同!这是为什么呢?a不是整型变量么,那我们+1去到的地址为何不同?这就引出我要说的问题,不同变量类型的指针,决定了我们对指针进行移动和访问的时候的权限有多大。当我们创建一个整型指针的时候,+1我们会相当于移动4个字节(32比特位),故会是C8,而char类型指针,我们+1相当于移动1个字节(8比特位),所以是C5。
故我们总结第一条:不同类型的指针变量,对其进行指针移动的时候,移动的步长是不同的。同理,访问的范围也如此,例如对于一个顺序排列的整型数组,当我们用整型指针去依次访问的时候,可以改变每一个元素,但当我们利用字符指针去依次访问的时候,只能改变一个整型的4分之一的数据。
2.指针-指针:
注意,指针减去指针,这适用的条件是在同一个顺序表或者其他有顺序排列的数据结构中。
比如,我们可以利用指针减去指针去模拟实现strlen函数(这里不过多赘述)。指针与指针之间的操作只有减法没有加法,因为加法没有意义。
3.指针与指针比较大小:
同理,比较大小也限于在同一个顺序表或者其他有顺序排列的数据结构中,比较的方式,目前在我的认知内,类似数组的下标的比较,大的下标的位置更大更靠后,小的下标的位置更小更靠前。
4.野指针:
野指针是一种程序中很致命的错误,弄不好很容易出现内存泄漏的问题,甚至会让程序整体出现巨大的崩溃,所以我们要养成好的习惯去避免这样的错误。
1.野指针的错误类型:
首先我们先谈谈野指针发生时可能的情况:
1.未初始化
未初始化,不同于默认的数组和整型变量,未初始化就会默认为0,指针未初始化是默认指向一个不知道的位置,倘若不用还好,一旦使用,解引用就会导致访问野指针出现错误,这一点和空指针一个道理,野指针和空指针是不能直接进行解引用操作的。
2.访问越界
常见于访问顺序表或者链表等数据结构情况时,当我们的循环却没设置好限定条件,就会让指针指向未知的位置导致其变为空指针。
3.指向的空间被释放:
例如,当我们free(str),这里的str是指一个对应的空间,但注意free本身是不具备置空功能的,所以一旦我们忘记置空,这个str指向的空间消失了,str就变成了指向未知位置的指针,即变成了野指针。
2.野指针如何避免:
经过以上的分析,我发现大多数的野指针都源于我们不好的习惯,所以这里我们重新养成一些良好的习惯即可避免野指针:
1.及时置空:
对于不用的指针,我们一定不要手懒,要么就直接赋给地址,要么就先置空后续再赋值,当使用free函数时,由于free函数本身不会置空,故我们一定要注意手动置空。
2.:合理控制循环的次数,以防越界
5.const修饰的使用:
const的作用是直接将所修饰变量变为变为无法改变的属性:详细作用如下:
1.当const修饰常规非指针变量时:
修饰后,指针变量即变为不变,不可改变
2.当const修饰指针变量时:
1.当位于 *左边时,指针所指向的内容不可改变,但指针变量的指向可以改变
2.当位于 *右边时,指针变量的指向不能改变,但指针变量的内容可以改变
3.当 * 的左边和右边都有const时,则既不可改指针指向的内容,也不可改变指针变量的指向
6.assert断言的使用:
首先,assert宏用于对于一些变量或者程序进行判断,若判定为空的情况则会报错,包括置空,为0,表达式为假的一系列情况等。使用assert时,要引用头文件#include<assert.h>。
assert一般用于debug版本程序员对程序进行调试时使用,在判定空指针和野指针方面很有用,当我们程序正确时,直接在程序前面加上NDEBUG即可终止掉所有的assert断言的效果。这样可以减少程序判断的次数从而让程序运行更快。
7.对数组名的理解:
数组名本质上就是数组首元素的地址! • 1
既然说到这里,我们就要清楚在函数内部,当传入的参数为数组的名字时我们是无法在函数内部求数组的长度的,因为此时的数组名表示第一个数组的地址,我们求值会等于1。这是很关键的一点要记住。
但是,并非所有情况下数组名都表示数组首元素的地址,有两种情况下的数组名就会表示整个数组:
1.sizeof(数组名)(注意,sizeof后面直接跟数组名,不要加其他任何东西时才表示整个数组)
2.&数组名
#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("&arr[0]+1 = %p\n", &arr[0]+1); printf("arr = %p\n", arr); printf("arr+1 = %p\n", arr+1); printf("&arr = %p\n", &arr); printf("&arr+1 = %p\n", &arr+1); return 0; } 结果如下: &arr[0] = 0077F820 &arr[0]+1 = 0077F824 arr = 0077F820 arr+1 = 0077F824 &arr = 0077F820 &arr+1 = 0077F848
通过这道题,显而易见我刚才的结果,对于&arr,对其加一是横跨一整个数组的,而&arr[0]就不是,只会一个位置一个位置跨越。
所以,当我们对于函数想传入数组时,我们传入指针形式(*)或者数组形式([n])都是可以的,因为本质上就是传数组首元素的地址。
8.字符指针变量的理解:
我们常见的字符指针的创建有这两种:
1.char arr1[ ]
2.char*arr1
那么,这两种写法有什么区别么?
那让我们看下面这道题:
*include <stdio.h> int main() { char str1[] = "hello bit."; char str2[] = "hello bit."; const char *str3 = "hello bit."; const char *str4 = "hello bit."; if(str1 ==str2) printf("str1 and str2 are same\n"); else printf("str1 and str2 are not same\n"); if(str3 ==str4) printf("str3 and str4 are same\n"); else printf("str3 and str4 are not same\n"); return 0; }
这道题的输出结果是什么呢?
为什么是这样的结果呢?
由此我们可以知道:char arr1[ ]这样开辟的字符串是可变的,也就是说,开辟几个相同的字符串都是不同的,所以str1不等于str2,而char*arr2这样开辟的字符串是常量字符串,地址也是唯一的,故str3等于str4,而且这样开辟的字符串不可修改。
9.指针数组和数组指针的理解和区分:
记住一句话:指针数组本质上是数组,而数组指针本质上是指针,首先两者的基本类型就不同,一个用来存储数据,一个用来存储变量的地址。
指针数组的形式:例如int*arr1[ ]
数组指针的形式:例如int(*pa)[ ]
所以,我们可以这样区分:带上括号的是数组指针,而不带括号的是指针数组,这是由于[ ]的优先级是大于 *号的,故先和[ ]结合就又变为指针数组了,所以我们加了括号,以保证优先级的分配。
指针数组的取法:&arr1=int(*pa)[ ]
所以,为了区分好,一定要注意括号。
10.函数指针变量:
1.函数指针变量的创建
注意格式:
例如: int (pa)(int x,int y)
我们可以总结:返回类型 ( *指针名字)(参数),参数其实只需要写类型即可,具体的名字可以不写,但写了也不会报错
2. 函数指针变量的使⽤
和函数调用一样,*号是不需要写,写了也不会错。
例如:1.printf(“%d\n”, (*pf3)(2, 3));
2.printf(“%d\n”, pf3(3, 5));
这两种形式都可以调用函数,也就是使用函数指针
11.函数指针数组:
主要注意格式:
例如:int (*pa[ ])(int x,int y)
其实,把pa[ ] 去掉,我们就明显看出是一个函数指针,类似int 那样,在括号内部加上名字和数组大小这样理解就清晰多了。
12.二维数组的理解和传参的本质:
二维数组,在我们的第一印象里应该是一个类似矩阵形式的样子,数据的摆放是呈现一面的而不是一行的,所以我们常采用i j两个变量来控制下标寻找元素,但是,i j控制的仅仅是行和列么?或者换句话说,i j的理解方式更加接近我们所学的指针是什么样呢?
倘若我构建一个指针数组:
int*arr1[ 3]={str1,str2,str3};
这个数组有三个元素,每个元素都对应一个整型的指针
接着,我在这个指针数组上面构建str1,str2,str3三个数组:
int str1[5]={1,2,3,4,5};剩下两个也是如此构建,我们便可以得到如下的图片:
如图所示,一个二维数组正好建立好了,综上,我们可以总结:二维数组的本质就是一个存储着一维数组指针的数组。
那么,二维数组的数组名代表什么呢?既然一维数组的数组名代表第一个元素的地址,那二维数组的数组名就代表第一个数组的头指针,故我们可以理解为二维数组的数组名就是第一行数组的地址。
假如我们想要访问1行2列这个元素,也就是2,用指针怎么访问呢?首先解引用*(arr1+i),得到第一行数组的首元素,然后*(*(arr1+i)+j),也就是在第一行数组的头指针位置开始加j个单位长度解引用,倘若我们用常规的arr1[i][j]也能访问到,所以,本质上我们的下标访问,实际上就是操控指针去解引用。
注意数组的细节:1.[ ]符号可以理解为解引用的意思,比如arr1[i]== * (arr1+i)
2.注意区分{}和()的区别,写入二维数组的元素是{}内部写,()是括号操作符,对应输入最右侧的元素,所以我们输入二维数组的元素别用(),要用{ }。
13.sizeof和strlen的区别和使用:(strlen头文件:#include<string.h>)
在很多人印象里这两个关键字都是用来求长度的,其实不然。
1.sizeof本质是操作符,而strlen本质是库函数。这是两者最本质的区别,要区分好。
2. sizeof是用来求任意变量的大小的,而strlen是求字符串的长度的
sizeof仅仅用来求变量大小,单位是字节,而且可以求任意变量,当我们想求长度的时候,要int sz=sizeof(arr1)/sizeof(arr1[0])这样的方式,而且是可以求任意类型数组的长度的。
而strlen仅仅用来求字符串长度,遇到’\0’才停止。
所以总结:sizeof作用于任意变量,而strlen仅作用域字符串,而且要有\0,否则不会停止下来。
14.typedef关键字
typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。
例如:typedef unsigned int uint ;
这样定义完后,unsigned int就可以使用uint来代替了。
值得注意的是:
函数指针和数组指针的typedef跟指针形式一样 ,要将名字写在里面。
例如:typedef int(*parr_t)[5];
typedef void(*pfun_t)(int);
15.总结:
以上就是指针关键点目前我总结的一些问题,后续还有我还会补充,指针作为难点,很关键,所以学习计算机语言的各位一定要多花时间去理解和校准。