怎么让指针从后向前C语言,C语言指针闲谈

指针闲谈

本文将采用循序渐进的方式,来简要谈一谈C语言中指针的定义和分析。

谈到c语言,就绕不开c语言中的一把利器--指针。

指针可以直接指向物理内存地址,对内存进行操作。在计算机中,我们把内存划分为一个个小的单元,每个单元对应一个编号(或者地址),而指针可以利用地址,直接找到该地址对应的变量值。通俗的理解就是指针像门牌号,我们可以通过门牌号找到对应房间,从而找到房间里的人。

因此,指针也是一个变量,用于存放地址的变量。在《c语言数据存储》一文中已经分析了,物理内存中是以一个字节为一个单元,因此,我们可以把内存想象成一辆在铁轨上的火车,每节车厢就相当于一个内存单元,在车厢内部,我们安装上八张连续的椅子。对每个车厢进行编号,第一节车厢为0X00000000,第二节车厢为0X00000001,依次类推,该编号为16进制,最后一节车厢编号为0XFFFFFFFF。可以看出一个指针变量所占的空间为8字节。(以32位平台为例,下文如果没有特殊指明,均按32位平台)1指针类型

1.1如何定义指针变量?

首先,让我们来看一看c语言中是如何定义变量的,例如:int i = 10;

我们定义了一个变量,其中i为变量的名称,int为该变量的类型,10为该变量的值。可以看出,在等号的左边,我们去掉变量的名称i,剩下的即为变量的类型。变量的类型决定了该变量所占字节大小。int类型的变量占用4个字节。

同样的,让我们来定义一个数组:int arr[5] = {0,1,2,3,4};

按照上述分析,在等号左边,arr为该变量的名称,去掉arr后剩下int [5],此即为变量arr的类型,其中[]表示变量arr为一个数组,[5]里面的5表示该数组有5个元素,int表示这5个元素都为整型,所以int [5]类型所占的字节数是4×5=20个字节。由于"[]"里的数字可以任意指定,所以我们称数组为自定义类型数据,与这种定义方式相似的还有结构体,枚举体以及联合体。

如果我们要定义一个指针变量,只需在变量名前加上*,所以我们可以定义如下指针变量:int *pi = &i;

同上述分析,在等号的左边,pi为变量名称,去掉pi剩下的int *为该变量的类型,其中*表示变量为指针,int表示该指针变量指向的元素是整型。等号右边,"&"表示取地址的意思,即取到i所在的车厢号,然后把该号码(地址)放到指针变量pi中。

同理,对于其他类型的变量(如:char,short,long,long long,float,double以及它们的无符号(unsigned)类型)也有上述定义方式:type *指针变量名 = &与type对应类型的变量;

对于字符指针char *,除上述用法外,还有另外一种用法,例如:char *pc = "Hello World";

此段代码不能简单理解为把"Hello World"放入变量pc中,而是在内存中有一块连续的内存,存放了字符串"Hello World",这句代码的意思是把其中字符"H"的地址放入到指针pc中。

1.2如何使用地址?(解引用)

按照正常的思维,拿到地址后只需按图索骥通过地址上的车厢号找到对应的车厢,然后对其进行其他操作,所以有* pi = 0;

这句代码的含义是把指针pi指向的变量的值改为0,pi存放的地址是i的地址,所以此句代码的含义是把i的值改为0。即等价于i = 0。*在此处表示的含义为解引用,指针的类型决定了对应指针的权限,如此处pi为int *,所以pi可以访问和操作四个字节的内容,对于char *类型的指针,则每次只能访问和操作一个字节的内容。

指针类型不仅仅可以决定该指针一次能访问字节的大小,也决定了每次能够跨多大步子,例如,对于一个int *类型的指针pi,如果pi里存放的地址是0X11FF2350,那么pi+1

对应的地址就为0X11FF2354,对于一个char *类型的指针pc,如果pc里存放的地址是0X11FF2350,那么pi+1对应的地址就为0X11FF2351,其他类型同理,即指针加上(或减去)一个整数,那么指针对应的地址就移动(该整数)乘(该指针指向类型所占字节数)。

1.3野指针

对于指针,在定义时必须对其进行初始化,即必须给指针一个明确的地址,否则会形成野指针。野指针就是指针指向位置是不可知的(随机的、不正确的、没有明确限制的),未初始化的指针就是随机的。而此时如果对该随机值指向的地址进行访问和操作,就会造成非法访问。正如你在大街上捡到一张地址(随机),而你自己也不清楚该地址里具体有谁,如果强行进入该地址,就会造成非法访问。

因此我们要避免野指针:对指针进行初始化,防止指针越界访问,指针指向的空间释放时把指针及时置NULL,使用指针之前检查指针的有效性。2指针与数组

首先,数组名表示首元素的地址(有两个例外,一个是数组名单独放在sizeof()函数的括号里,一个是放在&后边。这两种情况都表示整个数组的地址),即int arr[5]={0,1,2,3,4};

int *p = arr;

此时p里存放的是数组的第一个元素arr[0]的地址。而p+1就表示数组的第二个元素arr[1]的地址,依次类推。即p+i=&arr[i]。因此我们可以通过指针来访问数组中的元素,即*(p+i)就等价于arr[i]。前文说到,arr是数组名表示的是首元素的地址,p也是地址,arr+1表示的是第二个元素的地址,所以*(arr+1)与arr[1]等价,同理,*(p+1)也与p[1]等价。因此,下文中,我们不区分*(p+i)与p[i],因为两者是等价的。

如前文所述,当我们想获取某个整型i的地址时,直接&i,那么,&arr则取到的是整个数组的地址,对整型的指针变量的定义,我们直接在变量名称前添加*,即int *pi = &i,同理,对于数组的指针变量定义,我们也直接在变量名称前添加*,但是由于[]的优先级高于*,即在计算机编译代码时,首先把变量名与[]结合在一起进行处理(而先与[]结合,编译器就会认为该变量是个数组),为了防止这种情况出现,我们将*和变量名用()括起来,即int (*parr)[5] = &arr;

用上边的分析:parr为变量名,parr前边有*,所以parr是一个指针,去掉变量名parr,剩余的即为该变量的类型:int (*) [5],*表示为指针类型,从(*)向右看,是[5],说明该指针指向的是一个大小为5的数组,从()向左看,是int,说明这个数组里的5个元素类型都为int。对于其他类型的数组,分析也同上,例如char str[3] = {'a', 'b', 'c'};

char(* pstr)[3] = &str;

再来思考另一个问题,对于数组,首元素的地址和整个数组的地址一样,即int *p = arr;

int (*parr)[5] = &parr;

如果对p和parr进行输出,显然两者的值相同,那么两者又有何区别?前边在引入和讨论整型指针变量时,我们提到过,对于不同类型的指针,类型决定了该指针每次能够访问和操作字节数的大小。在这里,p的类型为int *,即该指针指向的元素为整型,是4个字节,parr的类型为int (*) [5],即该指针指向的元素为数组,有4×5=20个字节,所以p每次只能访问和操作4个字节,而parr可以访问和操作20个字节,对指针进行加减整数操作,移动的字节数也不相同,假设该数组的地址为0X1FC65804,那么p+1的值就为0X1FC65808,parr+1的值为0X1FC65818(20的十六进制为0X14)。

对于多维数组,分析同一维数组,以二维数组为例。假设有这样一个两行三列的二维数组:int arr[2][3] ={{1,2,3},{4,5,6}};

对于多维数组在内存中可以看成按行存放(实际上因为内存是连续的所以实际中内存没有行的概念),即该二维数组可以认为是由两个一维数组组合而成的,其中第一个一维数组为{1,2,3}(记为a1),第二个一维数组为{4,5,6}(记为a2)。所以二维数组的首元素为arr[0]=a1={1,2,3}。数组名表示首元素地址即arr为arr[0]={1,2,3}的地址,也就是说该地址指向的的是一个存有有三个元素一维数组,所以对应的指针变量为一维数组指针变量,即:int (*pa1) [3]= arr;//那么pa1+1即为a2的地址。

把arr[0]看作数组名,那么它表示的是arr[0]首元素的地址,即{1,2,3}中1的地址。同理arr[1]表示{4,5,6}中4的地址。所以*(arr[0])就和arr[0][0]等价,*(arr[0]+1)和arr[0][1],依次类推。

按照上边的定义方法,该二维数组的指针就可定义为int (*parr)[2][3] = &arr;

其含义为:parr为变量名称,类型为int (*)[2][3],*表示parr为指针,从(*)向右看,是[2][3],说明了指针指向的是一个两行三列的二维数组,从(*)向左看,是int,说明这个二维数组的元素为int。3函数指针

3.1函数指针定义

函数指针,顾名思义,是用来存放函数地址的指针。那么函数指针该如何来定义?首先让我们从定义函数看起:例如我们要定义一个算两个整数加法的函数add,那么我们需要给这个函数传入两个参数,而函数算完加法后,则向我们返回计算结果(整数)。所以:int add(int x, inty)

{

//实现主要功能的代码,不是本主题的讨论关键,略去不写

}

上述代码中,add为函数名,add后边的()说明add为一个函数,()里两个int变量说明该函数接收两个类型为int的变量,add前边的int说明函数的返回参数类型为int。

同数组的指针定义方式一样,我们采取同样的方式定义函数指针变量,即在变量前加上*,同样的由于()优先级更高,所以我们需要把*函数指针变量放在一个()里,即int (*padd) (int,int) = &add;

上述代码意思:等号左边padd为一个变量,去掉变量名padd后剩下int (*) (int,int),此即为变量的类型,(*)说明变量padd是一个指针,(*)的右边为(int,int)说明了该指针指向的是一个函数,这个函数接收两个类型为int的变量,(*)的左边是int,说明该指针指向的函数返回类型为int。等号右边取函数add的地址表示指针变量padd里存放的是函数add的地址。(由于不同函数功能不同,函数内部定义的变量不同,而函数的作用主要是用来被调用以实现其功能,所以我们不讨论函数指针可以访问和操作的字节数)。

在c语言中,函数名表示函数地址,所以上述代码也可以写为int (*padd) (int,int) = add;

我们调用函数时一般直接使用函数名即add(x,y),我们在使用地址时一般要解引用,即(*padd)(x,y)。既然函数名表示函数地址,而padd也为地址,所以也可以写为(*add)(x,y),padd(x,y)。即这几种情况含义相同,都是调用函数add。因此下文将不区分(*padd)(x,y)和padd(x,y)这两种表达方式。

3.2案例

3.2.1案例1

有了上述基础,让我们来看一下如下代码:(*(void (*)( ))0)( );//来源《c陷阱与缺陷》一书

首先,让我们来梳理一下()在C语言中的含义:

(1).改变优先级,即在算数运算中,例如一个有加减乘除的式子,先算乘除,再算加减,如果有()则先算()里的,再算其它的。这我们在初等数学中都学过,因此不再赘述。

(2).强制类型转换,一般放在变量或者数据的前边,把变量或数据强制转换为括号里对应的类型。例如(float)3,含义为把整数3强制转换为浮点数3;假设p为整型指针,(char *)p即把p强制转换为字符型指针。

(3).跟在控制语句后,例如if()…,while()…,for()…等。

(4).跟在函数名后,()里放函数的参数。例如add(x,y)。

(5).c语言中,有一类表达式叫做逗号表达式,即括号里有一系列以逗号隔开的表达式,运算方式从左到右。例如:a=1;

b=2;

a=(3,5,b=7,6);

运用逗号表达式的算法,最后a=6,b=7。

显然代码(*(void (*)( ))0)( );中的()没有跟在控制语句后面,也不是逗号表达式。在上边分析函数指针padd时提到过去掉padd后剩下的部分是变量类型,所以void(*)()是一个函数指针类型,让我们从中间的(*)开始分析,(*)说明是个指针类型,从(*)向它的后边看,紧跟着一个(),说明这个指针指向的是一个函数,这个函数不需要传参,从(*)向前看,是void,说明这个函数返回的参数类型是void(即没有返回参数)。所以void(*)()是一个"指向没有参数,返回值为void的函数指针"类型。它加了一个括号放在0前面,即(void(*)())0,含义是把0强制转换为该类型的函数指针,我们可以把此记为p1,所以p1是个函数指针。

在c语言中,*有如下两种含义:

(1).在定义变量时放在变量前边跟变量结合表示变量为指针;例如: int *p = &i;

(2).在使用指针变量时放在指针变量前表示解引用。例如:*p=2。

可以看出,这里我们没有定义变量,而是放在了指针变量(void(*)())0的前面,即*(void(*)())0也就是*p1,前边分析函数指针变量时,提到过对函数指针解引用,相当于调用函数,调用的这个函数没有参数,返回类型为void,即*p1()。而由于()优先级较高,会先与0结合,所以要把*(void(*)())0括起来即(*(void(*)())0)以防止0和后边的()先结合。

综上所述,因为0是数字,强制类型转换为指针后就表示地址,所以这句代码的含义为调用0地址处的函数,这个函数不需要传入参数,这个函数的返回类型是void。

3.2.2案例2

再来看另一个案例:void (*signal(int,void(*)(int)))(int); //来源《c陷阱与缺陷》一书

初看代码感觉很复杂,但是可以由内到外逐层分析。让我们再次回忆一下add函数的定义,我们在定义add函数时,其格式如下int add(int x,int y),其中add为函数名,(int x,int y)为给函数传入的参数类型,去掉函数名add和(int x,int y),剩下的int即为函数add的返回类型。

同样的,在上边这段代码中,由于()的优先级更高,所以signal先与()结合,表明signal是个函数,即signal即为函数名,该函数接收两个参数(int,void(*)(int)),这两个参数的类型一个是int,一个是void(*)(int)(可以看出这是个函数指针类型,其指向的函数接收一个int类型的参数,返回值为void,即这个类型是“指向一个接收int类型返回值为void的函数指针类型”),去掉函数名signal和其接收的参数类型(int,void(*)(int))后,剩下void (*)(int),所以signal函数返回值的类型为void (*)(int)。所以上述代码是一个函数声明。4指针数组

4.1整型指针数组

数组指针,指针数组,听起来像是在玩文字游戏,但由前边的介绍,数组指针是用来存放数组地址的指针,函数指针是用来存放函数地址的指针,整型数组是用来存放一组整型的数组,所以指针数组就是用来存放一组指针的数组。可以看出来,谁放在前面,谁就是一个修饰作用。那么,指针数组该如何定义?

让我们来回忆一下整型数组的定义,例如定义一个可以放五个整型变量的数组,我们有如下代码:int arr[5] = {1,2,3,4,5};

其中arr是数组名,[5]表示数组放了五个元素,int表示这些元素的类型是整型。所以,如果我们要定义一个可以存放三个整型指针的数组,则有:int* arr1[3] = {&i,&j,&k};

arr1表示变量名,因为[]的优先级更高,所以arr1优先与[]结合,表明arr1是个数组,去掉数组名arr1,剩下的int * [3]即为arr1的类型,从arr1向右看是[3],表明这个数组放了3个元素,向左看是int *,表明这三个元素的类型都是int *即整型指针。对于其他数据指针类型定义同理。

4.2数组指针数组

顾名思义,是用来存放多个数组指针的数组,如何定义呢?

让我们重新审视一下整型数组以及整型指针数组的定义。当我们需要定义一个整型变量时:int i = 2;

当我们需要定义一组整型变量时,即要把一组整型变量放在一起变成数组时,在上述代码的基础上,只需在紧挨着变量名右侧加上[],在[]里放上我们需要的整型个数,即int arr1[3] = {0, 1, 2};

我们采用了同样的方式定义了整型指针数组:int *pa = &a;

pa为一个整型指针,我们定义整型指针数组,只需在紧跟着变量名后边加上[]和需要的个数,即int *parr[3] = {&a, &b, &c};

parr[3]即为一个含有三个整型指针的整型指针数组。

因此,我们可以用同样的方法定义一个数组指针数组。例如:int arr1[3] = {0, 1, 2};

int arr2[3] = {0, 1, 2};

int arr3[3] = {0, 1, 2};

这是三个元素个数相等的整型数组,它们对应的数组指针类型都相等(数组只能放同类型的数据),即为int (*)[3];数组arr1的数组指针就可以定义为int (*p1)[3] = &arr1;

我们紧跟着变量名加上[]即可构造数组指针数组:int (*parr[3])[3] = {p1, p2, p3};

上述代码的含义:[]优先级较高,所以parr先和[3]结合,说明parr是个数组,去掉变量名parr后剩下int (*[3])[3],这个即为parr的类型,与parr紧挨的[3]说明数组里有三个元素,去掉parr和与parr紧挨的[3],剩下int (*)[3],这即为parr里边的元素类型,显然这个元素类型是数组指针类型。

4.3函数指针数组

函数指针数组是用来存放函数指针的一个数组。有了上边的分析,让我们来快速的写出一个函数指针数组(数组是用来存放同一类型的数据,所以存放的指针也要是同一类型)。

现在有四个函数,分别是加减乘除,它们的功能分别是用来计算两个整数的加减乘除,并将计算结果返回(返回值为整型),即:int add(int x, int y);

int sub(int x, int y);

int mul(int x, int y);

int div(int x, int y);

这四个函数的参数接收类型都为两个int,返回值为int。所以它们的函数指针类型相同,都为int (*)(int, int),所以add的指针可以写为int (*padd)(int, int) = add;

在紧挨着变量名的右侧加上[]即可变为函数指针数组:int (*pfun[4])(int, int) = {&add, sub, mul, div};//在上边介绍函数指针时提到过取地址函数名和函数名本身都代表函数地址,所以这里写成不同的就是为了再次提醒读者两者等价,实际使用时按一种风格书写即可。5指向数组指针数组的指针

这句话乍一读很是拗口,让我们来细细分析,首先最后落向了指针,所以这是一个指针,然后这个指针指向的是一个(数组指针数组)。如何定义呢?

前文说过,在定义某数据类型变量对应的指针时,只需在变量名前加上*即可,所以对于上文介绍的数组指针数组int (*parr[3])[3] = {&arr1, &arr2, &arr4};

只需在变量名前加上*即可变成对应类型的指针,即(由于[]优先级高一点,所以要将*和变量名括起来提高优先级)int (*(*pparr)[3])[3] = &parr;

分析:*与pparr结合,说明pparr是一个指针,去掉变量名pparr,剩下的int (*(*)[3])[3]即为pparr的类型从(*)向右看是[3]说明pparr指向了一个含有三个元素的数组,去掉(*)[3]剩下的int (*)[3]即为该数组里的数组元素类型(是指针,指向一个有三个元素的数组,元素类型为int)。

同样,我们可以定义一个指向数组指针数组的指针数组,假设有三个与pparr同类型的指针pparr1,pparr2,pparr3.那么只需在指向数组指针数组的指针的变量后边加上[3]即可定义一个指向数组指针数组的指针数组:int (*(*pparr1[3])[3])[3] = {pparr1,pparr2,pparr3};

....

我们可以按照上述方式,不断的“套娃”下去,但此时已经意义不大,因为实际操作中很难会写出这样的代码,即使写出来,也会很难维护(容易把人绕晕)。6指向函数指针数组的指针

同指向数组指针数组的方法一样,我们快速的写出int (*pfun[4])(int, int) = {add, sub, mul, div};

数组pfun[4]的指针:int (*(*pfun1)[4])(int, int) = &pfun;

类似的,我们也可以写出指向函数指针数组的指针数组,此处就不在赘述。7多级指针

聊完了上述让人头大的东西,让我们再来聊一些轻松愉快的东西。我们说,对于一个整型,可以定义一个指针,即int a = 2;

int *pa = &a;

我们称pa里存放了a的地址,那我们也想把pa的地址存起来,是否可以呢?答案是当然可以,按照我们前述的定义方式,我们在变量前加上*即可表示一个指针:int **ppa = &pa;

那么ppa里存放的就是指针pa的地址。pa存放了变量的地址,我们把pa称作一级指针,ppa存放了一级指针pa的地址,我们把ppa称作二级指针,同理,我们把存放ppa地址的指针就称为三级指针,以此类推。

我们对ppa进行解引用,拿到了pa,然后对pa再解引用就可以找到a,即**ppa=3;

就等价于a = 3;

由于时间问题,本文到此就结束了,对于结构体指针即其他指针相关的知识,有时间再叙。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值