目录
1. 指针是什么
指针理解的2个要点:
- 指针是内存中一个最小单元的编号,也就是地址
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量
2.内存
为了更好的理解指针,首先我们需要先了解一下什么是内存?
内存是计算机上的一种存储空间
一般为8g/16g/32g等
在程序运行时会载入内存,在程序中如果有数据的存储也会申请内存空间
那我们怎么能知道程序运行时会载入内存呢?
首先打开任务管理器 (同时按ctrl+alt+delete)
我们可以看到每一个程序都会占用我们的内存,因此程序运行时会载入内存
如何有效的使用内存空间
内存的使用如同我们生活中的楼房一样,每一个房间都有一个门牌号进行编号,如果我们需要定位它们我们只需要知道他们的编号即可,即
一个门牌号 -> 一个房间
内存的使用其实也参考了类似的思路:
假设一个大方格是我们的内存空间,我们把内存划分为一个一个小的内存单元(一个格子就是一个内存单元)
例如这样我们把每一个内存单元进行编号,内存单元编号也可以叫做地址,地址也有另一个名字叫指针
实践中每一个内存单元的大小为一个字节
内存单元的地址(指针)如何产生?
那问题来了,如果访问一个内存单元,那么内存单元的地址(指针)如何产生?
电脑一般分为32位机器或者64位机器,以32位机器为例:
32位机器上有32根地址线,地址线如果通电,因为电信号分为高电平和低电平,转化为数字信号就可能为0或1,那么32位地址线转化为数字信号最多就会产生2^32种二进制序列
这232个二进制序列就可以作为232个地址,就可以管理232个内存单元,即232个字节的内存空间
根据单位换算:
转换为10进制就是4294967296字节,换算过来就是4GB
64位机器也是同样的方法计算
总结:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节。
3.指针变量
我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
中,p就是一个之指针变量。
return 0;
}
a是一个整型变量,整形变量占用4个字节的空间,每个字节都有一个地址,取地址只是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量。
现在我们把这个地址存储到pa中
pa=&a;
pa就被称为指针变量,因为a是整形类型,因此我们可以把它写为
int* pa=&a;
因此,指针变量是一个变量,用来专门存放地址。 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
printf("%p\n", &a);
printf("%p\n", pa);
return 0;
}
4.指针变量的使用(解引用操作)
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
printf("%p\n", &a);
printf("%p\n", pa);
*pa = 20;//* - 解引用操作
printf("%d\n", a);
return 0;
}
我们可以得知a的地址存放在pa中,那我们怎么通过地址找到a呢?
这里的 *pa 就是解引用操作,通过pa找到a,这里 *pa = 20; 就是找到a并将20赋值给a,看结果
5. 指针和指针类型
经过上面的学习,大家有无困惑,为什么会有指针类型,在上面的讲解中:
32位机器地址是4个字节,指针变量大小是4个字节
64位机器地址是8个字节,指针变量大小是8个字节
那为什么还要区分 charp1 intp2 shortp3 floatp4?直接用一个通用类型的指针不就好了吗?那为什么还要这么做?
int型指针
#include <stdio.h>
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
return 0;
}
我们查看a的地址,只不过倒着放(这里因为是入门指针,暂不讲解)
走到下一步 *pa=0;时
a4个字节的值确实都变成了0
再看char型指针
#include <stdio.h>
int main()
{
int a = 0x11223344;
char* pa = &a;
*pa = 0;
return 0;
}
a的地址
当 *pa = 0; 时
我们可以发现char类型只改变了一个字节的值
总结:指针类型其实是有意义的:
1.指针类型决定了,指针进行解引用操作的时候,一次性访问几个字节,访问权限的大小
如果是char的指针,解引用访问1个字节
如果是int的指针,解引用访问4个字节
如果是float*的指针,解引用访问4个字节
#include <stdio.h>
int main()
{
int a = 0x11223344;
char* pc = (char*)&a;//int*
int* pa = &a;
char* pc = &a;
printf("%p\n", pa);
printf("%p\n", pa+1);
printf("%p\n", pc);
printf("%p\n", pc+1);
return 0;
}
补充:在16进制数字中,0到15一次表示为
0~9 a b c d e f
我们可以看到pa和pc起始地址是一样的,但是+1以后就有差别,int型的从58到5c,跳过4个字节,char型的从58到59,跳过一个字节
意义2: 指针类型决定指针的步长(指针+1到底跳过几个字节)
字符指针+1,跳过1个字节
整型指针+1,跳过4个字节
因此想要一个字节一个字节访问时用char类型
例如:
#include <stdio.h>
int main()
{
int a = 0x11223344;
char* pc = (char*)&a;//int*
int i = 0;
for (i = 0; i < 4; i++)
{
*pc = 0;
pc++;
}
return 0;
}
因为a为int型,用char类型接受地址时,需要将a的地址强制转化为char类型,即
(char*)&a
第一次进来
第二次循环
直到最后一次循环
你想怎么访问指针,便用什么类型去访问,这就是指针的灵活性
指针的不同类型提供了不同的观察和视角去访问内存
我们可以看出和指向的对象其实没有关系,只和访问时指针的类型有关
2.野指针
野指针概念
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因
1.指针未初始化
如:
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
局部变量指针未初始化,默认为随机值,即*p里面放的是随机值的地址,把20放进去,这是极为危险的,就好比喝醉酒随便进入一个房子住
正确的写法
#include <stdio.h>
int main()
{
int a=10;
int *p=&a;
*p = 20;
return 0;
}
像内存申请4个字节放10,有明确的指向
2 .指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%d ", *p);
p++;
}
return 0;
}
我们可以很明显看出这个数组越位访问了,但是让我们从指针的角度分析代码
但是随着循环的进行,访问到最后一次循环时
此时此刻,越界访问,变成了一个野指针
3. 指针指向的空间释放
#include <stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int *p = test();
printf("%d\n", *p);
return 0;
}
我们可以看出从地址的角度,我们可以找到原来那块空间,但我们进入函数后申请了一块空间,但一旦这个函数调用完毕,return 返回以后,这块空间便不在属于这个函数,还回操作系统,因此通过p找a的地址就找不到了,就变成野指针了
就和住酒店一样,你开房一天,并且告诉朋友你的居住位置门牌号,那里确实是你的地址,但你的朋友第二天找你你已经退房了,找不到你了
细心的小伙伴会发现,结果还是10呀,没问题
实际上这是一种侥幸结果
如果添加一行代码,问题就出现了
#include <stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int *p = test();
printf("你好世界\n");
printf("%d\n", *p);
return 0;
}
问题就出现了
6. 如何避免野指针
1.指针初始化
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;//指针的初始化
int* p = NULL;//NULL - 空指针,专门用来初始化指针
if (p != NULL)
{
}
return 0;
}
我们要把指针尽量初始化,如果实在不知道初始化什么,可以让它为空指针:NULL - 空指针,专门用来初始化指针
但是如果这时候访问空指针是不允许的,系统是会奔溃的,因此p!=NULL 时,我们才能使用它
- 小心指针越界
- 指针指向空间释放,及时置NULL
- 避免返回局部变量的地址
上面函数的那种 - 指针使用之前检查有效性
NULL指针是用户无法访问的
7.指针运算
指针± 整数
#define N 5
float values[N];
float *vp;
//指针+-整数;指针的关系运算
for (vp = &v[0]; vp < &values[N];)
{
*vp++ = 0;
}
补充:什么是 *vp++ = 0;
这里vp是后置++,这里表达式的值是vp,表达式执行完后才vp=vp+1;
相当于
*vp=0;
*vp++;
因此vp是从第一个元素开始被0赋值的
到最后全部为0
指针-指针
前提:两个指针指向同一块空间
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%d\n", &arr[9] - &arr[0]);
return 0;
}
指针减指针得到的是两个指针之间的元素个数
应用:求字符串的长度(其它方法见另一篇博客https://blog.youkuaiyun.com/m0_74308260/article/details/127942002)
#include <stdio.h>
int my_strlen(char* str)
{
char* start = str;
while (*str != '\0')
{
str++;
}
return str - start;
}
int main()
{
char arr[] = "abcde";
int len = my_strlen(arr);
printf("%d", len);
return 0;
}
求字符串长度即统计’\0’之前的元素个数,函数中传过去的*str是arr数组中的第一个元素,但是它是’a’,不等于’\0’,因此count++,str+1在找第二个元素,用while循环,以此类推,直到找到’\0’停止计数,并将count值打印出来返回给len打印出来,计算字符串长度完成
指针的关系运算
for(vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
这个代码从高地址走向低地址,循环结束后
可以完美的算出答案,但是有人对此做出优化
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
*vp = 0;
}
这个方法前面和优化前的步骤一样,但是到了最后一次循环完成任务后,vp仍然需要–,这样就会出现,它最终位置已经不在下标大于等于0的地址中了
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
因此这次优化有待商榷
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与
指向第一个元素之前的那个内存位置的指针进行比较。
8.指针和数组
二者的关系:
1.指针和数组是不同的对象
指针是一种变量,存放地址的,大小为4或8个字节
数组是一组相同类型的集合,可以存放多个元素,大小取决于元素个数和元素类型
2.数组的数组名是数组首元素的地址,地址可以放在指针中,通过指针访问数组
#include <stdio.h>
int main()
{
//1~10
int arr[10] = { 0 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//赋值
for (i = 0; i < sz; i++)
{
*p++ = i + 1;
}
//打印
p = arr;
for (i = 0; i < sz; i++)
{
printf("%d ", *p++);
}
return 0;
}
通过指针赋值打印数组
我们还有另外的写法,开始时我们让 int* p = arr; p指向数组的起始位置,现在我们写为 *(p+i),因为指针可以加整数,加i代表跳过i元素个地址
#include <stdio.h>
int main()
{
//1~10
int arr[10] = { 0 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//赋值
for (i = 0; i < sz; i++)
{
*(p + i) = i + 1;
}
//打印
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
这里的巧妙之处在于p没有移动,只是i在改变,所以就没有第一种写法打印之前让p = arr;
p重新变为数组首元素的过程,优化了代码
两种方法总结:
我们可以引申出三种形式
能这样引申是因为
[ ]是下标引用操作符
i和arr是[ ]这个操作符的操作数,就如同a+b可以写为b+a一般
9. 二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
这就是二级指针 。
#include <stdio.h>
int main()
{
int a = 10;
int * pa = &a;//pa是一级指针变量
int* * ppa = &pa;//ppa就是一个二级指针变量
//int** * pppa = &ppa;//pppa是一个三级指针变量
return 0;
}
经过上面的学习想必大家很了解这段代码,a在内存中开辟4个字节的空间存放10,将a的地址放到pa中,pa也应该有自己的地址,将pa的地址存放到ppa中
下面我们对这两行代码进行深入解剖:
int * pa = &a;
*号表示pa是一个指针变量,int是指pa指向的a是整形
int* * ppa = &pa;
号表示ppa是一个指针变量,ppa前面的那个是指ppa是一个指针变量,int*指的是ppa指向的pa的类型是int *
那如果是这样一段代码呢?我们又该如何理解
int** * pppa = &ppa;
那我们怎么通过二级指针找到a呢?
我们可以通过解引用两次,第一次解引用找到pa。第二次解引用找到a
#include <stdio.h>
int main()
{
int a = 10;
int * pa = &a;//pa是一级指针变量
int* * ppa = &pa;//ppa就是一个二级指针变量
printf("%d\n", **ppa);
printf("%d\n", a);
//int** * pppa = &ppa;//pppa是一个三级指针变量
return 0;
}
那二级指针可不可以修改呢?通过修改二级指针修改a?当然可以
#include <stdio.h>
int main()
{
int a = 10;
int * pa = &a;//pa是一级指针变量
int* * ppa = &pa;//ppa就是一个二级指针变量
**ppa=40;
printf("%d\n", **ppa);
printf("%d\n", a);
//int** * pppa = &ppa;//pppa是一个三级指针变量
return 0;
}
总结:二级指针变量就算用来存储一级指针变量的地址
10.指针数组
先思考这样一个问题:指针数组是指针还是数组?
是数组,用来存放指针的数组。
我们之前学过整形数组:放来存放指针的数组,
字符数组:用来存放字符的数组
int arr1[5]=0;
char arr2[5]=0;
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int e = 50;
int* arr[5] = {&a, &b, &c, &d, &e};
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", *(arr[i]));
}
return 0;
}
用这个数组存放地址
数组在地址中连续存放,a b c d e是不用的变量,地址可能不连续
使用一维数组存放二维数组
假设我想模拟出三行四列的二维数组,
我们分别用三个数组模拟三行
#include <stdio.h>
int main()
{
//假设我想模拟出一个3行4列的数组
int a[] = { 1,2,3,4 };
int b[] = { 2,3,4,5 };
int c[] = { 3,4,5,6 };
int* arr[3] = { a, b, c };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
这个指针数组中的三个指针分别指向整型数组a b c数组的首个元素,又因为数组就有连续性,所以我们可以通过每个数组的首个元素找到剩下的元素,如这里用i控制是分别进入a b c 三个数组,用j+1循环控制一个数组中的其它元素打印一行,换行,如此循环,最后打印出一个二维数组