const修饰指针-2
前面,咱们已经讲过了const修饰普通变量(非指针变量
)的特点。那么,今天我们来讲讲const修饰指针变量的特点吧。const
修饰指针变量很特别,有3种情况要进行解读。
- const放在
*
左边:咱们就用整形指针变量来进行举例。比如,const int *p=&a
或者int const *p=&a
这两种写法都是在*
左边的情况。直接说结论——const
放在*
的左边时,*p
是无法执行的(p解操作后,所对应的值是无法修改的),但是p存放的地址可以改变。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 10;
const int* p = &a; //或者 int const *p=&a
*p = 30; //const放在*的左边,*p是无法执行的
return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 10;
const int* p = &a; //或者 int const *p=&a
*p = 30;
return 0;
}
结果运行图:
- const放在
*
右边:放在左边,咱们已经知道了,放在右边,自然而然就能推理出来。比如,int * const p=&a
。放在右边就只有这一种写法,可不能这样写:int * p const=&a
。它所限制的是——指针变量所存放的地址是无法修改的,但是*p
(所指的变量内容是可以改变的)可以执行。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 10;
int c = 20;
int* const p = &a; //不可以写成这样: int* p const = &a;
p = &c; //无法执行
printf("%p\n", p);
return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 10;
int c = 20;
int* const p = &a;
p = &c;
printf("%p\n", p);
return 0;
}
结果运行图:
- const放在
*
的左边和右边:放在*
的左边是限制指针变量的解引用操作,放在*
的右边是限制指针变量的地址改变,那么放在*
的左右边呢?——即限制指针变量的解引用操作,又限制指针变量的地址改变。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 10;
int c = 20;
int const * const p = &a;
*p = 30; //无法执行
p = &c; //无法执行
printf("%d\n", a);
printf("%p\n", p);
return 0;
}
结果运行图:
指针运算
指针的基本运算有三种,如下。
- 指针+ - 整数:就是地址+ - 整数,咱们前面也讲过了 ,地址(指针)+ - 整数,能跨过多少字节取决于指针变量类型。以前我们写过访问数组的代码,比如,
printf("%d ",arr[i])
今天我们用指针进行访问,进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr; //我们讲过,数组名就是首元素地址,也可以这样写 int* p = &arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
结果运行图:
代码还可以这样写:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p++));
}
return 0;
}
结果运行图:
这里我们稍微讲一下p+i
和p++
的区别。这俩的本质上没什么区别——都是对数组进行访问。它们的访问方式不同,p+i
进行遍历访问,因为解引用操作是对p+i
整体进行的,不会改变原有的p的值,每加一个 i 就相当于一个新的p+i
的整体分身。但是,p++
就不同了,它每次++
后,p的值就改变了,就会进行跨越访问——向后移动。如图所示:
- 指针 - 指针:我们直接说结论:指针-指针(地址-地址)的绝对值就是两个指针之间的元素个数。这里有个大前提:两个指针所指的空间必须是同一块且连续的空间。这很好理解,只有同一个空间,且必须是连续的,咱们才能得到它们之间的元素个数。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int* pb = &arr[8];
int c = (int)(pb - p); //咱们最好强制转换一下类型,省的警告。
printf("%d\n", c); //不强制转换类型也可以。
return 0;
}
结果运行图:
当我们对数组进行指针- 指针运算的时候,可以直接用下标数字直接相减就OK.指针也是有大小指针的——大指针(大地址),小指针(小地址)。当小指针 - 大指针的时候,结果就是负值,但是它的绝对值也是元素个数——所以我们前面讲过了,它们运算的绝对值就是元素个数。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int* pb = &arr[8];
int c = (int)(p - pb); //小指针-大指针
printf("%d\n", c);
return 0;
}
结果运行图:
- 指针的关系运算:我们前面也讲过了关系运算符,比如,> ,< ,= ,!=。指针的关系运算无非就是这些关系运算符的操作数变成了指针而已。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int* pb = &arr[8];
if (pb > p) //大指针 > 小指针,所以条件成立
{
printf("hhhh");
}
return 0;
}
结果运行图:
我们已经学了指针的+ - 运算,指针 - 指针运算和指针关系运算。那么,我们来写个程序运用一下:
// 写一个程序,用来模仿 sizeof 来计算数组的元素个数
进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int* pb = &arr[9];
int count = 0;
while (p <= pb)
{
count++;
p++;
}
printf("%d\n", count);
int b = sizeof(arr) / sizeof(arr[0]);
printf("这是sizeof计算的结果:%d\n", b);
return 0;
}
结果运行图:
还可以模拟 strlen这个函数,大家可以自行尝试一下。
野指针
概念:就是指针所指的位置是不可知的,或者所指的位置是不属于自己的,就称为野指针。(随机的,不可知的,没有限制的)。接下来,我们讲解野指针的成因。
- 指针未初始化:指针变量也是变量,咱们以前在变量的那个章节中讲过——局部变量未初始化时,会被系统随机分配数值,全局变量不初始化时,会被默认分配为0。当我们指针变量为全局变量时,会被分配一个地址(是0地址),而且这个地址不会随程序每次调用而改变。指针变量当作局部变量的场景是最多的,所以我们后面所讲指针变量时,就默认都是局部变量。当我们不给指针变量初始化时,就会被系统随机分配地址。这是很危险的,我们知道可以通过指针来更改内存里的数据,这样的话,我们使用指针变量(未初始化)进行更改时,就有可能更改对于我们来讲很重要的数据(事与愿违)。进行代码展示:
当为全局变量指针时:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int* p ; //全局变量指针
int main()
{
printf("%p\n", p);
return 0;
}
结果运行图:
当为局部变量时:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//int a;
//int* p;
int main()
{
int* p; //局部变量
printf("%p\n", p);
return 0;
}
结果运行图:
- 指针越界访问:当我们确定好空间后,就只能在这个空间内进行数据的访问,不能访问本空间以外的空间。这个很好理解,我们有多少钱就花多少钱,不能去抢别人的钱。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i <= sz; i++)
{
printf("%d ", *(p + i)); //当访问到下标为10时,就是越界访问
}
return 0;
}
结果运行图:
- 指针访问的空间被释放:先进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d\n",*p);
return 0;
}
结果运行图:
当我们在调用函数test()
时,操作系统就会给a
分配好内存,当我们调用完后,这个内存就后被释放,还给操作系统。 这个知识,咱们在栈帧的创建与销毁中讲过。当test
把a
的地址传给p
时,p
就是野指针了。因为,当内存释放的时候,那块内存已经不属于a
的内存了,但是这个内存还存在,不会被销毁的(因为内存的地址是硬件设计),我们就没有权限进行访问了(相当于越界访问了)。野指针的成因有很多种,我们在这里举出最常见的原因。我们既然已经知道了野指针的成因,那么我们应该如何规避野指针呢?
- 指针初始化:当我们知道指针的明确指向时,我们就要进行赋值初始化。当我们没有明确的指向时,我们可以赋予
NULL
,NULL
是C语言中规定的一个标识符常量,它的值是0(0也是个地址,这个地址我们是不能访问的,一旦访问就会报错)。当一个指针变量被赋值NULL时,它就有个新的名字——空指针。展示它的定义:
# ifdef __cplusplus //在C++中,NULL就是0
# define NULL 0
# else
#define NULL ((void *)0) //在C语言中,NULL是由0强制转换类型而来的。
# endif
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 10;
int* p1 = &a;
int* p2 = NULL; //p2就是个空指针
return 0;
}
当我们用完指针后,不想再用时,也对其进行赋值NULL,防止成为野指针。我们来举个例子,以便我们理解NULL。野指针就相当于一条野狗,如果我们不进行管制的话,它就会有伤人的风险,当我们把它拴在一棵上,它救不会乱伤人了,而这棵树就相当于NULL。当一个指针被赋值NULL时,就表明它是个野指针(野狗),我们要绕道走,这就是NULL的作用,而且一但被NULL赋值的指针变量,我们是无法使用的,就像我们明明知道它是条野狗,非要去碰它,肯定会有危险。
- 避免越界访问:我们使用指针时,要注意不要越界访问,这个没什么好的方法,要注意下就OK了。
- 避免使用局部变量返回的地址,就像我们上面举例的代码(空间被释放)。
assert断言
是人都会犯错误,我们有时候不注意到野指针,就会报错。那么怎么避免呢?这个时候我们就可以用assert
进行一个判断。assert
的头文件为:<assert.h>
。assert的使用格式:
// assert(表达式)
// 表达式的结果为真(表达式成立),程序正常执行
// 表达式的结果为假(表达式不成立),程序就会立即中断,无法正常执行
进行代码展示:
1 #define _CRT_SECURE_NO_WARNINGS 1
2 #include<stdio.h>
3 #include<assert.h>
4 int main()
5 {
6 int* p = NULL;
7 assert(p != NULL); //条件不成立,程序无法执行
8 printf("hhh");
9 return 0;
10 }
结果运行图:
当我们修改代码后:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
int main()
{
int a = 10;
int* p = &a;
assert(p != NULL); //表达式为真,程序可以执行
printf("hhh");
return 0;
}
结果运行图:
assert
对程序员是很友好的,虽然程序走到这里要判断一下(会耽误点运行时间),但可以防止我们出错。一般我们可以在Debug
中使用,在Release
版本中选择禁用assert
就行,在VS这样的集成开发环境中,在Release
版本中,直接就是优化掉了。这样在Debug
版本写有利于程序员排查问题,在Release
版本不影响用户使用时程序的效率。我们不想使用assert时,除了直接删除以外,我们还可以在# include <assert.h>
前面加个# define NDEBUG
就会禁止assert的使用。进行代码的展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define NDEBUG //禁止了assert的使用
#include<assert.h>
int main()
{
int* p = NULL;
assert(p != NULL); //就是个摆设
printf("hhh");
return 0;
}
结果运行图:
assert不只是用来判断是否为空指针(空指针只是举个例子),还可以判断别的东西。进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
int main()
{
int a = 10;
assert(a != 20);
printf("hhh");
return 0;
}
结果运行图:
指针的使用和传址调用
- 传值调用:我们先写个程序来感受一下,然后在讲解。
// 我们写一个程序,当我们输入两个整数时,就会交换两个数据。
// 比如,输入 :a=100 , b=200 。
// 得到 : a=200 , b=100 。
进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void swap(int x, int y)
{
int c = 0;
c = x;
x = y;
y = c;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
swap(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
结果运行图:
我们发现,结果事与愿违。我们创建了swap函数,我们把a ,b的值传给了swap中的x , y了,也就是x就是a ,y就是b。我们进行交换,按理说应该就是交换的,但为什么不是这样呢?这就牵扯到形参与实参的知识了(前面咱们已经讲过了)。实参向形参传的是值,而且形参是单独创立的空间,形参是实参的一个copy
版本,形参的改变不会影响到实参。所以,形参里面无论怎样变化,都不会影响到实参,这也就是为什么前后结果一致。
- 传址调用:传址调用就是传递的是地址。上面我们进行传值是无法进行值的交换,那么我们就进行传址调用,是否能交换呢?进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void swap(int* x, int* y)
{
int c = *x;
*x = *y;
*y = c;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
swap(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
结果运行图:
我们可以看出来,传址调用改变了值,因为我们直接调用地址,直接就可以找到对应的值,就可以进行更改(类似远程遥控一样)。
彩蛋时刻!!!
https://www.bilibili.com/video/BV15H4y1F7fc/?spm_id_from=333.337.search-card.all.click&vd_source=7d0d6d43e38f977d947fffdf92c1dfad
每章一句:想,全是问题。做,才有答案。
感谢你能看到这里,点赞+关注+收藏+转发是对我最大的鼓励,咱们下期见!!!