目录
Part one(一)
1. const关键字
const可以修饰普通变量,也可以修饰指针变量,两种场景会有所差异。
1.1 const修饰普通变量
1.1.1 const的作用
const用来声明一个普通变量后,一旦该变量被初始化,它的值将不能再被
直接
改变。
(不能再通过赋值改变)
创建格式:
(1) const 数据类型 变量名 = 初始值; //如:const int a = 10;
(2) 数据类型 const 变量名 = 初始值; //如:int const b = 34;
(至于为什么会有两种格式,你可以在 知识点1.2 找到答案)
注意:const修饰的变量只能被初始化,不能被赋值,否则会报错。
(补充:在C语言中,被const修饰的变量并不是常量,而是常变量;但在C++中,被const修饰的变量是常量。详细请看《数组 基础知识 和 冷知识(超详细总结)》中的常变量冷知识)
1.1.2 指针绕过const
以一个例子引入:
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
n被const修饰后,在语法上加了限制。只要我们在代码中对n就⾏修改,就不符合语法规则并报错,致使没法直接修改n。
但是如果我们绕过n,使⽤n的地址,去修改n就能做到了。(虽然这样做违反了语法规则)
int main()
{
const int n = 0;
printf("n = %d\n", n);
int* p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
运行结果:
我们可以看到这⾥⼀个确实修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了 不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让 p拿到n的地址也不能修改n,那接下来怎么做呢?
1.2 const修饰指针变量
首先我们要明确一点:const 修饰的对象是变量,而不会是数据类型。(在判断const对什么起作用时,可以去掉数据类型再判断。)
1.2.1 const在 * 前
作用:const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量的值可变,也就是指针存入的地址可改变(可改变地址指向)。
创建格式:(由于const对数据类型没有影响,所以有2种写法)
(1) const 数据类型 *变量名 = 初始值; //如:const int *p1 = &a;
(2) 数据类型 const *变量名 = 初始值; //如:int const *p2 = &b;
例如:
const int a = 10;
const int *p = &a;
去掉数据类型再判断:
const *p = &a;
可以发现const此时修饰的是*p,所以*p的值不能改变。
而*p的值就是a的值,所以此时不能再通过*p间接改变a的值,这下const a成为了真正意义上的不可改变的变量。(没错,它在c语言中还属于变量)
代码演示:
1.2.2 const在 * 后
作用:const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的地址指向不能修改。但是指针指向的内容,可以通过指针改变。
创建格式:
数据类型* const p = 初始值; //如:int* const p = &c;
例如:
int a = 10;
int *const p = &a;
去掉数据类型再判断:
*const p = &a;
可以发现const此时修饰的是p,所以p的值不能改变。
而p装的是地址,所以指针p的地址指向不能变。
代码演示:
1.2.3 双重const修饰
那我们能不能既限定指针变量不能改变地址指向,也要求它不能改变所指向的内容?
答案是肯定的,我们可以用2个const来修饰指针变量。
创建格式:
const 数据类型* const 变量名 = 初始值; //如:const int* const p = &d;
Part two(二)
1. 野指针
概念:野指针是一种非法的指针,它所储存的地址具有(1)随机性、不可知性(2)越界访问性和危险性。[这两种性质一般不会同时存在]
* 野指针的成因
成因1:指针未初始化
int main()
{
int* p; 局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
这也是性质(1)的由来。
成因2:指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i <= 11; i++)
{
当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
这也是性质(2)的由来。
成因3:指针指向的空间被释放
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
这也是性质(1)的由来 。
函数被调用时,变量n才会被分配内存空间,函数调用结束会收回n的空间。此时返回的地址值是一个随机值。补充一下,成因3所说的指针指向的空间被释放,还包括malloc来的空间被free掉的情况。
野指针的产生我们是有办法规避的,但在讲规避方法之前,我想再补充几个必要的知识点并加以区分。
2. 泛型指针 void*
误区:有些同学会说,void类型被称为空类型,那void*一定就是空指针了。
这样理解是错误的。void*其实被叫作泛型指针,可以理解为⽆具体类型的指针;而空指针NULL其实是泛型指针中的一个特殊值(具体内容后面会说)。
相比于其他类型的指针,void*有这几种特性:
特性1
void*指针与其他指针一样,大小都是4个字节(32位环境)或8个字节(64位环境)。
int main()
{
printf("int型的大小是:%zd\n", sizeof(int*));
printf("char型的大小是:%zd\n", sizeof(char*));
printf("void型的大小是:%zd\n", sizeof(void*));
return 0;
}
以X64环境为例:
(至于大小为什么固定在4和8,详细请看《指针之旅(1)—— 指针基础概念知识》)
特性2
泛型指针可以⽤来接收任意类型地址。(所以有时候void*被称为万能指针)
如果将⼀个int类型的变量的地址,赋值给⼀个char*类型的指针变量,编译器给出一个提醒:
虽然这样写能成功通过编译,但这并不是正确的使用方法。因为指针变量不仅仅是用来存储地址这么简单,指针变量的数据类型还决定着指针访问内存的权限大小。(具体请看《指针之旅(1)—— 指针基础概念知识》中的“指针的访问范围”这一部分知识点)
但如果用void*的指针来存储这个int型的指针就完全没有问题。
特性3
void* 类型的指针不能直接进行(1)指针的+-整数和(2)解引⽤的运算。
我们知道,void类型本身没有长度。所以void*指针只能用来接收,它本身不具有访问地址的权限。
不知道每次访问的内存字节有多少,就无法进行指针的+-整数和解引⽤的运算。
【但有一种方法可以使得void*类型的指针也能进行指针的+-整数和解引⽤的运算,那就是强制类型转换。等到时候《指针之旅(5)》写出来的时候会讲,本篇重点在于区分野指针、空指针和void*指针】
3. 空指针 NULL
概念:NULL是C语⾔中定义的⼀个标识符常量(这一种宏定义),被称作空指针,它的值是0。
注意:0也是地址,这个地址是⽆法使⽤的,访问该地址会报错。(不止是0,访问其他比较低的地址也会报错,这是系统底层的地址,不允许随意更改的)
我们来看一下关于NULL的宏定义:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
从中我们可以看到,NULL的值是0,而且NULL算是一个void*类型的指针。
4. assert 断言
assert
是一个宏定义,用于在程序中插入断言语句。使用前要包含头文件<assert.h>
assert的用法格式:
1. assert( exp );
//exp是表达式
assert的运行逻辑:
(1)当exp为真时,程序会继续运行下去。
(2)当exp为假时,会提前终止程序,并提示你在第XX行触发了断言。
比如:
运行结果:
我们看到,assert触发后并没有执行“printf("a是%d", a);”,而是提前终止了程序,并打印出错误信息。(这个作用在代码量非常多的情况很有用,可以快速知道程序错误的位置并加以修改)
补充:assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。 ⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题, 在 Release 版本不影响用户使⽤时程序的效率。
5. 如何规避野指针
❶.指针初始化阶段:
如果明确知道指针指向哪⾥就直接赋值地址
如果不知道指针应该指向哪⾥,可以给指针赋值NULL,读写该地址会报错。(这样可以防止对随机值地址的非法访问,因为只有访问NULL时会报错)
❷.⼩⼼指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
❸.指针变量不再使⽤时,及时置NULL; 指针使⽤之前用assert检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。
因为⼀个约定俗成的规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。
假设有一个指针int *p,判断p是不是野指针可以用aseert(p) 或者 assert(p != NULL)。
我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来, 就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。
不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我 们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。
本期关于指针的知识分享完毕,感谢大家的支持Thanks♪(・ω・)ノ