当前位置:   article > 正文

【C语言笔记】C语言指针全解+深入理解_c语言指针笔记

c语言指针笔记

一、什么是指针(概念及作用)

在我们的计算机中有成千上万个内存单元,这么多内存单元如果不规范管理起来,那将是很麻烦的。事实上,我们可以将各个内存单元看做是一个个房子,把这些房子用一个唯一的房号标识起来,这样就好管理多了。
但是随着计算机的逐渐发展,内存空间也变得越来越大,那么这些“房号”势必也会变得越来越复杂和冗长,如果每次都要通过一个“房号”来寻找某个“房子”,那也是一件很让人头疼的事情。
如果能有一个工具来帮助我们,管理这些“房号”就好了,比如把“房号”存放在一个工具里,需要查询的时候只需要从这个工具中查询出对应的“房号”即可。
这个工具就是我们今天要讲的“指针”。

1、指针的概念

对于指针的理解有以下两个要点:

1、指针是内存中最小单元的编号,也就是地址。
2、平时我们口中所说的指针,通常指的是指针变量,是用来存放地址的变量

也可以这样理解:指针就是一个特殊辨识的地址,或者给地址取了个别名,便于更好地使用,程序员不在需要记住冗长的地址序列。

2、指针类型的作用

上面说到指针就是地址,我们知道地址的序列长度是固定的(32位平台是32比特,64位平台是64比特),那么指针的大小是不是也是固定的呢?
我们可以试验一下:
在这里插入图片描述
我们可以看到,不管 是什么类型的指针,其大小都是一样的。
那么C语言中为什么要将指针分为不同类型呢?
对于这个疑惑,我们可以看看以下代码:
在这里插入图片描述
通过观察我们发现,相对于pa,pa + 1跳过了4个字节,而相对于pc,pc + 1则只跳过了1个字节。
这说明了指针的类型规定了指针每走一步的步长是多少,也可以理解为是单位。

我们再看看以下代码:
在这里插入图片描述
我们会发现这样的现象有点奇怪。
要解释清楚这个现象我们必须要到内存中去看:
在这里插入图片描述
我们可以看到执行完对a的赋值之后,内存中a存储的值是原来赋给它的值(这里是小端存储)。
在这里插入图片描述
但执行完pa = 1024之后,内存中的a就变成了上图的形式,从这可以看出来int类型的指针解引用访问的是4个字节的空间。
在这里插入图片描述
但执行完了pc = 2之后,内存中的a就变成了如上形式,只更改了1个字节的空间,所以就相当于加了个2,这说明了char*类型的指针解引用时访问的是1个字节的空间。
从上面的例子中可以看出,指针类型规定了指针在解引用时访问的权限是多大(访问多少字节空间)

3、野指针

野指针是指指向的位置未知的指针,对野指针直接解引用是非法的。
比如:

#include <stdio.h>
int main() {
	int* p;
	printf("%d\n", *p);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

运行时编译器会报错:
在这里插入图片描述
产生野指针的原因:

1、指针未被初始化
2、指针越界访问
3、指针指向的空间被释放

指针越界访问就是使用了未被分配的内存空间,比如数组越界,指针指向的空间被释放就要好好说说了,比如下例:

#include <stdio.h>
int* test() {
	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int* p = arr;
	return p;
}
int main() {
	int i = 0;
	int* p1 = test();
	for (i = 0; i < 10; i++) {
		printf("%d ", p1[i]);
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这里的p1就变成了野指针,原因是数组的空间是在test函数内部分配的,当函数执行完后空间就被回收了,如果在对这些空间进行访问,那就是非法访问。
有些语法严格的编译器会在你写到int *p1 = test()时就已经报编译时错误。有些编译器就不会,但不报错就不代表这样的写法是对的。

4、指针的运算

指针的运算其实在上面也已经提到了,对指针的运算其实就是对地址的运算,指针加减整数其实就是跳过多少个“步长”。
例如:
指针±整数
在这里插入图片描述
我们可以看到访问结果是一样的,如果是减整数就是访问指针当前指向的元素的向前第几个元素。
指针-指针
指针减指针的绝对值是两个指针之间的元素的个数,但前提一定要是两个指针的类型是相同的,
例如:

对于指针+指针,C语言终是没有这种运算的,原因很简单:“地址+地址”并没有什么实际的意义,就像“日期+日期”一样。

二、数组与指针的异同

在C语言中,指针和数组虽然是两个不同的概念,但这两者之间却有着千丝万缕的联系,以至于有些人分对这两个概念傻傻分不清。

1、相同点

1.1、访问方式可以相同

对于指针和数组的访问方式为什么可以相同,我们先要来解释一下下表访问操作符“[]”。
在C语言中,下标访问操作符的两个操作数的顺序是可以随意的,就像a + b 与b + a 等价一样,所以arr[i] 和i[arr]也是等价的;
下标访问操作符的两个操作数可以解释为:一个是指向Type型对象的指针,另一个是一个整数,整个表达式表示访问从这个指针起跳过多少个“步长”的地址。
故arr[i] 等价于(arr + i)*
其实在C语言中,对于数组,我们能做的只有两件事:确定数组的大小和获得指向该数组下标为0的元素的指针。至于其他操作,哪怕它看上去像是以数组下标进行运算的,但实际上都是通过指针来进行的。
也就是说,arr[i] 都会被转化成(arr + i) 来进行*。

1.2、数组名和指针等价

其实上面的例子其实已经说明了数组名和指针等价了,因为arr[i] 都会被转化成*(arr + i) 来进行,所以数组名其实就是个指针,单数组名实际上是一个“指针常量”,所谓常量也可以理解为“字面值”,就像整型10就是一个整形常量,其字面值是10。所以指针常量其实就是地址的字面值,例如*100表示对地址为100的内存空间解引用,100就是一个指针常量。
数组名只有在两种情况下表示的才不是指针,分别是数组名单独放在sizeof内部和取地址数组名。这两个知识点是我们应该死记硬背下来的

2、不同点

2.1、赋值表达式的左操作符不可以是数组名

承接上面所说,数组名是一个指针常量,表示的是一个地址的字面值,但我们知道,对于常量是不可以赋值的,例如:

int a  =0;
10 = a;
  • 1
  • 2

就会报错。
指针常量当然也不例外;
但是对于指针变量就可以赋值,例如:

int a = 10;
int *p = &a;
  • 1
  • 2

指针常量和费指针常量是不同的,因为编译器在负责把变量赋值给计算机内存的位置,程序员无法事先知道某个特定的变量将会被储存在内存中的哪个位置。
而且事实上,当一个函数每次调用时,它的局部变量可能每次分配的内存位置都不相同。因此,若对指针常量进行赋值,则当函数结束后,指针常量对应的位置所储存的内容将不会被销毁。

2.2、指针的声明和数组的声明底层操作不同

声明一个数组时,编译器会根据声明所指定的元素数量为数组分配内存空间,然后再创建数组名,它的值是一个常量,指向着这段空间的起始位置。
但声明一个指针变量时,编译器只会为指针本身分配内存空间,也就是说指针变量所指向的空间是未知的。
例如:

int arr[4];
int *p;
  • 1
  • 2

我们可以用如下声明图来解释,他们之间的不同。
在这里插入图片描述
所以p是一个野指针,但arr并不是一个“野数组”,也就是说对于arr的空间可以随意访问。

三、数组指针和指针数组

1、数组指针

数组指针就是一个指向数组的指针,它的声明形式如下:

int (*p)[10];
  • 1

以上声明表达的是p是一个指向具有10个元素的int类型数组的指针。
第一次看到这样的声明可能感觉有点儿复杂,但有个方法可以让你快速理解,我们把它假设成一个表达式并对它求值即可:
下标引用操作符的优先级要高于间接访问操作符,但由于括号的存在,所以首先执行的还是间接访问,所以p是一个指针,但对它进行解引用能找到什么呢?
接下来执行的下标引用操作符,下标引用执行后找到的是一个int类型的值,所以对p解引用找到的就是一个int类型的数组,所以p就是一个指向数组的指针,数组的元素类型是int。

1.1、对数组指针+1跳过的是一个数组的长度

上面说过对指针进行加减整数,跳过的是类型的“步长”,也就是类型的类型的长度,那么数组的长度是多少呢?
我们知道,数组是一组相同类型的元素的集合,数组是存储一块连续的空间上的,那么数组的长度可想而知就是元素类型的长度乘上元素的个数。
我们可以验证一下:
在这里插入图片描述
通过计算我们得出,下面的地址比上面的地址多了40字节,原因是int类型的长度是4字节,10 * 4当然等于40了。

1.2、二维数组的数组名就是一个数组指针

我们已经知道,数组名表示的是首元素的地址,但是二维数组的收元素是什么呢?
这就要说说C语言中对于二维数组的定义了;
事实上C语言中是没有真正的“二维数组”的,C语言中只有一维数组,多有的多为数组都是用一位数组“模拟”出来的。
对于二维数组,它实际上是一个特殊的一维数组,它的每个元素也是一个一维数组。
所以我们就可以理解为什么二维数组的数组名就是一个数组指针了,因为数组名是首元素的地址,而二维数组的每个元素都是一个一维数组,一个一维数组的地址当然是一个数组指针了。

2、指针数组

指针数组顾名思义就是一个数组,其每个元素的类型是指针,它的声明形式如下:

int* arr[10];
  • 1

以上声明的意义是:arr是一个数组,数组有10个元素,每个元素的类型是一个int类型的指针。
那什么地方能应用到指针数组呢?
其实指针数组应用最广泛的就是对多个字符串进行操作的函数。
但首先我们要先弄清楚字符串常量的概念。

2.1、字符串常量

前面说过常量其实可以理解为是一个字面值,但当一个字符串常量出现在表达式中时,它的值其实是一个指针常量。编译器会把这些指定的字符的一份副本存储在内存的某个位置,并存储一个指向第一个字符的指针。
如上面所说,数组名也相当于是一个指针常量,那么我们对字符串所做的操作:下标引用、间接访问、加减整数。这些操作对字符串常量是不是也适用呢?
答案是:适用的。
例如:
“abc” + 1 :表示的是“指针 + 1”,它的结果是一个指针,指向字符串的第二个字符‘b’。
*“abc” :表示的是找到指针指向的内容,很容易知道这个表达是的结果是字符‘a’。
“abc”[2] : 其结果也是‘b’。
*(“abc” + 2) : 的结果也是‘b’。

所以若是要把很多个字符串常量存储到一个数组中,那么这个数组就应该是一个存放字符指针的指针数组。
例如:

char* KeyWord[] = { "do", "for", "hello", "world", "return" };
  • 1

这样我们就可以通过一个辅助的char*指针对数组中的常量字符串进行遍历,然后进行各种操作了。

四、函数指针

函数指针顾名思义就是指向函数的指针,其声明形式如下:

int (*pf)(int, int);
  • 1

以上声明的含义是,pf是一个指向一个“返回值为int参数为另个int类型变量的函数”的函数指针。
第一次听到函数指针的时候你一定会大吃一惊,你可能充满了疑问:函数也可以用指针指向的吗?如果函数可以用指针来指向,那么函数就有地址了,函数也有地址的吗?
答案是:当然,函数也有地址,不信我们可以打印来看一看:
在这里插入图片描述
事实说明函数确实是有地址的。

我们知道数组名和取地址数组名表达的意思是不一样的,那么函数名和取地址函数名是否也不一样呢?
答案是:函数名和取地址函数名是等价的
所以我们在给函数指针赋值的时候,也可以直接将函数名赋给指针变量:

#include <stdio.h>
int Add(int x, int y) {
	return x + y;
}
int main() {
	int (*pf1)(int, int) = &Add;
	int (*pf2)(int, int) = Add;
	// 这两种写法是一样的
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

而且还有一点更奇葩的,既然&Add = Add, 那么有*&Add = *Add,即Add = Add,所以pf2 = *Add,所以pf = Add。
是不是这样呢?我们可以试一试:
在这里插入图片描述
答案很显然是的。

当然我们比可能天天使用函数指针,但是它确实很有用,最常见的两个用途当属转换表回调函数
只不过这两种用法的逻辑过于复杂,这里就不做过多的讲解。

五、理解各种指针的声明

让很多人难以理解的其实并非是指针本身,而是C语言中对指针和数组的各种“套娃”式使用,例如:函数指针数组、指向函数指针数组的指针、指向一个“返回值为函数指针的函数”的函数指针……
这些概念如果让新手看见,那简直犹如天书。那有没一些方法能让人理清其中的逻辑呢?
其实在使用更高级的指针之前,我们必须先理解他们是怎么声明的。只有是很如理解了C语言中的声明,次才能更容易理解更复杂的指针。

1、C语言中声明

在《C和指针》中有提到:

其实任何C语言的声明都由两部分组成:类型以及一组类似表达式的声明符。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果

意思就是说:我们完全可以把类型右边的声明符当成一个表达式,对表达式求值就能得到一个相应类型的数据。
且C语言声明还有一大特点:声明的形式和使用的形式非常相似。这样做的好处就是使得各种不同的操作符的优先级在“声明”和“使用”时是一样的。
比如:

int a;
  • 1

这个声明表示的是,对a求值得到的是一个int类型的数据。

int sum();
  • 1

这个声明表示的是,对sum()求值得到的是一个int类型的数据。
我们再来一个复杂一点的:

int *(*p)(int, int);
  • 1

这里的函数调用操作符的优先界比间接访问操作符的优先级要高,但由于小括号的存在,所以优先执行的还是对p的解引用,对p解引用找到的是什么呢?我们来到外边看到了“(int , int)”,说明是一个函数调用,这就说明对p解引用找到的是一个函数,p是一个指向函数的函数指针;
那函数的这个函数的返回值类型是什么呢?
其实(*p)(int, int)已经返回了某个类型,我们再往外看,看到对这个“某个类型”也进行了解引用操作,说明这“某个类型”是一个指针,对它解引用找到的是一个int;
说明这“某个类型”就是一个int类型的指针。
所以p就是一个指向“参数为两个int,返回值类型是int指针的函数”的函数指针。

所以对C语言中那些复杂的“套娃”式的指针或数组,我们就有了大概的分析思路:先分析变量p到底是个什么,数组还是指针,在分析他们的元素类型是什么或者指向的类型是什么或者是返回值类型是什么?。

2、“剥白菜”式的分析方式

接下来我将用一个许多人看了都感觉眼花缭乱的例子,来再次逐步演算以下分析过程,这个例子就是“指向函数指针数组的指针”:

void (*(*ppfunArr)[5])(char*);
  • 1

第一步:对ppfunArr进行了解引用,并找到了一个东西1说明ppfunArr是一个指针。
第二步:[]的优先级大于*,所以是对上面解引用找到的东西1进行了下标引用,并找到了数组中的一个元素,说明ppfunArr指向的是一个数组,说明ppfunArr是一个数组指针,这里已经分析出了ppfunArr的本质是什么了,就下来就剩分析数组中的元素类型了。
第三步:对元素在进行解引用,并找到了一个东西2,说明这个元素也是一个指针。
第四步:对东西2使用了函数调用操作符,里面的参数是char*。说明东西2是一个函数,说明数组中的元素是一个函数指针。此外,最外层已经没有别的操作符了,所以函数返回的就是void。

之后就可以总结了:ppfunArr是一个数组指针,指向一个数组,数组的每个元素是一个指向“参数为char*返回值类型为void的函数”的函数指针。
所以综上所述:ppfunArr是一个**“指向函数指针数组的指针”**。

本篇参考书籍:
《C和指针》
《C陷阱和缺陷》
《C专家编程》
《C程序设计语言》

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/191932
推荐阅读
相关标签
  

闽ICP备14008679号