文章目录
大家好,又见面啦!这一次带来的是有关指针的内容,请享用。
每个人都在努力,我们也不能落下哦。加油加油加油!!!
仍然是:点赞加关注,追番不迷路,蟹蟹大家!
一:内存和地址
1.地址
导言:大家想象一下,如果宿舍楼没有门牌号,想找一个人是很困难的,但如果有门牌号,就很容易了。CPU and 内存(存放数据),CPU处理数据时,处理的数据从哪里来,从内存中来;处理完数据后,将数据放在哪里,也是内存中。那内存中这么多东西,内存空间如何高效的管理呢?
可以像宿舍楼一样,宿舍楼里面有无数个宿舍,一个宿舍八个人。将内存也分为一个个的内存单元,每个内存的大小取一个字节(即8个比特位,一个字节大小可以放8个比特位),可以想象为一个单元可以住8个小比特。
每个内存单元(宿舍)都有自己的编号,以便于CPU可以快速找到一个内存空间。生活中我们把宿舍叫为我们住的地址,在计算机中,我们把内存单元的编号也叫做地址,C语言中又把地址叫做指针。总结:编号= =即地址= =即指针
计算机中的单位:
bit(比特位)
Byte(8比特位=1字节Byte)
KB(1KB=1024Byte)
MB(1MB=1024KB)
GM(IGM=1024MB)
TB(1TB=1024GM)
二: 指针变量和地址
(1.)取地址操作符
(1). 理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量其实就是向内存申请空间
int main()
{
int a = 20;
//创建变量的本质就是:向内存申请空间
//int a=20意思是:向内存申请四个字节的空间(整型int的长度是4个字节)
//变量名是给程序员看的,编译器看的是地址,它是通过地址找到内存单元的
return 0;
}
(2)取地址操作符&
在刚刚操作中可以看出,变量是有地址的,那如何拿到地址呢,这就需要取地址操作符&了。&a
的意思是“取变量a的地址”,a有4个字节,每个字节都有地址,那取的到底是哪个地址?
我们可以尝试打印出a的地址:printf(“%p”,&a);
(%p打印地址)(在看地址的时候,可以改成x86,地址较短,方便观察)
由图可知:&a取出的是a所占4个字节中地址较小的字节的地址。只要我们知道了地址较小的字节的地址,顺藤摸瓜我们就可以找到其他三个字节的地址了。
(2.)指针变量
目前为止,我们已经找到地址了,那如果我们想储存地址该怎么办?我们可以创建一个变量来存放地址(指针),可以将这个变量叫做指针变量
int * pa = &a; //这里创建了指针变量pa来存放a的地址, int *是pa的类型
//*表示pa是指针变量
//int表示pa指向的变量a是int类型
char ch='q';
char* pc=&ch;
(3.)解引用操作符*(间接访问操作符)
那地址有什么用呢?我们可以通过地址找到原本存放的变量a。
- 在地址前+解引用操作符*就可以找到a(*pa实际上就是a)
- 我们还可以通过*pa来改变a的大小
- 指针里涉及到的两个操作符就是&和*
(4.)指针变量的大小(与类型无关)
int a = 20;
int* pa = &a;
printf("%zd", sizeof(pa)); //可以将地址的大小打出来看看
我们现在知道a的大小是4个字节,那指针变量pa的大小是多少嘞?指针变量是用来存放地址的,那地址的存放需要多大空间,那么指针变量的大小就是多大。
前面的内容我们了解到,32位机器(x86)假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做一个地址,【二进制的数字无非是0或1,占1比特位】那么一个地址就是32个bit位,需要4个字节才能存储。【8比特位 = 1字节(Byte)】
如果指针变量是用来存放地址的,那么指针变量的大小就得是4个字节的空间才可以。
同理64位机器(x64),假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。
32位平台下地址是32个bit位,指针变量大小是4个字节
64位平台下地址是64个bit位,指针变量大小是8个字节
那接下来想一下,刚刚是指针变量的类型是int*,那如果是char*,它的大小是多少?答案是也是4个字节(x86环境下)
大家想一下,如果a是整型,那地址取得是第一个字节的地址;char的长度是1个字节,它的地址取得也是第一个,所以还是4个字节(x86)。一个字符的地址 or 一个整型的地址,取得都是第一个地址,一个地址需要4个字节的空间,所以,指针变量(存放地址的变量)的大小与类型无关,只要在同一平台下(x86还是x64),大小就相同
三: 指针变量类型的意义
我们已经知道,指针变量的大小与类型没有关系,那为什么要有这么多类型,它有什么意义呢?
- 指针变量的类型决定了对指针解引用时的权限大小。
- 指针类型决定了指针向前或向后走一步的步长(±整数),单位是字节!
1.指针的解引用
*pa = 32;
这就是指针的解引用
从上面的3张图片我们可以看出:变量类型是int,指针变量类型是int*,我可以将变量int的4个字节全改为0;当指针变量类型是char*时,仅可以将变量int的1个字节改为0。
指针的类型决定 对 指针解引用时有多大权限(一次可以操作几个字节) ,int* 的指针解引用可以访问4个字节,char* 的指针解引用仅仅可以访问1个字节
2.指针±整数
int a = 20;
int* pa = &a;
char* pc = &a;
printf("&a=%p", &a);
printf("pa=%p", pa);
printf("pc=%p", pc);
printf("&a+1=%p", &a+1);
printf("pa+1=%p", pa+1);
printf("pc+1=%p", pc+1);
&a,pa+1之后都向后了4个字节,pc向后了1个字节
char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。
3.void*指针(无具体类型的指针)
void*指针可接受任意类型的指针,但它也有限制,不能进行解引用和±整数的操作(不能解引用因为不知道权限,不知道该访问几个字节。不能±整数,因为不知道该跳过几个字节)
唯一的用处就是当不知道别人将传过来什么类型的地址时,可以用void*接收。
int a = 20;
char ch = 'q';
void* pa = &a; //int*
void* pc = &ch; //char*
*pa = 90; //不可以解引用
pa + 1; //不可以±整数
⼀般 void* 类型的指针是使用在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据,在指针(4)中我还会提及。
四: const修饰
(1)const修饰“变量”
在平常,我们先对变量进行初始化,之后还可以赋值(改变 变量的值)。
int a = 10;
a = 300;
printf("%d\n", a); //打印出来的a是300
当我们用const修饰变量时,变量叫做常变量(本质还是变量,但不可修改)
const int a = 10; //这样的话,a是不可以改变的
我们可以通过地址找见它,再修改它,但是这样不太好,因为我们用const修饰a就是为了让它不被修改(违规操作把它修改了)
const int a = 89;
int* pa = &a;
*pa = 99;
printf("%d", a);
(2)const修饰“指针变量”
一般来讲const修饰指针变量,可以放在 * 的左边,也可以放在 * 的右边,意义是不一样的。
int a = 99;
int const* pa = &a; //和const int* pa = &a;一样 //const在*左边
int* const pa = &a; const在*右边
- 在此区分一下p,*p,&p
- p是指针变量,里面放着地址(注意注意,p可不是常量,它是指针变量,是可以后期赋值的)
- *p指向那个地址的对象
- &p是p变量的地址
int a = 9;
int b = 89;
int* pa = &a; //这里将a的地址放在p里
*pa = 99; //通过地址找到a并改变它的值
pa = &b; //这里将b的地址放在pa里,pa里不再是a的地址
- const修饰指针变量,放在 * 左边(int const * pa =&a),const修饰的是 * pa,自此之后 * pa变成常变量,不能再通过指针变量来改变 * pa所指向的对象。
( 上面的代码中,* pa指向的是a,所以不能通过 * pa来改变a的值,但是还是可以直接改变a的,比如a=88之类的)(上面的代码中,* pa=99;不能实现) - const修饰指针变量,放在* 右边(int * const pa = &a;),const修饰的是变量pa,自此之后pa变成常量,它的指向已确定,之后不能改变(pa里面就是a的地址,不能之后再变了,例如上面代码中pa = &b;不能实现),但是它所指向的内容(a)是可以改滴
五: 指针运算
1.指针±整数
大家还记得之前如何循环打印数组中的数字吗,现在我们可以通过指针±来实现
- 之前的打印方式
int main()
{
int arr[9] = { 1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int index = 0;
for (index = 0; index < sz; index++)
{
printf("%d ", arr[index]);
}
return 0;
}
- 用指针打印
int main()
{
int arr[9] = { 1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int index = 0;
int* pa = &arr[0]; //pa存放的是第一个元素的地址,*pa指向arr[0],第一个元素
for (index = 0; index < sz; index++)
{
printf("%d ", *pa);
pa++; //即p+1,整型指针+1,向后走一个整型
}
}
- 解释:
数组里的数字元素在内存里连续存放,我先得到第一个元素的地址放在pa里,解引用( * pa)就可就得到第一个元素了。
当指针变量+1时,由于指针变量是int*类型,会走4个字节,即一个整数,这是指针变量就是下一个元素的地址了,再解引用,得到第二个元素…
2.指针 - 指针
为什么只有指针-指针呢?以日期为例,你见过5月8日+天数(指针±整数),5月8日-4月9日=期间一共有多少天(指针-指针),但是你见过5月8+8月9=啥?(没有指针+指针)
指针 - 指针 = 指针和指针之间元素的个数的绝对值
指针 - 指针 ,计算的前提是:俩指针指向的是同一个空间
数组中,随着下标的增长,地址由低到高增长【下标小的地址小,下标大的地址大】
int arr[9] = { 1,2,3,4,5,6,7,8,9 };
int sz1 = &arr[8] - &arr[0]; //最后一个的下标:sz-1
int sz2 = &arr[0] - &arr[8];
printf("%d\n", sz1); //结果是8
printf("%d\n", sz2); //结果是-8,因为这里是低地址-高地址
先知:
1.strlen统计的是:字符串中,\ 0之前的字符个数
2.数组名其实是数组首元素的地址:arr == &arr[0]
过程:
(1)先将首元素的地址传过去,形参那里用char*…接收
(2)之后只要*…不是\0,我们就让count+1,之后再指针+1,如果还不是\0,再+1,循环起来
size_t computer_geshu(char* pc) //返回值类型是size_t,无符号整型(个数肯定是整数)
{
size_t count = 0;
while (*pc != '\0')
{
count++;
pc++;
}
return count;
}
int main()
{
char ch[] = "qwertyuiop";
int r = computer_geshu(ch); //数组名ch就是&ch[0]
printf("%d", r);
}
2.指针的关系运算
即让指针进行大小比较
只要我p里面存的地址小于&arr[sz]就能进入循环。
(在这里说一下为什么是&arr[sz],最后一位元素地址是&arr[sz-1],他的后面是没有的,即地址只能≤&arr[sz-1]才能进入循环,≤&arr[sz-1]不就相当于<&arr[sz]。)
//用比较大小来当作循环条件
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
int* p = arr; //这里的arr是数组名,数组名就是数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
for (p = arr; p < &arr[sz]; p++) //也可以写成while(p<&arr[sz]),后面添一个p++
{
printf("%d ", *p);
}
}
六: 野指针
何为野指针?野指针就是指向位置不明确,比如:随机,不正确,没限制
(没有权限访问它,就会成野指针)
(1.)野指针成因
- 指针变量指向 随机(指针未初始化)
int* p;
//p是指针变量,存放地址,但这里并没有说p指向谁
//局部(指针)变量如果没有初始化,它的值是随机的
printf("%p\n", p);
*p=20;
//没有初始化的局部变量p,它的值是随机的,如果将p中存放的值当作地址
//解引用操作符就会形成非法访问
-
指针越界访问
你访问你申请的空间没有问题,但不能越界,如果它指向了不属于自己的空间,而且还解引用访问人家(解引用第10位,则p就成野指针了) -
指针指向的空间被释放
在传给p之前都没有问题,但之后,它将p所指向的值(m)改为900,但m是函数那部分的,当进入函数,m被创建,并申请了4个字节的空间,但是只要一出函数,m就被销毁,申请的空间也还给操作系统了(虽然还给人家了,但它还是坚定地把地址传给p了,只要一但解引用,访问它,则p就成野指针了
(2.)如何规避野指针
- 指针初始化:如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL.(空指针)(NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。)
空指针是不能进行访问的。
- 小心指针越界
- 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
- 避免返回局部变量的地址
七:assert断言
assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。(断言通常被用来判断指针的有效性)
assert() 宏接受一个表达式作为参数。如果该表达式为真(返回值非零), assert() 不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写入一条错误信息,显示没有通过的表达式以及包含这个表达式的文件名和行号。
- 在我看来,它的效果和if差不多,断言是:如果你不对,它就会报错;但 if 是:如果你不对,不满足条件,他会不进入那个if里面,继续往后运行,不会给你报错的。
我这里的意思是,若 t 不是空指针,则令 t 指向的对象=900
我用了断言,它帮我判断出t是空指针,并报错了
- 有一种无需更改代码就能开启或关闭 assert() 的机制。
如果已经确认程序没有问题,不需要再做断言,就在 #include <assert.h> 语句的前面,定义一个宏 NDEBUG 。【即在最前面写#define NDEBUG】 - 缺点:引入了额外的检查,增加了程序的运行时间。【一般我们可以在 Debug 中使用断言,在 Release 版本中选择禁用 assert 就行。在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响用户使用时程序的效率。】
八:指针的使用和传址调用
- 传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;
- 函数中只是需要(主调函数中的变量值)来(实现计算),就可以采用传值调用。
- 如果函数内部要(修改主调函数中的变量的值),就需要传址调用。
仔细想了一下,这个还是需要例子才能说明的更清楚,我将单独写一个帖子,区分一下传值调用和传址调用。
好啦,今天的内容就到这里啦,还有就是谢谢大家的点赞和收藏,拜拜,下次见!