目录
1.内存和地址
1.1内存
下面举一个生活中的例子,假如有一顿楼,这顿楼有100个房间,但是这个房间没有房间号,有一天你的朋友想过来找你玩,如果想找到你,就得挨个房间去找,但是这样的效率太低了,如果我们给每个楼层的每个房间编上编号,如:
一楼:101,102,103,.....,110
二楼:201,202,203,.....,210
三楼:301,302,303......,310
...............
...............
十楼:1001,1002,1003....,1010
有了房间号,你的朋友知道房间号,就可以快速找到房间,找到你,大大提升了效率。
在生活中,每个房间有了房间号,就能提升效率,能快速的找到房间。
如果把上面的例子对应的计算机中,又会怎样呢?
我们在买电脑的时候电脑里面都有有内存,比如4G/8G/16G的内存
我们知道计算机上CPU(中央处理器)就像人的大脑,在处理数据的时候,需要的数据是在内存中读取的,处理后的 数据也会放回内存中,那我们买电脑的时候,电脑上内存是 8GB/16GB/32GB/64G 等,那这些内存空间如 何⾼效的管理呢?
其实也是把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节,就像上面的房间,每个房间都有房间号,当你朋友想找你的时候直接拿房间号去找。
计算机中常⻅的单位(补充): ⼀个⽐特位可以存储⼀个2进制的位1或者0
1.2编址
硬件编址也是如此
比如,现在有一根地址线,那么这一根地址线就有两种状态,导通或者不导通,就可以访问两个字节的地址空间,那么有两根地址线的话就有四种状态,两根都不导通,第一根导通第二根不导通,第一根不导通第二根导通,两个呢都导通,所以我们有n根地址线的话就有2的n次方种状态。
地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。
2.指针变量和地址
2.1取地址操作符(&)
在C语言中,创建变量的本质就是向内存中申请空间。
通过调试来看一下内存布局。
调试
⽐如,上述的代码就是创建了整型变量index,内存中 申请4个字节,⽤于存放整数30,其中每个字节都有地址,上图中4个字节的地址分别是:
0x012FF790
0x012FF791
0x012FF792
0x012FF793
既然内存中是通过编号找到地址的,那么在程序中index这个变量名有用吗?
这个index变量对编译器来说是没有用的,编译器不会通过index来找这块内存中的空间,也不会给这块空间起名字叫index,这个index只是给写代码的人看的,编译器通过地址就可以找到内存单元,而写代码的人操作index就操作这四个字节的内存单元,既然变量名是给自己和别人看的,所以这个变量名起的要有意义。
当我们前面调试的时候看见每个字节都有地址,那么在C语言中那我有没有办法拿到它们的地址呢?
这个时候我们就要使用一个操作符(&) - 取地址操作符。
index有4个字节,每个字节都有一个地址编号,当我&index的时候拿到的是第一个字节的地址, 地址编号小的地址,顺藤摸瓜就可以找到尾了,%p是打印地址的,打印的地址是十六进制的。
2.2 指针变量和解引⽤操作符(*)
2.2.1指针变量
那如果我想把取出的地址存起来该怎么做呢?
存放指针的变量的名字是p,int*是p这个变量的类型(指针变量)。
2.2.2 如何拆解指针类型
那应该怎么理解int*呢?
1.*表示p是指针变量。
2. int表示p指向的变量index的类型是int。
2.2.3解引用操作符
我们将地址保存起来有什么用呢?将来我们要使用这个地址,又该如何使用呢?
在C语言中,我们只要得到了地址(指针),我们可以通过地址(指针)找到地址(指针)所指向的对象,这里了解一个操作符(*)-解引用操作符。
*p 的意思就是通过p中存放的地址,找到指向的空间, *p其实就是a变量了;所以*pa = 20,这个操作符是把index改成了20,其实直接把index赋值为20,但是使用指针,写代码就会更加灵活。
& -> 取地址操作符
* -> 解引用操作符
取地址操作符就是取出index的地址,解引用操作符就是*p找到index,一来一回,可以相互抵消。
a&b -> 按位与
&a -> 取地址
a*b -> 乘法
*a -> 如果a是指针变量,那么*就是解引用操作符
2.3 指针变量的大小
p占多大内存呢?
从代码的运行结果可以看出,环境不一样,代码运行的结果也不一样。
指针变量是用来存放地址的,一个地址的存放需要多大的空间,那么指针变量的大小就是多大。
32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后 是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4 个字节才能存储。
如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要 8个字节的空间,指针变量的⼤⼩就是8个字节。
总结:
32位平台下,地址是32个bit位,就是4个字节。
64位平台下,地址是64个bit位,就是8个字节。
前面我们看到int* 类型的变量的大小是4或者8个字节,那char* short*呢?
从代码的运行结果可以看到,不管什么类型的地址的指针,只要是在32位环境下都是4个字节,64位环境下8个字节。
接下来我们通过调试看一下为什么所以类型的指针大小都一样。
从内存调试窗口和打印可以看出左上角图片取出的是一个整型的地址,取出的是第一个地址,在x86环境下就是(0x004FFE60)这样一个十六进制数字,存放就得4个字节,就像下面的x64环境下调试或打印的地址(000000B0A10FF984)这样一个十六进制数字,存放就得8个字节,4个字节(int)取地址取出的是第一个字节的地址,一个字节(char)取地址也取出的是第一个字节的地址,取出的都是地址,地址是没有区别的,地址是通过地址总线传过来的,不管是字符的地址还是整型的地址,都是编号,编号都是32个0和1组成的序列,地址存放需要4个字节或者8个字节,跟类型是没有关系的。
总结:
指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
3. 指针变量类型的意义
前面说到指针变量的大小和类型无关,在同一个平台下,大小都是一样的,为什么还要有各种各样的指针类型呢?
3.1指针的解引用
整型指针解引用操作并且赋值为0,操作的是四个字节。
字符型指针解引用操作并且赋值为0,操作的是一个字节。
总结:
指针类型没有决定它的大小, 但是决定它解引用的时候访问多大内存空间。
指针的类型决定了指针向前或者向后走一步有多大(距离)
3.2指针加减整数
从代码的运行结果可以看出,&a到&a+1跳了四个字节,pa到pa+1跳了四个字节,pc到pc+1跳了一个字节。
&a,取出a的地址,取出的是一个整型的地址,对于一个整型的地址加1,就跳过四个字节,因为一个整型就是四个字节。
当把a的地址赋给整型指针,对于pa+1来说也是跳过四个字节。
当把a的地址赋给字符指针,对于pc+1来说就是跳过一个字节,因为他是一个char*的指针,站在char*的指针的角度它认为它指向的是一个字符变量,其实指向int类型并不关心,char* pc,*表示pc是指针变量,char表示pc指向的是字符型,所以它认为它指向的是字符,所以pc+1跳过一个字节。
总结:
int* pa : pa+1 ---> +1*sizeof(int)
pa+n ---> +n*sizeof(int)
char* pc: pc+1 ---> 1*sizeof(char)
pc+n ---> n*sizeof(char)
那这个指针加减具体有什么用呢?
对于整型指针来说,我加1解引用就能访问一个元素,再加1解引用又能访问一个元素。
3.3 void* 指针
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指 针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的+-整数和解引⽤的运算。
从上面的代码可以看出将一个整型地址赋给一个字符型指针会报类型不兼容的错误,那应该怎么办呢?
其实使用void*类型就不会有这样的问题。
前面我们看到将一个整型地址赋给一个char类型的指针会导致类型不兼容,而上面的代码不管左边是什么类型的地址,都可以赋给void*的指针,而且也不会报警告。
void* --------> 无具体类型的指针,把任何类型的地址赋给void*的指针都是可以的。
当我对void*的指针p1解引用操作赋值为20的时候会报错,因为它是无具体类型的指针,如果是整型指针解引用的话访问四个字节,但现在是无具体类型的指针,解引用不指定访问几个字节,所以无具体类型的指针不能解引用操作。
当我对void*的指针做加减操作也是不行的, 因为它是无具体类型,加1的时候不知道跳过几个字节,p1的运算跟它指向的对象没有任何关系,它的运算是取决于它的类型的。
既不能解引用操作,也不能加减操作,那void*的指针是不是没用呢?
其实是有用的,我们设计函数参数的时候,接收那种不同类型的地址,你不知道别人给你传什么类型的地址,那你这个时候就可以用void*的指针来接收,这就是它的作用。
4.const 修饰指针
4.1 const修饰变量
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。 但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
变量a的值为什么可以修改呢?因为a是变量,那为什么b的值不能被修改呢?因为b变量前面加了const修饰,具有了常量的属性,const修饰变量的时候叫常变量,这个被修饰的变量本质上还是变量,只是不能被修改。
既然这里的n是变量,那n前面加上const是不是就可以了呢?
尽管我们加上const还是不行,因为前面说过了,const修饰变量本质上还是变量,只不过具有常属性, 不能被修改而已。
如果我们绕过n,使⽤n的地址,去修改n就能做到了,这样就打破了语法规则。
虽然不能直接修改a的值,但可以通过指针来修改a的值,这样也是可以的, 但是如果想修改a的值的话直接不加const就可以了,加上const就不希望a的值被修改,加上const通过指针修改值是不推荐也是不合法的。
4.2 const修饰指针变量
那通过指针也修改不了a的值,即使把a的地址交给p,也不能通过p来修改a的值,这个时候应该怎么写呢?
⼀般来讲const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不⼀样的。
int* p;//没有被const修饰
int const * p;//const放在*的左边
int * const p;//const放在*的右边
关于指针p有三个相关的值:
1.p,p里面放着一个地址
2.*p,p指向的那个对象
3.&p,表示的是p变量的地址
从上面代码可以看到,可以通过*p修改a的值,也可以直接修改p这个指针变量的值。
那如果我们不想*p所指向的变量或者p指针变量被修改,这个时候应该怎么写呢?
前面我们说const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不一样的。
const修饰指针变量,放在*的左边,限制的是指针指向的内容,也就是不能通过指针变量来修改它所指向的内容,但是指针变量本身是可以改变的。
const修饰指针变量,放在*的右边,限制的是指针变量本身,指针不能改变它的指向,但是可以通过指针变量修改它所指向的内容。
在*的左右两端都加上const,*p被限制了,p也被限制了。
5. 指针运算
指针的基本运算有三种:
指针+-整数
指针-指针
指针的关系运算
5.1指针+-整数
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。
通过下标遍历数组。
通过地址遍历数组
我们知道数组在内存中是连续存放的,arr是数组首元素的地址,赋给指针变量p,解引用就得到第一个元素,然后加1,整型指针加1跳过四个字节,得到第二个元素,以此类推。
只要得到首元素的地址,就可以通过指针加减整数遍历数组。
1.指针类型决定了指针+1的步长,决定了指针解引用访问多少字节。
2.数组在内存中是连续存放的。
也可以通过指针-整数的方式遍历 数组。
5.2指针-指针
指针-指针的绝对值是指针和指针之间元素的个数
指针-指针,计算的前提条件是两个指针指向的是同一个空间,否则是没有意义的。
那这个有什么用呢?
例子:写一个函数,求字符串的长度
库函数strlen求字符串长度
strlen统计的是字符串中\0之前的字符个数。
模拟实现strlen函数
上面代码就使用了指针减指针求出字符串的长度。
指针+-整数,指针-指针我们都知道了,那指针+指针呢?
举个例子:日期+-天数,今天往后20天,往前20天。日期-日期,可以算出中间隔了多少天,那日期+日期就没有意义了,就像指针+指针,也是没有意义的。
5.3指针的关系运算
每个字节都有地址,地址就有大或者小,指针的关系运算其实就是让指针大小的比较。
代码:
p每次访问一个元素,然后加加,当p和&arr[sz]相等的时候,while条件为假,跳出循环。
6.野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1野指针的成因
1.指针未初始化
p是局部变量,但是没有初始化,其值是随机值,如果将p中存放的值当作地址解引用操作符就会形成非法访问vs编译器比较严谨,不初始化的局部变量直接编译不过去。
2.指针越界访问
数组大小是10,只能放10个元素,但是for循环循环了11次,导致p一直++,p指针变量越界。它指向了不属于自己的空间,同时还要访问这块空间,就是野指针。
3.指针指向的空间释放
首先,程序从main函数进入,进入test函数,创建变量a并赋值为10,取出a的地址,返回给主函数的指针变量p,然后打印解引用p,因为a是局部变量,进入test函数开辟空间,出test函数将开辟的这块空间要还给操作系统,将地址返回给p还解引用并且打印这个不属于本程序的地址,形成了非法访问,p一点接收test返回来的地址,p里面存放的就是野指针。
6.2 如何规避野指针
6.2.1 指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
在C++中NULL就是0,在C语言中NULL也是0,只不过被强转为void* 类型, 希望0是个地址。
在创建指针变量的时候就初始化一个明确的地址,要不然就赋值为NULL。
空指针不能被访问,否则程序会崩溃。
6.2.2 小心指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
6.2.3 检查指针有效性
指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
当我们在使用指针变量之前可以判断以下,如果该指针不为空可以进行我们的指针操作,如果该指针为空,if语言不会执行。
6.2.4 避免返回局部变量的地址
不要返回局部变量的地址。
7. assert 断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏。这个宏常常被称为“断⾔”。
assert(p != NULL);
assert经常被用来判断指针的有效性。
前面我们用if语言来判断指针是否为空,其实也是可以不使用if语言的。
assert报错也会告诉在第几行出现问题。
当我的指针变量p不再是空指针的时候,运行代码,assert什么都没有发生。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣ 任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误 流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有几个好处:它不仅能⾃动标识文件和出问题的行号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
如果我们使用if语句来判断指针是否为空,还要注释或者删除if语言,非常麻烦。但是assert可以通过一个开关来决定assert要不要执行。
指针变量p为NULL,assert后面的表达式NULL != NULL,此语句条件为假,正常来说这里会报错,可是没有,因为前面加了#define NDEBUG,这会导致assert断言失效。
然后,重新编译程序,编译器就会禁用文件中所有的 assert() 语句。如果程序又出现问题,可以移 除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启用了 assert() 语 句。 assert() 的缺点是,因为引入了额外的检查,增加了程序的运⾏时间。
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题, 在 Release 版本不影响⽤⼾使⽤时程序的效率。所以我们可以看到同样的代码在Debug中会报错,在Release中不会报错。因为Release是发布版本,是供用户使用的,用户不需要排查问题。
8. 指针的使用和传址调用
8.1 strlen的模拟实现
库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数。
assert断言后面的表达是直接写指针也是可以的,如果指针是NULL,也就是0,0为假,也会报错。
const加在*左边限制的是*ch。 让我们的代码健壮性(鲁棒性)更好,为了防止有人故意破坏代码,这样也能更好的识别出错误。
size_t的底层其实是unsigned int类型。
8.2 传值调用和传址调用
学习指针的⽬的是使⽤指针解决问题,那什么问题,⾮指针不可呢?
写一个函数,交换两个整型变量的值
我们写出这样的代码,那让我们看看运行结果。
我们看到代码的执行结果,交换前是2,4,交换后怎么还是2,4呢?
那让我们调试一下
当我代码执行到箭头处的时候,从调试窗口可以看出a的值2确实传给了x,b的值4也确实传给了y。但是它们的地址不一样。
当我代码执行完的时候,确实发现函数中的x和y(形参)的值交换了,但是a和b(实参)的值没有交换, 原因就是当实参传递给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参,因为x,y有自己独立的空间,跟a和b没有关系。
修改参数可以通过变量名修改,也可以通过指针变量修改。
下面就可以用指针来实现一下这段代码
让我们看一下结果:
从代码的运行结果可以看出,通过指针真的可以交换两个整型变量的值。
调试代码:
当我们代码执行的箭头处的时候可以发现a的地址和x的地址一样,b的地址和y的地址一样,a的值是2,b的值是4。
当代码将交换函数执行完之后a的值为4,b的值为2,传址调用,形参是有能力找到主调函数中的变量的,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调用。如果函数内部要修改 主调函数中的变量的值,就需要传址调用。
标签:const,变量,assert,地址,理解,深入,指针,字节 From: https://blog.csdn.net/m0_74271757/article/details/138864931