1、C语言指针是什么?
在C语言中,指针是一个非常重要的概念,简单的说:指针是存储空间的地址,计算机的存储空间由一个个的字节(8位)组成,指针就是这些字节的编号,通过这个编号,就可以访问到特定的存储空间。但是,指针如果简单的来说,就没办法简单的理解,接下来,我们将前面的描述换一种方式进行描述。
首先,我们明确一点,在计算机内,所有在存储空间内的数据都是或多或少的 0/1 位流组成,再往上点,可以说所有的数据都是以字节中的 0/1 序列形式存在,无论是代码数据、音视频、文档,在存储空间内并没有本质区别,都是 0/1 字节序列,那么,怎样让相同的数据具有不同的表现形式?其中的关键就是对数据的解释规则
。
同样一个苹果,在中国它写成:苹果,在英国写成:apple,变的不是这个水果,而是对它进行解释的规则,利用不同的规则对它进行解释会展现出不同的表现形式。
每个人都可以定义自己的规则来解释同一个数据,我们平时所说的标准、规范,不过也就是一些应用更为广泛、更具解释效率同时也是大家约定俗成的一套数据解释规则而已。C语言,也是一套解释规则,这套规则用来帮助我们更有效的与计算机进行交流,除了怎么对数据进行解释,另外一个关键就是数据本身是什么,由此可以得到两个关键的东西:数据本身是什么,以及怎么解释这个数据
2、C语言指针初体验
思考下列代码:
#include <stdio.h>
int cnt;
int* cnt_ptr;
int main(int argc, char* argv)
{
cnt = 2155905152;
cnt_ptr = &cnt;
printf("cnt = %d, cnt_ptr = %p\n",cnt,cnt_ptr);
}
首先我们看int cnt
,按照C语言的规则,也就是语法,这句话表示向计算机申请一个变量,这个变量叫cnt
(存储空间别名:别名是为了便于人类自己记忆),同时,我对它的访问需要按照int
的规则进行,如果在本机上,int
数据类型有四个字节,那么这句话的最终意思就是需要计算机为我准备四个字节的存储空间,同时将存储空间的别名叫做cnt
,以后我叫到cnt
,计算机就知道我叫的是这四个字节的空间,同时,对这四个字节空间中的 0/1 字节序列按照int
的解释规则进行解释;
看到这里我知道你应该很困惑,如果实在搞不清楚,你只需要简单的理解:C语言语法全是规则,自己定义的变量全是别名;
接下来看下一句:int* cnt
,规则是:int*
,这告诉计算机我们申请的这个变量是拿来存储地址的,而且存储的还是一个int
数据类型的地址;
cnt = 2155905152
: 叫到了cnt
存储空间,=
规则告诉计算机把右边的这个数据2155905152
放到前面这个叫cnt
的地址空间里面去;
cnt_ptr = &cnt
:叫到了cnt_ptr
存储空间,&
在C语言中表示获取它后面存储空间的空间首地址(第一个字节的空间编号),=
规则告诉计算机把cnt
空间的空间地址给放入到叫cnt_ptr
的存储空间里面去;
到此为止,叫cnt
的存储空间里面放的是:2155905152
,叫cnt_ptr
的存储空间里面放的是cnt
存储空间四个字节中第一个字节的地址空间编号;
编译运行:
gcc -o ctest ctest.c
./ctest
输出结果:
诶?为什么cnt
的输出是负数?我赶紧查了查四字节int
的数据范围:-2147483648 到 2147483647
,原来我们给它的赋值超出了它最大值;你还会发现,你的第二个地址输出和我也不一致,甚至运行两次的结果都不一样,这不必惊讶,只是操作系统耍的一点点小心机而已,致辞,我们才终于有了一个小例子,离理解还差得远,那么,再探指针!
3、再次体验C语言指针
代码如下:
#include <stdio.h>
#include <stdint.h>
uint32_t cnt;
int* cnt_ptr;
int main(int argc, char* argv)
{
cnt = 2155905152;
cnt_ptr = &cnt;
printf("cnt = %d, cnt_ptr = %p\n",cnt,cnt_ptr);
}
和上一段代码唯一的不同是这次我们想要以4字节无符号数的方式对cnt
里面的数据进行解释,也就是说我们修改了这段内存中数据的解释规则,再次运行查看结果:
为什么还是负数?仔细一看,原来在输出的时候还有一次数据解释规则的应用:%d
表示输出的是整形,对无符号数的输出需要用到%u
,修改输出语句为:
printf("cnt = %u, cnt_ptr = %p\n",cnt,cnt_ptr);
结果如下:
终于,我们输出了正常的数据,我们可以发现,不同的数据解释规则可以把相同的数据解释为不同的表现形式,至此,你是否有了切身体会?理解了数据本身和数据解释规则,接下来我们继续改造这段代码,进入指针的深入学习。
改造代码如下:
#include <stdio.h>
#include <stdint.h>
uint32_t cnt;
uint8_t* cnt_ptr;
uint32_t* tmp_ptr;
int main(int argc, char* argv)
{
cnt = 2155905152;
cnt_ptr = &cnt;
tmp_ptr = &cnt;
printf("cnt = %u, cnt_ptr = %p, tmp_ptr = %p\n",cnt,cnt_ptr,tmp_ptr);
}
相比于上一段代码,cnt_ptr
的规则被换成了uint8_t*
,这个规则表示cnt_ptr
将被解释为一个一字节无符号数存储空间的首地址,这个是它的解释规则,那它的数据本身是什么?cnt_ptr = &cnt
告诉我们它的数据本身是cnt
存储空间的第一个字节的空间编号,同时,我们还声明了一个tmp_ptr
,其本身数据也是cnt
存储空间的第一个字节的空间编号,那真的是这样的吗?
编译运行:
确实如此,它俩的本身数据就是一样的,那不同的解释规则有什么不同呢?
改造代码如下:
#include <stdio.h>
#include <stdint.h>
uint32_t cnt;
uint8_t* cnt_ptr;
uint32_t* tmp_ptr;
int main(int argc, char* argv)
{
cnt = 2155905152;
cnt_ptr = &cnt;
tmp_ptr = &cnt;
printf("cnt = %u, cnt_ptr = %p, tmp_ptr = %p\n",cnt,cnt_ptr,tmp_ptr);
for (int i = 0; i < 4; i++)
{
printf("i = %d *cnt_ptr = %u\n",i,*cnt_ptr);
cnt_ptr ++;
}
}
在C语言中*cnt_ptr
规则是:先拿到cnt_ptr
这个存储空间里面的值(cnt
变量的第一个字节的空间地址),把拿到的值解释为地址空间编号,再去这个地址空间编号所对应的空间里面拿出数据来,怎么拿?cnt_ptr
是uint8_t*
类型,那就一个字节一个字节的拿,cnt
变量有四个字节,那么就可以拿四次才能把cnt
的四个存储空间拿完,编译运行如下:
我们拿了四次,每次都是128
,其二进制是:10000000
,每个字节里面的数据都是这个,四个字节就是:2155905152
,这是计算机按照我们给出的规则一个一个字节拿的结果。
那tmp_ptr
和cnt_ptr
的值一致,是否也可以这样操作呢?可以,但是它只能拿一次,因为它一次拿四个字节,一下就拿完了。
到这,你是否对指针有了初步的认识?是否理解到了数据本身和数据解释规则的重要性,要是还没有弄懂,我们可以试着拆解C语言里面的规则,灵活的组合各种C语言规则以实现自己需要的目的,这很灵活,但是这也是C语言指针的魅力;
4、拆解C语言二级指针
你也许通过,二级指针是指向指针的指针,那么你也许会写下如下的代码:
#include <stdio.h>
#include <stdint.h>
uint32_t cnt;
uint32_t* cnt_ptr;
uint32_t** cnt_ptr_ptr;
int main(int argc, char* argv)
{
cnt = 2155905152;
cnt_ptr = &cnt;
cnt_ptr_ptr = &cnt_ptr;
printf("cnt = %u, cnt_ptr = %p, cnt_ptr_ptr = %p ",cnt,cnt_ptr,cnt_ptr_ptr);
printf("*cnt_ptr = %u, **cnt_ptr_ptr = %u\n",*cnt_ptr,**cnt_ptr_ptr);
}
编译运行这个代码:
呵!二级指针,不过是C语言吓退初学者的小把戏而已,你只需要牢记:数据本身和数据解释规则;
上面的代码进行如下改造,使用C语言指针的规则拆解掉二级指针!修改代码如下:
#include <stdio.h>
#include <stdint.h>
uint32_t cnt;
uint32_t* cnt_ptr;
uint64_t cnt_ptr_ptr;
int main(int argc, char* argv)
{
cnt = 2155905152;
cnt_ptr = &cnt;
cnt_ptr_ptr = &cnt_ptr;
printf("cnt = %u, cnt_ptr = %p, cnt_ptr_ptr = %p ",cnt,cnt_ptr,cnt_ptr_ptr);
printf("*cnt_ptr = %u, **cnt_ptr_ptr = %u\n",*cnt_ptr,*((uint32_t*)(*((uint64_t*)(cnt_ptr_ptr)))));
}
先不解释,编译运行,结果如下:
经过我们的代码改造,在没有使用二级指针的前提下,实现了与使用二级指针的同样效果,这就是对复杂规则的拆解,但是也可以看见二级指针对后段代码的简化,拆解是为了理解,正常开发视情况而定,接下来我们利用数据本身和数据解释规则两大法宝来解析这两段代码;
注:前面我们一直提到地址数据本身,但是一直没有说地址本身数据的形式是什么?地址数据是地址空间的编号,那么编号的范围常常能限制机器可以寻找内存空间的范围,32位机器位宽为32位,为了效率,地址寻址肯定是最好一次就找到,那么地址空间的最大编号就是:
2^32 = 4GB
,对于64位机器,最大地址空间编号为:2^64
,这个数可就太大了,一般都没有用到64位,一般48位就足够了,可以寻址:2^48 = 256TB
,所以在32位上,地址数据本身是一个32位无符号数,64位上是64位无符号数;
首先,无论几级指针,其数据本身都是一个64位无符号数,不管这个指针是指向一个函数、数组、变量、常量还是执行一个指针变量,其数据本身不变,变的是数据的解释规则;
在第二段代码中:
cnt_ptr_ptr = &cnt_ptr;
cnt_ptr_ptr
是一个64位无符号数,这是它的解释规则,只是恰好它存储的数据是一个存储空间编号而已,难以理解的是下面这段代码:
*((uint32_t*)(*((uint64_t*)(cnt_ptr_ptr))))
按照括号对它进行展开:
前提1:uint32_t* cnt_ptr;
推论1:
1、cnt_ptr 本身数据是一个64位无符号数
2、这个无符号数被解释为一个32位无符号数所在存储空间的第一个字节的地址空间编号
前提2:uint64_t cnt_ptr_ptr;
推论2:
1、cnt_ptr_ptr 本身数据是cnt_ptr变量存储空间第一个字节的地址空间编号,是一个64位无符号数
2、cnt_ptr_ptr 被解释为一个64位无符号数
由内到外拆解:
/* 强制把 cnt_ptr_ptr 里面存储的数据解释为一个64位无符号数所在存储空间的第一个字节的地址空间编号
* 结合推论2.1,这就是变量cnt_ptr的首地址
* 注:得到cnt_ptr 的地址
*/
A = (uint64_t*)(cnt_ptr_ptr)
/*
* 按照A的规则,从变量cnt_ptr的首地址起取 uint64_t 得到的是变量cnt_ptr的本身数据
* 也就是变量cnt存储空间的首地址,其本身数据是一个 uint64_t
* 注:取出 cnt_ptr 的值
*/
B = *(A)
/*
* 将B中存储的 uint64_t 数据强制解释为一个 uint32_t 变量存储空间的首地址
* 注:得到 cnt 的地址
*/
C = (uint32_t*)(B)
/*
* 按照 uint32_t 的规则从 C 本身数据所代表的空间地址编号处取四个字节
* 注:取出 cnt 的值
*/
D = *(C)
说的很绕,也许水平有限没能讲清楚,哈哈哈!