初识指针
一、指针是什么?
指针理解的2个要点:
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
1.内存的形式
2.指针变量的定义
我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是指针变量
例子:
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;
printf("%p\n", &a);
printf("%p", p);
return 0;
}
ps:
1.其中%p是打印地址的意思 而且通过代码可以看出 &a是对a进行取地址, p里面存放的也是变量的地址
2.指针变量:用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
3.一个小的单元到底是多大?(1个字节)
3.前者的联系
4.地址线
Q:如何编址?
A:经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
…
11111111 11111111 11111111 11111111
这里就有2的32次方个地址。每个地址标识一个字节,那我们就可以给 (232Byte==232/1024KB==232/1024/1024MB==232/1024/1024/1024GB == 4GB) 4G的空间进行编址。
同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,各位好奇的可以去网上搜搜或者自己计算计算。
ps:一字节 = 8byte
这里我们就明白:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节。
(在x86系统下不同类型指针所占字节大小)
二、 指针和指针类型
这里我们讨论一下:指针类型 这个名词
我们都知道,变量有不同的类型,整形,浮点型等。
Q:那指针有没有类型呢?
A:答案是有的,指针也有对应的类型,下面就是几种类型。
char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;
1.指针的定义方式
指针的定义方式是: type + * 。
其实:
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。
2.含义
ps:以第一个为例,int* pi到底代表什么。 int* 是p的指针类型,int代表pi所指向的指针类型是int,*代表pa是指针。
1.指针的使用
通过地址 我们能够找到这个地址所对应的数值。
其中我们还可以看到里面是倒着放的(大小端)其中我们还可以看到里面是倒着放的(大小端)。
还有就是内存中的数据,会跟着变量的改变而改变
3.指针与地址
1)内存
我们可以看到即使用字符型指针也可以存的下地址,因为地址大小与变量类型大小无关,只和计算机的操作系统的位数有关。
2)指针的访问权限
因为pa是字符指针,它在访问的时候只能访问一个字节,只能改变一个字节的值,所以就会出现下面这种情况。
指针的类型决定了指针向前或者向后走一步有多大(距离),即决定操作步长
操作时的步长: type* p 跳过的是sizeof(type)字节
例:
整形指针+1跳过4个字节
字符指针+1跳过1个字节
...
4.解引用的作用
我们可以从上面两个例子看出解引用操作符与指针的关系
结论:指针类型可以决定指针解引用的时候访问多少个字节(指针权限的大小)。
例子: int的指针 解引用访问四个字节 而 char的指针 解引用访问1个字节
三、野指针
1.野指针的概念
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
比喻:野指针就是野狗,正常的指针就是被拴起来的家狗
2.野指针的成因
1) 指针未初始化
例:
#include <stdio.h>
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
2) 指针越界访问
例:
#include <stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
printf("%d", arr[10]);
}
3) 指针指向的空间释放
这里与动态内存开辟的有关
#include <stdio.h>
int* test()
{
int a = 100;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
printf("%d", *p);
return 0;
}
解析:
这里因为test函数里面创建的都是局部变量,所以出了函数就会被销毁,但是为什么第一次可以是实现打印呢,这是因为虽然变量所指向被销毁,但是值还存在里面,所以如果访问的还是这个地址,里面的值还是被接收。第一次接收就是这个原因,编译器为你优化了一下,认为你想访问的就是这个地方,它会为你保留一次,所以会正常打印,但是也可以看到第二次就变了,这是因为按原来的情况来说,test的返回值被销毁后,就是一个随机值,所以打印就是随机值。
解决方案:static修饰下变量就可以了
3. 如何规避野指针
1)指针初始化
1.知道初始化为谁的地址,就直接初始化为他 的地址。
2.如果不知道初始化为谁的地址,就让指针为空指针。(比喻:把野狗栓起来,只要不骚扰(用它)就不会有别的问题)
ps:空指针是不能直接使用的,因为它没有指向任何有效的空间
而且由转定义(即图片)可以看到 NULL实际上就是0 在C++环境下直接就是0,在C语言环境下直接将0强制类型转化为0的指针,实际上还是0
2)小心指针越界
多观察多注意就好了,尤其是数组
3)指针指向空间释放,及时将其置为NULL
4)避免返回局部变量的地址
5)指针使用之前检查有效性
四、指针运算
1.指针±整数
1) 例1
#include <stdio.h>
int main()
{
#define N_VALUES 5
float values[N_VALUES];
float* vp;
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}
}
解析:
2) 例2
例题:如果不用下标访问数组
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
for (int i = 0; i < 10; i++)
{
*p = i;
p++;
}
p = arr; //切记不可少这一步
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p+i));
}
}
通过运行结果可以看出这是正确的:
全部解析都在这张图里面:
2.指针 - 指针
1)例1
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%c\n", &arr[9]-&arr[0]);
}
解析:
通过调试窗口可以看出 :指针 - 指针的结果的绝对值是指针和指针之间的元素个数
2 )例2
而且要有个前提条件: 指针与指针指向的同一块地址,不然不能进行运算
例:
#include<stdio.h>
int main()
{
int arr1[10] = { 0 };
char arr2[10] = { 0 };
printf("%d\n", &arr1[9] - &arr1[0]);
printf("%d\n", &arr2[9] - &arr2[0]);
printf("%d\n", &arr2[9] - &arr1[0]);
}
运行结果: 可以看出需要有前提条件的
3.指针的应用
1)模拟实现strlen函数
1.循环实现
#include<stdio.h>
//循环实现
int my_strlen1(char* s)
{
int num = 0;
while (*s != '\0')
{
num++;
s++;
}
return num;
}
int main()
{
char arr[] = "abcdef";
int length = my_strlen1(arr);
printf("%d", length);
return 0;
}
解析:
2.递归实现
#include<stdio.h>
//递归实现
int my_strlen2(char* s)
{
if(*s != '\0')
return my_strlen2(++s)+1;
else
return 0;
}
int main()
{
char arr[] = "abcdef";
int length = my_strlen2(arr);
printf("%d", length);
return 0;
}
解析:
3.利用指针 - 指针实现
#include<stdio.h>
int my_strlen3(char* s)
{
char* a = s;
while (*s != '\0')
{
s++;
}
return s - a;
}
int main()
{
char arr[] = "abcdef";
int length = my_strlen3(arr);
printf("%d", length);
return 0;
}
解析:因为前面已经讲了 指针-指针 所以这个东西是很好理解
ps:因为**'\0’的ASCII值就是0** 所以可以while循环里都可以不写判断语句
4.指针的关系运算
指针是有大小的,指针的关系运算实际上就是比较指针间的大小。
#define N_VALUES 5
代码1:
for(vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
代码2:
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
*vp = 0;
}
结合图像来想:按理来说两种都能进行
虽然大部分编译器上都可以正常执行这两种代码,但是呢,尽量写第一种代码,而不是第二种。因为标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
五、指针与数组
1.数组与指针的联系
1.指针与数组之间有什么关系:
没什么直接上的关系。 指针变量就是指针变量,指针变量的大小只能是4/8,是专门用来存放地址的变量。 数组就是数组,它是一块连续性的空间,可以存放1个或者多个相同类型的数据。
2.指针与数组之间有什么联系:
数组名在大多数情况下表示的都是数组首元素的地址, 数组名—地址—指针。
当我们知道数组名表示的是数组首元素的地址后,又因为数组是连续存放的,所以通过指针就可以遍历数组,数组是可以通过指针访问的。
练习下 用指针访问数组(需要对指针理解很清晰)
ps:说一个很好判断数组类型的方法,把数组名去掉 剩下的就是数组类型 在日后可以更好地分辨一些数组类型
ps:只有在两种情况下,数组名不是首元素地址。
1.sizeof(数组名) 2.&数组名
2.二级指针(简单介绍下)
1)二级指针介绍
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a; //p是一级指针 存放的是变量a的地址
int** pp = p; //p是二级指针 存放的是指针p的地址
return 0;
}
解析 用图来表达下
两个*的意义
ps:二维指针和二维数组没有必然联系
3)二级指针的解引用操作
二级指针要连续解引用两次才能表达出变量a存的值
错误案例:这种情况下,不需要解引用两次也能实现操作
解析:因为理解有问题导致创建的pp的有问题,所以实际上传过去的是a的地址,直接一次解引用就可以了
正确操作:
二级指针与指针数组的联系
二级指针已经在上面介绍过,去掉数组名就是数组类型这个方法也已经讲解了,那么可以很明显看出圈上的是一个指针数组
先分析这个代码,如果这个代码这样写,有什么意义?特点是什么?
#include<stdio.h>
int main()
{
int arr1[] = { 1,2,3,4 };
int arr2[] = { 2,3,4,5 };
int arr3[] = { 3,4,5,6 };
int* parr[] = { arr1,arr2,arr3 };
return 0;
}
因为parr是指针数组,所以里面填arr这种数组的数组名是完全可行的。
逐步深入问题,是否可以通过parr数组访问到arr1/arr2/arr3中的元素呢?答案是可行的
从下图可以看出我们最开始创建的指针数组居然可以用二维数组的形式表达出来
接下来为大家分析原理:
指针来表达:
数组来表达:
因为数组与指针表达的关系 可以如下图所理解:
parr中存的就是arr数组的首元素地址