文章目录
- 一、const 使用指南
- 1. const修饰变量
- 2. const修饰指针变量
- 二、野指针
- 1. 野指针的概念
- 2. 野指针的成因
- 3. 如何规避野指针
- 三、assert(断言) 使用指南
- 四、传值调用和传址调用
- 1. 传值调用
- 2. 传址调用
一、const 使用指南
1. const修饰变量
众所周知,变量是可以修改的。但有时候,我们希望一个变量不能被修改,这就需要使用关键字const 了
#include<stdio.h>
int main()
{
int m = 12;
m = 24; //m可以被修改
const int n = 12; //使n不能被修改
n = 24; //n不可以被修改
return 0;
}
如上,如果执意修改这个n,那么程序运行后,就会报错:
但是我们绕过n,而利用n的地址去修改n,也能做到:
#include<stdio.h>
int main()
{
int m = 12;
m = 24;
const int n = 12;
int* p = &n;
*p = 24;
printf("%d",n);
return 0;
}
但是这一通操作下来,我不禁思考,const的目的本来就是防止变量被修改,但使用地址还是能修改它。const单纯修饰变量并不完善,应该让p拿到n的地址也不能修改n,那么该怎么做呢?
2. const修饰指针变量
我们可以让const修饰存有地址的指针变量,一般来讲,const可以写在*的左边,也可以写在它的右边,意义是不一样的。
int* p;
const int* p; //等价于 int const * p; 放在*的左边
int* const p; //放在*的右边
- const放在*左边:限制
*p
的改变,即指针指向的地址的内容不能通过指针被改变了。但指针变量本身可以改变,即指针变量指向的地址可以改变。
int n=12;
int m=24;
const int* p=&n;
*p = 24; //不可以(n的值不能改变了)
p = &m; //可以的(让p存放m的地址)
- const放在*右边:限制p的改变,即指针变量指向的地址不能改变了。但不限制
*p
的改变,即指针指向的地址的内容可以改变。
int n=12;
int m=24;
int* const p=&n;
*p = 24; //可以的(把n的值改成24)
p = &m; //不可以(不能让p存放的地址改变了)
当然,其实也可以在左右两侧都写上const,const int* const p = &n;
,意味着该指针指向的地址和该地址里的内容都不能改变了。
二、野指针
1. 野指针的概念
一言以蔽之,野指针就是指向位置是随机的、不正确的、没有明确限制的指针,像“野狗”一样。
2. 野指针的成因
野指针有以下几个成因:
1.== 指针变量未初始化==:
int* p;
,p存放的地址默认为随机值,p是野指针
- 指针越界访问:
int arr[10]={0};
int* p = &arr[11];
指针指向的范围超过了数组的范围,p是野指针
- 指针指向的空间释放:
int test()
{
int n = 12;
return &n;
}
int main()
{
int* p = test();
}
在上面的代码里,n是函数test里的一个局部变量,在离开test函数后变量n就被销毁了,它所占的内存空间(地址)也就被释放,传给p传了个寂寞,p就是野指针了。
3. 如何规避野指针
- 指针变量初始化
创建变量随即初始化是好文明,但如果在定义指针变量时暂且不知道它该指向哪里,可以赋值NULL,NULL是C语言中定义的一个标识符常量,值是0。int* p = NULL;
,后续知道了p该指向哪里时,再修改就行。
-
小心指针越界访问
-
避免返回局部变量的地址
-
指针变量不再使用时,置为NULL,指针使用前检查有效性
NULL的值是0,0也是地址,但是这个地址是无法使用的。编程界有一个约定俗成的规则是:只要是NULL指针就不去访问,使用指针前判断指针是否为NULL。那么如何检测指针是不是NULL呢?可以用到下面的 assert()
三、assert(断言) 使用指南
C语言的宏是一种在预处理阶段进行文本替换的工具,主要用于定义常量、简化代码或执行简单的文本替换任务。宏的定义使用#define
代码,例如#define pi 3.1415
,这样下面的代码每次出现pi时,都会被替换成3.1415
在assert.h头文件里,定义了宏assert()
,用于确保程序运行时符合指定条件,如果不符合程序就会报错终止运行,这个宏常常被称为“断言”,它接受任何一个表达式作为参数,如果该表达式为真(非0),assert()不会产生任何作用,程序正常运行;如果该表达式为假(0),assert()就会报错,并会写出没有通过的表达式,以及包含这个表达式的文件名和行号。
举个栗子:
#include<assert.h>
int main()
{
int* p = NULL;
assert(p != NULL); //p是NULL,所以这个表达式为假
return 0;
}
assert()在实际编写程序是非常有用的,它能自动标出出问题的文件和行号。不仅如此,assert()还有一种关闭的方式,在程序员修完bug后认为不需要再做断言时,就可以在#include<assert.h>
的前面,定义一个宏#define NDEBUG
,它可以使编译器禁用掉程序里所有的assert()语句,防止引入额外的检查而增加运行时间。倘若运行时又双叒叕发现了bug,就把#define NDEBUG
移除(或者注释掉),再次利用断言排查代码……直至程序没有问题。
四、传值调用和传址调用
我们学习指针是为了用到指针,那么什么问题非指针不可呢?
举个狠简单的例子:写一个函数交换两个整数的值
1. 传值调用
很多人可能会写出这样一段代码:
#include<stdio.h>
void Swap1(int x , int y)
{
int t = x;
x = y;
y = t;
}
int main()
{
int a,b;
printf("请输入a,b的值:");
scanf("%d %d",&a,&b);
printf("交换前:a=%d,b=%d\n",a,b);
Swap1(a,b);
printf("交换后:a=%d,b=%d\n",a,b);
return 0;
}
然鹅,结果不对
没有产生交换的效果,为什么o(╥﹏╥)o
我们观察一下这几个变量的地址看看:
可以看到,a和x的地址并不相同,b和y的地址并不相同,相当于a,b,x,y存放在独立的空间。实参传递给形参时,形参会单独创建一份空间来接受实参的值,而对形参的修改不影响实参。那么在Swap1函数内部只是交换了x和y的值,不会影响到a,b的值。
这种把变量本身传递给函数的调用,叫做传值调用。
2. 传址调用
为解决这个问题,就可以使用指针了。在main函数里将a和b的地址作为参数传递给交换函数,交换函数里通过地址参数找到a和b,进而完成交换:
#include<stdio.h>
void Swap2(int* x , int* y)
{
int t = *x;
*x = *y;
*y = t;
}
int main()
{
int a,b;
printf("请输入a,b的值:");
scanf("%d %d",&a,&b);
printf("交换前:a=%d,b=%d\n",a,b);
Swap2(&a,&b);
printf("交换后:a=%d,b=%d\n",a,b);
return 0;
}
结果就冇问题了!
调用Swap2函数是将变量的地址传递给了函数,这就叫传址调用。传址调用,可以通过地址,让主函数和函数之间建立真正的联系,在函数内部可以修改主函数中的变量。
综上,函数只需要主函数的参数值来计算的话,使用传值调用足矣;如果函数内部要修改主函数中变量中的值的话,就需要使用传址调用。
欲知后事如何,且听下回分解~
本篇完,感谢阅读!