指针是什么
我们对指针的理解大多数就是地址,那究竟是不是呢 ?
- 指针是内存中一个最小单元的编号,也就是地址
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
我们想要了解指针,还有一个东西要清楚,就是内存。
可以看到是一个字节一个字节分开的,但是为什么不是两个字节或者四个字节在一起呢 ?我们平常在写代码的时候就是经常用int (4个字节),但是可以想想如果你用4个字节4个字节,表示char的时候怎么表示。如果用4个字节表示是不是有点浪费啊!大家可以自己想一想这个问题。
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
//中,p就是一个之指针变量。
return 0;
}
32位下:
64位下:
这个时候我们可以想想为什么在32位下是4个字节,64位下是8个字节 ?
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0)
32个0和1序列可以组成2的32次方个地址
32个0和1序列正好是4个字节,1个字节是8位,正好可以放下。
64位的和32位的同理
指针和指针类型
在指针类型中我们可以类比一下变量,指针也是有类型的。
char *pc = NULL; //存放 char 类型变量的地址。
int *pi = NULL; //存放 int 类型变量的地址。
short *ps = NULL; //存放 short 类型变量的地址。
long *pl = NULL; //存放 long 类型变量的地址。
float *pf = NULL; //存放 float 类型变量的地址。
double *pd = NULL; //存放 double 类型变量的地址。
我们可以想一想指针类型的意义。
变量可以加减一个整数,指针可以吗 ?
答案是可以的。(展示是32位下)
我们可以看到 int 的4个字节,我们又知道地址是1个字节1个字节分的,int n=10,我们对这个n取地址,这个地址会是什么呢 ? 上面我们可以看到n的地址就是首字节的地址,而对指针进行加1,int 类型加了4,而char 类型加了1,我们就可以得出一个结论了,指针的类型决定了指针向前或者向后走一步有多大(距离)。
我们在对指针的解引用操作一下。
首先是 int* 类型的指针,我们可以看到 int* 对 int 的改变我们接下来看另一种情况。
我们通过对内存的观察可以清晰的看出不同类型的指针解引用的时候可以访问多少个字节。我们在此也可以得出一个结论。
指针的类型决定了对指针解引用的时候有多大的权限(能操作几个字节)。
就如上述的:char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
野指针
我们来聊一下什么是野指针呢 ?
野指针的概念是:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
造成野指针的原因都是些什么呢 ?
1.指针未初始化。
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
我们在定义局部变量的时候知道,如果我们不对这个变量进行初始化操作,那么这个变量就是一个随机值,而指针未初始化,也是一个随机值,只不过这个随机值变成了地址,我们可以试着想一下,如果我们不对这个指针进行初始化,我们就不知道这个指针所指向的位置是哪里。这是不是一件很危险的事情啊,假如我想对这个指针进行一些操作,我是不是要知道这个指针里面的内容啊。不知道这不是乱来吗 ?
2.指针的越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
我们可以想一下,我们定义了一个 int arr[10]的数组,就会为我们分配10个 int 类型大小的地址,当我们把10个都访问了,该访问下一个,就是上图所指向的地方,我们知道这个地址里面放的是什么吗 ? 我们并不知道,当我们访问他的时候,编译器就会强制报错,我们不能访问他。
3.指针指向的空间释放
这里先了解一下,这和动态申请内存有关,就是我们会动态申请一块内存,那这个内存释不释放的问题,后面会更。
我们既然知道野指针会对我们造成很不好的影响,那我们就应该要想办法规避野指针,那我们该如何的进行规避野指针呢 ?
- 指针初始化
int* p=NULL;
- 小心指针越界
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
- 指针指向空间释放,及时置NULL
这里是动态内存会有体现。
- 避免返回局部变量的地址
我们知道,函数在调用结束的时候,这块内存会被回收,就是说我们不能够在随心所欲的访问这一块的内容了,这个时候我们不是得到了一个地址,但是这个地址我们是不能够访问的,这个指针也就是一个野指针,就好比你曾经是这个房子的主人,但是你把这个房子卖给其他人了,但是这个房子的钥匙你还有,你有钥匙你就能随心所欲的进入这个房子吗 ? 这个时候,显然是不可以的啊。
- 指针使用之前检查有效性
指针运算
1.指针±整数
#define N_VALUES 5
int main()
{
float values[N_VALUES];
float* vp;
//指针+-整数;指针的关系运算
//下面两个for循环的意思是一样的。
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0; //这一句的意思和下面注释的两句意思是一样的
//*vp=0;
//vp++;
}
for (vp = &values[0]; vp < &values[N_VALUES];vp++)
{
//*vp++ = 0;
*vp=0;
}
}
上述代码就可以将5个元素设置成0
2.指针-指针
这里我们可以试着猜想一下,指针-指针是什么了,好像是两个指针之间的元素个数啊。我们在来个例子感受一下。
我们实现一个模拟求字符串长度的函数。我们平常的写法是这样的。
现在我们知道指针可以-指针了,我们又可以拓展一个新的写法。
是不是也能实现功能啊。这也更加的印证了指针-指针得到的是什么。
指针和数组
先来一段代码
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
我们可以看到结果是一样的,说明了数组名就是首元素的地址。
那既然数组名是首元素的地址,那我们可不可以拿一个指针来接受一下。
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址
我们来写一个例子看一下吧。
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr)/sizeof(arr[0]);
int i=0;
for(i=0; i<sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}
上述代码会怎么样输出呢 ?
这里我们可以看到 p+i 其实计算的是数组 arr 下标为i的地址。那我们就可以直接通过指针来访问数组。
用数组下标来遍历数组
用指针遍历数组
二级指针
我们学习了指针,这个时候又来了个二级指针,我们知道指针存放的是地址,那么二级指针又是存放的什么啊 ?
这里我们可以看出来二级指针存放的就是一级指针变量的地址。这里是不是有点套娃的感觉。*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa.
指针数组
指针数组又是一个什么东西呢 ?是指针 ? 是数组 ?
答案:是数组,用来存放指针的数组
我们接触过整形数组和字符数组。
但是指针数组是怎样的呢 ?
上述这样的。但是有什么用呢 ?
这样一对比是不是就有点意思了。
这样一看,这是不是有点像二维数组啊,我们可以看到当我们parr[ i ]的时候得到的是什么,是不是数组的元素啊,而数组的每个元素都是一个地址,地址就可以用指针来表示,parr[ i ][ j ]是不是也能取到每个数组里面的每一位啊。