谨记
听......黎明在远方呼唤清晨,别在等,人的一生必将经历许多磨难,所以在人生前行的道路上,我们不可对每件轻微的伤害而敏感,在生活的磨难面前,精神上的坚强和无动于衷是我们抵抗罪恶和人生意外的最好武器,前行的终点还很远,我们要不停脚步的往前走,遇到任何困难,不要退缩,要做到有舍才有得,有得必有失,当你迷失的时候,不要忘记了,你的人生理想就是你前行的动力和方向,在留言处写下你的理想。
前言
前一篇我们已经学习了关于指针的一些基础,那么在这篇文章,我们将更深层次的去理解和运用指针。
指针和数组(一维)
前面我们已经学习过数组了,相信大家对数组已经有一个深刻的认识了,那么我们来看看指针和数组之间存在着某种联系?
指针和数组名
我们先来看一段代码:
示例一:
int main(int argc, const char * argv[]) {
int a[] = {1,3,4,7,9,2,4,6};
for (int i = 0; i < sizeof(a) / sizeof(int); i++) {
printf("%d\n",a[i]);
}
return 0;
}
输出结果:
1
3
4
7
9
2
4
6
Program ended with exit code: 0
示例二:
int main(int argc, const char * argv[]) {
int a[] = {1,3,4,7,9,2,4,6};
for (int i = 0; i < sizeof(a) / sizeof(int); i++) {
printf("%d\n",*(a + i));
}
return 0;
}
输出结果:
1
3
4
7
9
2
4
6
Program ended with exit code: 0
>>>>>>结论:从示例一和示例二我们可以得出,在示例一我们通过遍历来显示数组a中的每一个元素,示例二是通过指针*(a + i)来得到和遍历数组中的每一个元素,但是,我们可以看输出结果,发现这两种方式是一样结果,以前在讲数组的时候,提到过数组的名称其实也是一个地址,对数组名取地址和数组的首个元素去地址,其实他们是一样的,也就是说,数组名其实就是一个数组的起始地址,如:int a[10]; a 和 &a[0],这里我需要提到的是数组指针的概念。
数组指针
数组指针是指向数组起始地址的指针,其本质为指针。一维数组的数组名为一维数组的指针。在这之前,我们对指针的运算已经讲解了,那么,从示例二我们可以看到,那里就用了一个指针的运算,那么可以得出指针的加法运算和数组的下标运算有如下的对应关系:
数组名 + i ---> 数组名[i]
箭头的左边是一个指针常量,它指向箭头右边的变量。事实上,在C语言中指针的效率往往高于数组下标。因此,编译器对程序中数组下标的操作全部转换为对指针的偏移量的操作。
int main(int argc, const char * argv[]) {
int a[] = {1,3,4,7,9,2,4,6};
//定义一个指针
int *p = a;
for (int i = 0; i < sizeof(a) / sizeof(int); i++) {
printf("%p %p %p %p\n", a, (a + i), p, (p+i));
}
return 0;
}
输出结果:
0x7fff5fbff810 0x7fff5fbff810 0x7fff5fbff810 0x7fff5fbff810
0x7fff5fbff810 0x7fff5fbff814 0x7fff5fbff810 0x7fff5fbff814
0x7fff5fbff810 0x7fff5fbff818 0x7fff5fbff810 0x7fff5fbff818
0x7fff5fbff810 0x7fff5fbff81c 0x7fff5fbff810 0x7fff5fbff81c
0x7fff5fbff810 0x7fff5fbff820 0x7fff5fbff810 0x7fff5fbff820
0x7fff5fbff810 0x7fff5fbff824 0x7fff5fbff810 0x7fff5fbff824
0x7fff5fbff810 0x7fff5fbff828 0x7fff5fbff810 0x7fff5fbff828
0x7fff5fbff810 0x7fff5fbff82c 0x7fff5fbff810 0x7fff5fbff82c
Program ended with exit code: 0
结论:利用指针,或者利用直接用数组名他们其实所表达的作用是一样的。
即有这样的说法:一维数组a的第i个元素,有下标法和指针法。假设指针变量p指向数组的首元素。 则有四种数组元素的表达方式:a[i] ⇔ p[i] ⇔ *(p+i) ⇔*(a+i)
注意点
需要特别说明的一点是,指针变量和数组在访问数组中元素时,一定条件下其使用方法具有相同的形式,因为指针变量和数组名都是地址量。但指针变量和数组的指针(或叫数组名)在本质上不同,数组在内存中的位置在程序的运行过程中是无法动态改变的。因此,数组名是地址常量,指针是地址变量。数组名可以在运算中作为指针参与,但不允许被赋值。
int main(int argc, const char * argv[]) {
int a[6], b[6];
int * p = b;
a = p;
for (int i = 0; i < 6; i++) {
printf("%d\n", b[i]);
}
return 0;
}
结论:这个程序会报错,原因是,a = p;因为a是数组名,不能对其赋值,他是一个地址常量,所以,一般我们可以称数组名为常量指针。
下图为数组和指针常见的等价操作:
指针和多维数组
指针遍历二维数组
多维数组就是具有两个或两个以上下标的数组。实际上,在C语言中并没有多维数组的概念,多维数组就是低维数组的组合。依然可以理解成若干个数据类型相同的变量的集合。这里,只介绍二维数组。
在C语言中,二维数组的元素连续存储,按行优先存,存储了第一行的元素,存第二行的,依次类推。基于这个特点,可以用一级指针来访问二维数组。
int main(int argc, const char * argv[]) {
int a[][3] = {9, 1, 4, 7, 3, 6}, i, j;
int *p, r, c, n;
p = &a[0][0];//为指针变量赋值
r = sizeof(a) / sizeof(a[0]);//得到数组的行数
c = sizeof(a[0]) / sizeof(int);//得到数组的列数
n = sizeof(a) / sizeof(int);//得到数组元素的个数
for (i = 0; i < r; i++)
for (j = 0; j < c; j++)
printf("%d %p\n", a[i][j], &a[i][j]);
printf("\n");
for (i = 0; i < n; i++)
printf("%d %p\n", *(p+i), p+i);
return 0;
}
输出结果:
9 0x7fff5fbff820
1 0x7fff5fbff824
4 0x7fff5fbff828
7 0x7fff5fbff82c
3 0x7fff5fbff830
6 0x7fff5fbff834
9 0x7fff5fbff820
1 0x7fff5fbff824
4 0x7fff5fbff828
7 0x7fff5fbff82c
3 0x7fff5fbff830
6 0x7fff5fbff834
Program ended with exit code: 0
从程序的输出结果,可以看到二维数组中,各元素的地址,如图7-6所示。由于一级指针p,p+i移动i个数,相当于移动了i列,因此也称指针p为列指针。该程序就是就通过列指针,对二维数组进行了遍历。
二维数组特点
从内存管理的角度,二维数组的元素和一维数组的元素的存储是类似的,都是连续存储,因此,可以用一级指针循环遍历了二维数组中的所有元素。
换一个角度来理解二维数组,把二维数组看作由多个一维数组组成。比如数组int a[2][3],可以理解成含有两个特殊元素:a[0],a[1]。元素a[0]是一个一维数组名,含有三个元素a[0][0]、a[0][1]、a[0][2],即二维数组第一行。元素a[1]也是一维数组名,含有三个元素a[1][0]、a[1][1]、a[1][2],即二维数组第二行。
二维数组名代表了数组的起始地址,在数组一章中,我们已经分析过,数组名加1,是移动一行元素。
int main(int argc, const char * argv[]) {
int a[2][3] = {{8, 2, 6}, {1, 4, 7}};
printf("a :%p a+1 :%p a+2 :%p \n\n", a, a+1, a+2);
printf("a[0]:%p &a[0][0]=%p\n", a[0], &a[0][0]);
printf("a[1]:%p &a[1][0]=%p\n", a[1], &a[1][0]);
printf("a[2]:%p &a[2][0]=%p\n", a[2], &a[2][0]);
return 0;
}
输出结果:
a :0xbfc8ec98 a+1 :0xbfc8eca8 a+2 :0xbfc8ecb8
a[0]:0xbfc8ec98 &a[0][0]=0xbfc8ec98
a[1]:0xbfc8eca8 &a[1][0]=0xbfc8eca8
a[2]:0xbfc8ecb8 &a[2][0]=0xbfc8ecb8
Program ended with exit code: 0
可以看出,二维数组名是一个很特殊的地址,参与运算时以行为单位移动,因此被称为行地址。在该程序中,a代表第一行的首地址,a[0](&a[0][0])代表第一行第一列元素的地址;a+1代表第二行的首地址,a[1](&a[1][0])代表第二行第一列元素的地址,依次类推。
那么,接下来我们讨论,如何表达二维数组中的任何一个元素。
问题1:数组int a[2][3],如何表达第一行第二列元素的地址?
第一、用下标表示法&a[1][1]。
第二、首先表示第一行的地址。数组名就是行地址,很容易写出a+1。
然后表示第一行第一个元素的地址。很容易想到,a[1]就是第一个一维数组名,就代表了第一行第一列元素的地址,即&a[1][0],前文还提到了a[1]等价于*(a+1)。这里的*修饰行地址,把行地址转换成了列地址。总结起来,第一行第一列元素的地址可表达为:&a[1][0]、a[1]和*(a+1)。
最后,表示第一行第二列的地址。列地址加1,就移动一列。
最终的表达式有:&a[1][0]+1、a[1]+1和*(a+1)+1。
问题2:数组int a[2][3],如何表达第一行第二列元素?
找到了第一行第二列元素的地址,在前面加一个*,就能引用到元素。可能的表达式如下:
a[1][1] ⇔*(&a[1][0]+1) ⇔*(a[1]+1) ⇔*(*(a+1)+1)
注意点
确定指针偏移量“1”所代表的单位是通过“1”之前的元素单位来定的。在二维数组中,当偏移量前的元素单位为整个数组时,偏移值单位为行;当偏移量前的元素单位为行时,偏移值单位为行中的元素。
多级指针
以前学过数组,比如一维数组、多维数组,以此类推,那么多维数组就是把一个指向指针变量的指针变量,称为多级指针变量。对于指向处理数据的指针变量称为一级指针变量,简称一级指针。而把指向一级指针变量的指针变量称为二级指针变量,简称二级指针。
这里就简单的举一个例子来说明,因为一般很少用多级指针。
int main(int argc, const char * argv[]) {
int a = 100;
int *p;
p = &a;
int **q;
q = &p;
**q = 200;
printf("a = %d *p = %d **q = %d\n",a, *p,**q);
return 0;
}
输出结果:
a = 200 *p = 200 **q = 200
Program ended with exit code: 0
在上面这个示例中,当然,读者还可以分别打印他们的地址,你也许会发现什么哦,q相当于一级指针,得到的是q的目标,即变量p(&a)。*q相当于int类型,可得到变量a的值。
多级指针的运算,这里就不做过多的介绍,基本和前面的指针运算差不多。
指针数组
所谓指针数组是指由若干个具有相同存储类型和数据类型的指针变量构成的集合。指针变量数组的一般说明形式:
<存储类型> <数据类型> *<指针变量数组名>[数组大小]
例如: int *p[5], char ch[6];
就是定义了一个指向int类型和char类型的指针数组。要注意,这里由于“[]”的优先级高于“”,因此,数组名p先与“[]”结合,这就构成了一个数组的形式。是一个含有两个元素的一维数组,每个数组元素都是一个一级指针。指针数组名就表示该指针数组的存储首地址,即指针数组名为数组的指针。
指针数组初始化
先看一个例子:
int main(int argc, const char * argv[]) {
int m = 50, n = 100;
int* p[2];
p[0] = &m;
p[1] = &n;
printf("sizeof(p)=%lu\n", sizeof(p));
printf("&m=%p &n=%p\n", &m, &n);
printf("p[0]=%p p[1]=%p\n", p[0], p[1]);
printf("p=%p &p[0]=%p &p[1]=%p\n", p, &p[0], &p[1]);
printf("\nm=%d n=%d\n", m, n);
printf("*p[0]=%d *p[1]=%d\n", *p[0], *p[1]);
printf("**p=%d **(p+1)=%d\n", **p, **(p+1));
return 0;
}
输出结果:
sizeof(p)=16
&m=0x7fff5fbff80c &n=0x7fff5fbff808
p[0]=0x7fff5fbff80c p[1]=0x7fff5fbff808
p=0x7fff5fbff820 &p[0]=0x7fff5fbff820 &p[1]=0x7fff5fbff828
m=50 n=100
*p[0]=50 *p[1]=100
**p=50 **(p+1)=100
Program ended with exit code: 0
得出结论:在该程序中指针数组,存储了两个整数的地址,p[0]指向m,p[1]指向n;若取得m的值,可以用*p[0]或**p,取得n的值,可以用*p[1]或**(p+1)。可以发现,指针数组中相邻两个元素的地址差4(&p[0]和&p[1]差4)。由于任何指针都占4个字节,所以,指针数组中每个元素占4个字节。
指针数组名
对于指针数组的数组名,也代表数组的起始地址。由于数组的元素已经是指针了,数组名就是数组首元素的地址,因此数组名是指针的地址,是多级指针了。
比如指针数组int* p[N]; 数组名p代表&p[0],p[0]是int *,则&p[0]就是int **。若用指针存储数组的起始地址p或&p[0],可以这样用:int **q = p;
示例代码:
int main(int argc, const char * argv[]) {
int a[3][2] = {9, 6, 1, 7, 8, 3};
int* p[3], i, j;
int **q;
p[0] = a[0];
p[1] = a[1];
p[2] = a[2];
q = p;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 2; j++)
{
printf("%d %d %d ", *(p[i]+j), *(*(p+i)+j), p[i][j]);
printf("%d %d %d ", *(q[i]+j), *(*(q+i)+j), q[i][j]);
}
printf("\n");
}
return 0;
}
输出结果:
9 9 9 9 9 9 6 6 6 6 6 6
1 1 1 1 1 1 7 7 7 7 7 7
8 8 8 8 8 8 3 3 3 3 3 3
Program ended with exit code: 0
该程序完成了一个功能,通过指针数组来遍历二维数组中的所有元素。首先二维数组中,有几行,则指针数组就有几个元素,因此,对于二维数组int a[3][2],对应的指针数组是int * p[3]。p[0]是一级指针,指向第一行第一个元素;p[1]指向第二行第一个元素;p[2]指向第三行第一个元素。举个例子,若想访问第二行第一列的元素,先找到第二行第一个元素,即表达式p[1]或*(p+1),再继续找到该行的第二个元素,即表达式p[1]+1或*(p+1)+1,最后通过*,得到元素的值,用表达式*(p[1]+1)或*(*(p+1)+1)或p[1][1]。最后一个表达式,是利用了规则a[i]无条件等价于*(a+i),因此,*(p[1]+1)可写成p[1][1]。
二级指针q存储了指针数组的数组名,q+1指向指针数组的第二个元素,即&p[1],(q+1)或q[1]就是p[1]。关于q的表达式有((q+1)+1),(q[1]+1) ,q[1][1])。*(q+1)或q[1]。
总结
本篇文章介绍了数组指针、指针数组以及多级的指针等,希望读者认真阅读,同时也希望读者对指针有一个深刻的理解。后面再讲解字符指针、const指针。
结尾
最后,希望读者在读文章的时候发现有错误或者不好的地方,欢迎留言,我会及时更改,感谢你的阅读和评论已经点赞收藏。