漫谈C语言指针(三)

漫谈C语言指针(三)更多博文,请看音视频系统学习的浪漫马车之总目录 上一篇漫谈C语言指针(二) 主要讲了指针和数组的关系、字符串指针、指针变量作为函数参数,就像电影最后进入高潮一样,接下来,指针的内容将进入深水区,大家坐

更多博文,请看音视频系统学习的浪漫马车之总目录

C内存与指针:

漫谈C语言内存管理

漫谈C语言指针(一)

漫谈C语言指针(二)

漫谈C语言指针(三)

上一篇漫谈C语言指针(二) 主要讲了指针和数组的关系、字符串指针、指针变量作为函数参数,就像电影最后进入高潮一样,接下来,指针的内容将进入深水区,大家坐稳扶好,马上出发~

今天要讲的内容有:二级指针、二维数组、指针数组、函数指针、函数指针数组、结构体指针。为什么说的深水区呢?因为前面的2篇只要看懂概念基本就能学会,但是这一章,概念开始变绕,不再平坦,甚至有点花俏,所以需要消费一些脑细胞才可以理解,不过不用担心,我对我的表达有自信(看不懂也勿喷。。)

二级指针

二级指针,顾名思义,就是指针的指针,一个指针指向另一个指针,即指针存放的数值是另一个指针的地址。

假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示: 二级指针 用代码定义就是:

int a =5;
int *p1 = &a;
int **p2 = &p1;

以此类推还有多级指针,但一般用到二级指针,更多级的指针很少用到。 那二级指针的作用是什么呢?

在函数内部修改函数外部一个指针的指向:

漫谈C语言指针(一) 中的“通过指针在函数内部修改函数外部变量值”章节讲过,要在函数内改变函数外定义的变量的数值,要通过指针来传参,那以此类推,二级指针的一个作用就是通过二级指针传参,在函数内部修改函数外部一个指针的指向。

比如:

void secondPointer(int **a) {
    int *p = static_cast<int *>(malloc(sizeof(int)));//用malloc保证离开函数后内存不会释放
    *p = 5;
    *a = p;//改变a函数外传入的二级指针指向的指针的指向
    printf("secondPointer *a %#X\n", *a);
}

int main() {
    int i = 10;
    int *a = &i;
    printf("before secondPointer a %#X\n", a);//改变a之前的指向之前a的指向地址

    secondPointer(&a);
    printf("after secondPointer a %#X\n", a);//改变a之后的指向之前a的指向地址
    printf("after secondPointer *a %d\n", *a);//改变a之前的指向之前a的指向地址
}

运行结果:

before secondPointer a 0X62FE1C
secondPointer *a 0XDB1C40
after secondPointer a 0XDB1C40
after secondPointer *a 5

可以看到通过secondPointer函数的处理,a成功指向了5(即函数中p指向的变量),地址也发生了改变。

二维数组

二级指针的另一个常用的作用,就是对二维数组的操作。

先说下二维数组,简单来说就是数组的数组,每个数组元素都是一个数组,在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。以下面的二维数组 a 为例:

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

从概念上理解,a 的分布像一个矩阵:

0   1   2   3
4   5   6   7
8   9  10  11

但在内存中,a 的分布是一维线性的,整个数组占用一块连续的内存: 二维数组在内存中的存储

C语言中的二维数组是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节(假设当前环境int占用4字节),整个数组共占用 4×(3×4) = 48 个字节。

首先要引入数组指针的概念,即指向数组的指针,定义如下:

int (*p)[4] = &a;//数组指针p指向数组a

与数组名不同的是,数组名在表达式中表示的是数组第一个元素的首地址,所以对数组名算术运算的步长是数组元素的长度,而数组指针的步长是真整个数组的大小(如果对步长不理解,请看:漫谈C语言指针(一) 指针算术运算一节):

int a[] = {1,2,3,4,5,6};
    int (*p)[6] = &a;
    printf("secondPointer sizeof(int) %d\n", sizeof(int));
    printf("secondPointer p %#X\n", p);
    printf("secondPointer p+1 %#X\n", p+1);

运行结果: secondPointer sizeof(int) 4 secondPointer p 0X62FE00 secondPointer p+1 0X62FE18

注意到 p+1比p大了24字节(10进制),刚好是6个int类型元素的大小。

C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。

假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示: 在这里插入图片描述漫谈C语言指针(二) 中说过,一维数组a的数组名在表达式中指向数组首元素首地址,指向的类型是数组元素类型。以此类推,上图的二维数组a,a指向第一个元素(即数组a[0],这里a[n]表示第n个数组(第n行))因为a作为指针指向的类型为数组,所以步长为数组的长度,在这里即为4*sizeof(int)。而因为a[n]表示第n个数组,所以a[n]本身可以看作第n个数组的数组名,即a[n]本身是一个指向第n个数组的首元素首地址的指针,指向的类型为第n个数组的元素类型,所以步长为数组元素类型大小,在这里即为sizeof(int)。

 int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
    printf("sizeof(*(a)):%d\n", sizeof(*a));//求出a指向的数据的大小
    printf("a:%d\n", a);
    printf("a+1%:d\n", a+1);//求a的步长(其实和sizeof(*a)一样大)

    printf("sizeof(*a[0]):%d\n", sizeof(*a[0]));//求出a[0]指向的数据的大小
    printf("a[0]:%d\n", a[0]);
    printf("a[0]+1:%d\n", a[0]+1);//求a[0]的步长(其实和sizeof(*a[0])一样大)

    printf("&a:%d\n", &a);//要记得数组名在&符号的时候不会转化为指针,所以是对数组取地址
    printf("&a+1:%d\n", &a+1);//看下&a的步长

运行结果: sizeof(*(a)):16 a:6487536 a+1%:d sizeof(*a[0]):4 a[0]:6487536 a[0]+1:6487540 &a:6487536 &a+1:6487584

可以看出,a和a[0]指向的地址都是整个二维数组第一个数组的第一个元素的首地址,但是a步长是16,即指向的类型是int[4],而a[0]步长为4,即指向类型为int。

根据结果可以进一步可以推导出,a+n指向的是第n+1个数组的首元素首地址,指向的类型是一个一维数组,所以(a+n)表示的是一个数组。a[x]+y指向的是第x+1数组第y+1个元素首地址,指向类型是一个数组元素,又因为之前讲过a[x]等同于(a+x),又因为a[x]+y相当于一维数组a[x]偏移了y个步长,即a[x]+y等价于*((a+x)+y),表示的是一个数组元素**。 总结下就是: a[i] ==(a+i) a[i][j] == * (a[i]+j) == * (*(a+i)+j)

从而进一步可以优化二维数组的遍历:

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

for(int i=0; i<3; i++){
   for(int j=0; j<4; j++)
      printf("value :%d\n",*(*(a+i)+j));
}

另外从运行结果可以看出,&a+1的地址比&a大了48,正好是12*sizeof(int),也就是整个数组a占用的空间大小,这个和漫谈C语言指针(二) 说的一维数组一致

指针数组

指针数组,顾名思义,指针的数组,即数组的元素为指针。

首先要注意的是指针数组的定义: 前面将的数组指针的定义是:

int (*p)[2];

这里因为[]优先级高于 ,所以加了(),让和p先结合成一个指针,然后才和[]结合使指针指向数组。

而指针数组的定义为:

int *p[2];

没错,只是少了个(),概念就完全不同,让p先和[]结合,所以p是个数组,然后和*结合,指明数组元素类型为指针。

指针数组可以定义如下:

int a = 1, b = 2, c = 3;
//定义一个指针数组
int *arr[3] = {&a, &b, &c};//创建一个指针数组

之前我们说数组名在表达式中会转化为指向数组首元素的指针,那指针数组的数组名也一样会在表达式中转化为指针,该指针自然也是指向数组首元素,而数组首元素是指针,所以这个数组名转化的指针就顺理成章成为了一个二级指针。

因为数组名arr转化为二级指针,所以要加2个**来取值:

int a = 1, b = 2, c = 3;
//定义一个指针数组
int *arr[3] = {&a, &b, &c};//创建一个指针数组
printf("%d, %d, %d\n", **(arr+0), **(arr+1), **(arr+2));//因为数组名arr转化为二级指针,所以要加2个**来取值

运行结果: 1, 2, 3

指针数组常用的一个地方莫过于和字符串数组的使用:

 char *str[3] = {
            "man",
            "woman",
            "pig"
    };

记住这里str必须是一个指针数组,因为在字符串本身存储在常量区,在表达式中会转化为一个指向char的指针(首字符地址)。

所以在读取字符创数组的某个字符串的某个字符的时候,往往使用二级指针去操作,比如打印出上面str中第二个字符串的第4个字符:

printf("%c\n", *(*(str+1)+3));//*(str+1)是得到"woman"的首地址,*(*(str+1)+3))是"woman"的首地址偏移3个char之后,再取值,即‘a’。

运行结果: a

函数指针

一个数组占用一段连续的内存区域,一个函数同样也占用一段连续的内存区域,在C语言的设计中,也和数组一样,函数名也可以代表函数的首地址~~那么,让一个指针去持有函数的首地址也变得顺理成章了,这个指针,理所当然就叫做函数指针。

函数指针的定义如下:

int(*p)(int, int);

这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“ * ”,即( * p); 其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。

该指针p指向的类型就是int (int,int)函数(是的,把函数也看做一个类型,由入参和返回值决定)

总结下通用格式就是: 函数返回值类型 ( 指针变量名) (函数参数列表);*

那么函数指针的意义是什么呢? 熟悉Java的童鞋一定知道回调接口这个东西,当我们在一个方法或者类中有一段逻辑需要由外部传入的时候,经常会用回调接口,熟悉kotlin的童鞋一定知道函数在kotlin中是可以作为成员变量和参数的,那么在C中函数指针的作用是很类似的。

比如calculate是一个计算的函数,但是具体怎么计算需要外部传入,这里函数指针p作为这个传入的参数表示某一个计算具体逻辑:

/** * 加法运算 * @param a * @param b * @return */
int plus(int a,int b){
    return a+b;
}

/** * 减法运算 * @param a * @param b * @return */
int sub(int a,int b){
    return a-b;
}

/** * 计算函数,但具体方式由传入的函数p决定 * @param a * @param b * @param p * @return */
int calculate(int a,int b,int (*p)(int,int)){
    return p(a,b);
}

int main() {
    int a = 2;
    int b = 1;
    //此时决定calculate具体的计算方式
    printf("calculate plus %d\n", calculate(a,b,plus));
    printf("calculate plus %d\n", calculate(a,b,sub));
}

运行结果: calculate plus 3 calculate plus 1

结构体指针:

在看过前面几篇文章之后,这个其实就是指向一个结构体对象的指针,和Java的引用如出一辙,很容易理解。

假如有一只猫的结构体:

struct Cat{
        int head;//猫头
        int tail;//猫尾
    };

    Cat cat;
    cat.head = 5;
    Cat* c = &cat;
    printf("c->head: %d\n", c->head);

(其实也没什么好讲的,只是把基本类型改为结构体类型,这对熟悉面向对象语言的童鞋来说太easy了~~)

不过这里重点要讲个有意思的东西,这也是解释为什么指针是个强大而危险的工具的一个例子:

原本cat.tail是int类型,但是假如我们闲的蛋疼试试这样强转cat.tail的指针类型,然后再对转化后的指针取值获取其head的值,会发生什么事呢:

((Cat*)&(cat.tail))->head=12;
printf("&(cat.head): %d\n", &(cat.head));//为了更好说明,打印出head地址
printf("&(cat.tail): %d\n", &(cat.tail));//为了更好说明,打印出tail地址
  
printf("cat.head: %d\n", cat.head);
printf("cat.tail: %d\n", cat.tail);

运行结果: &(cat.head): 6487572 &(cat.tail): 6487576 cat.head: 0 cat.tail: 12

为什么打印出来会是cat.head: 0,cat.tail: 12?明明是给head赋值了12的。 结果是不是有点神奇,再缕下我们做了啥。

((Cat*)&(cat.tail))->head=12;

这行代码中,首先我们给了计算机一个猫尾巴(cat.tail),然后又告诉计算机,把这尾巴当做一个猫来看待!再给这只猫的头赋值12,然后计算机开始晕车了,猫头猫尾早已分不清。。

如果你真的理解了漫谈C语言指针(一) 中谈到的指针步长,那应该不难知道原因。因为其实计算机根本不管你给的是猫还是狗,它只管内存,这里给的结构体Cat,对于计算机来说就是开辟了一段内存空间,而计算机当然也不会理解head和tail是什么,只知道这是Cat开辟的内存内部的2段内存空间,

通过观察运行结果中head和tail的地址,可以看出,head和tail是在一块连续的内存中的,像这样:

在这里插入图片描述 当&(cat.tail)操作是,指针指向是这样的: 在这里插入图片描述 关键点来了,当强转指针类型的时候,(Cat*)&(cat.tail),计算机把Cat当做上方蓝色部分的:

在这里插入图片描述 此时执行((Cat*)&(cat.tail))->head=12;,计算机已经把真正的tail当做head了,所以表面上是赋值给head,其实那是假的,真正的Cat是赋值给了tail,所以printf(“cat.tail: %d\n”, cat.tail);打印出来才是12。

类似的例子还可以看下数组:

int arr[5];
arr[3] = 1 << 16;
**((short*)arr)[6] = 2;**//关键,对arr指针进行强转为short类型,然后指针往前偏移6个步长
printf("arr[3]: %d\n", arr[3]);

打印结果怎样呢?

运行结果: arr[3]: 65538

经过了结构体的例子,大家应该可以推测出原因了。

在运行了arr[3] = 1 << 16;之后,在内存中的状态是:

在这里插入图片描述 而执行了((short*)arr)[6] 指针强转之后,在计算机眼里,arr变成了:

(当前环境是小端模式) 在这里插入图片描述 执行了((short*)arr)[6] = 2;之后:

在这里插入图片描述 执行printf(“arr[3]: %d\n”, arr[3]);的时候,上图的2个小格子又合并起来,得到结果为65538(小端模式,所以右边的小格子是高位)

当然因为C语言数组是不做越界检查的,所以非常自由,还可以做如下骚操作:

int i = *(((short*)(&arr[-1]))+4);

这个大家基于上岸的例子应该能想到答案吧~~

之所以讲了上面几个骚味十足的操作,目的在于让大家可以真正从内存的角度看待指针,不要被表面所迷惑,一定要根据漫谈C语言指针(一) 中指针算术运算小节最后讲的灵魂拷问去研究指针相关问题,真正从本质理解指针。

关于C语言的博文暂时到这里,下一篇开始C++的博文:初尝C++的世界

如果觉得本文有帮助,别忘了点赞关注哦~

今天的文章漫谈C语言指针(三)分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/16808.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注