因何而成指针 -- 我对指针的回顾

前言

本文记录我对指针的认识,以讲授的方式行文,用以巩固知识,若能对你有所帮助,自然是更好。

我看过一些csdn里关于C语言指针的文章,我发现我的文章会探讨一些关于指针的比较深的主题

就像题目所言:因何而成指针。这是我对指针的理解,后面我会详细讲。

最简单的类型

这部分简单,故写的简单一些

int* p,这代表了p的类型是int* ,这是很自然的,*好像和int连在一块了,于是有了一个新的类型:int*,它的意思是p指向的元素类型是int

但是,这并不是唯一的写法,int  *p这种写法编译器也是承认的,不会报错,这种写法有一个存在的意义在于在一行代码初始化多个指针变量,比如:

int* p1,*p2,*p3,n;  

我来解释一下,p1,p2,p3都是int*类型的指针,n是一个int类型的变量,如果你仔细思考过,就会发现一个问题:为何如此,为什么都要加*。若int*确实是一种类型,就像int,char这样的类型(它们初始化多个变量的时候写char c1,c2,c3;   这样就好了),那么当我们写下

int* p1,p2,p3;  

的时候我们理所当然的认为p1,p2,p3都是int*类型的才对,而事实上,只有p1是int*类型,p2,p3都是int类型,这样看来,int   *p;  似乎是更本质的写法,可C语言为何如此设计,为什么不干脆把int*就设计成纯粹的一个类型呢?

这个问题笔者暂且按下不表,这其实是因何而成指针的开始。

一维数组与数组指针

数组名

int arr[5] = { 1, 2, 3, 4, 5 };    //后面的一维数组一律以此为例

我们知道arr是数组名,我们知道arr代表了首元素地址,是的,绝大多数情况都是这样,不会有错的,只有两个例外:

一是当使用sizeof的时候,sizeof(arr),这时候arr就代表的使整个数组了,

二是当&arr的时候,arr也代表了整个数组,&arr得到的就是数组指针了

对于第一点,我需要解释一下,sizeof不是函数,为什么要解释这一点呢,因为你们肯定都经历过,当你们想把一个数组传入函数中,不管是

void fun(int arr[])

还是

void fun(int* arr)   //其实与上面是等价的,没有任何区别

会发现在函数里arr只是一个非常非常普通的int*类型的指针了,sizeof再也不能计算数组的大小了,于是只能非常不情愿的再传入一个参数用以表示数组大小

如果sizeof是一个函数的话,理应会有相同的困境,即传入的参数变成了一个普通的int*类型指针,数组又没有像字符串的'\0'结尾,但是我说了,sizeof不是一个函数,你可以这么写

sizeof arr;

不会有错,sizeof只是一个操作符而已

数组指针

&arr是一个数组指针,那该用什么类型的指针变量来储存它呢

首先明确几点,

一、&arr不是二级指针,并不是对首元素地址再进行取地址后的二级指针(arr已经是一个地址了,本来是不能用取地址符&的,但C规定了这样写有意义,“假二维数组”有更详细的讨论)

二、&arr的值还是首元素地址,我的意思是:

printf("%p\n",&arr);
printf("%p\n",arr);

打印出来的结果是一样的,只是看待这个byte的方式不一样了,这可以体现在&arr+1上(值会增加20)

三、如果你这么写: *(&arr);   或者用了p来储存它,然后写*p, 它就会变成首元素地址也就是arr(这个特性很重要)

来看数组指针到底如何储存:

int (*p)[5] = &arr;

因何而成指针呢,不管 = 左边多么花哨,有一点是明确的: p的值是&arr,然后带入左边

*p就可以得到首元素地址,于是左边形式变成了int arr[5],这是在说 *(arr + i)都是int类型,i从1到5

仔细想想当你想要定义一个数组的时候,你写:

int arr[5] = { 1, 2, 3, 4, 5 }; 

意思就是*(arr + i)都是int类型,  [i] 其实就是 *(arr + i)

也就是说,int (*p)[5]  这样写是合理的,到这里为止,我们只是分析了这个写法的合理性

但其实,经过后面的学习,当你想要储存一个地址的时候,你是可以自己设计出指针变量的类型该怎么写,也可以根据指针类型的写法判断它到底是什么

再说一下为什么int (*p)[5]有一个括号,这是因为[]的优先级比*要高,后面很快就会分析不加括号的情况

现在,之前遗留的int* 问题终于可以解决了

int* p1,*p2,*p3,n;

p1,p2,p3当然都得储存一个整数的地址,*p后就是int类型,*是一个操作符,自然只对一个p起作用最后一个n当然就是int类型,如果你觉得这还不够,看完后面就懂了

二维数组

“假二维数组”

我们先不分析传统的二维数组,先来看这句代码

int *p[5];   //这就是我之前说的不加()的情况,我们来分析这到底代表了什么

不要看到p就觉得p是指针,需要对它加以分析来确定它是什么,首先,p与[5]结合,再与*结合,意味着p的5个元素(p[i]的意思是*(p+i)) ,每个元素解引用后都是int类型

当然,经过之前的int*分析和一维数组,有些步骤可以跳过了,可以这么想,p与[5]结合,每个元素都是int*类型(int*类型之所以为int*类型,就是因为解引用后是int类型),这么写可能更明显:

int*  p[5];   //int*就是数组里元素的类型

也就是说,int *p[5] 是一个指针数组,有两种可能,

一是有人比较无聊,把几个整数的地址放在了一个数组里面,

二是把几个数组的数组名(首元素地址)放在了这个指针数组里,这样其实可以模仿二维数组

int main()
{
    int arr1[] = {1,2,3,4};
    int arr2[] = {2,3,4,5};
    int arr3[] = {3,4,5,6};
    int* parr[3] = {arr1,arr2,arr3};
    for (int i = 0;i < 3; i++)
    {
        for (int j = 0; j < 4; j++)
            printf("%d  ",*(*(parr+i)+j));  //和parr[i][j]是等价的,后面会解释
        printf("\n");
    }
    return 0;
}

这段代码我需要解释一下,这种“二维数组”和真的二维数组是不同的,parr是开辟了一个连续的,8 * 3 = 24个字节的空间,parr是首元素地址,其实parr是一个非常神奇的东西,有人可能会想,parr是首元素也就是arr1的地址,那不就是数组指针吗? 其实不是,

又有人会想,arr1在放入parr数组的时候已经丧失了作为数组的特性,变成了一个平常的int*类型,所以parr是int**类型的二级指针。  这句话前半句是对的,'所以'后面的话严格说来是有一点问题的

这到底是为什么呢,我们以房子是内存,门牌号是地址的方式叙述

我们知道,每个内存都有一个地址,而这些地址是不需要再开辟空间去储存的,它们是直接由硬件生成的,而当我们定义指针变量的时候我们才开辟了空间来储存某个地址,也就是打开了8个房间,用8个房间放了一个门牌号(一个地址由8个字节储存),

所以当*parr的时候,就像这个人就站在房间外面,然后他推开了门,而对于一个指针变量p,*p相当于去p这个房间拿到门牌号,再一路找到门牌号所对应的房间,然后推门走进去,parr只是一个门牌号,p却是8个房间

也就是说,一般来说我们想要用一个名称来表示一个地址得另外开辟空间,二数组名也可以表示地址却不需要在开辟空间储存(数组的首元素直接拿出了它的门牌号而不用让别人复制一份再放到一些房子里存起来,数组名让我们可以直接使用门牌号)

而当我们想要将parr储存起来的时候,就是开辟了一块空间, p = parr;  那么p的类型如何设计呢,我们知道*p后得到了一个门牌号,在对这个门牌号进行*操作就可以的到一个int类型的数据,或者跳过这个步骤,*p后就是int*类型, 那么p的类型自然就是int**,也就是说:

int** p = parr;

现在我们来看看*(*(parr+i)+j)),parr对应的房间是指针类型的,所以parr+i以8为步长,*(parr+i)相当于走进了这个房间,拿到了门牌号(还没去找门牌号对应的房间),这个门牌号(int*类型的地址)的步长是4,+j之后再用解引用操作就相当于拿着+j后的门牌号去找对应的房间,然后拿到了一个int类型的数据

真二维数组

在讲完一维数组和假二维数组的情况下,这个真正的二维数组其实没有很多内容了

int Arr[][3] = { 1,2,3,4,5,6,7,8,9 };

这是一个3*3的二维数组,它的数组名Arr当然是首元素的地址(所谓Arr[i][j]也是对也是对首元素地址进行操作),而首元素是{1,2,3},它是一个数组,数组的地址当然就是数组指针(严格说来是具有数组指针的性质,不过为了叙述方便,称它为数组指针)   ,Arr+i就是第i个数组的指针了。看下面的代码

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

这个*(*(arr+i)+j)虽然形式上和假二维数组是一样的,但是有区别的,对(Arr+i)使用*,就像我在数组指针中解释的一样,解引用操作之后还是一个地址,不过只代表一个一维数组的首元素地址了(也就是int*类型),然后+j再来一次解引用操作就是一个整数了

那么,这个数组的数组名Arr可以用什么类型的指针存起来呢?

p = Arr,  然后对p解引用后得到的是一维数组的首元素地址,*(*p + i)后是一个整型(也就是(*p)[i]是整型),所以:

int (*p)[3] = Arr;

发现了吗,和一维数组的数组指针完全一样,想一下就会知道这是很合理的,毕竟Arr就是首元素也就是一个数组的地址 

二维数组指针

首先,p = &Arr  ,对p解引用后得到数组的首元素地址,这个地址进行每个元素,也就是数组[i][j]后会是一个整数,也就是这么写:

int (*p)[][3]

前面的[ ]也可以加上3。  

void* 类型的指针

在函数指针前先讲一下void*指针,因为后面有些例子会出现void*指针,它们都非常经典,有一些出自《C陷阱与缺陷》

void* 是一个万用的指针类型,他可以接受任何类型的指针

int arr[][3] = {{1,2,3},{4,5,6},{7,8,9}};
void* p1 = &arr;
void* p2 = arr;
void* p3 = &arr[1][1];
char c = 'a';
void* p4 = &c;

它经常用于函数传参中,用于增强函数的弹性,也就是说你不必为不同的参数类型而设计不同的函数,直接用void*指针就好了,内存函数的实现离不开它,但这里并不打算深入探讨内存函数

void*类型除了能比较大小之外几乎什么也干不了,不能解引用,没有+-操作,这是因为void*只是单纯的储存了个地址而已,并不知道该如何看待这块地址,是char* ,int* ,还是什么其他奇怪的类型不过只要对它进行强制类型转换它就很有用了

char str[3] = "ab";
void *p = str;
printf("%c\n",*((char*)p+1));

虽然这段代码有点愚蠢,但这确实是void*的用法, 并且,无论你怎样强制类型转换,p的类型永远是void*,这是不会改变的

以及,当你给函数传入参数时

void test (void* p)

如果给这个函数传入一个int*类型的数据pn,p并不会去检测pn的类型然后把void*改为int*

不过,C++的template<typename  >有这个功能

函数指针

我们先来看看函数指针会有哪些性质

int test(int n)
{return n;}
int main()
{
    printf("%d",&test == test);
    return 0;
}

打印出1,说明&test == test  ,这说明了一件事情,当我们想用p来储存函数指针的时候,我们不必写 p = &test;  直接写p = test; 也是一样的,还有一个特性,无论对函数名解引用多少次,都不会有什么变化,也就是说test,*test,**test,************test都是相等的,这个特性有时让*变成了摆设,是个吉祥物一样的存在。当我们想存储函数指针的时候:

int (*p)(int) = &test;  //这是标准的写法

分析一下这段代码代表了什么,首先有p = &test = test = *p;然后后面一个括号其实代表了*p是个函数,括号里面的int代表了这个函数有一个参数且第一个参数的类型是int,最前面的int表示这个函数的返回类型是int,之所以*p要用括号括起来,原因和数组的一样,()的优先级比*高

简化一下可以这么写

int (*p)(int) = test;

我得说明的是,左边(*p)是不能改写成p的,我猜测这是为了规范性,毕竟p是一个函数指针应该得解引用之后才是一个函数吧(虽然由于函数名乱七八糟的性质导致并不是这样的)

不过,当我们想使用p的时候不必写(*p)(3),  也可以直接写p(3)   ,也就是这个函数的参数是3

有了基础知识,现在我们来看这段代码

void (*signal (int, void (*)(int)))(int);

了解了这段代码会让你对因何而成指针有深刻的了解

这句代码也在《C陷阱与缺陷》中

首先问题是:signal是什么,既然都在函数指针里讨论了,那么肯定和函数有关吧,然后仔细看,signal首先是与()结合而不是*(如果是指针的话,无论如何都是先与*结合的),也就是说,signal大概可能会是一个函数吧,那如果这样看,因为最后面有个分号,所以这是个函数声明喽。那signal后面的括号(int, void (*)(int))就应该是参数列表了,一个int类型,一个void(*)(int)类型,也就是一个参数类型为int且没有返回值的函数指针

我们分析完了signal的内容,接下来就是*也就是解引用操作,对一个函数解引用(不是对函数名)代表了什么呢,在想一想函数声明,有参数名有参数类型了,最后只剩一个返回类型了,和返回类型有关,如果你足够大胆把这个函数移走,剩下的会是void (*)(int)  它正可以作为一个返回类型,这句代码的的意思也就是这个

但是,但是,为什么要这样写,如此让人迷惑的写法,如果我们这样写:

void (*)(int) signal (int, void (*)(int));

这不会更好吗,清晰明了,然后编译器就报错了

因何而成指针呐,假使某个东西是指针,那么必得从*开始分析才能知道它的类型是什么,它指向的对象是什么,对一个普通的函数而言,比如int test(char c)  这里定义了一个函数,可以看出函数名是test,函数的第一个参数类型是char,那么返回类型呢, test(char c)这个会返回一个值,往前一看,是int类型的。  然后我们仿造上面流程:

signal (int, void (*)(int)) 会返回一个值,然后这个值就碰上了*,也就是说这个值是个指针,解引用之后就碰上了括号(int),说明这是个函数指针,它有一个int类型的参数,没有返回值,这一切都得从*开始分析,正因为有了这些分析的过程,才使得它成为了指针,决定它是什么类型的指针,这就是因何而成指针。   所以这个看上去清晰明了的代码

void (*)(int) signal (int, void (*)(int));

signal的返回值出来后它并不“知道”前面的void (*)(int)到底是什么。  你可能会有疑问,           signal (int, void (*)(int))  这个函数的参数里面也有void (*)(int),为什么这个就没有奇怪的写法,那是因为它没有什么可以分析的,如果定义而不是声明一个函数,就得从*开始分析了,也就是            void (*p)(int),我在这里总结一遍,今后不再说了:一个与指针有关的类型之所以是这个类型,是因为从*开始分析解构后一切都符合要求,而一个类型名就是简单的把变量名去掉就得到了

当然,上面这些想法肯定不是计算机的想法,是基于我对C语言指针的了解总结出来的一套可以用于解释所有的指针行为的理论(目前看来一切正常,都可以很快且没有错误的看透指针),我猜测C语言创始人也是以同样的想法来设计指针的(也许他的会更好)

现在,可以再看看几个例子来巩固

int (*parr[10])[5];

parr先与[ ]结合,所以parr是一个有10个元素的数组,1到10每个元素的类型都是int (*)[5],也就是有五个元素的数组指针类型,这里显然跳过了一些步骤,但如果你前面真的看懂了,你会知道你可以从心所欲而不逾矩

如果你足够敏锐,可能可以发现,在因何而成指针这条线外,一直有一条隐藏的线,那就是: 

因何而成数组 

和指针差不多,因何而成数组就是从[ ]开始分析才能得到这是一个数组                                         比如最简单的int arr[5] = { 1,2,3,4,5 };  arr与[ ]结合,于是它是个有5个元素的数组,每个元素的类型是int,当然如果与指针结合起来了,那就继续从* 分析,来看一个例子,这是刚开始就从*开始分析的,不过并无大碍

void (*(*p)[4])(int, void (*)())

p与*结合说明是一个指针,解引用后就与[ ]结合,这说明了p是一个数组指针,(*p)这个数组每个元素的类型是void (*)(int, void(*)()),也就是数组指针,没有返回,两个参数类型,第一个是int类型,第二个是无参无返的函数指针类型。

你可能会突发奇想,想设计一个可以返回数组的函数,比如一个简单的

int fun()[5];  

编译器就会告诉你不允许使用返回数组的函数,如果你这么写:int [5] fun();编译器甚至都认不出来这是什么,仔细想想就会知道,想要返回数组能返回什么,基本就只有数组名了吧,可是数组名是指针啊,于是返回指针就好了

typedef怎么用

typedef存在的意义就在于把一些复杂的类型变得更加简洁明了,比如如果你觉得unsigned int太长了,那么就可以写:

typedef unsigned int uni;
uni n = 10;

typedef一般放在main函数外面,可以在整个程序中生效

有了它就可以简化之前一个特别复杂的例子

void (*signal (int, void (*)(int)))(int);

它的返回类型是void (*)(int),之前说过如果直接把这个类型名放在函数的最前面是不行的,得从*开始分析嘛。但是有了typedef我们近似的做到这一点了,想要把void (*)(int)重命名也得从*分析,不然就会报错,就像下面这个例子

typedef void (*)(int) pf

本意是想把这个类型命名为pf,然后就失败了,正确的写法是

typedef void(*pf)(int);

没错,真的让人眼熟,pf之所以是这个类型,是因为从*分析(或者说解构)后得到的是函数指针类型于是复杂的例子就可以简化为

pf signal(int,pf);

对,就是这么简单

一个例外:强制类型转换

来看一下来自《C陷阱与缺陷》的例子

( *( void (*)() )0 )();

如果你不知道标题且没有接触过这种代码,那么确实很难猜出来这是什么,这里有个突兀出现的0

首先,这里有我们熟悉的成分:void (*)(),这是一个无参无返的函数指针类型,用括号括起来了,后面跟了一个0,这其实是一个强制类型转换,至少形式是一样的,它把0地址转换为了函数指针类型,然后再解引用它(由于函数指针特性,这一步可以省略),最后一个括号是给这个函数传递参数的,虽然并没有参数。这段代码其实是有用处的,详见《C陷阱与缺陷》

再来看一个例子

int arr[5] = {1,2,3,4,5};
int (*p1)[5] = &arr;
void* (*p2)(int) = (void* (*)(int))p1;

最后一句代码是强制类型转换,这些代码都把工作完成的很好,唯一的问题是,它们并没有遵循我所总结的原则,原因是这是类型转换,如果你想把p1从*开始分析是行不通的,毕竟p1已经有类型了,解引用是以p1的类型来进行解引用的,所以不能从*开始分析了,就按照(类型)的方法强制类型转换就好了,我们之前的讨论都是在围绕如何设计一个指针,或者说初始化,所以符合原则

其实这里有一个很有意思的点,在于C的编译器可以直接看得懂括号里面到底是什么类型的,也就是说,本来void (*)(int)  signal (int, void (*)(int)); 这样子写是有可能的,但大概是因为C语言创始人觉得因何而成指针的方式更好一点吧

到这里就结束了,当然如果你觉得你又更好的理解方法,可以不按我的来,毕竟这只是我对指针的一个解释罢了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值