C语言指针讲解(二)

谨记

听......黎明在远方呼唤清晨,别在等,人的一生必将经历许多磨难,所以在人生前行的道路上,我们不可对每件轻微的伤害而敏感,在生活的磨难面前,精神上的坚强和无动于衷是我们抵抗罪恶和人生意外的最好武器,前行的终点还很远,我们要不停脚步的往前走,遇到任何困难,不要退缩,要做到有舍才有得,有得必有失,当你迷失的时候,不要忘记了,你的人生理想就是你前行的动力和方向,在留言处写下你的理想。

前言

前一篇我们已经学习了关于指针的一些基础,那么在这篇文章,我们将更深层次的去理解和运用指针。

指针和数组(一维)

前面我们已经学习过数组了,相信大家对数组已经有一个深刻的认识了,那么我们来看看指针和数组之间存在着某种联系?
指针和数组名
我们先来看一段代码:

示例一:
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指针。

结尾

最后,希望读者在读文章的时候发现有错误或者不好的地方,欢迎留言,我会及时更改,感谢你的阅读和评论已经点赞收藏。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 指针是C语言中广泛使用的一种数据类型。 运用指针编程是C语言最主要的风格之一。利用指针变量可以表示各种数据结构; ...
    朱森阅读 3,387评论 3 44
  • 数组在程序设计中,为了处理方便, 把具有相同类型的若干变量按有序的形式组织起来。这些按序排列的同类数据元素的集合称...
    朱森阅读 3,780评论 2 13
  • 前言 最近真的是忙的不可开交,公司一直给安排任务,连学习和写笔记的时间都没有了,落下好几次课的笔记都没有写,所以我...
    Xiho丶阅读 1,478评论 1 12
  • 在云南,海与天空总是连在一起 夕阳西下,总能在水镜上看到他们的影子,或打渔,或划船,举手投足之间皆蕴藏着海的风气。...
    胶布绘灵阅读 358评论 0 3
  • 相信生活中大家都会遇到这样的苦恼: 第一个例子: 一个在体制内工作的朋友诉说:刚刚调动了工作,一切都是陌生的,都需...
    唐人生阅读 890评论 0 0