秒懂C语言的指针(上)

码农公社  210.net.cn   210= 1024  10月24日一个重要的节日--码农(程序员)节

序言

?指针是C语言学习者绕不过的一道坎,也是C语言学习者不得绕过的一道坎。辨别一个人C语言学的好赖就看他对指针的理解怎么样。指针内容也是工作面试经常问到的问题。本文将带你重新认识那个绊倒你的指针,以解大家的心头之惑(恨)。

为什么要学习指针?

?有同学就要说了,既然指针这么难,这么不通俗易懂,为什么要学习他呢?其他高级语言都是把这块基本屏蔽掉了,不在让程序员直接操作指针,这里不直接操作指的是不让程序员用指针进行运算和强转而不是彻底没有了。举个java的例子

Object obj= new Object();
Object sec= obj;
sec = new Object();


如果你去仔细研究他们的行为,就会发现 obj, sec 都只是一个指向对象的东西,可以为空,也可以修改指向,所以它们其实都是指针,只是 Java 的教材里面不在去提这东西而已,具体原因看我后面讲解便知道了。

继续说为什么学习指针,为什么学习指针就必须要说到指针的优点了。

  • 指针可以直接操作变量地址,所以很灵活。

  • 指针操作会减少很多变量的拷贝使得程序性能提升。

  • 可以动态分配内存。

?这些优点使得很多后台性能要求很高的系统、游戏内核、一些高并发的中间件都是使用C&C++语言开发出来的。比如强大的linux系统、nginx,mysql、redis等等。

?曾经看到一个搞笑的评论,hhh

道生一,一生二,二生三,三生万物

        电子计算机生汇编,汇编生C , C生C++,C/C++生万物

指针是什么?

?其实指针看起来复杂,听起来复杂,学起来复杂,但是总结下来指针到底是个啥,也就一句话。

指针就是地址,指针变量就是一个存放内存地址的变量

你没看看错,是的就是这么简单明了。通常我们说的指针就约等于说的是指针变量。

指针和内存地址的关系

?很多人不明白指针其实也就是不明白内存地址,所以要想明白指针必须先明白指针和内存之间的关系。在讲内存和指针之间的关系之前先说下什么是内存。

?先明白一个问题,什么是内存?编程人员常说的内存指的是什么?

内存是电脑的一个硬件组成部分。

从单片机的组成我们可以看到,CPU、内存和输入输出接口,就组成一个完整的电脑,其他统统属于外设。

内存是可以被CPU通过总线进行操作的,也就是与CPU之间有总线相连接的。

电脑所有的输入输出,都是要从内存来实现的。

内存包括只读内存ROM和读写内存RAM,但在个人电脑(PC)中,我们通常所说的内存,是指读写内存。

程序人员常说的内存其实是虚拟内存,程序直接操作的是虚拟内存而不是真正的物理内存。


程序都是操作的虚拟内存? 那虚拟内存是个啥东西?

这里先给大家画张C语言程序的内存布局图。

1.jpg


这个图很好的描述了内存地址的布局,指针变量里面存放的地址也就是这个内存地址。顺便说下啥是内存地址,用十六进制表示出来的一串数字编号(就好比你家的门牌号),只是这个数字是给内存标号的。32位系统下这个编号是4byte(32个bit)表示的,64位系统下是8byte(64bit)表示的。(这个小问题面试会被问到的)

如何使用指针?

指针的声明

int *p;
char *p1;
float *p2;

?声明还是很简单,指针的类型 * 变量名即可声明一个指针变量。

int num = 5;
int *p = #

?此时就是一个int类型的指针变量指向一个int变量,画个图解释下。


1.jpg

以很清楚的看到指针p存放着变量num的地址,我们通常说指针p指向变量num,当p知道变量num之后,p就可以对变量num为非作歹了,比如

int main(){
    int num = 5;
    int *p = #
    printf("*p=%d,num=%d\n",*p,num);  //此时num的值就变为5
    p+=1;
    printf("*p=%d\n",*p);    //此时p指向了哪里?这句代码会不会报错?

指针的大小和类型

?从上面的声明实例可以看到我定义了三种类型的指针,可以看出指针是有类型的。这里有同学就有疑问了,不是存放内存地址的么,内存地址不就是一串十六进制表示的数字么(其实底层都是二进制),哪来的什么类型一说呢,为什么又需要类型呢?

?这个疑问很好,我当时学习的时候也是很疑惑。首先我们明白了指针是一个存放地址的变量,明白这点还不够还必须理解另外一个问题就是

?字节(Byte)是用于计量存储容量的一种单位,每一个字节由8位组成(1Byte = 8bit)。地址可以理解为在一片内存中,每个字节(Byte)的编号。

?所以很多人肯定会明白了,指针存放的是一个变量的首个字节的地址,那么问题来了。

int a = 5; 
int *p = &a;

?我们声明指针p指向变量a的地址,也就是说指针p里面存放着变量a的首地址,在32位平台下,int a 是4字节,指针去取a的值的时候找到的是a的首地址,那怎么拿到变量a,聪明的同学已经恍然大悟,是的,没错,所以我们的指针需要类型的,编译器去取指针指向的内容时候会根据指针的类型去取。画个图如下

1.jpg1.jpg

此刻我相信你对指针已经有了很高的理解了。指针的大小很好理解 就是存放地址的范围,地址的范围是操作系统地址线的根数决定,所以指针的大小是随操作系统的寻址范围决定的,一般32位系统地址总线也是32根,寻址范围是2^32次方

?顺便说下32位操作系统和64位操作系统的区别在哪里,系统的位数代表运算能力,所谓32位就是能计算的字长是32位的,64位系统能计算的字长是64位。处理器的字长越大,说明它的运算能力越强。

指针的操作符

?目前我们已经了解到指针的解引用 *p 和 & 两个操作符,下面将带你了解更多的操作符。

操作符名称含义
*p解引用取出指针对应的内容
->指向用来访问指针引用的字段
+ & +=加 和 加等指针的加法
- & -=减 和 减等指针的减法
==等于比较两个指针
!=不等于比较两个指针
> &>=大于 和 大于等于比较两个指针
< & <=小于 和 小于等于比较两个指针

?不知道还记不记得上面代码中的这个例子。

1.jpg

这里使用到了两个指针的操作符,*p 和 p+=1。重点说下p+=1,他代表的是p=p+1,p指针此时指向了num变量的最后一个字节的下一个地址,指向了一个未声明的内容。不知道大家有没有尝试那句代码会不会报错?

还是要多练习一下的,说下结果,那句代码不会报错,会很顺利的打印出一个内容。是不是好危险,这个内容明明不是你的,是的操作指针就是很危险,但是同时也会很灵活。(世界就是这样子你得到一些东西就会失去一些东西。此时你得到了指针的高效,灵活,你就必须去平衡一些其他问题。还有一个很明显的例子,算法中常常面临到时间和空间的平衡问题。)

?平时用到最多的是前面六个,含义上面表里已经罗列,我重点介绍下+和-操作符。指针的+1代表指针的位置移动指针对应大小个字节。看个图,大家肯定瞬间明白。

1.jpg

多级指针&多级指针的解引用

?看到这里,我相信聪明的大家对一级指针有了很明确的认识了,接下来带大家了解下多级指针和多级指针的解引用问题。很多人看到多级指针这就想放弃了,但是龙叔我就想说

把握一个核心本质就是,指针就是地址,指针变量就是存放地址的变量 多级指针无非就是饶了几个弯子,本质没变的。例如

int num = 9;
int *p = &num;
int **sp = &p;
int ***ssp = &sp;

?没错吧,本质没变的,就是饶了几个弯子。所以学习这件事情一定要把握本质的东西,那些变着花样也不过如此,岂能难道我辈。

?当然这只是举例子,多级指针很明显不是这种用法,这样用符合逻辑,但是没啥意义。看个多级指针的示例

int arr[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
int **p = arr;
//此时要操作该二维数组,完全可以使用指针即可,很多时候减少了数据拷贝。
//在多维数组,字符串,复杂数据结构使用上,多使用指针,会减少很多数据拷贝。

?说到使用指针,减少数据拷贝,我就想到经历的一个bug,差点线上瘫痪。

工作case

?写了一个简单的代码,展示下伪代码。

bool fun(string a,string b){
  //对a ,b字符串进行一通操作,逻辑完全没问题
}

?就这样简单的一个功能函数,写完,自己测试ok,还让同事review了代码。完全没问题,大概十二点左右我发布上线了,一般发布之前会小流量灰度,发布也是轮流上线,这些都ok。但是晚上九点多,我意外收到报警了,内存瞬间飙升95.7%,我没有丝毫犹豫,我自己今天发布代码了,留了一台机器现场,其他立马回滚,一通操作。老大跑过来一起review代码,观察监控数据。老大还是厉害,扫了一眼代码,立马发现问题,你这里函数参数为啥不使用引用或者const char* 类型,你把这个改了自己压测下。

?错误很明显,就是没用指针导致函数参数数据拷贝,在晚上流量高峰的时候,内存被瞬间抽干。这样的例子看起来很简单,我们日常写代码时候可能不会太在意,但是在处理高并发、强实时的问题上一定要慎重处理。

?今天就说到这里吧,下一篇给大家讲解指针的特性和指针的安全。



评论