(一)内存,指针变量和地址
(二)指针变量类型的意义及const修饰
(三)指针运算及野指针
(四)assert断言及指针的使⽤和传址调⽤
一.内存,指针变量和地址
1.内存和地址
在生活中,我们有门牌号就可以快速找到房间,而在计算机中,将内存分为一个个单字节大小的内存单元(一个字节占8个比特位,一个比特位可以存放一个二进制位),每个内存单元都有一个编号以便CPU快速找到一个内存空间,我们称这个编号为地址。与此同时,在C语言中我们又将地址称为指针。
常见的单位换算如下:
1Byte=8bit;
1KB=1024B;
1MB=1024KB;
1GB=1024MB;
1TB=1024GB;
1PB=1024TB;
......
我们可以补充一个地址总线,以VS2022为例,我们使用X86环境时机器是32位,32位有机器有32根地址总线,每根线“1”代表有电磁脉冲,“有”,“0”代表没有电磁脉冲,“无”,这样可以组成2^32种含义,每种含义代表一个地址。
2.取地址符号&
在C语言中,我们创建一个变量,就是向内存申请一个空间。
无论是32位还是64位的机器,普通int类型都是4个字节,且每个字节都有对应的地址。
(上面是向内存申请4个字节的空间,来存放整型10)
我们试一下取出a的地址:
实际上,取出的a的地址是a较小的地址,a的实际地址为:
008FFBA4
008FFBA5
008FFBA6
008FFBA7
虽然整型变量占4个字节,但是只要我们知道了他的第一个地址,就能顺藤摸瓜访问他的其他地址,其他的数据。
3.指针变量
为了方便使用,我们把地址存放在指针变量中,存放在指针变量中的值,都会被理解为地址。
这里我们引入一个解引用操作符“*”,它可以解开该元素对应的地址,进而找到这个元素,或者定义这个是指针变量。
int*代表这个变量(p)是指针变量,*代表p是指针类型,p是a的地址,int代表p直接指向的a是整型类型(可以理解为给p解引用的结果类型)。
*p代表把p解引用,即顺藤摸瓜通过a的地址直接找到a(*p=a)。(以便未来要使用)
这样可以通过指针变量p来改变a的值,如:
*p=0;
printf("%d",a);
输出结果是a为0。
我们再引入一个指针变量的大小,前面我们提到32位环境下一般有32个地址总线,可以存放32个二进制位,也就是4个字节,地址在32位环境下是32个比特位,以后用这个地址可以访问它指向的元素。在64位环境下同理,指针变量大小是8个字节。
经过实验证明,在同一环境下,任何指针变量的大小是一样的。
4.指针解引用
#include"20230812定义.h"
int main()
{
int a = 10;
int* p = &a;
*p = 0;
char ch = "abcdef";
char* pc = &ch;
*pc = 0;
return 0;
}
由于指针类型不同,int*类型的可以把a的地址改变4个字节,char*类型的可以把ch地址改变1个字节。
我们看,整型指针+1跳过4个字节,刚好对应一个整型所需的四个字节,而字符指针+1跳过1个字节,刚好对应一个char类型所需的一个字节,真的就像“一地一址”。
也就是说,指针类型决定了跨越的幅度有多大。
二.指针变量类型的意义及const修饰
1.意义:
有助于我们以后对不同类型元素的访问。
2.const修饰
const在*左边,即指针变量解引用的值不可变(即原元素的a或b或c的值不可以改变但可以后期改变它的地址),const在*的右边,即指针变量解引用的值可以变,但相应的地址不能改变。(即涉及到能不能通过指针改变这个元素的值,能不能通过指针改变这个元素的地址)
三.指针运算及野指针
1.指针运算
指针有三种基本运算,分别是:
a.指针+-元素
b.指针-指针
c.指针的关系运算
a.指针+-元素
其实我们可以这样想:我们用地址就是为了有朝一日能够取出地址对应的元素,而对于一个集合里的元素,都有唯一确定的地址,就像一个坑一个蛋。
举个例子:在32位环境下,int*,char*都要占用4个内存,但是元素对应的地址+-一个整数,到时候我们用循环打印地址的解引用,就可以有选择的去打印元素。
这样就有点像地址间距对应元素了。(刚好一个int类型元素占4个字节,一个char类型元素占1个字节)
我们也拿一维数组看一下效果:
效果还是很明显的。
b.指针-指针
当然指针的相减也是有一定规则的,一定要是在内存中连续存放的元素才好地址相减,比如一个字符串中“hellobit”中的8个单词就是在内存中连续存放的,通过如上的例子加一个循环就可以很方便的打印每个单词了,但如果它们不是连续存放的话就相减一下不知道是什么鬼了。
这样我们又学到了新的求字符串长度的方法:偷偷把他们的地址传参到函数中,通过函数把形参和实参产生联系,再结合连续内存的特点指针相减确定元素个数或字符个数。(如果不传指针就比较难产生联系)
c.指针关系运算
我们看一下用地址循环打印:
又是一种新方法。
2.野指针
野指针就是没有经过初始化的指针变量,因为你永远不知道他要指向谁,指向哪个空间。
*为什么会产生野指针?
a.没有初始化
我们看一下下面这段代码:
int main()
{
int* p;
*p = 20;
printf("%d", *p);
return 0;
}
系统报错了,说局部变量“p”未初始化。
系统都看不下去了。
b.指针越界访问
如:
int main()
{
int arr[10] = { 3,1,6,3,7,9,0,4,2,3 };
int* p = arr;
for (p = arr; p < arr+12; p++)
{
printf("%d ", *p);
}
return 0;
}
多出来两个乱的数字,很明显是指针越界访问了,这样的指针明显是野指针,很危险,容易造成数据泄露。
c.指针指向的空间释放
如下代码:
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("p=%p\n", p);
printf("*p=%d\n", *p);
return 0;
}
形参是实参的一份临时拷贝,形参用完被销毁,可是还要返回一个地址,这就不讲武德了。
*怎么避免野指针?
a.初始化指针
int main()
{
int a = 113;
int* p = &a;
int* q = NULL;
int *o;
return 0;
}
对比一下,显然初始化更好。
b.不允许指针越界
这样就相对安全些。
c.指针变量不再使用时,及时安置为“空”,且在使用前检验其有效性
如下:
int main()
{
int arr[10] = { 3,1,6,3,7,9,0,4,2,3 };
int* p = arr;
int i = 0;
for (i=0;i<10;i++)
{
*(p++) = i;
}
p = NULL;//p越界,设为空
p = arr;//重新取,再使用
if (p != NULL)
{
//多一层保护
}
return 0;
}
d.避免返回局部变量的地址
就像前面那个在函数里还返回n的地址,就很不讲武德。
四.assert断言及指针的使用和传址调用
a.assert断言
使用asert断言时需要包含头文件assert.h。
如图:
不满足assert括号里的条件系统会自动报错,并显示在哪个文件里哪一行。
假如我要让它失效,在它的头文件前面这么弄:
这样运行系统就不会报错了。
b.传址调用
以前我们在学习函数传参的时候,用的是传值调用,而我们为什么要学习传址调用呢?
例如,我们可以写个函数来交换两个变量的值:
这是为什么?
前面我们学习函数传参的知识:函数传参时形参是实参的一份拷贝,用完后会销毁,你说交换又有什么用呢?
如果我们偷偷地“伪造”一个地址传过去,那就可以搞定这个问题,这是指针联系了函数内和函数外,达到了“偷梁换柱”的效果。
如下图:
#include"20230812定义.h"
void Add(int* pa, int* pb)
{
int tmp = 0;
tmp = *pa;
*pa =*pb ;
*pb = tmp;
}
int main()
{
int a = 5;
int b = 9;
printf("交换前:a=%d,b=%d\n", a, b);
Add(&a, &b);
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
当形参类型和实参类型都是指针类型时,函数内的内容按照置换原则,地址就像纸盒,里头的内容就像纸巾,我们在函数里面只是把纸巾互换位置,但是纸盒变不了,这样函数执行完就发现盒子不变,纸巾换掉了。(和传值调用的思路差不多,就是传值变为传地址,传值时连纸盒也没有,换了纸有个毛线用)(给形参里的值有了容身之处)
c.strlen模拟
如下图:
#include"20230812定义.h"
int my_strlen(const char* p)//我不希望里面的元素改变
{
char* d = p;
assert(p);//不要让p变为空指针而使程序崩溃
int count = 0;
while (*p!='\0')
{
p++;
count++;
}
return count;
}
int main()
{
char arr[4] = "abc";
int c = my_strlen(arr);//上下两个表达是等效的,传数组(名)相当于传地址
int d = my_strlen("abc");
printf("c=%d\n", c);
printf("d=%d\n", d);
return 0;
}
计算器原理。