指针闲谈
本文将采用按部就班的方式,来简要谈一谈C语言中指针的定义和分析。算法
谈到c语言,就绕不开c语言中的一把利器--指针。数组
指针能够直接指向物理内存地址,对内存进行操做。在计算机中,咱们把内存划分为一个个小的单元,每一个单元对应一个编号(或者地址),而指针能够利用地址,直接找到该地址对应的变量值。通俗的理解就是指针像门牌号,咱们能够经过门牌号找到对应房间,从而找到房间里的人。ide
所以,指针也是一个变量,用于存放地址的变量。在《c语言数据存储》一文中已经分析了,物理内存中是以一个字节为一个单元,所以,咱们能够把内存想象成一辆在铁轨上的火车,每节车箱就至关于一个内存单元,在车箱内部,咱们安装上八张连续的椅子。对每一个车箱进行编号,第一节车箱为0X00000000,第二节车箱为0X00000001,依次类推,该编号为16进制,最后一节车箱编号为0XFFFFFFFF。能够看出一个指针变量所占的空间为8字节。(以32位平台为例,下文若是没有特殊指明,均按32位平台)函数
1指针类型
1.1如何定义指针变量?
首先,让咱们来看一看c语言中是如何定义变量的,例如:spa
int i = 10;
咱们定义了一个变量,其中i为变量的名称,int为该变量的类型,10为该变量的值。能够看出,在等号的左边,咱们去掉变量的名称i,剩下的即为变量的类型。变量的类型决定了该变量所占字节大小。int类型的变量占用4个字节。指针
一样的,让咱们来定义一个数组:code
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;
因为时间问题,本文到此就结束了,对于结构体指针即其余指针相关的知识,有时间再叙。