C语言越学到后面,越会感到恐慌,听到指针、结构体等等这些,想必很多人不自觉的就会感觉很难,就想打退堂鼓了。哈哈哈哈,被小博猜到了吧!!悄悄告诉你们,小博刚开始学习的时候也是。但是时过一年,小博又是一条好汉,又重新回来拾起了这个曾经把我吓得连连后退的语言了。接下来,跟着小博来学习指针吧!!
一、内存和地址
指针是什么,这里小博先给你们打个预防针,指针==内存单元的编码==地址,首先你应该接受这个概念。好了,那么内存单元又是什么?内存单元的地址有什么用?
我们给计算机输入数据,计算机都是要通过内存存到一个空间里面的,等我们需要的时候,计算机再从内存中取出数据供我们使用。数据很多时,计算机如何才能准确的找到我们所需的数据呢?可以把内存看成一栋大楼,里面有很多个房间,每个房间都有门牌号,计算机每次把读到的数据存入对应的房间,但需要的时候,计算机会自动的通过门牌号找到该房间所存的数据。每个房间就是内存单元,门牌号就是内存单元的编码,即地址。
听“都瑞咪发嗦啦”,钢琴被谁按动了。你有没有想过为什么每个键能发出不一样的声音,并且弹奏者能够准确定位到每个琴弦的位置?是的,这些都是被制造商提前在硬件层面上设计好的。然而计算机也一样,计算机也是由各种各样的硬件组成,各个硬件之间协调工作,使得他们之间可以相互进行数据传递。学过计算机组成原理的同学都知道,计算机的CPU和内存之间存在大量的数据传输,并且通过“线”进行传输。这里小博带你更深的了解计算机里的内存。
计算机中内存可划分为一个一个内存单元,每个内存单元的大小为1个字节,一个字节里有8个比特位。其中每个内存单元都有各自的编号。
补充:
- 1 byte(字节)= 8 bit
- 1 KB = 1024 byte
- 1 MB = 1024 KB
- 1 GB = 1024 MB
- 1 TB = 1024 GB
- 1 PB = 1024 TB
计算机中CPU和内存之间通过线进行数据交互,主要有如上三类。当然这里我们主要讲地址线,即CPU是通过地址线找到内存单元的编码的。进行数据传输的时候,肯定不会用一根地址线了,那样多慢了,计算机在被制作的时候,是会被设定好有多少根地址线进行传输的,这些地址线统称为地址总线。如:32位的机器有32根地址线,1根地址线只有0或1两种状态,表示电脉冲有无。依次类推,2根地址线有4种状态,3根有8种.......32根地址线就有2^32种状态,每种状态都代表一个地址,就可以通过该地址找到对应的数据。
二、指针变量和地址
1、指针变量
我们的重头戏来了!!首先要弄清楚两概念:
指针:指的是地址
指针变量:是指存放地址的变量
我们通过上面已经知道了什么是地址,这只是一个概念,我们要怎么用计算机语言将地址表示出来呢,当然要通过一个变量表示了,即指针变量。
int a = 10;
printf("%p\n", &a); //&a-----&取地址操作符,单目操作符
int* p = &a; //p是一个变量(指针变量),是一块空间,表示取出a的地址放到p中去
这里要注意,指针变量p的类型为int *,*相当于指针变量p的标识符,int 是指p中存放的地址对应的变量a是int类型,记住一句话,指针变量中的内容就是地址。
2、取地址操作符(&)
上面我们学的 int* p = &a; 是对指针变量的定义,其中&为取地址操作符,用来遍历指针变量,
&a表示对a进行取地址。
如:
int a = 10;
printf("%p\n", &a); //&a-----&取地址操作符,单目操作符
int* p = &a; //p是一个变量(指针变量),是一块空间,表示取出a的地址放到p中去
其中a为整形变量,会向内存空间中申请4个字节,且每个字节都有对应的地址。
然而,printf("%p\n", &a); 打印出的则为:0x006FFD70,即取出的为4个字节中地址较小的字节的地址。
总结:&a取出的是a所占4个字节中地址较小的字节的地址。
而我们口头语中说的p是一个指针,其实指的是指针变量,只是我们通常习惯这么说而已。
3、解引用操作符(*)
上面我们讲到将变量的地址存起来,当然我们也得找到他。那么问题来了,内存空间那么大,内存单元那么多,指针变量如何才能精准的定位到目标地址?这时候就要用到解引用操作符(*)了,如:
int a = 10;
printf("%p\n", &a); //&a-----&取地址操作符,单目操作符
int* p = &a; //p是一个变量(指针变量),是一块空间,表示取出a的地址放到p中去
*p; //*---解引用操作符(间接访问操作符)
也就是说在指针变量的旁边放上一个*,就可以准确的找到a的地址。
如上,我们通过 *p 找到a的地址之后,将其中的变量a改为0,则打印出的结果就为0。
再次梳理一下上面讲的1、2、3:
&a :将整形变量a的地址取出。
p = &a; 将a的地址取出存放到变量p中,故p称为指针变量。
int* p = &a;
- int* 为指针变量p的类型,
- * 相当于指针变量p的标识符,
- int 是指p中存放的地址对应的变量a是int类型
*p:找到整形变量a的地址。
其中,& 和 * 相当于一对的,一个取出,用于存放;一个找到,用于解引用。
三、指针变量的类型
1、指针变量的大小
在64位的机器中有64根地址线,这64根地址线中,每一个地址都是由64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节,和类型无关。
如,在x64(64位)环境下运行的结果
如,在x86 (32位) 环境下运行的结果
总结:
- 32位平台下地址是32个bit位,指针变量大小是4个字节。
- 64位平台下地址是64个bit位,指针变量大小是8个字节。
- 指针变量的大小和类型无关,在相同的平台下,指针大小都是相同的。
2、指针变量类型的意义
有没有发现,指针变量无论是哪种类型,在同一平台下,其大小都是一样的,那么定义这些类型又有什么意义呢?
来看一下这里两个指针变量的区别,同样是对整形变量a进行取地址,int * 和char * 申请到的内存空间的大小一样大,即指针变量的大小相同。但当对指针变量进行解引用操作的时候,int * 改变了内存空间中的4个字节,而char *则改变了内存空间中的1个字节。
所以说,指针类型决定了指针进行解引用操作的时候访问了几个字节,也就是决定了指针的权限。
3、指针 + - 整数
当我们定义两个指针变量类型,int * 和char *,分别对其进行+1操作,可发现int *类型的指针变量跳过了4个字节,char *类型的指针变量只跳过了1个字节。
因此,指针类型决定了指针进行+1,-1的时候,一次跳过的距离。
- int * + 1 ——>跳过4个字节
- char * + 1——>跳过1个字节
4、void *指针
根据上文,我们了解到指针变量有很多类型,如:char *,int *,short *等等类型。然而,这里有一种特殊的类型void *类型,可以理解为无具体类型的指针(泛型指针),可以接受任意类型的地址,但无法进行指针的+-整数和解引用运算。一般用于,当你取出了一个变量的地址,但不确定放在什么类型的指针变量中,就可以用void *这种类型来替代这个不确定的指针变量的类型。
这里我们不着重讲了。
四、const修饰指针
看到const,你是否似曾相识呢?是的,我只知道他是一个关键字,平常我也没用过啊!!😥😥
哈哈哈哈,没关系的,小博带你再温习一遍。
1、const修饰变量
const:常属性(不能被修改的属性),通俗点说,就是一个变量被const修饰后,该变量的值就不能被修改了。如:
#include <stdio.h>
int main()
{int a = 10;const int b = 10;a = 20; //a是可以修改的b = 20; //b是不能被修改的printf("%d %d\n", a, b);return 0;
}
如上,被const修饰后的b是不能被修改的,在语法上加了限制,只要我们在代码中对b进行修改,编译器就会报错,致使无法直接修改b。
const int b=10; 这里我们要知道,const修饰了变量b,使b具有了常属性,不能被修改了,但b本质上还是变量,称常变量。(在VS中,c++语言编译模式下,常变量就是变量,即使b被const修饰,b的值依旧可以被修改)。怎么用呢?当你有一个变量,不想让别人修改的时候,可以直接用const修饰。
然而,天无绝人之路,你说改不了,我就偏要改!!哈哈哈哈,固执的大学生是这样的。还别说,真有办法,虽然被const修饰后的变量不能被直接修改,但是可以通过找到改变量的地址,用解引用操作,将其中的数值修改。如下:
2、const修饰指针变量
和普通变量相比,const在修饰指针变量时稍有不同,首先我们需要先理解这张图:
大家一定要把上面的图中各变量的关系搞清楚,首先是有一个变量a中存放的值为10,指针变量p中存放的是变量a的地址,然而指针变量p也有自己的地址。
理解了上面的关系,我们再来看const在修饰指针变量时有什么不同,
首先,位置不同:
1、const 放在 *右边
int* const p = &a;
和修饰变量很相似,被const修饰过的指针变量依旧无法直接被修改。
依旧相似,如果想改变被const修饰过的指针变量,同样可以用解引用操作。
如此以来,当const放在*右边修饰指针变量时,其用法和修饰变量的相似。
然而我们要知道他的意义,const放在*右边修饰指针变量时,const限制的是指针变量p本身,指针变量不能被修改了,但是可以通过指针变量,修改指针变量指向的内容。
2、const 放在 *左边
int const * p = &a;
有了上面的基础,该代码就更好理解了,我们直接来总结一下,const在修饰指针变量,放在*的左边,限制的是指针变量的内容,不能通过指针来修改指向的内容,但是可以修改指针变量本身的值(修改指针变量的指向)。
3、* 前后都放const
如果是int const * const p这种形式,以上两种情况都会受到限制。
总结来说,const只限制其后面的的内容,共有三种类型:
int * const p ——> p = &n ; //err
int const * p ——> * p = 100 ; //err
int const * const p ——>
- *p = 100 ; //err
- p = &n ; //err
五、指针运算
1、指针+、- 整数
其实在上面的讲解中,我们已将了解过了指针的+、- 运算。如:
int a=10;
int * p = &a;
printf("%d\n", p+1);
- int * p ——> p + 1 跳过4个字节
- char * p ——> p + 1 跳过1个字节
- type * p ——> p + 1 跳过 1*sizeof( type )个字节
- p + n 跳过 n*sizeof( type )个字节
2、应用举例
例:给定一个数组,将其打印出来
方法一:
按照正常的思路,我们会想到用for循环的方式打印:
#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int i = 0;for (i = 0; i < 10; i++){printf("%d ", arr[i]);}return 0;
}
方法二:
当然我们也可用指针的方法:
#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int i = 0;int* p = &arr[0];int sz = sizeof(arr) / sizeof(arr[0]); //计算数组中元素的个数for (i = 0; i < sz; i++){printf("%d ", *(p+i)); //解引用操作}return 0;
}
方法三:
对于方法二稍做修改:
#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int i = 0;int* p = &arr[0];int sz = sizeof(arr) / sizeof(arr[0]); //计算数组中元素的个数for (i = 0; i < sz; i++){printf("%d ", *p); //解引用操作p++;}return 0;
}
其实,当我们熟悉了数组存放的底层逻辑之后,并不难写出如上代码。即数组在内存中是连续存放的。
3、指针-指针
指针1 + 整数 == 指针2
指针2 - 指针1 == 整数
即:指针 - 指针的绝对值 ——> 得到的两个指针元素之间的个数
指针- 指针计算的前提条件是:两个指针指向的必须是同一块空间。
printf("%d ",&arr[9]-&arr[0]);
4、指针的关系运算
指针和指针比较大小
举个例子来说,依旧用上面的例题:给定一个数组,将其打印出来
方法四:
#include <stdio.h>
int main()
{int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr; //&arr[0]while (p < arr + sz){printf("%d ", *p);p++;}return 0;
}
这又是一种新的方法,利用了指针的运算关系,打印数组。
这些方法都比较新颖,还需我们多多积累,反复复盘,深入理解指针和内存的底层逻辑。
好了,关于指针的基础知识,小博先讲这么多,指针涉及到的知识点还有很多,小博还会陆续更新,因为小博也是新手,所以哪里有不对的地方还请多多指教。如果你也是小白,那么跟着小博一起来学习吧!!给自己几天时间,一定可以学会的,加油!!
这里小博送给大家自己喜欢的一句话:“言语压君子,衣冠镇小人”。