指针之旅(2)——const修饰词 && 野指针、空指针与泛型指针

目录

Part one(一)

1. const关键字

1.1 const修饰普通变量

1.1.1 const的作用

1.1.2 指针绕过const

1.2 const修饰指针变量

1.2.1 const在 * 前

1.2.2 const在 * 后

1.2.3 双重const修饰

Part two(二)

1. 野指针

* 野指针的成因

2. 泛型指针 void*

特性1

特性2

特性3

3. 空指针 NULL

4. assert 断言 

5. 如何规避野指针


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♪(・ω・)ノ

评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值