1.指针是什么?
指针是最小内存单元(大小:1byte)的编号,即地址(把内存单元的编号称为地址,地址也叫指针)
口头语中说的指针通常指的是指针变量,用来存放地址的变量
int main()
{
int a = 10;//a是整型变量,占4个字节的内存空间
int* pa = &a;//pa是一个指针变量,用来存放地址的
//取地址取出的是起始字节的地址
return 0;
}
总结:
- 指针就是地址,口语中的指针通常指的是指针变量
- 指针变量里面存放的是地址,而通过这个地址,就可以找到一个内存单元
- 指针变量是用来存放地址的,地址是标识一块内存空间的值;指针的大小在32位平台是4个字节,在64位平台是8个字节
2.指针和指针类型
X86 32位的环境 X64 64位的环境
#include <stdio.h>
int main()
{
char *pc = NULL;
short *ps = NULL;
int *pi = NULL;
double *pd = NULL;
//sizeof返回值类型是unsigned int,用%u打印比较合适
//%zu打印最准确(为sizeof准备的打印格式)
printf("%zu\n", sizeof(pc));
printf("%zu\n", sizeof(ps));
printf("%zu\n", sizeof(pi));
printf("%zu\n", sizeof(pd));//都是4/8
return 0;
}
发现指针大小和指针类型没有关系,只和编译环境是32/64位有关,那类型到底有什么用,为什么要有这么多指针类型?
指针类型的意义
- 指针类型决定了指针进行解引用时有几个字节的访问权限
- 指针+-整数一次跳过几个字节
int a = 0x11223344;
int* pa = &a;
*pa = 0;
一次改动改了4个字节
int a = 0x11223344;
char* pc = (char*) &a;
//int*的地址强制类型转换char*就没有warning
//pc在x86环境下是4个字节可以存放下a的地址
*pc = 0;
pc有能力存放下a的地址 pc=&a 说明地址存进去了
只改动了一个字节
指针类型的意义1:指针变量的类型不决定指针变量所占大小,指针类型决定了指针变量在被解引用操作时访问几个字节,是整型指针int*解引用访问4个字节,是字符型指针char*解引用访问1个字节,double型的指针解引用访问8个字节
int a = 0x11223344;
//进制只是表示数字的不同形式
int* pa = &a;
char* pc = (char*)&a;
printf("pa=%p\n", pa);
printf("pa+1=%p\n", pa+1);
printf("pc=%p\n", pc);
printf("pc+1=%p\n", pc+1);
pa与pc都是取出a的起始字节的地址 pa+1跳过4个字节 pc+1跳过1个字节
指针类型的意义2:指针的类型决定了指针变量+1-1操作时,跳过几个字节,决定了指针的步长,int*指针+1跳过4个字节 char*指针+1跳过1个字节
不同指针类型的指针变量解引用访问字节数和+1-1跳过字节数不同,访问内存更加方便
//int*想访问一个字节强制类型转换为char*
int* pi = &a;
pi+2; //跳过8个字节
char* pc = &a;
pc+2; //跳过2个字节
//+-后不仅可以跟1,只要是整数就可以
对于相同长度的类型可以进行混用吗? 不可以 类型是一种视角
int a = 0;
int* pi = &a; //pi解引用访问4个字节 pi+1跳过4个字节
float* pf = &a;//pf解引用访问4个字节 pf+1跳过4个字节
*pi = 100;
*pf = 100.0;
int*和float*是不是可以通用?
不能,虽然这两个指针的权限都一样(解引用访问空间大小,+1-1跳过大小),但是站在pi角度,它认为它指向的内存的是整型数据(按整数的存储方式存进去),站在pf角度,它认为它指向的内存空间放的是浮点型数据(按浮点数的存储方式存进去),浮点数和整数在内存中的存储方式是有差异的,存进内存的值是有所差异的,所以不能混用
按整数的存储方式存进去
按浮点数的存储方式存进去
注意:
- 指针变量的大小跟通过指针变量访问内存空间的大小没有关系,不是一回事,指针变量访问空间大小取决于类型。
- 指针变量的大小(4/8取决于地址大小)和指针变量所指向的空间的大小(取决于类型)没有关系
3.野指针
指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
3.1 野指针成因
1.指针未初始化
int* p;
//p没有初始化,就意味着没有明确的指向
//一个局部变量不初始化,放的是随机值:0xcccccccc
//这个地址是非法的,不属于我们自己的地址,通过地址找到的空间也不是我们的
*p = 10;
//非法访问内存 没有初始化进行解引用,这里的p就是野指针
2.指针越界访问
int arr[10] = {0};
//arr[i]访问数组元素 现在用指针来访问数组
int* p = arr;//arr数组名,首元素地址即&arr[0]
int i = 0;
for(i = 0; i <= 10; i++)
//循环11次 第11次访越界问了 此时的p进行解引用,p就是野指针
{//当指针指向的范围超出数组arr的范围时,p就是野指针
*p = i;
p++;
}
p指向数组的首元素,每+1跳过1个int,解引用也是访问一个整型
3.指针指向的空间释放
int* test()
{
int a = 10;
return &a;
}
//a是局部变量,一出test()就会销毁(空间还给操作系统了,空间不属于当前程序,不能用)
//空间销毁了,这块空间(变量a的空间)不属于当前程序
//调用结束后,当前空间还给操作系统,当前程序没有这块空间的使用权限
//内存中这块空间还存在
//p里面保存着a的地址,p有能力通过地址找到这块空间,但是p不能访问使用这块空间
int main()
{
int *p = test();
return;
}
3.2 如何规避野指针
1.创建指针变量要初始化明确值(知道应该赋的值就赋明确值),不知道初始化什么值就初始化为空指针:NULL(指针变量没有指向,是个空指针)
int a = 10;
int* p1 = &a;//初始化 赋明确值
*p1 = 20;//使用指针进行修改a
int* p2 = NULL;//不知道初始化什么值就初始化为空指针
//NULL 本质上是0 相当于int b=0;
//不是说NULL没有指向嘛,空指针可以用它吗
//*p2 = 100;//可以这样使用空指针吗 不可以 err
//0地址,没有指向有效空间 不能直接访问
if(p2!=NULL)//想使用p2 要先进行判断,不为NULL(有了有效指向),就可以使用
{//也可以写成if(p2)但是还是建议写成p!=NULL(不是野指针,指向有效空间)更加直观
*p2 = 100;
}
//但是也不是决对安全的,避免不了因为局部变量的空间释放造成的野指针
不能直接使用空指针
变量的空间销毁了为什么还会打印输出10?
此处判断失效,没有起到作用
p中存放了地址,肯定不为空指针 判断失效 不能避免野指针出现
p通过非法地址找到那块空间(空间不属于当前程序,但是还是存在的)时当前空间没有被覆盖(没有被使用)所以打印输出10
p访问非法地址指向的空间,打印结果是不一样的(随机的)
p访问非法地址指向的空间,打印结果是随机的
2.小心指针越界
3.指针变量指向的空间被释放,及时置NULL
4.避免返回局部变量的地址
5.指针使用前检查有效性
4.指针运算
指针+-整数 指针-指针 指针的关系运算 指针的解引用都是指针运算
4.1 指针+-整数
vp指向首元素,先*vp=0 再 vp+1往后移动
//vp没有初始化是野指针 还没有使用 不会有问题
//vp=&values[0] 给指针赋值,指针有指向了,不再是野指针
*vp++=0;
//*vp=0;
//vp++;
*vp++;
//表达式的值是vp的值,vp先进行解引用vp* 再进行vp++ (没有对指向对象++) 地址++
//等价于
*vp;
vp++;
(*vp)++;//先进行vp*找到vp指向的对象,对vp*(vp指向的对象)进行++
int arr[5] = {0};
int* p;
p < &arr[5];
//算不算数组下标越界,可以这样使用,虽然空间不属于此程序
//只是取地址比较 没有问题
//数组初始化
int arr[10] = {0};
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for(i = 0; i < sz; i++)
{//数组下标写法:全初始化为1
arr[i] = 1;
}
//指针写法
int* p = arr;//数组名:首元素地址
for(i = 0; i < sz; i++)
{//第一种写法
*p = 1;
p++;
//第二种方法
//*(p+i) = 1;
}
4.2 指针-指针
指针-指针的绝对值得到的是指针之间元素的个数(不是空间相减,只是指针变量相减;&arr[i]是地址,地址是存放到指针变量的,理解为两个指针变量相减,而不是两个地址相减)
int arr[10] = {0};
printf("%d\n", &arr[9]-&arr[0]);//9
printf("%d\n", &arr[0]-&arr[9]);//-9
//分析:
//&arr[9] 和 &arr[0] 分别是指向第10个元素和第1个元素的指针
//int* p = &arr[0];//&arr[0]首元素地址
//也可以写成
//int* p = arr;
//p+9 = &arr[9];
//所以 &arr[9]-&arr[0]=(p+9)-p=9
int arr[10] = {0};
char ch[5] = {0};
printf("%d\n", &arr[5]-&ch[0]);//err 没有意义
注意:
- 不是所有的指针都能相减,指向同一块空间的两个指针(相同类型)才能相减,这样才有意义
- 指针+指针(即地址+地址)没有意义
#include <stdio.h>
#include <string.h>
int main()
{//用库函数求字符串长度
int len = strlen("abcdef");
printf("%d\n", len);//6
return 0;
}
#include <stdio.h>
/*int my_strlen(char* str)//方法1 //方法2递归
{//自定义求字符串长度的函数
int count = 0;
while(*str != '\0')
{
count++;
str++;
}
return count;
}*/
//方法3:指针-指针
int my_strlen(char* str)
{//\0地址-首元素地址就是元素个数
char* start = str;
while(*str != '\0')
{
str++;//跳出循环时指向\0
}
return (str-start);
}
int main()
{
int len = my_strlen("abcdef");
//字符串传参时,传的是首字符的地址
printf("%d\n", len);// 6
return 0;
}
4.3 指针的关系运算
比较大小
vp开始指向数组往后越界的元素,先vp--往前移动 再*vp=0
分析:
*--vp = 0;
vp = vp-1;
*vp = 0;
分析:vp最后指向在数组首元素前面一个的元素处,但是要避免出现这种情况
允许p1和p2(向后越界)比较,不允许p1和p3(向前越界)比较,可以往后越界的指针与之比较,不允许与向前偏移的指针比较
5.指针和数组
//数组:一组相同类型元素的集合(一组数据,内存中连续开辟的一块空间)
//数组的大小取决于元素的个数,元素的类型
//指针变量:存放地址的变量
//指针的大小4/8 取决于32位环境还是64位环境
//联系:数组名是首元素地址 通过指针来访问数组
int mian()
{
int arr[10] = {0};
//arr是首元素地址
//&arr[0] 首元素地址
int* p = arr;
//指针变量p和数组arr就建立了联系 p存放了数组首元素地址 p指向了数组arr
//通过指针来访问数组
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for(i = 0; i < sz; i++)
{
printf("%d", *(p+i));
//p+i p是整型指针,p+i跳过i个整型,指向下标为i的数组元素
//*p p指向的数组元素 p+1 p指向第一个元素
//*(p+i) 跳过i个元素 进行解引用就是下标为i的数组元素
}
return 0;
}
#include <stdio.h>
int mian()
{
int arr[10] = {0};
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for(i = 0; i < sz; i++)
{
printf("%p--------%p\n", &arr[i], p+i);
//&arr[i] 下标为i的元素地址 p放的是首元素地址 p+i跳过i个元素(整型)
}
return 0;
}
地址完全相同
说明这个指针变量p指向数组的首元素时,p+i和&arr[i](下标为i的数组元素的地址)一样的,p+i,跳过i个整型,指向下标为i的数组元素,就是下标为i的数组元素的地址,即&arr[i]。
//打印数组元素 三种方式等价
printf("%d ", arr[i]);
printf("%d ", *(p+i));
//又因为int* p=arr; p中存放首元素地址arr
printf("%d ", *(arr+i));
//arr数组名表示首元素地址
//arr+i,跳过i个元素,指向下标为i的数组元素,就是下标为i的数组元素的地址
arr[i]与*(arr+i)等价,arr[i]本质上计算时是通过arr+i(下标为i的元素的地址)找到下标为i的数组元素,然后解引用。( arr[i]<=====>*(arr+i) )
//数组传参
#include <stdio.h>
/*void test(int* p, int sz)
{
int i = 0;
for(i = 0; i < sz; i++)
{//指针形式
printf("%d ", *(p+i));//p就是首元素地址
}
}*/
void test(int arr[], int sz)
{
int i = 0;
for(i = 0; i < sz; i++)
{//数组形式
printf("%d ", arr[i]);//arr[i]计算时还是*(arr+i)
}
}
int main()
{
int arr[10] = {0};
test(arr,10);//传参传的是首元素地址
return 0;
}
6.二级指针
#include <stdio.h>
int mian()
{
int a = 10;
int* pa = &a;//pa是一个指针变量,一级指针变量
//一级指针变量:通过pa直接去找a,找一次就找到了,对pa进行一次解引用就找到a了
//即*pa=a
*pa = 20;
printf("%d\n", a);//20
//pa也是变量,也是有自己的空间,也有地址
int** ppa = &pa; //ppa是二级指针变量 存放ppa地址
//int* *ppa 或 int** ppa都可以
//通过ppa找到变量a需要进行两次解引用 所以叫二级指针变量
//*ppa--->pa *pa--->a 即 **ppa
**ppa=20; //**ppa与*pa等价
printf("%d\n", a);//20
//起始地址
return 0;
}
*pa = a 通过修改*pa达到修改a的效果
ppa存放指针变量pa的地址,是二级指针变量
指针类型的拆解:
int* pa; //*说明pa是指针 int说明pa所指向的对象的类型是int
int** ppa;//*(后面的*)说明ppa是指针 int*说明ppa所指向的对象pa的类型整体叫int*
总结:二级指针变量是用来存放一级指针变量的地址
7.指针数组
存放指针的数组就是指针数组
//一组相同类型的指针变量的集合
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
//多个整型变量 可以创建一个整型数组
int arr[10];
int* pa = &a;
int* pb = &b;
int* pc = &c;
//多个整型指针 可以创建一个整型指针数组
int* parr[10] = {&a,&b,&c};
//int*数组元素是整型指针
//parr存放指针的数组 指针数组
//parr数组名,是首元素地址
//访问指针数组
int i = 0;
for(i = 0; i < 3; i++)
{
printf("%d\n", *(parr[i]));
}
return 0;
}
可以用指针数组模拟出一个二维数组
int arr[3][4] = {1,2,3,4,2,3,4,5,3,4,5,6};
//1 2 3 4
//2 3 4 5
//3 4 5 6
//访问二维数组
int i = 0, j = 0;
for(i = 0; i < 3; i++)
{
for(j = 0; j < 4; j++)
{
printf("%d\n", arr[i][j])
}
printf("\n");
}
模拟的二维数组
int arr1[4] = {1,2,3,4};
int arr2[4] = {2,3,4,5};
int arr3[4] = {3,4,5,6};
//关联3个数组:数组名 首元素地址
int* parr[3] = {arr1,arr2,arr3};//parr数组元素是首元素地址
//指向了每个数组(arr1 arr2 arr3)的首元素
int i = 0 , j = 0;
for(i = 0; i < 3; i++)
{
for(j = 0; j < 4; j++)
{
printf("%d\n",parr[i][j]);
}
printf("\n");
}
//parr[i]拿到的是数组arr1 arr2 arr3起始地址(首元素地址)
//也就是数组名:arr1 arr2 arr3
//访问数组元素采用(arr[i]) 数组名[下标] 此处数组名是parr[i]下标用j
//即 parr[i][j]
把三个一维数组关联起来, 达到二维数组效果