目录
指针相信了解过计算机的小伙伴们都知道,他十分的难,并且在学习计算机的路上我们无时无刻都需要他。所以今天小白想要好好说一说这个指针,帮助大家理解。
一、内存和地址
1.内存
在这之前呢,小白现举一个生活中的例子,大家都知道我们要去看一场自己喜欢的电影都需要买票,而票上都有对应的座位号,可以防止我们走错,找错,防止别人占座。同时如果你中途进场可以准确找准自己的位置。同理,指针也是这个作用,它可以让CPU通过座位号准确找到一个内存空间,让程序员快速访问。
而在计算机中我们把座位号叫做地址,而地址也就是指针。所以我们可以理解为:
座位号 == 地址 == 指针
在这里我们也说一下计算机中的单位换算:
而我们又知道内存与指针息息相关,内存划分为一个个的内存单元,每个内存单元的大小是1个字节,并且变量创建的本质其实是在内存中申请空间,而地址则是将这些单元命名,赋给他们编号,方便指针寻找。
这里的0x00000001就是地址。
2.编址
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节
很多,所以需要给内存进行编址(就如同座位很多,需要给座位号⼀样)。
计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
就如同乐器一样,上面并没有标明七阶音符,但是制造商会告诉你哪里发什么样的声音,使人们达成了一种共识,计算机也是一样。
首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同⼯作的。所谓的协同,至少相互之间要能够进行数据传递。
但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。
而CPU和内存之间也是有⼤量的数据交互的,所以,两者必须也用线连起来。
不过,我们今天关心一组线,叫做地址总线。
硬件编址也是如此:
我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么
⼀根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含
义,每⼀种含义都代表⼀个地址。
地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊
CPU内寄存器。而地址总线:找内存单元是地址来做,地址总线传递地址信息,进行定位。
二、指针变量与地址
1.&(取地址操作符)
我们知道变量创建的本质是在内存中申请空间。而&取地址操作符可以取出地址让我们观察。
从图片我们可以直观的看到变量a的地址,而整型a占4个字节,所以向内存申请了4个字节用于存放10。
而且我们可以看到每个字节都有一个地址,并且是连续的,所以我们只需知道第一个地址就可以顺藤摸瓜知道其他。
2.指针变量和解引用操作符(*)
1.指针变量
当我们取出一个变量的地址时,我们需要将他存储起来,所以这时候创建的变量存储地址的就叫做指针变量。
而其形式为:
int a = 10;
int* pa = &a;//这里的pa就是指针变量
2.理解指针变量
3.*(解引用操作符)
//int main()
//{
// int a = 30;
// int* pa = &a;
// *pa = 200;//注意:这里不是多此一举,这里给变换a的值提供了一种路径,让代码更加灵活。
// printf("%d\n", a);
// return 0;
//}
* -- 解引用操作符(间接访问操作符),是用来pa所存放地址中,地址所指向的内容,通过pa找到a, *pa就是a,正如代码所说的那样,通过*pa来改a并不是多此一举,会多一中方法,使代码更加灵活。
4.指针变量大小
1. 指针变量是用来存放地址的,一个地址的存放需要多大空间,那么指针变量就是多大;
2.指针变量的大小取决于地址的大小。
32位平台下地址是32个bite位,指针变量大小是4个字节;
64位平台下地址是64个bite位,指针变量大小是8个字节。
int main()
{
printf("%zd\n", sizeof(int*));
printf("%zd\n", sizeof(char*));
printf("%zd\n", sizeof(float*));
printf("%zd\n", sizeof(double*));
printf("%zd\n", sizeof(_Bool*));
return 0;
}
注意:指针变量的大小和类型是无关的,只要指针类型的变量在相同平台下,大小都是相同的。
三、指针类型的意义
通过前面我们可以了解到指针变量的大小和类型是无关的,到这里也许会有小伙伴要问了,既然大小都一样,那为什么还要分这么多类型呢?接下来就让我们探讨一下吧!
1.指针的*
int*类型一次访问了4个字节,调试后可以看出会将a的4个字节全部改为0。
char*类型一次访问了1个字节,发现a的4个字节只有一个变为了0。
结论1:指针类型决定了我们进行解引用操作时到底访问了几个字节,char*访问1个,int*访问4个,数组解引用访问一个数组。
2.指针的+-
//int main()
//{
// int a = 0x22334455;
// int* pa = &a;
// char* pc = &a;
// printf("%p\n", &a);
// printf("%p\n", pa);
// printf("%p\n", pc);
//
//
// printf("&a-1=%p\n", &a-1);
// printf("pa-1=%p\n", pa-1);
// printf("pc-1=%p\n", pc-1);
//
// return 0;
//}
这里我们演示减法,加法同理。我们可以看到char*-1后退了4个字节,int*-1后退了4个字节,所以这就是指针类型不同带来的差异。
结论2:指针类型决定了指针向前或者向后走一步有多大距离。
我们也可得出一个计算式:
3.void*指针
void*指针又是无具体类型指针(泛型指针),它可以接受任意类型指针,但是无法直接进行指针运算,也就是无法进行指针+-以及解引用操作。
而他最大的作用就是在于它可以接受任意类型指针,可以作为函数参数来接收不同类型数据的地址,因为当别人传过来一个数据时,当我们无法判断是什么类型数据时,可以用void*来接收,会更加安全方便。
四、const指针
1.const修饰变量
const修饰变量的时候叫常变量。这个被修饰的变量本质上还是变量,只是不能被修改。
int main()
{
const int num = 100;
num = 200;//err
printf("%d\n", num);
return 0;
}
但是当我们用指针进行操作时,发现num的值可以被修改,但是我们希望不管是任何方法都不可以修改num的值,所以我们应该在指针层面上进行限制,让他无法被修改。
2.const修饰指针变量
需要小伙伴们知道是const修饰指针变量,可以放在左边,也可放在右边,但是其意义是不一样的。
1.左边
放在*左边,限制的是指针指向的内容,也就是不能通过指针变量来修改它所指向的内容,但是指针变量p本身可以改变,因为p是变量,是变量就有地址。
int main()
{
int a = 20;
int b = 30;
int const* p = &a;//const指针放在*左边,限制的是指针变量p指向的内容a,也就是说此时不能通过p来改变a了
//但是其本身的地址可以改变.//const int* p = &a;也属于左边。
*p = 200;//err
p = &b;//ok
return 0;
}
2.右边
放在*右边,限制的是指针变量本身,指针不能改变它的指向(地址),但是可以通过指针变量修改它所指向的内容。
int main()
{
int a = 20;
int b = 30;
int * const p = &a;//const指针放在*右边,限制的是指针变量p本身,也就是说此时能通过p来改变a了
//但是其本身的地址不可以改变.
*p = 200;//ok
p = &b;//err
return 0;
}
3.*两边都存在const
如果都存在const,则会此限制全部,既不能修改指针所指向的内容,也不能修改指针变量本身。
声明:这里对于p有三个相关的值
1.p:p里面放着一个地址(a的);
2.*p:p指向的那个对象;
3.&p:表示的是p变量的地址。
五、指针运算
(1)指针+-整数;(2)指针-指针;(3)指针的关系运算
注意:没有指针+指针,因为没有意义,就拿日期来说,如果拿2024年6月7号加2025年6月7号,即得不出天数,也得不出日期,所以没意义。
1.指针-指针
- 指针-指针的绝对值是指针和指针之间元素的个数;
- 指针-指针,其计算的前提条件是两个指针指向的是同一空间(路径)。
题目:写一个代码,计算字符串的长度。
思路:
方案一:这里的数组名其实是数组首元素的地址,以及传参的本质,后面我会给大家详细介绍。
#include <string.h>
/*1*/
//循环
//size_t ZF_strlen(char* p)//size_t是无符号整型
//{
// size_t count = 0;//计数器
// for (*p; *p != '\0'; p++)
// {
// count++;
// }
// return count;
//}
int main()
{
char ch[] = "abcdefg";
size_t len = ZF_strlen(ch);//数组名其实是数组首元素的地址;ch == &ch[0],也就是第一个元素的地址
printf("%zd\n", len);
return 0;
}
方案二:
#include <string.h>
/*2*/
//指针++
//size_t ZF_strlen(char* p)//size_t是无符号整型
//{
// size_t count = 0;//计数器
// while (*p != '\0')
// {
// count++;
// p++;//指针++
// }
// return count;
//}
int main()
{
char ch[] = "abcdefg";
size_t len = ZF_strlen(ch);//数组名其实是数组首元素的地址;ch == &ch[0],也就是第一个元素的地址
printf("%zd\n", len);
return 0;
}
方案三:指针-指针
思路:
#include <string.h>
/*3*/
//思路:a,b,c,d,e,f,g,\0,将p的值给start,则代表第一个地址是start,end通过循环遍历,一步步接近\0,
//最后到末尾地址,在通过指针减法法则得结果
//size_t ZF_strlen(char* p)//size_t是无符号整型
//{
// char* start = p;//将p的值给start
// char* end = p;//将p的值给end
// while (*end != '\0')//前几个可以直接写*end,因为\0的地址就是0,0为假,所以循环停止
// {
// end++;
// }
// return end - start;//指针-指针
//}
int main()
{
char ch[] = "abcdefg";
size_t len = ZF_strlen(ch);//数组名其实是数组首元素的地址;ch == &ch[0],也就是第一个元素的地址
printf("%zd\n", len);
return 0;
}
2.指针运算
用指打印1~10
方案一:
//int main()
//{
// int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
// int sz = sizeof(arr) / sizeof(arr[0]);
// int* p = &arr[10];
// int i = 0;
// for (i = 0; i < sz; i++)
// {
// printf("%d ", arr[i]);
// p = p + 1;//指针+整数
// }
// return 0;
//}
方案二:
//倒着打印1~10
//int main()
//{
// int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
// int sz = sizeof(arr) / sizeof(arr[0]);
// int* p = &arr[sz-1];
// int i = 0;
// for (i = 0; i < sz; i++)
// {
// printf("%d ", *p);
// p = p - 1;//指针-整数
// }
// return 0;
//}
方案三(错误方案):用char*打印1~10,理由我一次跳过一个字节,那么我乘4也可以跳过4个字节,所以可行。但其时不可以,小伙伴们有兴趣可以试试调试一下。
//用char*打印1~10虽然你能够成功,但还是不对,因为这里数字太小,一旦数字大了,会发生错误。
//int main()
//{
// int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
// int sz = sizeof(arr) / sizeof(arr[0]);
//
// char* p = &arr[10];
// int i = 0;
// for (i = 0; i < sz; i++)
// {
// printf("%d ", arr[i]);
// p = p + 4;
// }
// return 0;
//}
六、野指针
1.指针未初始化
p是局变,但没有初始化,其值就是随机值,如果将p中存放的值当做地址,解引用后会形成非法访问。
//int main()
//{
// int* p;
// *p = 200;//err 未初始化
// return 0;
//}
2.指针越界访问
//int main()
//{
// int arr[10] = { 0 };
// int* p = &arr[0];
// int i = 0;
// for (i = 0; i <= 10; i++)//越界访问
// {
// *p = i;
// p++;
// }
// return 0;
//}
3.指针指向的空间释放
n为局部变量,当代码调用test()结束时,n会被释放,而p却还在访问地址,则会成为野指针。
//int test()
//{
// int n = 200;//n为局变,函数调用完之后就会被销毁,如果还去返回的话,p会成为野指针。
// return &n;
//}
//
//int main()
//{
// int* p = test();
// printf("%p\n", p);
// return 0;
//}
七、assert断言
assert.h头文件定义宏assert(),用于运行时确保程序是否符合指定条件,如果不符合就会报错终止运行,这个宏常被称为“断言”。
写法:assert(p != NULL);
作用:验证变量p是否为NULL,防止野指针的产生。
assert()接受一个表达式作为参数,用于判断表达式是否为真,如果为假就会报错,并且显示具体位置 ,所以对程序员非常友好。
如果不想要断言,只需要在头文件前加#define NDEBUG,
一般是在Debug版本中使用,在vs的Release中,直接就被优化了。
八、传值调用和传址调用
1.鲁棒性
e.g.求一个字符串长度,要求增加代码鲁棒性(使用const和assert可以增加代码的稳定)
//size_t ZF_strlen(const char* p)
//{
// int count = 0;
// assert(p != NULL);
// while (*p)
// {
// p++;
// count++;
// }
// return count;
//}
//int main()
//{
// char arr[] = "abcdef";
// size_t len = ZF_strlen(arr);//数组名表示数组首元素的地址
// printf("%zd ", len);
//
// return 0;
//}
2.传值和传址调用
传值调用:函数在使用时,是把变量本身传给了函数。
传址调用:函数在调用时,将变量的地址传给了函数。
写一个函数交换整型变量的值
传值调用:
我们发现a和并没有交换,这是因为形参只是实参的一份临时拷贝,形参有自己的独立空间,有自己的地址,也就是说void swap2 (int x, int y),只是x,y进行了交换,与a,b无关,因为地址不同,对形参的修改不影响实参。
那应该如何解决呢?
很容易我们只需要将a和b的地址传给函数,我们就可以进行交换。
传址调用:
void swpl(int* px, int* py)//将a,b的地址传入当中,通过地址彻底交换a,b,使形参与实参的地址一致
{
int z = 0;
z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 6;
int b = 8;
printf("交换前a=%d b=%d\n", a, b);
swpl(&a, &b);
printf("交换后a=%d b=%d\n", a, b);
return 0;
}
到这里指针1的内容也就结束,文章很长,但干货很多,需要大家慢慢消化,在这里小白也祝大家早日理解指针,成为大牛!也希望大家多多支持,你们的支持就是我前进的动力。