指针
文章目录
- 一、指针是什么
-
- 1.内存划分
- 2.指针和指针变量
- 二、指针和指针类型
-
- 1.指针解引用方面
- 2.指针加减整数
- 三、野指针
-
- 1.野指针定义
- 2.野指针成因
- 3.如何规避野指针
- 四、指针运算
-
- 1.指针±整数
- 2.指针-指针
- 3.指针的关系运算
- 五、指针和数组
- 六、二级指针
- 七、指针数组
一、指针是什么 1.内存划分 内存是一块很大的空间,由一个个小的占1个字节(bit)的内存单元组成,每一个内存单元对应着一个地址,即对内存单元的编号。
现实中,每个人有身份证,可以通过身份证找到这个人,内存单元也是如此,可以通过地址找到该内存单元。
内存,看图:
文章图片
2.指针和指针变量 上代码:
#include
int main()
{
int a = 10;
//在内存中开辟一块空间
int *pa = &a;
//这里我们对变量a,取出它的地址,可以使用&操作符。
//将a的地址存放在p变量中,p就是一个指针变量。
return 0;
}
我们定义一个变量
a
,在内存中会分配出4个字节的空间给 a
。变量a
有4个字节且第一个字节的地址是 0x0012FF43
,它就代表着变量a
的地址。通过该地址我们可以找到变量a
,也可以说该地址指向了变量a
,因此地址被形象化地称为指针。那什么是指针变量呢?
没错,就是存放指针(地址)的变量,上述代码中,
int *p = &a;
含义:以int*
类型创建变量pa
,并取a的地址(指针)存进pa
。变量pa
中存储的是地址(指针),因此称它为指针变量。当然创建变量pa
,也需在内存空间中分配空间。如图:
文章图片
总结:
1.指针即地址,地址即指针。注:指针变量经常被人们简称为指针,我们要去从语境中区分他人说的是指针还是指针变量。
2.指针变量是存放地址的变量,其中的内容都被当作地址处理。
那么就有问题来了:
- 一个小的单元到底是多大?
- 如何编址?
【C语言基础|【C语言入门必看】指针】对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或者0)。
那32根地址线有多少种0、1组合呢?
一根地址线有2种,那32根则有232种,即从32个全0到32个全1。
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111110
11111111 11111111 11111111 11111111
若每一个这样的二进制序列作为内存单元的编号,则232个地址可以管理232个 内存单元,因此有232个内存单元可供使用,为了更加直观表达,转化为十进制232=4 294 967 296。
计算机存储容量单位换算(从左往右÷):
文章图片
若每个内存单元大小是1个bit,那每个内存单元是一个kb呢?4294967296
/8/1024/1024/1024=0.5GB
这样的话,管理的空间实在太小了,且一个char
类型的变量就得花掉8个地址,int
类型就花掉32个地址,实在太浪费了。
因此,一个地址管理一个char类型更合理,即每个内存单元大小是1个byte,转化后是4GB
不行,因为当你创建一个
char
类型,内存就得申请1个kb的空间,这就显得小题大做。同样的逻辑,也可以推导64位机器编址空间。
这里我们就明白:
- 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
- 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
- 指针变量是用来存放地址的,地址是唯一标示一块地址空间的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
int
、char
等不同的类型,可知指针变量也有不同的类型。 int a = 10;
int* pa = &a;
*
代表pa
是个指针。int
代表指针pa
所指向的变量类型是int
类型
C语言中,指针变量是区分类型的,它这么干有意义的,主要体现在两方面:
- 指针解引用
- 指针加减整数
int a = 0x11223344;
//十六进制,每一个数字可转化成4个bit
int* pa = &a;
*pa = 0;
我们创建并初始化一个变量
a
,并用指针变量pa
指向a
,然后对pa
解引用把a
赋为0,调试并在内存中发现:文章图片
接着,我们更改指针变量的类型,
int* pa
改为char* pa
文章图片
对比下,我们发现同样
*pa
的操作,解引用访问int*
创建的pa
可以修改4个字节的内容,而解引用访问char*
创建的pa
只能修改一个字节的内容。说明:
意义1:指针类型决定了指针解引用访问时,一共可以访问几个字节(访问内存的大小)。
char* -指针解引用访问1个字节
int* -指针解引用访问4个字节
一一对应:char*指向是char型,char型恰好1个字节,int*同理。
2.指针加减整数 现在我们用不同类型的指针分别指向同一个变量
a
,并对指针+1
。如:文章图片
可见,
int*
型的指针+1向后跳了4个字节;char*
型的指针+1向后跳了1个字节。说明:
指针类型决定了(指针总结:±
整数)的步长(跳过几个字节)。
指针类型决定了:
- 指针解引用访问几个字节(访问的内存空间大小)
- 指针类型决定了指针±整数跳过几个字节(步长)
int arr[10] = { 0 };
int i = 0;
int* pa = arr;
for (i = 0;
i < 10;
i++)
{
*(pa + i) = 1;
}
int arr[10] = { 0 };
int i = 0;
char* pa = &arr;
for (i = 0;
i < 40;
i++)
{
*(pa + i) = 1;
}
调试并在内存中可见:
文章图片
第①种以4个字节为一跳;第②种以1个字节为一跳。看图:
文章图片
三、野指针 1.野指针定义 概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
2.野指针成因 1.指针未初始化
int *p;
//局部变量指针未初始化,默认为随机值
*p = 20;
定义指针时就要对其初始化,即指向一个变量的地址,若暂时无,可先初始化为空指针NULL。2.指针越界访问
int arr[10] = { 0 };
int i = 0;
int* p = &arr;
for (i = 0;
i <= 10;
i++)//当指针指向的范围超出数组arr的范围时,p就是野指针
{
*p = i;
p++;
}
p++执行10次后,此时的p指向的地址不再属于数组地址,它指向的是后面的内存,属于越界访问,越界访问是非法的。3. 指针指向的空间释放
int* test()
{
int a = 10;
return &a;
//返回a的地址,假设是0x0012ff40
}int main()
{
int* p = test();
//p存的是a的地址,0x0012ff40
printf("%d\n", *p);
return 0;
}
显示:10
我们知道函数调用完成,其局部变量会自动释放,虽然指针变量但执行程序却打印出10,这是为什么?p
存的还是原来a的地址,但这块内存已经被编译器归还给操作系统了,若再访问便是非法访问。
这是因为变量在打印a
的内存空间归还给操作系统,但并没有对其销毁,而且没有其他函数覆盖它,编译器会对其作一次保留,*p
仍然会打印出10。
*p
前再调用一次printf()
函数:int* test()
{
int a = 10;
return &a;
//返回a的地址,假设是0x0012ff40
}int main()
{
int* p = test();
//p存的是a的地址,0x0012ff40
printf("hehe\n");
printf("%d\n", *p);
return 0;
}
显示:
hehe
5
因为内存中的栈区分配空间是从高地址到低地址的。调用问:为什么只有printf("hehe\n");
会覆盖原来test()函数所在的空间,此后,*p
将会非法访问。
printf("%d\n", *p);
时,该printf函数不会把test()
覆盖呢?因为传参先于调用,在调用看图:printf()
时,*p
已经是10了
文章图片
栈区的空间是先用高地址再到低地址:
文章图片
3.如何规避野指针
- 指针初始化
int a = 10;
int* pa = &a;
//定义指针时就得指向地址
int* pb = NULL;
//不知道指向谁,先置为空指针
- 小心指针越界:访问数组元素时
- 指针指向的空间释放后,立即置为
NULL
- 避免函数返回局部变量地址
提一嘴:返回函数静态变量地址是有效的,因为静态变量地址不会销毁。
疑惑:为什么可以返回局部变量,却不能返回局部变量地址呢?
函数调用完毕,两者都会销毁,返回的局部变量是一份拷贝(将数值拷贝),后续不用对其访问;而返回的局部变量地址,后续若对其访问,则是非法的。
- 指针使用之前检查有效性
空指针不可解引用,所以在使用指针前可以先判断指针是否为空指针NULL。
if(p != NULL)
{
*p=20;
//检验不为空指针,再使用
}
//若是空指针,不执行操作
四、指针运算 1. 指针± 整数
2. 指针-指针
3. 指针的关系运算
另外,指针解引用也是指针运算。
但为什么没有指针+指针运算呢?
指针+指针是合法的,但是无意义的。1.指针±整数
打个比方,指针是"日期",整数是"天数",指针±
整数仍是指针,相当于,日期±
天数还是日期。
指针-
指针是元素的个数,相当于日期-
日期是天数;日期+
日期并没什么实在意义,就如同指针+
指针。
#define N_VALUE 5
int main()
{
float values[N_VALUE];
float* vp = values;
for (vp = &values[0];
vp < &values[N_VALUE];
)
{
*vp++ = 0;
//*(vp++)
}
return 0;
}
程序分析:
①看图:*vp++
,根据优先级,相当于*(vp++)
,后置++
:先使用vp
并解引用,再++
。指针++
逐次跳过一个类型大小的字节
②当vp
指向values[N_VALUE]
,不满足条件判断,结束循环。
③&values[N_VALUE]
该地址不属于数组,仅用于判断,并没有访问
文章图片
2.指针-指针
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
printf("%d\n", &arr[9] - &arr[0]);
//9
printf("%d\n", &arr[0] - &arr[9]);
//-9
得到的数值的绝对值是指针和指针之间元素的个数为什么不是36呢?
语法规定指针-指针是两个指针之间的元素个数,默认是所占字节大小除以类型大小。那是不是随便两个指针都可以相减?
当然不是。
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
char ch[5] = { 0 };
printf("%d\n", &arr[9] - &ch[0]);
//err
程序能正常执行并打印出13。这时候有很大的疑问,
&arr[9] - &ch[0]
这两个地址之间有什么关系呢?相减所得的数值(元素个数)是以int类型还是char类型呢?所以得到的数值是没意义的。从这里我们明白:
- 指针-指针的前提:两个指针指向同一块区域(同一个数组)。
- 指针-指针:两个地址之间的元素个数。
计算字符串长度-
strlen
函数int my_strlen(char* s)
{
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count;
}
用指针
-
指针实现int my_strlen(char* s)
{
char* start = s;
while (*s != '\0')
{
s++;
}
return s-start;
//指针-指针:两指针之间的元素个数
}
3.指针的关系运算 将指针±整数的代码拿来修改,从末尾向前遍历。
第一种:
for(vp = &values[N_VALUES];
vp > &values[0];
)
{
*--vp = 0;
}
第二种:*--vp
:先--
后对vp
解引用置为0
,vp逐次减1并与&value[0]
比较
for (vp = &values[N_VALUE-1];
vp >= &values[0];
vp--)
{
*vp = 0;
}
区别:最后一次循环,上图:vp
指向&value[0]
前面的那一块地址,最后不满足判断条件,结束循环。
文章图片
我们应该避免使用第二种方法,因为C语言语法规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。即:允许
vp
和p1
比较,但不允许vp
和p2
比较。原因:
p2
区域可能存储数组相关信息,如数组元素个数等,所以越界的时候,风险会大,这是编译器不想看到的。文章图片
地址本质也是一个数值,是可以比较大小的,即指针的关系运算是合理的。
五、指针和数组 指针和数组之间有什么联系呢?
- 指针变量存储地址,是一个变量,指针的大小固定为4 (32位) / 8 (64位)。
- 数组是一个相同类型元素的集合,其中元素存放在连续的空间中。数组的大小取决于元素类型和元素个数。
数组的每一块内存单元都是有地址的,而指针就是地址,我们把数组元素的地址赋给指针,通过指针来访问数组,这样便建立了联系。
int arr[10] = { 0 };
printf("%p\n", arr);
//0x0012ff40
printf("%p\n", &arr[0]);
//0x0012ff40
由此可知,数组名就是数组首元素的地址,但前提是除了以下两种情况
顺便提一嘴:1.sizeof(arr)
2.&arr
这两种情况是整个数组的地址,+1
会跳过整个数组,数组章节提过。
printf("%d", sizeof(&arr));
这种情况,&arr是一个地址,地址的大小固定为4 (32位) / 8 (64位)。
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
for (int i = 0;
i < sz;
i++)
{
printf("&arr[%d] = %p <===> p+%d = %p\n", i, &arr[i], i, p + i);
}
运行结果:
文章图片
所以
p+i
其实计算的是数组arr
下标为i
的地址。那我们就可以直接通过指针来访问数组六、二级指针 二级指针变量:存放一级指针变量的地址,通过二级指针访问一级指针。
文章图片
对于二级指针的运算有:
*ppa
通过对ppa
中的地址进行解引用,这样找到的是pa
,*ppa
其实访问的就是pa
int b = 20;
*ppa = &b;
//等价于 pa = &b;
**ppa
先通过*ppa
找到pa
,然后对pa
进行解引用操作:*pa
,便找到a
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
有n级指针,就得n次解引用才找到变量类型中"*"的理解a
文章图片
绿框代表是指针变量,红框代表该指针指向的变量类型,"*"有n个,就是n级指针。
如:二级指针
ppa
前面的int*
代表的是ppa指向的变量pa的类型是int*
七、指针数组 指针数组是指针还是数组?
是数组。是存放指针的数组。
我们已经知道整型数组和字符数组。
int arr[5];
//整型数组 - 存放整型变量的数组
char ch[5];
//字符数组 - 存放字符变量的数组
通过类比,我们可以得出指针数组
int* parr[5];
//整型指针数组 - 存放整型指针变量的数组
char* pch[5];
//字符型指针数组 - 存放字符型指针变量的数组
指针数组的使用
int a = 10;
int b = 20;
int c = 30;
//int* pa = &a;
//int* pb = &b;
//int* pc = &c;
//...当需要创建大量指针变量
int* arr[] = { &a,&b,&c };
int i = 0;
for (i = 0;
i < 3;
i++)
{
printf("%d ", *arr[i]);
//printf("%d ", **(arr + i));
}
整型指针数组,数组中每一个元素都是整型变量的地址,因指针变量大小固定是4byte(32位)或8byte(64位),数组大小仅由数组元素个数决定。
文章图片
初阶指针到此结束,后续再深入了解指针。
推荐阅读
- C语言基础|【Visual Studio 2019】 实用调试技巧,学会了都说好
- C语言基础|【C语言入门必看】结构体
- C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
- 《C陷阱与缺陷》读书笔记--第一章语法陷阱1
- C语言|C语言两种方法计算一个数所有位上的数的总和
- C语言|C语言实现简单计算器
- Kubernetes|Kubernetes 中的服务发现与负载均衡
- 蓝桥杯|蓝桥杯第十届C语言b组——B: 年号字串
- 蓝桥杯|蓝桥杯.颠倒的价牌(暴力枚举)