最新消息: USBMI致力于为网友们分享Windows、安卓、IOS等主流手机系统相关的资讯以及评测、同时提供相关教程、应用、软件下载等服务。

逗比老师带你搞定C语言指针

IT圈 admin 1浏览 0评论

逗比老师带你搞定C语言指针

    哈喽!各位同学们大家好哇!逗比老师又回来了!好久都没有见到大家了真是想死我了!

    最近呢,我有一个亲戚,还在读大学,正在学C语言,然后他在我的博客上看到了我之前写过的C教程,结果没有几篇就戛然而止了,于是就攒了很多问题来问我。这里给大家抱歉哈,真的不好意思,逗比老师实在是太忙了,顾不上给大家更新详细的C教程,这个后面慢慢来。不过有些重要的知识点还是可以单独拉出来给大家重点攻克一下的。

    以前我记得有个人来想让我给代课,讲一些关于软件开发的知识,我就问他基础怎么样,C语言会吗?他说,C语言学得挺好的,就除了指针不太会以外。然后我告诉他,那你压根就不会C,从头好好学吧!其实真的是这样,说得夸张一点,C基本上就是玩了个指针,如果你不会指针的话,那不要说你会C!

    好啦,暂时就先扯这么多,我们来进入实际内容。

1. 指针是什么?

    教科书上的概念就不说了,这里想让大家知道的是,C语言因为是一个比较底层的语言,所以,它的很多设计都是更接近于机器的思维的,而不是我们人的思维,所以,如果你能把C当中的很多语法现象用机器运行的方式去解释,而不是用我们人类思考的方式去解释的话,那就方便得多。

    那么我们来回答这个问题,指针是什么?记住!指针就是一个数而已。这就是本质!其实不只是指针,C当中的任意一个数据类型本质上都是数。我们知道计算机当中存储数据,最本质都是比特位,我们以8个比特位作为一个单位来分析(字节)。而所谓的数据类型,只不过是为了照顾程序员,编译器按照一种特殊的方式来读取这部分的数据罢了。

    举一个简单的例子来说:有这样一个字节:10010010,你能知道它表示什么意思吗?要想知道意思,我们就得知道它的类型,因为,同样的是这一个字节,类型不一样意义不一样,假如说它表示一个无符号整数,那么他应当是146,如果它表示一个有符号的整数,那么他应该是-109,如果它表示字符,那应该是'm',所以,数据的本质就是一个数,而类型就是我们解读这个数据的方式。

    指针也不例外,他的本质就是一个数,但是这个数表示的含义并不是数值,而是表示一个内存地址。这里啰嗦几句,计算机主要存储数据的地方就在内存中,因此内存也被成为主存,而我们要向知道这个数据到底在内存的什么位置,就需要对内存进行编号。我们把每一个字节,也就是8个bit作为一个单位进行编号,假如说一个4GB的内存,那么他的地址就应该是从0x00000000到0xFFFFFFFF,换算成十进制也就是从0到4294967295。照理来说,应当只有内存才有地址,但是在有些架构的设备上,采用了统一编址,也就是将一些其他硬件(例如寄存器)也编上了内存地址,我们常用的x86体系中,也是把显存和内存一起编址的,不过这些具体的硬件实现不影响我们上层逻辑,我们还是认为这个地址就是内存地址。

    所以,所谓的指针类型,其实就是存了一个可以表示地址的数,仅此而已。

2.指针类型有多大?

    在讲解这个问题之前,需要先了解一个概念,就是CPU的字长,我们常说的32位CPU还是64位CPU,其实指的就是CPU的字长,又或是叫做寻址空间,也就是说,CPU可以通过多少位二进制来表示一个内存地址,然后访问它,当然了,这里说的是理论上最大。

    我们假如CPU的寻址空间只有1位,那么这1位就只能表示0和1这两个地址,那么也就是说,1位CPU的寻址空间是2字节。同样的,如果是2位CPU,那么可以表示00,01,10,11这4个地址,也就是说,2位CPU的寻址空间是4字节。以此类推,n位CPU的寻址空间应当是(2^n)个字节,那么我们计算一下32位CPU的寻址空间应当是4294967295,也就是0xFFFFFFFF,也就是4GB,换句话说,32位CPU最大支持4GB的内存,然而这里面还有一部分要分给显存等其他硬件,实际可用的内存也就不够4G了,这也是我们为什么一定要升级到64位CPU的原因,因为在这个年代,4GB的内存显然已经不够用了。而如果是64位CPU,计算一下,可以访问的空间理论上来说最大有16EB,没见过EB这个单位吧?1EB=1024PB=1024*1024TB,TB总该见过了吧!现在有个10TB的硬盘已经感觉很大的,这家伙可是10多万TB,所以足够发展几十年了!

    那么,既然指针是用来表示地址的,那么它总得有一个能放得下所有地址的长度吧!所以,32位系统下指针是8字节(也就是32位),64位系统下指针是16字节(也就是64位)。什么?你看到的打印只有5位!还有7位的?不要惊讶,那只不过是你格式符调的让他省去了前面的0而已,可以试试用sizeof运算符计算一下,就知道我说的对不对了:

printf("%size of pointer is:lu\n", sizeof(char *));

 3.指针的类型

    其实这个标题是有问题的,C语言就只有一种指针类型,所有的指针都是这一种指针类型,所以,也就没有什么所谓的指针的类型,但是我看到很多数据和资料,包括很多人都这样说了,那我也就入乡随俗这么跟着叫吧。所谓的指针的类型,并不是说这个指针本身的类型,而是表示的是这个指针所指的对象的类型,换言之,就是假如有一天,我们想通过这个指针找到实际那个地址中的数据的时候,我们应该把那个数据当做什么类型来处理。举例来说:

int a = 146; // 假如a的地址是0x0000FF01
int *p = &a; // p的值就是0x0000FF01

    需要注意的是,这里p的类型,int *,是我们自己加上去的,也就是说我们告诉编译器,p存放了一个地址,并且这个地址里的数需要按照int的方式解析。当然,我们也可以很任性地给p换成别的类型,比如说:

unsigned short a = 36864;
unsigned short *p = &a; // *p是36864
short *p2 = (short *)&a; // *p2是-28672
char *p3 = (char *)&a; // *p3是'\0'
unsigned *p4 = (unsigned *)&a; // *p4不确定

    那么这里,我们看到,p,p2,p3,p4的值都是&a,也就是说,这四个指针的值是相等的,都等于a的地址,我们说他们都“指向”a,但是,我们分别对他们进行解指针运算以后,为什么得到了不同的结果呢?首先,我们需要了解的是,a这个变量,到底在内存中怎么存储的。

    我们看,a是一个unsigned short类型,我们知道短整型占2个字节,也就是说,存储36864这个数,应该有2个字节的,这2个字节应该分别有自己的地址。我们假设这两个地址分别是0xFF00和0xFF01。我们把36864转换成二进制,应当是1001 0000 0000 0000,也就是0x9000。在x86体系的计算机中,一般采用大段序,也就是高位存在低字节,那么也就是0xFF00这个地址存放的是0x00,0xFF01这个地址存放的是0x90。那我们给a进行取值运算,到底取的是哪个呢?是这个变量的首地址,也就是0xFF00,所以,&a的值是0xFF00,那么,p,p2,p3,p4也就都是0xFF00。既然,这个指针变量中仅仅存的是一个首地址,那么,当我们需要解指针的时候,编译器怎么知道,这个地址所表示的内存究竟是单独一个字节作为一个数呢?还是连续两个字节表示一个数呢?还是连续n个字节表示一个数呢?首位究竟是表示数值还是符号呢?这个数究竟是表示整数呢还是浮点数呢?答案是,不知道!因为我们现在仅仅知道这个0xFF00是某一个数据的首地址,仅此而已,这时候我们是不能够进行解指针运算的,原因就像刚才说的,计算机并不知道怎么处理这个内存中的内容。

    所以,我们需要告诉计算机,以何种方式来读取这个地址中的内容。这里p是unsigned short *类型,也就是我们指明,用“无符号短整型”的方式来读取0xFF00中的内容,那么,所谓的“无符号短整型”的方式,也就是说,数据保存在连续的2个字节中,且0xFF00表示低位,0xFF01表示高位,并且,最高位表示数据。计算机就会将其整合成+1001000000000000,也就是36864。

    同样的道理,p2是short *类型,也就是我们指明用“有符号短整型”的方式来读取,同样的是连续两个字节,但是首位表示符号,那么计算机就将其整合为-(0010000000000000),由于负数是以补码形式存在的,因此求其补,也就是-1110000000000000,也就是-28672。再来说说p3,char *类型,表明用字符形式读取,连续的一个字节,因此,只会读取0xFF00这一个字节,这个字节的内容是0x00000000,也就是0,但是,这里是字符,所以应该是0对应的ASCII码,也就是字符串结尾符'\0'。p4也是同理,但是unsigned类型是读取连续4个字节,当然了,这里我们无法预见0xFF02和0xFF03中的内容是什么,所以你运行出来的结果可能每次都不太一样,但是,原理都是,把后面两个当做了次高位和最高位,然后首位当做数值计算出来的结果。

    所以,说了这么多其实就想让大家明白一点,指针本身只是一个数,一个可以表示内存地址的数,仅此而已,所谓的“指针类型”,其实指的是,解指针操作时使用的数据解析方式。那么我们有没有办法定义一个赤裸裸的指针?也就是说,我们仅仅说明它是个指针,但并不制定它指向的数据的类型。有的!那就是void *

4. 泛型指针

int a = 8;
void *p = &a; // p仍然是指针,值仍然是a的地址
// *p = 0; // error

    所以这个的void *和其他的写法一样,还是表示,p是一个指针类型,里面存的数是表示一个内存地址的。只不过区别在于,使用void *定义的指针不能够进行解指针运算,道理很简单,因为我们没有告诉计算机到底用哪种方式来解析这个指针所指向的数据。将这样的指针,教科书上普遍称为“泛型指针”,之所以这样命名,我想可能是因为他觉得这样的指针什么类型都能指所以叫“泛型”。但是,根据我之前的描述,相信大家也都能看出来,其实根本就不存在泛型不泛型这一说,&a就是一个简简单单的数而已,你存到什么类型的指针下都是允许的,甚至,我们还可以存到一个整型变量里,像这样:

int a = 0;
unsigned long long p = (unsigned long long)&a;

    这样写一点问题都没有,这样我们定义的p,同样是a的地址,但是我们之后可以把它当做一个普通的整数来对待。当然了,反过来一样可以,比如这样:

unsigned long long a = 0xFF00;
int *p = (int *)a;
*p = 0xAE08;

    如果你很勤快的话,读到这里你一定会在心里偷偷骂逗比,说,我试过了!这样明明不行,你看,我的编译器都给我报错了!唉,逗比心里苦啊。你先别急,你真的都比逗比了。要想解释为什么,首先我们需要读懂这3行代码。第一行,定义了一个无符号64位整型变量,这没什么说的。第二行,把a的值赋值给了p。那么p的值就是0xFF00,但是因为p是指针类型,所以,表示的含义就是0xFF00这个内存地址了。第三行,解指针p,也就是给0xFF00这个内存地址(由于是int,也包括后面3字节)的位置按照大段序存放0xAE08,并且首位表示符号。也就是说,给0xFF00存0x08,0xFF01存0xAE,0xFF02存0x00,0xFF03存0x00。这个绝对可行,但是为什么你在IDE上这一行会报错呢?那是因为,我们在IDE上编译出的程序默认是应用程序,应用程序是受控于操作系统的,也就是说,操作系统负责这个进程的内存管理,你只能用操作系统分配给你这个进程的内存,而不能使用其他的内存。因此,我们指定他往0xFF00这个地方写数据,显然就越权了,所以,这个操作被禁止了。如果我们写的程序是运行在16位实模式下的,那么这样是完全OK的,程序运行到此句的时候,就会给0xFF00-0xFF03这4个字节中写数据。

    说来说去,还是再强调一下指针的本质,就是一个数,就这么简单。

5.多级指针

    我相信,教科书上,甚至很多资深老专家在讲解C指针的时候,都一定会把多级指针列成一个专门的一节,然后去给你非常耐心地讲解什么是单级指针,什么是多级指针,它们之间有什么不同,使用时应该分别注意什么问题。所以我在这里也使用这样一个标题。但是,说真理不怕得罪人。其实根本就不存在什么单级多级指针,就像我之前说的,指针其实就一种类型,就是指针类型。单级指针也好,多级指针也好,都是指针,所以,逃不出这个本质,他存的就是个可以表示内存地址的数,仅此而已。

    那么,我们常说的这个多级指针是什么呢?请看下面的例子:

int *a = 0;
int *p = &a;
int **q = &p;

    我想这应该是很多人见过的讲解多级指针很经典的例子。p是单级指针,q是多级指针。为什么呢?因为p前面是1颗星,q前面是2颗星。emmm....这种解释,乍听起来确实有道理,但其实这和几颗星星没什么关系。我们来解读一下这3行代码。前两行不用解释了,直接看最后一行。q是个(暂时我们就先按习惯叫二级)指针吧,它的值是p的地址。而p,我们不管他是不是指针,也不管他多少级,总之不可否认的是,p在这里是一个变量,就和a一样,都是一个普通的变量,里面存着某一个值。那么,既然p是个变量,那么这个变量也肯定是要在内存中存储的吧。既然要在内存中存储,那这片内存空间也肯定是有自己的地址的吧。举个例子来说,假如a是在0xFF02这个位置,p在0xAF00这个位置,那么,a的值是5,所以0xFF02就是0x05,0xFF03-0xFF05都是0x00,然后,p的值是a的地址,也就是0xFF02,所以,0xAF00的值是0x02,0xAF01的值是0xFF,0xAF02和0xAF03都是0x00,用个表格来表示如下(为了方便,以下指针用32位系统中的。如果是64位的类似,只不过要占8字节):

地址备注
0xAF000x02p的最低字节
0xAF010xFFp的次低字节
0xAF020x00p的次高字节
0xAF030x00p的最高字节
……  
0xFF020x05a的最低字节
0xFF030x00a的次低字节
0xFF040x00a的次高字节
0xFF050x00a的最高字节

    看了这张内存分布表,我想大家应该更明白了,这个指针p不是何方妖孽,就是个普普通通的变量而已,和a一样的。所以,我们给p进行取址运算,就会得到p的首地址,也就是0xAF00,然后,我们再把它存在q当中。假如q的地址是0x8400,那么补充上表:

地址备注
0x84000x00q的最低字节
0x84010xAFq的次低字节
0x84020x00q的次高字节
0x84030x00q的最高字节

    所以我们看到了,q也是一个普普通通的变量而已,和p和a都一样。那么这个类型定义前面让人费解的两颗星到底是什么呢?是这样的,C语言中,如果一个类型是Type,那么如果我们定义一个指针,并且指定用Type这种方式来解指针的话,我们就将这种指针类型定义为Type *,所以,a是int型,p是a的指针,因此p的类型就是int *; 而q是p的指针,p的类型是int *,所以q的类型就是int **。如果还是不太清晰的话,不妨我们介绍一个技巧,typedef重命名变量类型,假如我们将“指向int型变量的指针”这种类型定义为Type1,那么就有:

typedef int *Type1; // Type1就表示int *
int a = 0;
Type1 p = &a;
Type1 *q = &p;

    我们把int *类型表示为Type1,那么自然,p就是Type1类型的,这个Type1就相当于一个新的类型,可以和int, double, char这些同方法使用。那么既然q是指向p的,也就是说,当我们对q解指针的时候,要按照Type1这种方式解,那么自然,q的类型就是Type1。现在我们再来说,q是一级指针还是二级指针?显然没有意义了。

    指针问题把握住两点,第一,指针就是个数,再普通不过的数;第二,指针类型其实都是同一种类型,所谓的类型只是指定解指针时使用的数据读取方式。这就OK了。

    至于明明都是一样的地址,为什么要进行强转操作,这是因为控制类型安全,这是编译器控制的,它怕你写错,毕竟,我们从int *转化成char *这种操作还是不常见的,所以,显式写出来防止出错。默认情况下,Type类型取址得到Type *类型,Type *类型解指针得到Type类型,所谓的多级指针,把星放在类型里,整体替换那个Type就行了。如果显式强转的话,那就随意了,根本没有类型和所谓级数的限制,我们把char ***转成int *,甚至转换成int都没有任何问题。

6.数组指针

      如果你前面的都听懂了,或者是,熟悉我的套路的话,应该就能猜到我下面想说什么了。没错,数组指针也是个指针,只不过解指针的时候用“数组”这种方式来解。把握住这个原则就错不了,相信逗比!

    不过数组指针的语法可能稍微复杂一些。在详细介绍之前,先给大家灌输一个印象,就是说,C语言中的类型表示(我这里单纯指的是形式上)有两种,一种叫简单类型,一种的复杂类型。(不要嫌我啰嗦,再强调一遍,这是形式!形式!不是实际的简单复杂,只是形式上。其实他们可以转化的。)使用简单类型定义变量(或者是别名)的时候,类型在左,变量名(或者别名)在右。复杂类型时,类型在两边,变量名(或者别名)在中间。什么意思呢?我们看下面这个例子:

int a; // 简单类型
int b[3]; // 复杂类型

    int就是一个简单类型,我们定义变量的时候,int在左,a在右。简单类型的指针类型同样是个简单类型,比如说int *, int **等。而b是一个复杂类型,b的类型其实是int [3]类型,也就是说左边的int和右面的[3]共同表示b这个变量的类型。复杂类型的指针一般也是个复杂类型,比如说现在要将的数组指针。

int b[3];
int (*p)[3] = &b;

    b的类型是int [3],解释为,一个拥有3个整型元素的数组类型。管他是什么什么数组还是什么,归根结底就是个类型呗,那b也就是个变量呗,是变量就要存在内存里呗,存在内存里又得有首地址呗,那&b不就表示这个b的首地址了嘛。我们自然可以有一个指针来存这个地址,那要是想解指针之后得到一个int [3]类型,那么这个指针的类型就是int (*)[3]。大家注意这种写法,C语言中遇到复杂类型时有一个解读原则,就是先找小括号,如果有小括号,那么,以小括号为起点,如果没有小括号则找中心,以中心为起点,从里向外读。比如说这里int (*)[3]类型,先找到小括号,小括号里括着一颗星,那么就表示,这种类型应该是{外面的类型}的指针。那么,外面的类型是什么?是int [3]。所以,这种类型就是一个int [3]类型的指针,解释为,一个指向{拥有3个整型元素的数组}的指针类型。所以简称为数组指针,那么,它是指针,自然就满足我之前说的所有的这一切的特性,不再赘述。我们再来看下面这个例子:

int *c[3];

    请问c是什么类型?也就是说这个int *[3]类型怎么解释呢?按照刚才的原则,没有括号所以找中心。中心怎么找呢?如果规范来写,变量名本身中心,如果不规范写或者省略了变量名的话,这时候大家也不要怕,中心的左边一定是一个单词(就是找英文字母啦)或是*,中心的右边一定是括号。所以,单词或*和括号的分界处就是中心。所以这里的中心就是*和[的中间,往外一层发现左边是*,右边是[],到底跟哪个呢?这里另一个原则是,星号*的意义跟着左边走。虽然我们在书写的时候,规范要求是把*贴右来写,但是,在解释类型的时候,*的意义要跟左边走。所以,这里的*应该和左边是一个整体,那么自然就应该先读[3]了,也就是说,这是一个有3个元素的数组。然后再往外,右边空了,左边还有一个int *,于是,这种类型被解释为:一个拥有3个{指向整型的指针}类型的元素的数组。简称为指针数组,所以,它是数组,并不知指针,但它的元素是指针。也就是说,b是数组,但是b[0]就是指针。

    我刚才提到说,规范的写法是*要贴右,但是解释的时候要贴左,这是为什么呢?因为C语言允许这样的语法:

int a, *b, **c; // a是int,b是int *,c是int **
int *d, e, f; // d是int *,e和f是int

    怎么说呢,这样用起来有时候还挺方便的,但是,可能C语言的设计者也没想这么多,可能是一种设计缺陷吧,所以,如果你*贴左的话,下面这种写法可能有容易让人误会:

int* a, b, c; // a是int *,b和c都是int

    上面这行代码可能就会让人误以为a,b,c都是int *,而其实只有a是int *。当然,这还没完,还有更容易被误解的写法:

int (*p)[3], a, *b;

    有木有晕掉?哈哈,这一行代码,p是一个数组指针,a是int,b是int *。要不要来点更刺激的?

int* p, (*q)[3], *r[3];

    你能顺利解析出p,q和r的类型吗?这里p是一个int *类型,q是一个数组指针,r是一个指针数组。所以,为了避免这种反人类的写法误导他人,我们还是别这么搞了!小心过几天你自己都看不懂……

    不过为了把问题彻底搞懂,我们再来看另外一种方式的反人类的类型,请看下面的代码,解释q是什么类型:

int (*(*p[3]))[4];

    按照我刚才介绍的方法,看看你能不能正确解析出p的类型,答案是:p是一个有3个{指向{指向{有4个整型元素的数组}类型的指针}类型的指针}类型元素的数组。如果你这都能答对,那么恭喜你,你升华了!如果没有,没关系,杀手锏还没用呢。遇到这种比较复杂的类型,建议大家不要直接这么去写,因为真的太反人类了,还是用typedef吧,按照刚才说的原则,新类型名和变量名放的位置是一样的,比如说上面那种反人类的类型,就可以用下面的方法来定义:

// int (*(*p[3]))[4];
typedef int Type1[4];
// Type1 *(*p[3]);
typedef Type1 *Type2;
// Type2 *p[3];
typedef Type2 *Type3;
// Type3 p[3];

    是不是看起来舒服多了?OK,关于数组这里还有一点要强调的就是,当一个数组类型被写在函数参数中,这个数组类型会自动转化为指针类型,也就是一个Type [n]类型的数组,无论n是几,写在参数中都会变成Type *。值得注意的是,仅仅是参数本身是数组类型才会转化为指针,和数组元素的类型没有关系,下面举个例子:

void test(int a[5],int b[],int *c[3],int (*d)[3],int **e,int **f[3],int *(*g)[4],int h[2][3],int i[4][7][9]
) {// a -> int *// b -> int *// c -> int **// d -> int (*)[3]// e -> int **// f -> int ***// g -> int *(*)[4]// h -> int (*)[3]// I -> int (*)[4][7]
}

    总之一句话,只有数组才会变成相对应的指针,如果它本身不是数组,那么该是什么还是什么(当然也包括指针)。正是因为C语言的这种设计,才造成了我们如果给一个函数传数组的话,就不得不再专门传一个参数来表示元素个数以防止缓冲区溢出。因为虽然形式上是数组,但其实传递的是数组首元素的指针,所以,并不能从这个首元素的指针这个参数上得知数组的元素个数以及数组上介。

    还有一点值得注意的是,数组类型转化为其对应的指针类型(也就是数组首元素的指针类型),换句话说就是Type [n]类型转化为Type *类型不需要强转,可以隐式转换,比如下面的例子:

int arr[3];
int *p = arr; // 不用写成int *p = (int *)arr;

    觉得自己把上面的都掌握了,很高兴是吗?那我告你,高兴太早啦!我还没讲完呢!

7. 函数指针

    我们知道函数是用来存储代码块的,不过熟悉冯·诺依曼体系结构的同学应该都清楚,在我们当前这种计算机体系下,数据和指令拥有二进制等价性,也就是说,数据和指令本身没有本质区别,只不过我们把它认为是数据时就可以进行数据的操作,认为是指令时就可以执行(当然就是机器码层面的执行了)。所以,函数这种东西,它用来存储代码块,也就是用来存储指令呗。那既然指令和数据没啥区别了,那它应该和其他的变量(也就是数据)一样嘛,也是存在内存里的嘛,也有首地址嘛,所以也就有对应的指针类型嘛。所以,函数指针存储的就是这段函数的首地址,自然,我们可以通过一个地址找到一个函数,然后按照函数的执行方式执行它。

int sum(int a, int b) {return a + b;
}

    假如我们有这样一个像上面这样的函数,要想获取地址怎么办呢?这里需要注意的是,对于变量,变量名本身代表的是变量的引用(也就是代表实体),而要表示地址,需要进行取址运算,也就是&符号。但是函数不一样,对于函数,函数名本身就代表的是函数的地址,而函数名后面加小括号则表示执行函数。所以,我们可以用这样的方式得到sum函数的地址:

void *p = sum; // 直接写函数名就表示函数首地址了,但是注意不能写sum(),或者sum(1, 2)这样

    不过这样写有点逃避主题了,虽然说指针都是一种类型,这样用泛型指针来存储没有问题,但是毕竟,我们存指针的目的,还是需要在适当的时候来通过指针找到实体的(也就是解指针),所以,还是应该掌握实际的函数指针类型的书写方式:

int (*p)(int , int) = sum;

    这里的写法和数组指针类似,函数类型也可以看做一个复杂类型,只不过这里不是数组的中括号了,而是参数列表的小括号。对于数组来说,左边的部分表示元素类型,右边表示数组上界的元素个数。而对于函数来说,左边是返回值,右边的参数列表。记住这样的原则就没问题了。当赋值过以后,我们可以通过给p加参数来调用sum函数的方法。比如说p(1, 2),其实就等于sum(1, 2)。

    但是,并不能把p和sum当做相同的东西,因为p的本质还是指针,我们也可以给p进行取址操作,还可以给p赋值,比如说

int (**q)(int, int) = &p; // 取址
p = NULL; // 赋值

    p也满足所有变量的作用域和生命周期。但函数一定是全局的(特指C函数哈,C++可不一定),作用域也仅仅和static关键字有关,也不能取址和赋值,所以它和指针是不一样的语法体系,要区分开。

    好啦,准备好迎接挑战了吗?试试下面几个变量类型的解释吧:

int (*(*p)[3])(int (*)[5]);
int ((*q)(int *))(int (*)());
int r(void (*)(int (*)()));

    emmm...如果实在看不出来的话,就别为难自己了吧,实际应用的时候记住typedef!这里公布一下答案吧:

    p是一个指向{拥有3个{指向{返回值为int,参数为{一个参数,第一个参数是{一个指向{存储了5个整型元素的数组}类型的指针}}的函数}类型的指针}类型元素的数组}类型的指针。

    q是一个指向{返回值为指向{返回值为整型,参数为{一个参数,第一个参数是{返回值为整型,参数为空的函数}}的函数}的指针,参数为{一个参数,第一个参数是一个指向整型的指针}的函数}类型的指针。

    r是一个返回值为整型,参数为{一个参数,第一个参数是{一个指向{返回值为空,参数是{一个参数,第一个参数是{返回值为整型,参数为空的函数}}的函数}类型的指针}}的函数。(这是一个函数声明)。

8.指针综合应用示例

    呼~~~~好啦,终于是把指针讲完了,最后,我们来实战一下吧。看问题:

    请实现一个函数,返回两个参数中较大的一个,需要支持任意类型。(默认大端序存储)

    这个问题很简单,但是难点就在,需要支持任意类型。如果确定了类型,那这个代码自然好写,但是不确定类型,我们就需要通过一些参数来确定这个类型的性质。C语言没有类似于C++的模板这样的语法,不支持泛型编程,因此,我们只能从单个字节入手。由于题目制定了大端序,我们就不用判断字节序了。首先变量的长度是需要知道的,其次,需要比较的两个变量的首地址是需要知道的。除此之外还有一个非常重要的因素就是,我们得知道,这种类型下,到底什么才算大,什么才算小。这是什么意思呢?如果我们处理的类型是数值,那么,自然对于大小比较有着数学上的定义,但是,万一不是数值呢?万一这个类型是个结构体呢?比如说它表示学生信息,两个学生信息到底怎么算大怎么算小?这些也是我们需要知道的。

    所以,总结下来,我们的函数应当接受4个参数,分别是:

1. 需要比较的第一个变量的首地址

2. 需要比较的第二个变量的首地址

3. 要比较的类型的长度

4. 要比较的类型的大小判断方法

    返回值是较大的变量的地址。

    前两个参数,由于我们不知道变量类型,所以,只能使用泛型指针void *,当然,返回值也应该是void *,第三个参数好说,整型就好(当然也可以传无符号整型,这个的选择依赖看你想支持的数据的最大长度,二来时看你想不想用类似于-1这样的负数来表示异常值,这里就不详细说明了)。第四个参数,由于是一种判断方法,那么,调用我们这个程序的人,应该是用函数实现的,通过传两个参数,来判断第一个参数是否大于第二个参数,我们需要在函数里调用这个比较函数得到结果,因此,这里应当传入函数指针。

    说到这个函数指针,自然,我们还需要明确这个函数的参数和返回值了。我们要求调用我们接口的人,应当实现一个函数,来判断这种类型下的数据,第一个数据是否大于第二个数据,参数应当是:

1. 需要比较的第一个变量的首地址

2. 需要比较的第二个变量的首地址

    返回值是0或1,表示不大于或大于。

    好了,设计完毕我们就可以开始编码了,我编的代码是这样的:

void *getBiggerOne(void *n1, void *n2, int type_size, int (*isBigger)(void *, void *)) {if (isBigger(n1, n2)) { // 如果n1 > n2return n1;}return n2;
}

    那么,我们可以写一个测试用例,比如说我用一个结构体类型来测试,这个结构体表示学生的信息,而我定义,学校较大的就是学生年龄较大的,可以这样来测试:

typedef struct {int id;int age;char name[20];
} Student;int isBigger_Student(void *n1, void *n2) { // 用来判断n1是否大于n2Student *s1 = (Student *)n1;Student *s2 = (Student *)n2;if (s1->age > s2->age) { // 比较两个Student的方式是比较其agereturn 1;}return 0;
}int main(int argc, const char * argv[]) {Student s1 = {id: 1,age: 15,name: "Jack"};Student s2 = {id: 2,age: 13,name: "Tom"};Student *bigger = getBiggerOne(&s1, &s2, sizeof(Student), isBigger_Student);printf("The information of bigger Student:\nid:%d\nage:%d\nname:%s\n", bigger->id, bigger->age, bigger->name);return 0;
}

    这是输出结果:

The information of bigger Student:
id:1
age:15
name:Jack

    我们成功地将年龄较大的学生打印了出来。

9.总结

    好啦!总算是找到一个机会把指针给大家讲完了,真的是累死逗比了!到现在,不知道你还认同不认同我一开始说的那句话,如果你不会指针,那就别说你会C!真的,C语言有一多半都是在玩指针,只有你指针玩好了,才能说你完全掌握了C语言。

    最后,再次总结要点:指针本身就是一个数,只不过可以表示地址。指针只有一种类型,而所谓的类型只不过是用于解指针时指定数据的解析方法。复杂类型是名称在中间,类型在两边,解析时要从里往外。数组类型在传入函数参数时会转化为数组首元素的指针。

    最后的最后,如果大家还有什么问题欢迎在下方留言,我们来共同探讨。

    【本文为逗比老师全权拥有,允许转载,但是务必在开头标注转载源链接和作者信息,不得恶意拷贝和更改。】

逗比老师带你搞定C语言指针

    哈喽!各位同学们大家好哇!逗比老师又回来了!好久都没有见到大家了真是想死我了!

    最近呢,我有一个亲戚,还在读大学,正在学C语言,然后他在我的博客上看到了我之前写过的C教程,结果没有几篇就戛然而止了,于是就攒了很多问题来问我。这里给大家抱歉哈,真的不好意思,逗比老师实在是太忙了,顾不上给大家更新详细的C教程,这个后面慢慢来。不过有些重要的知识点还是可以单独拉出来给大家重点攻克一下的。

    以前我记得有个人来想让我给代课,讲一些关于软件开发的知识,我就问他基础怎么样,C语言会吗?他说,C语言学得挺好的,就除了指针不太会以外。然后我告诉他,那你压根就不会C,从头好好学吧!其实真的是这样,说得夸张一点,C基本上就是玩了个指针,如果你不会指针的话,那不要说你会C!

    好啦,暂时就先扯这么多,我们来进入实际内容。

1. 指针是什么?

    教科书上的概念就不说了,这里想让大家知道的是,C语言因为是一个比较底层的语言,所以,它的很多设计都是更接近于机器的思维的,而不是我们人的思维,所以,如果你能把C当中的很多语法现象用机器运行的方式去解释,而不是用我们人类思考的方式去解释的话,那就方便得多。

    那么我们来回答这个问题,指针是什么?记住!指针就是一个数而已。这就是本质!其实不只是指针,C当中的任意一个数据类型本质上都是数。我们知道计算机当中存储数据,最本质都是比特位,我们以8个比特位作为一个单位来分析(字节)。而所谓的数据类型,只不过是为了照顾程序员,编译器按照一种特殊的方式来读取这部分的数据罢了。

    举一个简单的例子来说:有这样一个字节:10010010,你能知道它表示什么意思吗?要想知道意思,我们就得知道它的类型,因为,同样的是这一个字节,类型不一样意义不一样,假如说它表示一个无符号整数,那么他应当是146,如果它表示一个有符号的整数,那么他应该是-109,如果它表示字符,那应该是'm',所以,数据的本质就是一个数,而类型就是我们解读这个数据的方式。

    指针也不例外,他的本质就是一个数,但是这个数表示的含义并不是数值,而是表示一个内存地址。这里啰嗦几句,计算机主要存储数据的地方就在内存中,因此内存也被成为主存,而我们要向知道这个数据到底在内存的什么位置,就需要对内存进行编号。我们把每一个字节,也就是8个bit作为一个单位进行编号,假如说一个4GB的内存,那么他的地址就应该是从0x00000000到0xFFFFFFFF,换算成十进制也就是从0到4294967295。照理来说,应当只有内存才有地址,但是在有些架构的设备上,采用了统一编址,也就是将一些其他硬件(例如寄存器)也编上了内存地址,我们常用的x86体系中,也是把显存和内存一起编址的,不过这些具体的硬件实现不影响我们上层逻辑,我们还是认为这个地址就是内存地址。

    所以,所谓的指针类型,其实就是存了一个可以表示地址的数,仅此而已。

2.指针类型有多大?

    在讲解这个问题之前,需要先了解一个概念,就是CPU的字长,我们常说的32位CPU还是64位CPU,其实指的就是CPU的字长,又或是叫做寻址空间,也就是说,CPU可以通过多少位二进制来表示一个内存地址,然后访问它,当然了,这里说的是理论上最大。

    我们假如CPU的寻址空间只有1位,那么这1位就只能表示0和1这两个地址,那么也就是说,1位CPU的寻址空间是2字节。同样的,如果是2位CPU,那么可以表示00,01,10,11这4个地址,也就是说,2位CPU的寻址空间是4字节。以此类推,n位CPU的寻址空间应当是(2^n)个字节,那么我们计算一下32位CPU的寻址空间应当是4294967295,也就是0xFFFFFFFF,也就是4GB,换句话说,32位CPU最大支持4GB的内存,然而这里面还有一部分要分给显存等其他硬件,实际可用的内存也就不够4G了,这也是我们为什么一定要升级到64位CPU的原因,因为在这个年代,4GB的内存显然已经不够用了。而如果是64位CPU,计算一下,可以访问的空间理论上来说最大有16EB,没见过EB这个单位吧?1EB=1024PB=1024*1024TB,TB总该见过了吧!现在有个10TB的硬盘已经感觉很大的,这家伙可是10多万TB,所以足够发展几十年了!

    那么,既然指针是用来表示地址的,那么它总得有一个能放得下所有地址的长度吧!所以,32位系统下指针是8字节(也就是32位),64位系统下指针是16字节(也就是64位)。什么?你看到的打印只有5位!还有7位的?不要惊讶,那只不过是你格式符调的让他省去了前面的0而已,可以试试用sizeof运算符计算一下,就知道我说的对不对了:

printf("%size of pointer is:lu\n", sizeof(char *));

 3.指针的类型

    其实这个标题是有问题的,C语言就只有一种指针类型,所有的指针都是这一种指针类型,所以,也就没有什么所谓的指针的类型,但是我看到很多数据和资料,包括很多人都这样说了,那我也就入乡随俗这么跟着叫吧。所谓的指针的类型,并不是说这个指针本身的类型,而是表示的是这个指针所指的对象的类型,换言之,就是假如有一天,我们想通过这个指针找到实际那个地址中的数据的时候,我们应该把那个数据当做什么类型来处理。举例来说:

int a = 146; // 假如a的地址是0x0000FF01
int *p = &a; // p的值就是0x0000FF01

    需要注意的是,这里p的类型,int *,是我们自己加上去的,也就是说我们告诉编译器,p存放了一个地址,并且这个地址里的数需要按照int的方式解析。当然,我们也可以很任性地给p换成别的类型,比如说:

unsigned short a = 36864;
unsigned short *p = &a; // *p是36864
short *p2 = (short *)&a; // *p2是-28672
char *p3 = (char *)&a; // *p3是'\0'
unsigned *p4 = (unsigned *)&a; // *p4不确定

    那么这里,我们看到,p,p2,p3,p4的值都是&a,也就是说,这四个指针的值是相等的,都等于a的地址,我们说他们都“指向”a,但是,我们分别对他们进行解指针运算以后,为什么得到了不同的结果呢?首先,我们需要了解的是,a这个变量,到底在内存中怎么存储的。

    我们看,a是一个unsigned short类型,我们知道短整型占2个字节,也就是说,存储36864这个数,应该有2个字节的,这2个字节应该分别有自己的地址。我们假设这两个地址分别是0xFF00和0xFF01。我们把36864转换成二进制,应当是1001 0000 0000 0000,也就是0x9000。在x86体系的计算机中,一般采用大段序,也就是高位存在低字节,那么也就是0xFF00这个地址存放的是0x00,0xFF01这个地址存放的是0x90。那我们给a进行取值运算,到底取的是哪个呢?是这个变量的首地址,也就是0xFF00,所以,&a的值是0xFF00,那么,p,p2,p3,p4也就都是0xFF00。既然,这个指针变量中仅仅存的是一个首地址,那么,当我们需要解指针的时候,编译器怎么知道,这个地址所表示的内存究竟是单独一个字节作为一个数呢?还是连续两个字节表示一个数呢?还是连续n个字节表示一个数呢?首位究竟是表示数值还是符号呢?这个数究竟是表示整数呢还是浮点数呢?答案是,不知道!因为我们现在仅仅知道这个0xFF00是某一个数据的首地址,仅此而已,这时候我们是不能够进行解指针运算的,原因就像刚才说的,计算机并不知道怎么处理这个内存中的内容。

    所以,我们需要告诉计算机,以何种方式来读取这个地址中的内容。这里p是unsigned short *类型,也就是我们指明,用“无符号短整型”的方式来读取0xFF00中的内容,那么,所谓的“无符号短整型”的方式,也就是说,数据保存在连续的2个字节中,且0xFF00表示低位,0xFF01表示高位,并且,最高位表示数据。计算机就会将其整合成+1001000000000000,也就是36864。

    同样的道理,p2是short *类型,也就是我们指明用“有符号短整型”的方式来读取,同样的是连续两个字节,但是首位表示符号,那么计算机就将其整合为-(0010000000000000),由于负数是以补码形式存在的,因此求其补,也就是-1110000000000000,也就是-28672。再来说说p3,char *类型,表明用字符形式读取,连续的一个字节,因此,只会读取0xFF00这一个字节,这个字节的内容是0x00000000,也就是0,但是,这里是字符,所以应该是0对应的ASCII码,也就是字符串结尾符'\0'。p4也是同理,但是unsigned类型是读取连续4个字节,当然了,这里我们无法预见0xFF02和0xFF03中的内容是什么,所以你运行出来的结果可能每次都不太一样,但是,原理都是,把后面两个当做了次高位和最高位,然后首位当做数值计算出来的结果。

    所以,说了这么多其实就想让大家明白一点,指针本身只是一个数,一个可以表示内存地址的数,仅此而已,所谓的“指针类型”,其实指的是,解指针操作时使用的数据解析方式。那么我们有没有办法定义一个赤裸裸的指针?也就是说,我们仅仅说明它是个指针,但并不制定它指向的数据的类型。有的!那就是void *

4. 泛型指针

int a = 8;
void *p = &a; // p仍然是指针,值仍然是a的地址
// *p = 0; // error

    所以这个的void *和其他的写法一样,还是表示,p是一个指针类型,里面存的数是表示一个内存地址的。只不过区别在于,使用void *定义的指针不能够进行解指针运算,道理很简单,因为我们没有告诉计算机到底用哪种方式来解析这个指针所指向的数据。将这样的指针,教科书上普遍称为“泛型指针”,之所以这样命名,我想可能是因为他觉得这样的指针什么类型都能指所以叫“泛型”。但是,根据我之前的描述,相信大家也都能看出来,其实根本就不存在泛型不泛型这一说,&a就是一个简简单单的数而已,你存到什么类型的指针下都是允许的,甚至,我们还可以存到一个整型变量里,像这样:

int a = 0;
unsigned long long p = (unsigned long long)&a;

    这样写一点问题都没有,这样我们定义的p,同样是a的地址,但是我们之后可以把它当做一个普通的整数来对待。当然了,反过来一样可以,比如这样:

unsigned long long a = 0xFF00;
int *p = (int *)a;
*p = 0xAE08;

    如果你很勤快的话,读到这里你一定会在心里偷偷骂逗比,说,我试过了!这样明明不行,你看,我的编译器都给我报错了!唉,逗比心里苦啊。你先别急,你真的都比逗比了。要想解释为什么,首先我们需要读懂这3行代码。第一行,定义了一个无符号64位整型变量,这没什么说的。第二行,把a的值赋值给了p。那么p的值就是0xFF00,但是因为p是指针类型,所以,表示的含义就是0xFF00这个内存地址了。第三行,解指针p,也就是给0xFF00这个内存地址(由于是int,也包括后面3字节)的位置按照大段序存放0xAE08,并且首位表示符号。也就是说,给0xFF00存0x08,0xFF01存0xAE,0xFF02存0x00,0xFF03存0x00。这个绝对可行,但是为什么你在IDE上这一行会报错呢?那是因为,我们在IDE上编译出的程序默认是应用程序,应用程序是受控于操作系统的,也就是说,操作系统负责这个进程的内存管理,你只能用操作系统分配给你这个进程的内存,而不能使用其他的内存。因此,我们指定他往0xFF00这个地方写数据,显然就越权了,所以,这个操作被禁止了。如果我们写的程序是运行在16位实模式下的,那么这样是完全OK的,程序运行到此句的时候,就会给0xFF00-0xFF03这4个字节中写数据。

    说来说去,还是再强调一下指针的本质,就是一个数,就这么简单。

5.多级指针

    我相信,教科书上,甚至很多资深老专家在讲解C指针的时候,都一定会把多级指针列成一个专门的一节,然后去给你非常耐心地讲解什么是单级指针,什么是多级指针,它们之间有什么不同,使用时应该分别注意什么问题。所以我在这里也使用这样一个标题。但是,说真理不怕得罪人。其实根本就不存在什么单级多级指针,就像我之前说的,指针其实就一种类型,就是指针类型。单级指针也好,多级指针也好,都是指针,所以,逃不出这个本质,他存的就是个可以表示内存地址的数,仅此而已。

    那么,我们常说的这个多级指针是什么呢?请看下面的例子:

int *a = 0;
int *p = &a;
int **q = &p;

    我想这应该是很多人见过的讲解多级指针很经典的例子。p是单级指针,q是多级指针。为什么呢?因为p前面是1颗星,q前面是2颗星。emmm....这种解释,乍听起来确实有道理,但其实这和几颗星星没什么关系。我们来解读一下这3行代码。前两行不用解释了,直接看最后一行。q是个(暂时我们就先按习惯叫二级)指针吧,它的值是p的地址。而p,我们不管他是不是指针,也不管他多少级,总之不可否认的是,p在这里是一个变量,就和a一样,都是一个普通的变量,里面存着某一个值。那么,既然p是个变量,那么这个变量也肯定是要在内存中存储的吧。既然要在内存中存储,那这片内存空间也肯定是有自己的地址的吧。举个例子来说,假如a是在0xFF02这个位置,p在0xAF00这个位置,那么,a的值是5,所以0xFF02就是0x05,0xFF03-0xFF05都是0x00,然后,p的值是a的地址,也就是0xFF02,所以,0xAF00的值是0x02,0xAF01的值是0xFF,0xAF02和0xAF03都是0x00,用个表格来表示如下(为了方便,以下指针用32位系统中的。如果是64位的类似,只不过要占8字节):

地址备注
0xAF000x02p的最低字节
0xAF010xFFp的次低字节
0xAF020x00p的次高字节
0xAF030x00p的最高字节
……  
0xFF020x05a的最低字节
0xFF030x00a的次低字节
0xFF040x00a的次高字节
0xFF050x00a的最高字节

    看了这张内存分布表,我想大家应该更明白了,这个指针p不是何方妖孽,就是个普普通通的变量而已,和a一样的。所以,我们给p进行取址运算,就会得到p的首地址,也就是0xAF00,然后,我们再把它存在q当中。假如q的地址是0x8400,那么补充上表:

地址备注
0x84000x00q的最低字节
0x84010xAFq的次低字节
0x84020x00q的次高字节
0x84030x00q的最高字节

    所以我们看到了,q也是一个普普通通的变量而已,和p和a都一样。那么这个类型定义前面让人费解的两颗星到底是什么呢?是这样的,C语言中,如果一个类型是Type,那么如果我们定义一个指针,并且指定用Type这种方式来解指针的话,我们就将这种指针类型定义为Type *,所以,a是int型,p是a的指针,因此p的类型就是int *; 而q是p的指针,p的类型是int *,所以q的类型就是int **。如果还是不太清晰的话,不妨我们介绍一个技巧,typedef重命名变量类型,假如我们将“指向int型变量的指针”这种类型定义为Type1,那么就有:

typedef int *Type1; // Type1就表示int *
int a = 0;
Type1 p = &a;
Type1 *q = &p;

    我们把int *类型表示为Type1,那么自然,p就是Type1类型的,这个Type1就相当于一个新的类型,可以和int, double, char这些同方法使用。那么既然q是指向p的,也就是说,当我们对q解指针的时候,要按照Type1这种方式解,那么自然,q的类型就是Type1。现在我们再来说,q是一级指针还是二级指针?显然没有意义了。

    指针问题把握住两点,第一,指针就是个数,再普通不过的数;第二,指针类型其实都是同一种类型,所谓的类型只是指定解指针时使用的数据读取方式。这就OK了。

    至于明明都是一样的地址,为什么要进行强转操作,这是因为控制类型安全,这是编译器控制的,它怕你写错,毕竟,我们从int *转化成char *这种操作还是不常见的,所以,显式写出来防止出错。默认情况下,Type类型取址得到Type *类型,Type *类型解指针得到Type类型,所谓的多级指针,把星放在类型里,整体替换那个Type就行了。如果显式强转的话,那就随意了,根本没有类型和所谓级数的限制,我们把char ***转成int *,甚至转换成int都没有任何问题。

6.数组指针

      如果你前面的都听懂了,或者是,熟悉我的套路的话,应该就能猜到我下面想说什么了。没错,数组指针也是个指针,只不过解指针的时候用“数组”这种方式来解。把握住这个原则就错不了,相信逗比!

    不过数组指针的语法可能稍微复杂一些。在详细介绍之前,先给大家灌输一个印象,就是说,C语言中的类型表示(我这里单纯指的是形式上)有两种,一种叫简单类型,一种的复杂类型。(不要嫌我啰嗦,再强调一遍,这是形式!形式!不是实际的简单复杂,只是形式上。其实他们可以转化的。)使用简单类型定义变量(或者是别名)的时候,类型在左,变量名(或者别名)在右。复杂类型时,类型在两边,变量名(或者别名)在中间。什么意思呢?我们看下面这个例子:

int a; // 简单类型
int b[3]; // 复杂类型

    int就是一个简单类型,我们定义变量的时候,int在左,a在右。简单类型的指针类型同样是个简单类型,比如说int *, int **等。而b是一个复杂类型,b的类型其实是int [3]类型,也就是说左边的int和右面的[3]共同表示b这个变量的类型。复杂类型的指针一般也是个复杂类型,比如说现在要将的数组指针。

int b[3];
int (*p)[3] = &b;

    b的类型是int [3],解释为,一个拥有3个整型元素的数组类型。管他是什么什么数组还是什么,归根结底就是个类型呗,那b也就是个变量呗,是变量就要存在内存里呗,存在内存里又得有首地址呗,那&b不就表示这个b的首地址了嘛。我们自然可以有一个指针来存这个地址,那要是想解指针之后得到一个int [3]类型,那么这个指针的类型就是int (*)[3]。大家注意这种写法,C语言中遇到复杂类型时有一个解读原则,就是先找小括号,如果有小括号,那么,以小括号为起点,如果没有小括号则找中心,以中心为起点,从里向外读。比如说这里int (*)[3]类型,先找到小括号,小括号里括着一颗星,那么就表示,这种类型应该是{外面的类型}的指针。那么,外面的类型是什么?是int [3]。所以,这种类型就是一个int [3]类型的指针,解释为,一个指向{拥有3个整型元素的数组}的指针类型。所以简称为数组指针,那么,它是指针,自然就满足我之前说的所有的这一切的特性,不再赘述。我们再来看下面这个例子:

int *c[3];

    请问c是什么类型?也就是说这个int *[3]类型怎么解释呢?按照刚才的原则,没有括号所以找中心。中心怎么找呢?如果规范来写,变量名本身中心,如果不规范写或者省略了变量名的话,这时候大家也不要怕,中心的左边一定是一个单词(就是找英文字母啦)或是*,中心的右边一定是括号。所以,单词或*和括号的分界处就是中心。所以这里的中心就是*和[的中间,往外一层发现左边是*,右边是[],到底跟哪个呢?这里另一个原则是,星号*的意义跟着左边走。虽然我们在书写的时候,规范要求是把*贴右来写,但是,在解释类型的时候,*的意义要跟左边走。所以,这里的*应该和左边是一个整体,那么自然就应该先读[3]了,也就是说,这是一个有3个元素的数组。然后再往外,右边空了,左边还有一个int *,于是,这种类型被解释为:一个拥有3个{指向整型的指针}类型的元素的数组。简称为指针数组,所以,它是数组,并不知指针,但它的元素是指针。也就是说,b是数组,但是b[0]就是指针。

    我刚才提到说,规范的写法是*要贴右,但是解释的时候要贴左,这是为什么呢?因为C语言允许这样的语法:

int a, *b, **c; // a是int,b是int *,c是int **
int *d, e, f; // d是int *,e和f是int

    怎么说呢,这样用起来有时候还挺方便的,但是,可能C语言的设计者也没想这么多,可能是一种设计缺陷吧,所以,如果你*贴左的话,下面这种写法可能有容易让人误会:

int* a, b, c; // a是int *,b和c都是int

    上面这行代码可能就会让人误以为a,b,c都是int *,而其实只有a是int *。当然,这还没完,还有更容易被误解的写法:

int (*p)[3], a, *b;

    有木有晕掉?哈哈,这一行代码,p是一个数组指针,a是int,b是int *。要不要来点更刺激的?

int* p, (*q)[3], *r[3];

    你能顺利解析出p,q和r的类型吗?这里p是一个int *类型,q是一个数组指针,r是一个指针数组。所以,为了避免这种反人类的写法误导他人,我们还是别这么搞了!小心过几天你自己都看不懂……

    不过为了把问题彻底搞懂,我们再来看另外一种方式的反人类的类型,请看下面的代码,解释q是什么类型:

int (*(*p[3]))[4];

    按照我刚才介绍的方法,看看你能不能正确解析出p的类型,答案是:p是一个有3个{指向{指向{有4个整型元素的数组}类型的指针}类型的指针}类型元素的数组。如果你这都能答对,那么恭喜你,你升华了!如果没有,没关系,杀手锏还没用呢。遇到这种比较复杂的类型,建议大家不要直接这么去写,因为真的太反人类了,还是用typedef吧,按照刚才说的原则,新类型名和变量名放的位置是一样的,比如说上面那种反人类的类型,就可以用下面的方法来定义:

// int (*(*p[3]))[4];
typedef int Type1[4];
// Type1 *(*p[3]);
typedef Type1 *Type2;
// Type2 *p[3];
typedef Type2 *Type3;
// Type3 p[3];

    是不是看起来舒服多了?OK,关于数组这里还有一点要强调的就是,当一个数组类型被写在函数参数中,这个数组类型会自动转化为指针类型,也就是一个Type [n]类型的数组,无论n是几,写在参数中都会变成Type *。值得注意的是,仅仅是参数本身是数组类型才会转化为指针,和数组元素的类型没有关系,下面举个例子:

void test(int a[5],int b[],int *c[3],int (*d)[3],int **e,int **f[3],int *(*g)[4],int h[2][3],int i[4][7][9]
) {// a -> int *// b -> int *// c -> int **// d -> int (*)[3]// e -> int **// f -> int ***// g -> int *(*)[4]// h -> int (*)[3]// I -> int (*)[4][7]
}

    总之一句话,只有数组才会变成相对应的指针,如果它本身不是数组,那么该是什么还是什么(当然也包括指针)。正是因为C语言的这种设计,才造成了我们如果给一个函数传数组的话,就不得不再专门传一个参数来表示元素个数以防止缓冲区溢出。因为虽然形式上是数组,但其实传递的是数组首元素的指针,所以,并不能从这个首元素的指针这个参数上得知数组的元素个数以及数组上介。

    还有一点值得注意的是,数组类型转化为其对应的指针类型(也就是数组首元素的指针类型),换句话说就是Type [n]类型转化为Type *类型不需要强转,可以隐式转换,比如下面的例子:

int arr[3];
int *p = arr; // 不用写成int *p = (int *)arr;

    觉得自己把上面的都掌握了,很高兴是吗?那我告你,高兴太早啦!我还没讲完呢!

7. 函数指针

    我们知道函数是用来存储代码块的,不过熟悉冯·诺依曼体系结构的同学应该都清楚,在我们当前这种计算机体系下,数据和指令拥有二进制等价性,也就是说,数据和指令本身没有本质区别,只不过我们把它认为是数据时就可以进行数据的操作,认为是指令时就可以执行(当然就是机器码层面的执行了)。所以,函数这种东西,它用来存储代码块,也就是用来存储指令呗。那既然指令和数据没啥区别了,那它应该和其他的变量(也就是数据)一样嘛,也是存在内存里的嘛,也有首地址嘛,所以也就有对应的指针类型嘛。所以,函数指针存储的就是这段函数的首地址,自然,我们可以通过一个地址找到一个函数,然后按照函数的执行方式执行它。

int sum(int a, int b) {return a + b;
}

    假如我们有这样一个像上面这样的函数,要想获取地址怎么办呢?这里需要注意的是,对于变量,变量名本身代表的是变量的引用(也就是代表实体),而要表示地址,需要进行取址运算,也就是&符号。但是函数不一样,对于函数,函数名本身就代表的是函数的地址,而函数名后面加小括号则表示执行函数。所以,我们可以用这样的方式得到sum函数的地址:

void *p = sum; // 直接写函数名就表示函数首地址了,但是注意不能写sum(),或者sum(1, 2)这样

    不过这样写有点逃避主题了,虽然说指针都是一种类型,这样用泛型指针来存储没有问题,但是毕竟,我们存指针的目的,还是需要在适当的时候来通过指针找到实体的(也就是解指针),所以,还是应该掌握实际的函数指针类型的书写方式:

int (*p)(int , int) = sum;

    这里的写法和数组指针类似,函数类型也可以看做一个复杂类型,只不过这里不是数组的中括号了,而是参数列表的小括号。对于数组来说,左边的部分表示元素类型,右边表示数组上界的元素个数。而对于函数来说,左边是返回值,右边的参数列表。记住这样的原则就没问题了。当赋值过以后,我们可以通过给p加参数来调用sum函数的方法。比如说p(1, 2),其实就等于sum(1, 2)。

    但是,并不能把p和sum当做相同的东西,因为p的本质还是指针,我们也可以给p进行取址操作,还可以给p赋值,比如说

int (**q)(int, int) = &p; // 取址
p = NULL; // 赋值

    p也满足所有变量的作用域和生命周期。但函数一定是全局的(特指C函数哈,C++可不一定),作用域也仅仅和static关键字有关,也不能取址和赋值,所以它和指针是不一样的语法体系,要区分开。

    好啦,准备好迎接挑战了吗?试试下面几个变量类型的解释吧:

int (*(*p)[3])(int (*)[5]);
int ((*q)(int *))(int (*)());
int r(void (*)(int (*)()));

    emmm...如果实在看不出来的话,就别为难自己了吧,实际应用的时候记住typedef!这里公布一下答案吧:

    p是一个指向{拥有3个{指向{返回值为int,参数为{一个参数,第一个参数是{一个指向{存储了5个整型元素的数组}类型的指针}}的函数}类型的指针}类型元素的数组}类型的指针。

    q是一个指向{返回值为指向{返回值为整型,参数为{一个参数,第一个参数是{返回值为整型,参数为空的函数}}的函数}的指针,参数为{一个参数,第一个参数是一个指向整型的指针}的函数}类型的指针。

    r是一个返回值为整型,参数为{一个参数,第一个参数是{一个指向{返回值为空,参数是{一个参数,第一个参数是{返回值为整型,参数为空的函数}}的函数}类型的指针}}的函数。(这是一个函数声明)。

8.指针综合应用示例

    呼~~~~好啦,终于是把指针讲完了,最后,我们来实战一下吧。看问题:

    请实现一个函数,返回两个参数中较大的一个,需要支持任意类型。(默认大端序存储)

    这个问题很简单,但是难点就在,需要支持任意类型。如果确定了类型,那这个代码自然好写,但是不确定类型,我们就需要通过一些参数来确定这个类型的性质。C语言没有类似于C++的模板这样的语法,不支持泛型编程,因此,我们只能从单个字节入手。由于题目制定了大端序,我们就不用判断字节序了。首先变量的长度是需要知道的,其次,需要比较的两个变量的首地址是需要知道的。除此之外还有一个非常重要的因素就是,我们得知道,这种类型下,到底什么才算大,什么才算小。这是什么意思呢?如果我们处理的类型是数值,那么,自然对于大小比较有着数学上的定义,但是,万一不是数值呢?万一这个类型是个结构体呢?比如说它表示学生信息,两个学生信息到底怎么算大怎么算小?这些也是我们需要知道的。

    所以,总结下来,我们的函数应当接受4个参数,分别是:

1. 需要比较的第一个变量的首地址

2. 需要比较的第二个变量的首地址

3. 要比较的类型的长度

4. 要比较的类型的大小判断方法

    返回值是较大的变量的地址。

    前两个参数,由于我们不知道变量类型,所以,只能使用泛型指针void *,当然,返回值也应该是void *,第三个参数好说,整型就好(当然也可以传无符号整型,这个的选择依赖看你想支持的数据的最大长度,二来时看你想不想用类似于-1这样的负数来表示异常值,这里就不详细说明了)。第四个参数,由于是一种判断方法,那么,调用我们这个程序的人,应该是用函数实现的,通过传两个参数,来判断第一个参数是否大于第二个参数,我们需要在函数里调用这个比较函数得到结果,因此,这里应当传入函数指针。

    说到这个函数指针,自然,我们还需要明确这个函数的参数和返回值了。我们要求调用我们接口的人,应当实现一个函数,来判断这种类型下的数据,第一个数据是否大于第二个数据,参数应当是:

1. 需要比较的第一个变量的首地址

2. 需要比较的第二个变量的首地址

    返回值是0或1,表示不大于或大于。

    好了,设计完毕我们就可以开始编码了,我编的代码是这样的:

void *getBiggerOne(void *n1, void *n2, int type_size, int (*isBigger)(void *, void *)) {if (isBigger(n1, n2)) { // 如果n1 > n2return n1;}return n2;
}

    那么,我们可以写一个测试用例,比如说我用一个结构体类型来测试,这个结构体表示学生的信息,而我定义,学校较大的就是学生年龄较大的,可以这样来测试:

typedef struct {int id;int age;char name[20];
} Student;int isBigger_Student(void *n1, void *n2) { // 用来判断n1是否大于n2Student *s1 = (Student *)n1;Student *s2 = (Student *)n2;if (s1->age > s2->age) { // 比较两个Student的方式是比较其agereturn 1;}return 0;
}int main(int argc, const char * argv[]) {Student s1 = {id: 1,age: 15,name: "Jack"};Student s2 = {id: 2,age: 13,name: "Tom"};Student *bigger = getBiggerOne(&s1, &s2, sizeof(Student), isBigger_Student);printf("The information of bigger Student:\nid:%d\nage:%d\nname:%s\n", bigger->id, bigger->age, bigger->name);return 0;
}

    这是输出结果:

The information of bigger Student:
id:1
age:15
name:Jack

    我们成功地将年龄较大的学生打印了出来。

9.总结

    好啦!总算是找到一个机会把指针给大家讲完了,真的是累死逗比了!到现在,不知道你还认同不认同我一开始说的那句话,如果你不会指针,那就别说你会C!真的,C语言有一多半都是在玩指针,只有你指针玩好了,才能说你完全掌握了C语言。

    最后,再次总结要点:指针本身就是一个数,只不过可以表示地址。指针只有一种类型,而所谓的类型只不过是用于解指针时指定数据的解析方法。复杂类型是名称在中间,类型在两边,解析时要从里往外。数组类型在传入函数参数时会转化为数组首元素的指针。

    最后的最后,如果大家还有什么问题欢迎在下方留言,我们来共同探讨。

    【本文为逗比老师全权拥有,允许转载,但是务必在开头标注转载源链接和作者信息,不得恶意拷贝和更改。】

与本文相关的文章

发布评论

评论列表 (0)

  1. 暂无评论