程C指针学习笔记
单纯的指针
最简单的指针带*的:野指针(确信)
1 | int *p; //一只快乐的野指针,未初始化的指针,指向随机地址(野指针) |
这个表示定义了指针,不过注意,前面的类型是要自己设定的,不能乱写
但是提到指针,就不得不提到&(取地址符)了
&的用法:
&表示取地址符,比如这样:
1 | int a=1; |
这一句表示输出a变量对应的地址位置,详细见书上的解释,获取地址的开头方便输出
然后指针所能存储的恰好是地址,所以可以这么写:1
int *p=&a;
表示p指向a对应的地址
指针的初始化
可以把指针设定为NULL,这里表示p不指向任何一个地址
要是没有初始化,p可能指向任何一个位置,可能导致代码错误,所以被称作野指针
1 | int *p = NULL; // 未初始化指针(野指针) |
指针的多种含义
在实际程序中,*p,可能有多种含义:
- 表示p所指的变量,即
*p=a - 表示p对指的变量的值,即a的值
下面是例子:
1 | int a=1; |
这里两种情况怎么理解呢?
首先加上*表示对指针解引用,即访问指针指向的内存位置
- case 1:这里指把p所指的位置修改为1,所以表示p所指的变量
- case 2:这里因为表示的是p所指的位置,所以我们在输出时就是从这个位置开始逐个输出,表示的就是所指对应变量对应的值了。
更详细的解释:
在C++里有两种值叫做左值(Location Value) 和 右值(Read Value)
- 左值表示一个内存位置,可以被赋值
- 右值表示一个数据值,只能被读取
前者表示的是地址,被写入数据的那个变量,所以表示地址
后者只是获取其内部数据,表示的就是对应的值了
所以,当*p出现在赋值号左边时,它是左值,表示一个内存位置(即p指向的变量);当*p出现在其他表达式中(如赋值号右边、函数参数等),它是右值,表示该位置存储的值
这样基本用法就能理解了
指针表示一维数组
指针指向的是一个元素的位置,要是指向的是一个数组的开头呢?
指针指向一个元素的位置,如果指向一个数组的开头,就可以通过指针来访问数组
那么问题来了,我们该怎么获取中间元素的值呢?
这里我们就需要知道一下数组下标的工作原理了:
实际上程序会计算要往后访问几个位置,根据数据类型来决定往后跳几步。
所以我们可以这么理解a[3]:
a开头位置加上3×步长,得到这个元素的地址开头,就能正常访问了,
这个过程相当于是一种规则,表示我们要往后几步
对于指针也是如此,首先我们已经保证了指针p所指向的变量类型和数组a的元素类型一致,所以我们知道了p的开头以后,我们只要往后加上对应的步长就能找到我们要访问的元素了
那么C++中规定了,指针+1表示往后一个步长,[n]表示往后n个步长,所以我们从这里理解了,为什么数组的下表是从0开始的,以及怎么利用指针来访问数组
诶那我们是不是发现了数组a怎么和p这么类似啊,都能通过[n]来访问元素
确实,实际上数组名在大多数情况下可以看作指向数组首元素的指针,我们用a+1也能访问元素,a表示的是a数组的开头地址,所以要用p遍历a的时候,我们只要p=a就能完成初始化,因为p存储的不正是地址吗?
所以我们在代码里一般可以这么写:
1 | int a[10]={0,1,2,3,4,5,6,7,8,9}; |
注意:数组名a在大多数情况下可以转换为指针,但数组名不是指针,它是整个数组的标识符。在sizeof(a)时,它返回整个数组的大小,而不是指针大小
值得注意的是,因为我们这里只是规定了向后移动几步,没有规定范围,所以理论上可能超出数组规定的范围访问,但是这会导致段错误,因为程序理论上没有权限访问这个位置的值
错误示例:
1 | // 指针越界访问 |
理解了指针和数组的关系,也就理解了指针的本质,我们接下来就可以理解指针运算了。
指针运算
要是直接加减常数,表示的是移动多少个位置,要是是两个指针相减,表示的是中间差了几个单位,当然,我们也可以进行大小比较,表示指针在一个数组中的前后位置关系
1 | // 指针与整数的加减 |
二级指针
1 | int **p; |
表示指向存储指针位置的指针,分析时一层一层去掉就行了
要是等级更高也是类似的理解,比如实际上可以创建一个三级的指针
1 | int ***p; |
指针与函数
首先函数中可以直接传入指针变量,这里可以理解为直接把对应的开头地址传过来了
比如我们写一个swap函数:
1 | void swap(int *p1,int *p2){ |
这里操作完后,主函数里a,b的值也会互换,因为我们传入的是地址,在函数内我们又对这个地址对应的位置进行了操作,那么就能修改这个位置的值了,然后注意在函数调用时要传入的是地址,因为要和函数所需参数(指针)相对应。
注意,以下几种写法都是错误的:
1 | void swap_w1(int *p1,int *p2){ |
最后这个为什么是错的?因为不是赋值语句,表示的是右值,是对应的值*p1表示的是a的值,即1,类型是int,而函数需要int*,类型不匹配
那么中间的函数为什么是错误的?
我们来详细分析一下:
1 | 函数开始时 函数结束时 回到主函数(删除了作为形参的p1,p2) |
所以没有任何变化,这个函数既没有修改p1,p2对应的位置的值,也法修改a和b对应的地址位置,所以没有产生任何变化。
传入数组也是类似的:
1 | void f(int a[],int n){ |
然后因为数组传入都是传入首位置的,所以在函数内对数组操作是会改变原数组的
我知道大家一直有一个问题,为什么要这么大费周折的学习指针呢?
其实在这一块就给出了一个答案:可以让函数有多个返回值
注意一下函数的返回值,不能返回指针形参的地址,因为正如前面所说,函数结束后,这个位置就被删除了
1 | int* bad_func() { |
指针与二维数组
遍历方法类似,因为二维数组一般是连续的,所以可以p+i一直加下去来遍历,不过我们这里要讲解一下一个新的概念——行指针
行指针写法:
1 | int (*p)[4]; |
根据之前的学习我们知道,这其实也是规定了一种遍历规则,*p指向的是一个长度为4的一维数组的开头位置,要是+1的话表示跳过4个长度,然后加上括号表示是p指向数组,如果写成int *p[4],则表示一个长度为4的指针数组。
那么我们就可以通过p来访问这个二维数组了,
首先p表示的是对应哪一行,*p是这一个数组对应的开头,就变成一维数组的访问了
例如:
1 | int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}}; |
和前面一位数组的理解类似二维数组名a也可以看作指向第一行(整个一维数组)的指针,指向的是第一行这个一维数组,这里的调用相当于进行了两次一维数组的查询操作,先确定行再确定列。
在行上,我们有:
1 | int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}}; |
再加上列,我们就有四种组合了,本质上是等价的
1 | cout<<p[1][1]<<endl; |
行指针和函数
因为在行指针里,每次+1跳过几个位置的规则是十分重要的,所以在把行指针传入函数时,两者的第二给参数,即规则,得完全一致,不然程序会报错
1 | void f(int arr[][4]){ |
或者:
1 | void f(int (*arr)[4]){ |
只有规则相同才能调用
动态分配数组空间
一维数组
1 | int *p=new int[n]; |
或者
1 | int *p=(int*)malloc(sizeof(int)*n); |
相当于每个位置都给了一个int的空间
注意:malloc返回void*,需要强制类型转换。
例如,下面这段代码是错误的:
1 | int *p = malloc(sizeof(int) * 100); |
二维数组
在C语言中,动态分配二维数组(不连续)
1 | int **matrix = new int *[ROW]; |
或者
1 | int **matrix = (int**) malloc ( ROW * sizeof( int* )); |
这里的二级指针存储的是(每一行开头的元素的地址)的地址
为什么要这么麻烦呢?但是逐个分配的好处是方便删除,也可以实现各行长度不同(锯齿数组)
在这种情况下,我们就不能+1来查询了(因为行之间不连续),但可以通过行指针数组来访问每一行。
如果希望分配连续的二维数组,可以使用以下方法:
1 | int (*p)[COL] = (int(*)[COL])malloc(ROW * COL * sizeof(int)); |
或者
1 | // 方法一:一次分配所有内存(连续) |
这样分配的内存是连续的,可以用p[i][j]访问,且p是一个行指针,p+1会跳过一行(COL个int)
指针与字符串
C++语言中字符串常以字符数组表示,也可以用指针
1 | char str[] = "Hello"; |
字符串常量可以用指针指向:
1 | char *p = "Hello"; // p指向字符串常量,注意字符串常量不可修改 |
const与指针
这里介绍基本的const用法,以及基本的const指针
const表示的是常量,表示后面的是不可修改的
首先const int *p1表示p1的类型是const int,所以是指向常量的指针
第二个int *const p2表示p2的类型是const,p2 的值是不变的,指向同一个位置,但是他所指向的地址的值却是可以改变的
第三个表示指向不变,且指向的是常量
1 | // 四种const指针 |
指针数组与行指针区分
指针数组,每一个位置都是一个指针,指向一个位置
在指针数组,+1表示移动到指针数组的下一个位置,比如names+1表示name[1]的首地址,再加上*表示一维数组,有相似之处,但是和行指针在访问规则上还是有区别的
1 | // 指针数组:数组的每个元素都是指针 |
指针相关题目
下面是程C课上提到的题目,我们来检测一下你有没有理解
例1:
1 | int a=10,b=20,c=30; |
这一题我们需要学习一下C++中的运算符的优先级,从高到低可以理解为:
后缀运算>前缀运算=单目运算>双目运算
双目运算的规则还是比较清晰的,这里就不解释了
详细解释可以看这个网站
C++ 运算符优先级
所以函数中的*y++表示的就是*(y++),先将地址使用,再后移一格,相当于没有对原先位置产生变化,所以得到答案:
1 | 11,20,30 |
例2:
1 | void fun(int* x,int* y,int* z){ |
这道题的意图还是比较明确的,即利用二维数组空间的连续性,计算斐波那契数列
注意主函数中的用法,取的是a[0]的地址,因为p1不是行指针,只能指向一个一位数组的开头,所以写成p1=a[0]
答案:
1 | 1 1 2 |
例3:
1 | int a[5] = {1, 2, 3, 4, 5}; |
答案:输出5
此题需要注意的是’(&a + 1)’这里是指向a这个一维数组的地址,那么就是行指针了,所以+1要按照行指针的规则来执行,接下来再强制转化为正常指针,所以-1按照正常指针的规则
如果你对上面的解释还不是很清楚,也欢迎来私信我和我交流