目录
一、指针是什么
二、指针和指针类型
1. 指针+-整数
2. 指针的解引用
3.练习
三、野指针
1.野指针成因
2.规避野指针的有效方法
四、指针运算
1.指针+-整数
2.指针-指针
3.指针的关系运算
五、指针和数组
1.测试通过数组下标和指针访问数组元素 的地址:
2.尝试用指针遍历数组并打印数组元素:
3.指针访问数组具体的某个元素
六、二级指针
七、指针数组
八、结语
理解指针的两个点:
1. 指针是个变量,用来存放内存单元的地址(编号)
2. 存放地址的变量叫指针变量
什么是内存?
内存是一块大的空间,把内存划分成了一个一个的内存单元,一个格子就是一个内存单元,取一个字节为基础的内存单元。
为了管理这些内存单元,又给每个内存单元进行了编号。内存单元的编号也叫内存单元的地址,通过地址可以很好的找到内存单元,也就是说,这个地址指向了该内存单元,所以地址也形象地称为指针。
什么是指针变量?
通过&(取地址操作符)将变量的地址取出,再把地址存放到另一个变量中,
这个变量,就是指针变量。
代码示例:
#include
int main()
{
int a = 10;//在内存种开辟一块空间
//将a的地址存放到怕中
int* pa = &a;//拿到的是a的4个字节中第一个字节的地址
*pa = 20;//通过pa访问a的值并将a的值改为20
return 0;
}
一颗*说明pa是指针变量
int说明pa指向的对象a是int类型的
⚠️
注意:指针就是变量,用来存放地址的变量(存放在指针中的值,都被当成地址处理)
现在有问题:
一个小的单元到底是多大?(一个字节)
如何编址?
对于32位的机器,假设有32分地址线,那么,假设每根地址线在寻址的时候产生一个电信号,正电/负电,也就是1或者0。
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
......
11111111 11111111 11111111 11111111
这里就有2的32次方个地址。
每个地址标识一个字节,那我们就可以给(2^32Byte == 2^32/1024KB==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB)
4G的空闲进行编址。
那64位的机器,64根地址线产生的地址会有多大空间?
可以自己尝试计算一下!
⚠️
注意:
1️⃣指针是用来存放地址的,地址是唯一标识一块地址空间的。
2️⃣指针的大小在32位平台是4个字节,在64位平台是8个字节。
#include
int main()
{
int arr[10] = { 0 };
//数组名是数组首元素的地址
int * p = arr;
char * pc = arr;
printf("%p\n", p);//打印p的地址
printf("%p\n", p + 1);//打印p+1的地址
printf("%p\n", pc);//打印pc的地址
printf("%p\n", pc + 1);//打印pc+1的地址
return 0;
}
字符指针加1,相当于跳过了1个字符。
整形指针加1,相当于跳过了1个整型。
因为变量p和变量PC的类型不同,才会产生加4和加1的区别。
总结:指针的类型决定了指针向前或向后走一步能有多大,能走多远(距离)
1️⃣char类型的指针
int main()
{
//a是16进制数字
int a = 0x11223344;//4个字节
char * pc = &a;
*pc = 0;
return 0;
}
2️⃣ int 类型的指针
int main()
{
//a是16进制数字
int a = 0x11223344;//4个字节
int * pa = &a;
*pa = 0;//将a的值改为0
return 0;
}
总结:指针类型决定了指针解引用的权限有多大(能操作几个字节)
例如:char*的指针解引用就只能访问一个字节,而int*的指针解引用可以访问4个字节。
int *指针的写法
#include
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
//指针变量+1指向数组后一个元素
*(p + i) = 1;//将数组所有元素改为1
}
return 0;
}
如果是整形的指针,一个整型的操作完之后跳过一个整形。(也就是一个整形)
char *指针的写法
#include
int main()
{
int arr[10] = { 0 };
char *p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = 1;
}
return 0;
}
如果是char类型的指针,一个字节操作完之后跳过一个字节,而且操作的时候是一个字节,一个字节初始化的
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
#include
int main()
{
//这里的p就是一个野指针
int* p;//p是一个局部的指针变量,局部变量不初始化的话,默认的是随机值
//申请一块空间才可以去访问,而p并没有申请一块空间
*p = 20;//非法访问内存
return 0;
}
#include
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
//当指针指向的范围超出了数组arr的范围时,就是越界访问,p就是野指针
*(p + 1) = i;
}
return 0;
}
#include
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
*p = 20;
return 0;
}
test()函数创建临时变量a并返回a的地址,指针变量p接收,a变量生命周期结束,变量销毁。(所谓的销毁,就是把申请的空间还给操作系统了)a变量已经销毁了,再去访问它的时候就访问不到了,也就是非法访问。
#include
int main()
{
//如果不知道p应该初始化为什么地址的时候,直接初始化为NULL
int* p = NULL;
//明确知道初始化的值
int a = 10;
int* ptr = &a;
return 0;
}
C语言本身是不会检查数据的越界行为的,所以我们自己要保证它不会越界。
当一个指针指向的空间已经被释放了,那么应该及时的把指针变量置成空指针。当指针是一个空指针的时候,就不会去使用它了。
当指针指向的空间已经不属于我了,我不能再访问了,就要把一个指针变量及时的置为空指针来防止野指针的出现。
检查指针有效性的办法:
#include
int main()
{
int* p = NULL;
//判断p是不是空指针
if (p != NULL)
{
*p = 10;
}
return 0;
}
#include
#define N_VALUES 5//定义符号
int main()
{
float values[N_VALUES];//值是5
float* vp;
//指针+-整数:指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
//语法支持可以省略调整部分
//vp<5
*vp++ = 0;
}
return 0;
}
打印数字元素
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;//数组名是首元素的地址
int* pend = arr + 9;
while (p <= pend)
{
printf("%d ", *p);
p++;//指向下一个元素
}
}
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", &arr[9] - &arr[0]);
return 0;
}
指针-指针的前提:
两个空间指向同一空间
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
char c[5];
//指针和指针相减的前提是:
//两个指针指向同一空间
printf("%d\n", &arr[9] - &c[0]);
return 0;//会有警告
}
用指针-指针求字符串长度
#include
int my_strlen(char * arr)
{
char* strat = arr;
while (*arr != '\0')
{
arr++;
}
return arr - strat;
}
int main()
{
char arr[] = "hehe";
my_strlen(arr);
printf("%d\n", my_strlen(arr));
return 0;
}
#include
#define N_VALUES 5
int main()
{
float values[N_VALUES];
float* vp;
for (vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
return 0;
}
简化写法:
#include
#define N_VALUES 5
int main()
{
float values[N_VALUES];
float* vp;
for (vp = &values[N_VALUES]; vp >= &values[0]; vp--)
{
*vp = 0;
}
return 0;
}
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针相比较,但是不允许与指向第一个元素之前的那个内存位置的指针相比较。
⚠️
注意:
简化写法实际上,在绝大部分的编译器上是可以顺利完成任务的,然而,我们还是应该避免这样写,因为标准并不保证它可行。
#include
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%p <==> %p\n", &arr[i], p + i);
}
return 0;
}
可以看到通过数组下标访问的地址和指针访问的地址打印出来的是一样的
#include
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
//将指针指向的下标元素置为i
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
//打印数组元素
printf("%d ", *(p + i));
}
return 0;
}
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;//数组名
printf("%d\n", arr[2]);
printf("%d\n", p[2]);//p[2]-->*(p+2)
//[] 是一个操作符 2和arr是两个操作数
//a+b
//b+a
printf("%d\n", 2[arr]);
printf("%d\n", arr[2]);
//arr[2]-->*(arr+2)-->*(2+arr)-->2[arr]
//arr[2]<==>*(arr+2)<==>*(p+2)<==>*(2+p)<==>*(2+arr)==2[arr]
//2[arr]<==>*(2+arr)
return 0;
}
指针变量也是变量,是变量就有地址。
那指针变量的地址存放到哪里? 这就是二级指针
#include
int main()
{
int a = 10;
int* pa = &a;//pa是指针变量,一级指针
//后一颗*说明ppa是指针
//前一颗*说明ppa指向的对象是pa,而pa的整体叫int*
//ppa就是一个二级指针变量
int** ppa = &pa;//pa也是变量,&pa取出pa在内存中的起始地址
return 0;
}
a的地址存放在pa中,pa的地址存放在ppa中。
pa是一级指针,而ppa二级指针。
指针数组是指针还是数组?
答案是数组,是存放指针的数组。
#include
int main()
{
int arr[10];//整形数组 - 存放整形的数组就是整形数组
char ch[5];//字符数组 - 存放的是字符
//指针数组 - 存放指针的数组
int* arr[10];//整形指针数组
char* ch[10];//字符指针指针数组
return 0;
}
肝了3天终于是完成了✌️ ✌️ ✌️
这次总结了初阶指针的内容,大家可以在评论区给我的文章提提意见,好坏都可以。有什么不懂得问题也可以在评论区问我,只要是我会的都会依次解答。这次的文章涉及的数组,如果感兴趣的话大家可以去我的主页浏览,绝对不会让你们失望的。本篇文章还是比较长的,感谢大家可以看到这里,愿大家都能拿到想要的offer。
最后我脸皮稍微厚一下下,希望大家可以给我的文章点赞并收藏我,小小的一个赞,对我是很有帮助的。原创不易,还请三连,期待下次与各位在知识的海洋里一起畅游。