进阶指针
导言
经过前面的学习,目前我们才探索了指针的冰山一角,指针作为C语言中一个非常非常非常重要的知识点,它远远不止我们前面了解的这些内容,从今天的篇章开始,我们将陆续揭开指针的面纱,希望各位朋友能够通过这段时间的学习,更近一步的了解指针。
十四、字符指针变量
char*
类型的变量被我们称为字符指针变量,我们在使用时有以下几种使用方式:
//char*指针变量
int main()
{
char a = 'a';
char ch[5] = "abcd";
char* pa = &a;//指针指向字符变量
char* pc = ch;//指针指向字符数组首元素地址
char* p = "abcd";//指针指向字符串地址
return 0;
}
对于指向字符变量和指向字符数组这两种用法大家应该都是比较熟悉的,现在我们要探讨的就是第三种用法,指向字符串的地址,这个字符串的地址是什么?下面我们来看一下:
在监视窗口中我们可以看到,此时字符串中存放了5个元素,字符指针p指向的是字符串的首字符a,大家此时有没有一种熟悉的感觉?字符串好像和字符数组有点相似,那字符串与字符数组到底是不是一样的呢?下面我们就来探讨一下;
14.1 字符串和字符数组
为了更直观的看到字符串与字符数组的相关信息,我们还是借助监视窗口来观察:
此时我们可以看到,从内容的存储上来对比的话,字符串与字符数组是没有区别的,元素都有对应的下标,并且下标都是从0开始依次递增。
唯一有区别的就是数组名表示的是数组的起始地址,也就是数组首元素的地址,而字符串的值就是字符串,但是当我们用字符指针变量来指向字符串时,指针指向的是字符串的首字符地址。
既然它们的区别不大,那我是不是可以通过字符指针对字符串进行像字符数组一样的操作呢?
从结果中我们可以看到此时我们正常对字符数组的元素进行了修改,并成功进行了输出,但是通过字符指针变量对字符串进行修改后并未进行输出,为什么会这样呢?我们还是通过监视窗口来进一步观察:
此时我们可以看到程序在运行到通过指针进行对字符串元素修改时,程序进行了报错,报错内容为写入访问权限冲突,也就是此时是不可以进行写入的。为什么会这样呢?
这里我们就需要引入一个新的概念——常量字符串。在介绍常量字符串之前,我们先要弄清楚什么是常量。
14.2 常量
常量顾名思义就是不变的量,在C语言中常量有四种分类:
- 字面常量
- const修饰的常变量
- 枚举常量
- #define定义的标识符常量
const修饰的常变量
在前面我们已经介绍了const修饰的局部变量,当局部变量被const修饰后,我们经过测试发现并不能对局部变量直接进行修改,这就表示此时的局部变量拥有了常量属性,即不能被更改的属性;
但是我们可以借助指针来对局部变量的值进行修改,这就表示此时的局部变量还是一个变量可被修改。
所以被const修饰的局部变量我们就将其称为常变量。
字面常量
字面常量是我们现在要重点介绍的内容。所谓的字面常量,我们可以简单的理解为我可看到的1/2/3/4……这些数字、a/b/c/d……这些字符、以及由这些字符组成的字符串等这些已经被定义好的值。
对于这些常量来说它们都有一个共同点——值是确定的不可被修改的。如下所示:
从程序报错中我们可以看到,此时的报错内容都是表达式必须是可修改的左值,这就是常量的属性——不可被修改。
14.3 常量字符串
对于一个明确的字符串来说,它本身是一个常量,当我们将字符指针指向常量字符串时,此时的字符串可以通过指针进行访问:
但是我们不能通过指针对字符串中的元素进行修改。
常量字符串与字符数组类似,字符串中的元素也是有对应的元素下标,并且下标是从0开始逐渐递增。当我们通过字符指针指向常量字符串时,指针指向的实质上是常量字符串的首元素地址。
为了能够避免出现使用字符指针来修改常量字符串的内容,所以我们在定义字符指针时,最好是通过const对指针变量进行修饰:
//const修饰字符指针变量
int main()
{
const char* p = "abcd";
return 0;
}
当我们将常量字符串放在数组中时,实质上是在函数栈帧上开辟了一块新的空间,在空间内存放了对应的字符,我们通过指针或者是数组名[下标]对数组元素进行更改时,实质上是在对新开辟的这块空间存储的内容进行更改,并不是对这些常量字符进行更改:
在计算机内存中,常量都是有自己的地址的。
我们将常量值存放在数组中时,计算机就会通过常量的地址找到对应常量的值,并将该值存放在数组中对应的元素地址下,所以此时我们是可以修改数组元素存放的值;
但是对于常量字符串来说,我们将其用字符指针指向时,是指向的常量字符串自己本身的地址,如果我们通过字符指针对其进行修改,就好比我们要强行的实现1=2这样的等式,这显然是不合理的;
下面我们再来看一个代码:
//常量字符串
int main()
{
const char* p1 = "abcd";
const char* p2 = "aefg";
const char* p3 = "abcd";
printf("p1 = %p\n", p1);
printf("p2 = %p\n", p2);
printf("p3 = %p\n", p3);
return 0;
}
大家觉得在这个代码中,对于这三个字符指针存储的地址是相同的还是不相同的?
从测试结果中我们可以看到:
- 指针p1和p3因为都是指向的常量字符串
"abcd"
,所以它们此时存储的地址是相同的; - 指针p2指向的是另一个常量字符串
"aefg"
,这个字符串的起始地址与"abcd"
的起始地址肯定是不一样的。 - 在这个例子中,这三个指针指向的常量字符串虽然它们首元素存储的值都是字符a,但是此时它们就相当于是两个字符数组,只是首元素存储的值一样,但是数组在内存中申请的空间却不是同一块;
- 指针p1和p3指向的是同一个字符数组,所以它们指向的地址是同一个地址;
- 指针p2指向的是不同的字符数组,所以它指向的地址是不同的地址;
下面我们再来看一个代码:
//常量字符串
int main()
{
char ch1[5] = "abcd";
char ch2[5] = "abcd";
char* p1 = ch1;
char* p2 = ch2;
printf("p1 = %p\n", p1);
printf("p2 = %p\n", p2);
return 0;
}
在这个例子中,大家觉得此时这两个指针所指向的地址是否相同呢?
从测试结果中我们可以看到,此时两个指针存储的地址并不相同。这是因为:
- 此时这两个指针指向的是两个字符数组,虽然两个字符数组中存储的元素是相同的,但是数组在内存上申请的空间地址并不是同一块;
14.4 总结
相信大家此时对字符指针以及常量字符串与字符数组的区别已经理解了,下面我们来对这一块内容做个总结:
- 字符指针在指向常量字符串时需要使用const进行修饰;
- 常量字符串相当于一个不可被修改的字符数组,字符串的元素下标是从0开始依次递增;
- 我们可以通过下标引用操作符对常量字符串中的元素进行访问,但不可对其进行修改;
- 同一个常量字符串的地址是相同的;
- 不同的常量字符串即使首元素相同,首元素的地址也不相同;
十五、数组指针
指针类型我们前面介绍的有字符型指针、整型指针、浮点型指针等等,我们现在要介绍的是另一种类型的指针——数组型指针。
15.1 数组类型
这时有朋友就会说了,啊!怎么还有个数组型?数组型是什么数据类型?别着急,我们先回顾一下数组:
//数组的创建格式
type arr_name[size];
//type——数组元素数据类型
//type[size]——数组数据类型
//arr_name——数组名
//size——数组大小
之前我们一直关注的是数组元素的数据类型、数组名以及数组大小,我们一直忽略了一个信息——数组数据类型type[size]
。我们根据这里的创建格式可以看到,所谓的数组数据类型,实质上就是数组元素数据类型+数组大小。
现在我们知道了什么是数组类型,接下来我要给大家介绍一种新的观点来看待数组——数组类型的变量;
15.2 数组类型的变量
对于数组来说,其实我们可以用变量的角度来看待数组,如下所示:
//数组类型的变量
type variate_name[size];
//type[size]——数据类型
//variate_name——变量名
//size——数据类型向内存申请的空间个数
通过数组类型创建的变量,我们将其称之为数组变量,简称数组。对于数组变量来说,它具有以下的性质:
- 变量在内存中会申请
size
个连续的空间;- 变量中能存放的数据个数与空间个数相同,并且这些数据会从低地址到高地址依次连续存放;
- 变量名存放的是这块空间的起始地址,也是第一块空间的地址,即首元素地址;
- 这个连续的空间都有对应的编号,这些编号是从0开始依次递增,我们将其称之为下标;
- 通过
变量名+下标
我们可以找到对应空间的地址;- 当我们向变量中存放数据时,这些数据会从0下标开始依次存放进对应的空间中;
- 对空间地址进行解引用我们可以找到空间中存放的数据,这些数据我们称之为数组元素;
- 我们还可以通过下标引用操作符和对应的下标来访问下标对应的数组元素;
- 通过下标访问数组元素的形式有:
变量名[下标]
[变量名]下标
[下标]变量名
下标[变量名]
*(变量名+下标)
*(下标+变量名)
- 当我们对数组元素进行初始化时,未被初始化的数组元素会自动初始化为0;
15.3 数组变量的创建和初始化
我们以数组变量的观点来对数组创建并初始化的话,我们就可以写成以下的形式:
//数组变量的创建与初始化
int main()
{
int arr[3] = { 1,2,3 };
//int[3]——数组类型
//3——数组类型在内存空间中申请了3个连续的空间
//int——申请的每个空间所占内存大小为int类型所占空间大小
//arr——数组变量名,简称数组名
//{1,2,3}——申请的空间中存放的数据
return 0;
}
现在大家通过这个数组变量的观点,对数组应该有了新的认识了。
下面大家可以根据这个观点回答以下两个问题吗:
- 为什么数组名与指针等价?
- 为什么解引用操作符与下标引用操作符等价?
- 为什么数组名与指针等价?
因为数组名存放的是地址,而存放地址的变量,我们将其称之为指针。
- 为什么解引用操作符与下标引用操作符等价?
因为下标引用操作符的实质是通过下标找到对应的空间地址,再对其进行解引用操作;
在了解了数组类型以及数组变量后,我们再来看一下数组指针。
15.4 数组指针变量的创建与初始化
指针我们前面说过他的本质就是一个变量。前面我们介绍的指针的创建格式如下:
//指针的创建格式
point_type* variate_name;
//point_type*——数据类型为指针类型;
//variate_name——变量名
现在我们可以用一个新的角度来看待指针的创建——数据类型+指针变量名
,如下所示:
//指针的创建格式
type *point_variate_name;
//type——数据类型;
//*point_variate_name——指针变量名
可以看到不管是第一种角度还是第二种角度,对于决定是指针类型还是指针变量的关键在于*
,所以对于指针来说*
才是指针定义的关键。
当我们以第二种角度来看待指针变量的创建的话,我们此时如果将数据类型变成数组类型时,我们就可以得到数据类型为数组类型的指针变量,即数组指针变量,简称数组指针:
//数组指针的创建格式
type(*point_variate_name)[size];
//type[size]——数组类型
//*point_variate_name——指针变量名
注:对于
*
和[]
这两个操作符来说,[]
的优先级高于*
,所以我们需要使用()
使*
与变量名先结合。
根据指针和指针变量的定义,指针就是地址,指针变量就是存储指针的变量。我们可以很容易的得到结论——数组指针存放的是指向对象的地址;
//数组指针的创建与初始化
int main()
{
int a = 10;
int(*p)[1] = &a;
return 0;
}
这里我们通过数组指针p存储了变量a的地址,因为此时数组指向的对象存储的数据只有一个,所以我们只需要像内存申请一块空间就可以进行存放了。
在这个代码中此时数组指针p的元素下标为0,我们可以通过下标引用操作符找到对应空间中存放的信息——变量a的地址:
在找到变量a的地址后,我们可以通过对其解引用来找到a中存放的数据:
因为解引用操作符和下标引用操作符是等价的,所以对于数组指针变量我们可以写成以下形式:
- 当我们通过两次解引用操作来访问变量a中存放的数据时,此时的数组指针就和二级指针类似;
- 当我们通过两次下标引用操作符来访问变量a中存放的数据时,此时的数组指针就和二维数组类似;
15.5 数组指针与指针数组
前面我们介绍了指针类型的数组——指针数组,它与数组指针的创建格式如下所示:
//指针数组的创建格式
point_type* arr_name[size];
//point_type*[size]——指针数组类型
//arr_name——数组名
//size——数组大小
//数组指针的创建格式
type(*point_variate_name)[size];
//type[size]——数组类型
//*point_variate_name——指针变量名
//size——数组类型向内存申请的连续空间的数量
通过对比这两种创建格式,我们不难发现它们之间的区别就是指针标志*
的结合对象不同:
*
与数据类型结合就是指针数组;*
与变量名结合就是数组指针;
现在可能有部分朋友感觉更迷糊了,难道它们就只是指针标志移动了一下位置,其实它们是同一个东西? 这个答案是否定的,指针数组与数组指针它们是两个东西,它们有着本质的区别。下面听我慢慢道来。
15.5.1 数组与指针区别
在前面介绍时我们有说过,在某些特定的情况下数组名代表的是数组在内存空间中的起始位置,在大部分情况下数组名就是指针;
所以我们对数组名和指针的处理也是采取的模糊化的方式,将数组名就是看成指针,指针看做的就是数组名,但是我们要清楚的是数组并不等于指针。
数组与指针的区别主要由以下几点:
- 数组和指针在内存中的空间不相同
- 数组在内存中申请空间时,申请的是一片连续的空间;
- 指针指向的空间是内存空间中的一块空间;
- 数组和指针的存储数据个数不同
- 数组在内存中会根据自己的大小申请对应的空间个数,每个空间中都能存放一个数据,所以数组能存放与数组大小数量相同的数据;
- 指针在内存空间中只申请了一块空间,所以指针也只能存储一个数据;
- 数组和指针的工作原理不同
- 数组是先通过数组名找到数组的空间起始位置,再通过对应编号找到要访问的空间,最后对空间地址进行解引用找到空间中存储的数据;
- 指针则是通过存储的数组首元素地址找到数组的首元素,再通过首元素的地址加上各个空间的编号找到各个空间的地址,最后通过地址进行解引用找到空间中存储的数据;
15.5.2 数组指针与指针数组的区别
在理解了数组与指针的区别后,我们再来看一下指针数组与数组指针的区别:
- 指针数组与数组指针的内存空间不同
从反汇编界面我们可以看到:
- 指针数组在申请空间时,会根据数组大小来申请对应数量的空间;
- 数组指针则只在内存空间中申请了一块空间用来存放地址;
- 指针数组与数组指针的存储数据个数不同
- 对于指针数组来说,它能存储的数据个数与数组的空间大小是一致的,就如上图所示,对于指针数组arr来说,它能存储3个数据;
- 对数组指针来说,因为它在内存中只申请了一块空间,所以,它能存储的数据也只有一个;
此时我们可以看到,数组指针在存放3个元素时系统会报错——初始值设定项值太多。
- 指针数组与数组指针的工作原理不同
- 指针数组在读取数据时,是先通过数组名找到数组空间的起始位置,然后通过空间编号找到对应的空间,再对空间中存储的地址找到该地址对应的空间,最后通过对该地址进行解引用找到空间中存放的数据;
- 数组指针在读取数据时,是先通过存储指向空间的起始地址找到对应的空间的起始位置,再通过空间编号找到对应的空间,最后通过对该空间的地址进行解引用找到空间中存储的数据;
现在我们知道了指针数组与数组指针是两个东西了,对指针和数组的详细剖析,我们知道了指针和数组还是有区别的。前面我们通过两次解引用找到了数组指针指向的对象存储的数据,这种工作方式与二级指针是一样的,那是不是说明其实数组指针与二级指针是同一个内容的不同形式呢?接下来我们就来探讨一下数组指针与二级指针的异同点;
15.5.3 数组指针与二级指针
对于同为指针的数组指针和二级指针来说,它们有很多相同的地方:
- 内存中申请的空间相同
对于指针来说,指针指向的是对象的地址,指针在内存中申请空间只会申请一块空间来存储指向对象的地址。
不管是一级指针、二级指针还是数组指针也好,只要是指针它们都只会在内存空间中申请一块空间来存放数据;
- 工作原理相同
- 二级指针是先通过存储的一级指针的地址找到一级指针,再对一级指针进行解引用找到一级指针中存储的变量的空间地址,然后通过变量的空间地址找到变量,最后再对变量的空间地址进行解引用找到变量中存放的数据;
- 数组指针是先通过存储的对象在内存空间中的起始地址找到对应空间的起始位置,在通过空间编号找到对应的空间地址,最后通过对空间地址进行解引用找到地址中存放的数据;
二级指针是通过两次解引用来找到对应的数据,而数组指针通过空间编号找到对应空间的这个过程就是一次解引用的过程,所以两种指针在寻找数据的工作原理上是相同的都是通过两次解引用来找到对象中存储的数据;
但是二者又有很多不同的地方:
- 指向的空间不同
- 二级指针指向的是一块空间;
- 数组指针指向的是一块连续的空间;
- 指向的对象不同
- 二级指针存放的是一级指针的地址,指向的是一个一级指针;
- 数组指针存放的是一块连续的空间的起始地址,能够在内存空间中申请一块连续空间的对象,目前我们所学的就是数组,所以数组指针指向的其实是一个数组;
从监视窗口我们可以看到,二级指针存储的对象的数据类型为int*
即指针类型,所以二级指针指向的是一个指针; 数组指针存储的对象的数据类型为int
型,并且指向的对象有三个元素,所以数组指针指向的是整个数组,并且这个数组是一个一维数组;
综合上面的异同点,我们可以得出结论:
- 数组指针和二级指针没有关系,它们是两种类型的指针;
- 数组指针指向的是一个一维数组;
在前面的探讨中我们发现数组指针在访问数据时的方式和二维数组也很相似,那数组指针和二维数组又是什么关系呢?
15.5.4 数组指针和二维数组
在探讨数组指针和二维数组的关系前,我们先来回顾一下对应的知识点:
- 二维数组是同一类型的一维数组的集合,二维数组的数组名存放的是二维数组在内存空间中的起始位置,同时也是二维数组第一个元素的地址,即一个一维数组的地址;
- 数组指针是一个数组类型的指针,数组指针指向的是一个连续空间的起始地址,即一个一维数组的地址;
既然二维数组名表示的是一个一维数组的地址,而数组指针指向的也是一个一维数组的地址,那是不是说明二维数组与数组指针等价呢?下面跟着我的思路咱们一步一步的来探索;
- 首先我们通过一维数组的角度来看待二维数组:
//用一维数组的角度看二维数组
type arr_name[size][num];
//type[num]——数据类型
//type[size][num]——数组类型
//arr_name——数组名
//num——数据类型在内存空间中申请的空间个数
//size——数组大小
此时如果我们通过数组下标来访问二维数组的元素时,我们是以arr_name[下标]
的形式进行访问,如果写成指针的形式则是*(arr_name+下标)
。
- 然后当我们访问首元素时,此时的下标为0,也就是说我们可以写成
*arr_name
这种形式; - 最后当我们继续通过数组下标对首元素的数组元素进行访问时,此时的数组名就是指向首元素的指针,也就是说,我们可以通过指针来代替数组名,即
*point_name[下标]
;
下面我们再来数组指针:
//数组指针
type(*point_name)[size];
//type[size]——数据类型
//*point_name——指针名
//size——数据类型在内存空间中申请的空间个数
- 当我们通过数组指针访问指向的数组时,我们需要通过数组的下标找到数组元素的地址,即
point_name[下标]
; - 当我们找到数组元素的地址后,我们可以对其进行解引用来访问下标对应的数组元素,即
*point_name[下标]
;
经过对比,可以看到,数组指针和二维数组不能说是一模一样吧,只能说是没有区别。那是不是这样呢?下面我们通过代码来验证一下:
从监视窗口中我们可以看到,指针存储p存储的内容为数组arr首元素的地址,指针p在加1后指向的地址是数组第二个元素的地址。
也就是说数组指针p可以通过+数组元素下标来访问二维数组的各个元素,此时的指针p是与二维数组的数组名等价的。为了进一步验证这个结论,我们来进行以下的测试;
- 通过数组下标访问数组的各个元素
此时我们通过两次解引用不管是使用数组名还是指针名都成功的访问到了数组各每个元素;
- 通过解引用操作访问二维数组的各个元素
通过解引用操作,我们也成功的访问到了二维数组的各个元素;
- 通过数组指针接收二维数组
我们在对二维数组进行传参时,数组指针很好的接收二维数组并成功通过下标对数组元素进行了访问;
4.通过二维数组接收数组指针
我们在对数组指针进行传参时,二维数组很好的接收了数组指针并成功通过数组下标对指针指向的数组的数组元素进行了访问;
经过咱们对数组指针深入且细致的探讨,我相信大家应该已经完全理解了数组指针,下面我们就来对数组指针做一个总结:
15.5.5 总结
- 数组指针是一个数组类型的指针,指向的是一个一维数组;
- 当二维数组的数组名指向首元素时,数组指针与二维数组的数组名等价,数组名可以与数组指针相互转换;
- 数组不等于指针;
- 数组指针与指针数组是两个不同的概念,数组指针是一个一级指针,而指针数组相当于一个二级指针;
结语
今天咱们花了大量的篇幅对字符指针以及数组指针进行了全面的剖析,希望今天的内容能帮助大家更好的理解这两种类型的指针以及相关的知识点。最后感谢各位的翻阅,咱们下一篇再见。