指针进阶
Posted 阿C_C
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了指针进阶相关的知识,希望对你有一定的参考价值。
本节继续讨论C语言中的指针。
指针数组&数组指针
先理解一下什么是指针数组和数组指针,指针数组的实质是个数组,这个数组中存储的元素都是指针变量,而数组指针实际上是一个指针,这个指针指向一个数组。
通过表达式区分不同
指针数据和数组指针的表达式有一定的区别,关键在于号的位置,如果号和变量结合性强,表示是指针,否则,就是数组,所以,符号的优先级在这里非常重要,决定了两个符号同时和变量结合时,谁先运算的问题。
查优先级表可以知道,[]的优先级高于号,所以,举例来说:int *p[5];
这段代码中,p就是一个指针数组,而int (*p)[5];
中,p由于先和结合,所以p是一个数组指针;
函数指针和typedef
函数指针,实质上还是一个指针变量,所以函数指针变量也占4个字节,和普通指针区别就在于指针指向的变量类型不同。
函数指针的实质
函数实质上是一段代码,这段代码在内存中是连续的,一个函数体中的所有语句编译后生成的可执行程序在内存中是连续分布的,所以函数中第一句代码的地址,就是函数的地址,在C语言中使用函数名来表示,由于函数指针实质就是一个普通的变量,类型是函数指针,值就是某个函数的地址,也就是函数名这个符号在编译器中的值。
函数指针的书写和分析
由于C语言本身是强类型语言,所以需要明确指针的类型,所以在书写函数指针的时候要按照要求来,这样编译器才会知道这个指针所指向的变量的类型,函数指针一般书写为“返回值类型 (*指针变量名) (参数类型)”,例如int (*p) (double);
。
typedef
在C语言中,可以使用typedef来定义类型,C语言可以分为两种类型,一种是C语言自带的原生类型,一种是使用typedef定义的自定义类型,函数指针也是一种自定义类型,如果函数指针定义非常繁琐,为了简化书写,可以使用typedef来自定义该函数指针类型为一个简单易写的名称,需要注意的是,typedef定义出来的都是类型,而不是变量,typedef的使用如下:
typedef char* (*pType)(char *, const char *);
上面定义了一个pType类型的函数指针。
函数指针
函数指针是如何使用的呢?
函数指针的调用
定义如下函数指针:
typedef int *(pFunc)(int,int);
指向一个返回值为int,参数为两个int的函数,则如果有函数是:
int add(int a,int b)
return a + b;
则可以定义函数指针变量,并进行调用:
// 创建函数指针变量
pFunc p1 = NULL;
// 赋值
p1 = add;
// 调用
int result = p1(1,1);
这样相当于调用了add函数进行加法运算,如果用该函数指针指向不同的函数,就可以实现不同的结果,只需要该函数的参数类型和返回值类型符合该函数指针的定义即可。
结构体内嵌指针
使用结构体内嵌指针,可以实现程序的分层设计。
程序分层
我们拿计算器程序为例,先创建一个框架文件,命名为framework.c,再创建具体的计算器实现文件cal.c,这就分成了两个层次,一个是框架层,一个是实现层,两个层次可以由不同的人员完成,通过调用组合来共同工作,cal.c直接完成计算器的工作,但是其中的关键部分是调用framework.c中的函数来完成的,framework.c的代码如下:
头文件定义
#include "cal.h"
// framework.c中应该写实际业务关联的代码
// 计算器函数
int calculator(const struct cal_t *p)
return p->p(p->a, p->b);
定义cal.c的头文件cal.h,作为头文件的联系纽带:
#ifndef __CAL_H__
#define __CAL_H__
typedef int (*pFunc)(int, int);
// 结构体是用来做计算器的,计算器工作时需要计算原材料
struct cal_t
int a;
int b;
pFunc p;
;
// 函数原型声明
int calculator(const struct cal_t *p);
#endif
实现文件
实现层包含具体实现计算步骤的函数:
#include "cal.h"
#include <stdio.h>
int add(int a, int b)
return a + b;
int sub(int a, int b)
return a - b;
int multiply(int a, int b)
return a * b;
int divide(int a, int b)
return a / b;
int main(void)
int ret = 0;
struct cal_t myCal;
myCal.a = 12;
myCal.b = 4;
myCal.p = divide;
ret = calculator(&myCal);
printf("ret = %d.\\n", ret);
return 0;
在结构体中,pFunc是一个函数的指针,将这个结构体变量传入到 calculator这个函数中之后,会被调用,从而完成运算。
经过分层设计实现了解耦,每个层次专注各自的领域和任务,不同层次之间使用头文件交互,上层为下层提供服务,上层的代码在下层中被调用,上层注重实现业务逻辑,与目标相关联但是没有具体实现,下层注重直接工作,为上层填充实例并将实例传递到上层中的接口当中,来调用接口函数,下层代码中核心是结构体变量的填充,将结构体变量传到接口中即可实现对应的功能。
再论typedef
C语言中有两种数据类型,內建类型和用户自定义类型,使用typedef就可以实现用户自定义类型,typedef操作的是类型,而不是变量,typedef和#define是有区别的,#define定义出来的是一个宏,在宏扩展阶段进行文本替换,而typedef是在编译阶段,进行类型的重新定义,例如typedef char *pChar;
,与#define pChar char *
,就是这种区别。
typedef和结构体
结构体在定义和使用时,需要先定义结构体类型,然后使用结构体类型来定义变量,例如,有如下结构体:
struct student
char name[20];
int age;
则可以定义student的结构体变量struct student s1;
,struct student合在一起是变量s1的类型。
为了简化书写,我们可以在定义结构体的时候,使用typedef,
typedef struct student
char name[20];
int age;
student_t;
这样,就把这个结构体重新定义成了student_t类型,以后再定义变量的时候,就可以这样写了student_t s1;
,可见,typedef简化了我们编程时类型的书写。
定义结构体指针
有时我们也需要直接定义结构体变量的指针,如下
typedef struct teacher
char name[20];
int age;
int mager;
teacher, *pTeacher;
通过这样,一次定义了三个类型,结构体类型struct teacher,teacher以及结构体指针类型struct teacher *pTeacher;则变量可以如下定义:
teacher t1;
pTeacher p1 = &t1;
可见指针类型是可以被结构体变量赋值的。
typedef和const
假设有typedef int *pInt;
,我们定义了一个pInt类型,如果有以下定义:
int a = 1;
pInt p1 = &a;
则是完全可以理解的,但是,如果是写成这样const pInt p2= &a;
,则结果是怎么样呢?这相当于int *const p2;
,也就是说,指针p2为const的,但是其指向的变量的值是可以变的,这点需要搞清楚;
那么如果写成这样呢?pInt const p2 = &a;
,效果其实和上边是一样的。
typedef通常用于简化类型的描述和书写,产经就是上面第一个例子的场景,来重命名或者叫做重定义一个比较复杂的类型为一个比较简短或者容易书写理解的类型,另外,typedef也通常用于定义与平台无关的类型,C语言內建类型通常和平台紧密相关,例如int在32位机器上是32bit,在64位的机器上是64bit,为代码的移植造成了很大的影响,为了解决这样的问题,可以使用typedef来定义一些中间类型来进行转换,例如C语言中就有这种定义:
typedef int size_t;
使用size_t来替代int,以屏蔽不同平台上int所占字节不同的差异。
二重指针
二重指针本质上来说,和一重指针没有什么不同,本质都是指针变量,而指针变量的本质就是变量,所以追根究底是一样的。
二重指针概念
一重指针和二重指针本身都是占用4个字节的内存空间,都可以指向另外一个变量,例如char **p1;
就是一个二重指针,表示的是指针指向的变量是一个char *类型,也就是说指针指向了另外一个指针,在由另外一个指针指向一个变量。
二重指针和普通指针的差别就在这里,它所指向的变量类型是一重指针,二重指针其实也是一种数据类型,编译器在编译时会根据二重指针来做静态类型检查,如果发现数据类型不匹配,编译器就会报错。
C语言中的二重指针
C语言中的一重指针完全可以承担二重指针的任务,但是某些任务或者变量,使用二重指针会更加灵活和方便,指针被定义时,使用数据类型来标记,编译器通过该数据类型进行类型检查,可以帮助编程者发现潜在错误
二重指针的用法
二重指针必须指向一重指针的地址,也可以指向指针数组,我们知道数组名做右值的时候,实质就是数组首元素的地址,就是一个指针,所以数组指针可以被看做是一个二重指针,因为数组首元素就是一个int*类型,再用一个指针指向它,就是二重指针了。
二维数组
单从内存角度来看的话,二维数组和一维数组本质上没有任何区别,甚至二维数组都可以用一维数组来表示,而且在内存使用和访问效率上是一样的,但是在某些情况下,二维数组比较容易理解,代码利于组织。例如进行矩阵操作,平面坐标轴操作等等。
二维数组的第一维本身也是个数组,它的元素的数据类型是数组类型,第二维本身也是个数组,它的元素类型是普通元素,第二维的数组本身作为第一维数组的元素存在于第一维数组中。
二维数组的访问方式
二维数组中的元素可以通过下标以及指针两种方式来访问,假设有二维数组:int [2][5];
则可以定义指针int **p = a;
,访问等价表示如下:
a[0][0]
等价于*(*(p + 0) + 0)
- 所以有
a[i][j]
等价于*(*(p + i)+ j)
二维数组的指针及其运算
二维数组的运算分为三种情况。
指针指向数组名
假设如下代码:
int[2][5] a= 1,2,3,4,5,6,7,8,9,0;
int (*p)[5] = a;
则二维数组的指针书写如上所示,表示p是一个数组指针,该数组指针指向一个数组,数组中有5个int类型的元素。
a是二维数组的数组名, 做为右值时表示第一维数组首元素的首地址(其实就是数组的地址),也就是&a[0],所以其实可以写成int (*p)[5] = &a[0];
,但是&a[0]又等同于a,所以就写成那样了。其实都代表数组的首地址,至此,我们就可以使用指针的方式来访问数组中的各个元素,从而获取其中的元素来参与运算。
由此可见,二维数组的数组名表示的是第一维数组首元素的地址,第一维数组首元素也就是第二维数组的地址,和一维数组的符号含义是相同的,所以不要认为我们是使用一维数组的指针来指向二维数组,其实本质上还是指向的一个数组,只不过这个数组中的元素是数组罢了。
指向二维数组的第一维
再进一步,如果想要表示第二维中某个元素的地址,可以这样写int *p1 = a[0];
,因为我们知道a[0]表示第二维整体数组的数组名,又等于第二维整体数组的首元素地址,也就等价于&a[0][0],同理第二维中第一个数组的第四个元素表示为*(p1 + 4);
。
指向二维数组的第二维
如果想用指针指向二维数组的第二维,严格来说是不可以的,因为二维数组的第二维中的元素其实就是基本类型了,不能用指针类型进行赋值了,除非对该元素进行取地址操作,例如int *p = &a[1][3];
,这样就和直接用下标访问没什么区别了。
二维数组和指针关键在于:
- 数组中各个符号要弄清楚
- 二维数组的指针式访问要弄清楚,不要从简单的书写方式上来理解,要从指针的特性上来理解以及数组地址,数组首地址,二维数组和一维数组的地址来理解。
以上是关于指针进阶的主要内容,如果未能解决你的问题,请参考以下文章
116. 填充每个节点的下一个右侧节点指针 二叉树 逆序BFS O(n)/ 利用 father.next遍历下层节点 O