进阶指针
导言
大家好,很高兴又和大家见面了!!!
在上一个章节中,咱们深入探讨了一下指针与数组之间的联系,在探讨的过程中我们发现对于指针数组与二级指针来说,它们实质上就是一维数组和一级指针,它们之间的关系也是遵从指针与数组之间关系。为了更好的学习指针,在今天的内容中,我们将介绍指针的一些补充知识点。下面就开始咱们今天的内容吧!
十一、void*指针
在函数中我们有介绍过void
表示的是无类型,函数中的void
表示无返回类型。在指针中同样也有void*
这种类型的指针,这类指针我们可以称它为无具体类型的指针,也可以称为泛型指针。
在前面指针类型的意义中我们提到过,指针的类型决定了指针对数据进行一次修改时的可操作空间大小;
- 对于
char*
的指针来说,它修改一次数据可以操作的空间为1个字节。我们让char*
的指针接收char类型的对象的地址是比较合适的,这样我们在修改内容时可以对char类型的地址中存放的内容进行一个字节一个字节的修改; - 对于
int*
的指针来说,它修改一次数据可以操作的空间为4个字节。我们让int*
的指针接收int类型的对象的地址是比较合适的,这样我们在修改内容时可以对int类型的地址中存放的内容进行四个字节四个字节的修改; - 对于
void*
类型的指针来说,它可以接收所有类型的对象的地址,并不能对其进行解引用以及进行指针的运算;
下面我们来通过实例验证一下:
从报错中我们可以看到,void*
类型的指针在接收不管是char
类型还是int
类型的对象的地址时都是没有问题的,但是我们在对其进行解引用、加减整数、以及进行指针-指针时都有出现报错,报错的内容总结下来就是——对象具有void类型,并且void*的大小是未知的;
那对于void*
类型的指针来说他能做什么呢?下面我们来测试一下:
从测试结果中我们可以看到,我们可以正常的对void*
指针进行关系运算、打印存储的地址以及赋值的操作;
也就是说对于void*
指针来说,它是无法实现指针的+-整数运算、解引用以及指针-指针运算这些运算的,但是我们可以对指针变量进行基本的操作。
对于void*指针的使用我们会在后面的内容进行介绍,大家不要心急,耐心往下继续阅读;
十二、关键字const
对于const这个关键字,它的中文翻译为常数、恒量;恒定的,不变的;它在C语言中的作用正如它的意思一样,将操作对象变成不变的,这个关键字我们前面几乎没有遇到过,它具体有什么作用呢?下面我们就来一起探讨一下;
12.1 变量
12.1.1 变量的分类
对于C语言来说,变量可分为全局变量和局部变量,下面我们来看一下什么是局部变量,什么是全局变量:
//变量的分类
int a = 10;//全局变量
test()
{
int d = a;//局部变量
}
int main()
{
int b = 20;//局部变量
if (a < b)
{
int c = a + b;//局部变量
}
return 0;
}
在这个例子中,我们分别定义了四个变量,根据代码的注释我们可以看到变量a为全局变量,变量b和变量c为main函数内部的局部变量,变量d为main函数外部test函数内部的局部变量;
在C语言中,我们将花括号{}
称为代码块,因为我们所有的代码都是需要再{}
内部编写的。对于变量来说,在{}
外面定义的变量称为全局变量,在{}
内部定义的变量称为局部变量;
12.1.2 变量的生命周期和作用域
变量的生命周期我们可以简单的理解为就是变量的创建与销毁的周期;
变量的作用域我们可以简单的理解为就是变量可以使用的区域。
对于全局变量与局部变量来说,它们的生命周期与作用域是有区别的:
- 全局变量的生命周期是跟随整个工程的,全局变量在创建后,除非关闭这个工程,否则它会一直存在,它的作用域也是作用于整个工程的;
- 局部变量的生命周期是跟随创建变量的
{}
,在{}
内部创建好局部变量后,一旦出了{}
,局部变量就被销毁了,它的作用域也是对应的{}
;
下面我们通过代码来对全局变量以及局部变量的生命周期和作用域进行说明:
在这个代码以及测试结果中,我们可以得到以下信息:
- 对于全局变量a来说,不管是在test函数内部还是在main函数的内部以及if语句的代码块内部都是可以正常使用的,所以此时我们可以说全局变量a此时的使用范围是从它创建后的任何地方都可以进行使用;
- 对于局部变量c来说,它能在if语句的代码块内部使用,也可以在if语句外,main函数的代码块内进行使用,所以此时我们可以说局部变量c的使用范围是在main函数的代码块内部;
- 对于局部变量b和局部变量d来说,它们都是可以在自己对应的代码块内部进行使用的,所以此时我们可以说局部变量b和局部变量d的使用范围是在它们对应的代码块内部;
下面我们继续看下面的代码:
可以看到,此时代码出现了6处报错,报错内容都是未声明的标识符,也就是说在报错的这些地方是不存在这些变量的。
现在有朋友可能就有疑问了,局部变量出现这种情况我都能理解,此时它是因为出了自己的作用域就被销毁了嘛,但是你都说了全局变量是跟随整个工程的,你这现在不是自己打自己的脸吗?
别着急,下面我们下面我们继续介绍一个新的关键字——extern——引入外部符号(可以引用其它源文件内部定义的全局变量),现在我们再来看一下下面的代码:
从这次的结果中我们可以看到此时通过关键字extern对全局变量a进行声明后现在再到test函数中使用变量a是没有任何问题的。但是我们通过对b、c、d进行extern的声明后,此时报错了,错误内容为无法解析的外部符号,这也就是说,extern只能对全局变量使用。
因此这个例子再一次证明了局部变量的生命周期与作用域都是自己对应的代码块内部;
而对全局变量来说,我们可以通过关键字extern对变量进行声明,所以全局变量的生命周期和作用域是在整个工程内部的。
12.1.3 变量的优先级
现在我们设想一下,全局变量和局部变量可不可以同名呢?如果可以同名,那应该是全局变量优先使用还是局部变量优先使用呢?下面针对这两个问题,我们来通过代码测试一下:
从测试结果中我们可以看到,在局部变量a的代码块内部打印的是局部变量a的值,而当局部变量被销毁后打印的则是全局变量a的值,也就是说当局部变量与全局变量的变量名相同时,程序优先执行的是局部变量。因此,我们在今后写代码时,尽量避免局部变量与全局变量同名的情况。
12.2 const修饰局部变量
const这个关键字,它是可以对变量进行修饰的,当他修饰变量时,会给变量赋予一个常属性,使变量不可被改变,如下所示:
可以看到此时程序的报错内容为表达式必须是可修改的左值,也就是说,此时被const修饰后的局部变量b是不可像局部变量a一样被修改的。但是为什么我们说它const修饰的局部变量只是拥有了常属性呢?这是因为我们此时可以通过指针对其进行修改,如下所示:
此时我们可以看到程序是正常运行的,而且b的值此时也被修改为了20,所以被const修饰的局部变量只是拥有了常属性,不能直接对其进行更改,但是它的本质还是一个变量,所以我们可以通过指针来对它的值进行修改。
这种通过地址来修改变量的值的方式是绕过了C语言的语法规则,打破了const的规则限制,这显然是不合理的,那我们应该怎么做才能保证即使拿到了变量的地址也无法对变量进行修改呢?
12.3 const修饰指针变量
为了能够在拿到变量地址后也无法修改变量的值,我们可以通过const对指针进行修饰。但是应该如何修饰呢?下面我们来看一段代码:
在这个代码中,我们通过将const放在指针变量的不同位置来对const进行修饰。可以看到,此时的程序报错内容是指针pa和pa2这两个const在*
左边修饰的指针,而对于const在*
右边修饰的指针系统并未报错,那是不是代表我们可以通过指针来修改变量的值呢?下面我们继续测试:
从测试结果中我们可以看到,此时变量的值确实通过指针pa被修改了,也就是说如果我们想限制指针无法通过解引用修改指向的对象中存储的内容,那我们就需要将const放在*
左边对指针进行修饰才行。
根据const的语法规则,如果我们将const放在*
右边时,此时const限制了什么内容呢?下面我们继续测试:
在这个代码中,我们想通过指针变量p完成对变量a以及变量b的修改,可是我们可以看到,在完成对变量a的修改后,我们将指针p指向b时,此时系统报错了,报错内容为此时的变量p是无法被修改的。
那如果此时const放在*
的左边能不能对指针指向的对象进行修改呢?我们继续测试:
可以看到,此时的指针p是可以对指向的对象进行修改的。通过上面的测试我们可以得到结论:
- 当const在指针*左边修饰指针变量时,限定的是
*p
,即无法对*p
中存储的内容进行修改,但是可以对指针p指向的对象进行修改;- 当const在指针*右边修饰指针变量时,限定的是p,即无法对指针p指向的对象进行修改,但是可以对
*p
中存储的内容进行修改;
在前面我们在介绍野指针时有说过,我们可以通过下面五点来规避野指针:
- 给指针进行初始化;
- 避免指针越界访问;
- 不要返回局部变量或者临时变量的地址;
- 当指针指向的地址不再使用时,将指针置为空(NULL);
- 在使用指针前,检查指针的有效性;
既然我们需要再使用指针前检查指针的有效性,那我们应该怎么做呢?这就是我们现在要介绍的一个新的知识点——assert断言;
十三、assert断言
在头文件assert.h
中定义了一个用于在运行时确保程序符合指定条件,如果不符合就报错终止运行的宏——assert()
。这个宏常常被称为“断言”。
13.1 assert工作原理
assert()
这个宏可以接收一个表达式作为参数。如果表达式为真(返回值非零),assert()
不会产生任何作用,程序继续运行。如果表达式为假(返回值为零),assert()
就会报错,在标准错误流stderr
中写入一条错误信息,显示没有通过表达式,以及包含这个表达式的文件名和行号。
借助这个宏,我们就可以在使用指针前来检查指针的有效性,如下所示:
可以看到,当assert
的括号内的条件不满足时,此时系统就会报错,在报错中会显示文件的路径以及报错的具体位置,同时系统也会弹出调试错误的窗口。
13.2 NDEBUG
当我们在确保程序没问题后,不需要进行断言时,我们可以在头文件语句前定义一个宏NDEBUG
。此时在重写编译程序时,编译器就会禁用文件中的所有assert()
语句。当遇到新问题时,我们只需要将这个宏注释掉,就能继续启用assert()
语句来检测程序的问题了。
可以看到,此时虽然指针是空指针,但是因为NDEBUG
的加入,assert()
并未启用,所以正常打印了hehe,如果我们将它注释掉,它就又会正常启用assert()
,如下所示:
13.3 assert的优缺点
对于程序猿来说,assert()
还是非常友好的:
- 它能识别并自动表示文件和出现问题的行号;
- 通过与
NDEBUG
来配合使用,就能实现开启或关闭assert()
机制;
但是因为引入了额外的检查,所以在使用assert()
时会增加程序的运行时间。
对于断言功能,一般我们是在Debug
版本中使用,这样能够帮助我们来排查程序中存在的问题;为了不影响用户使用程序的效率,我们在Release
版本中禁用assert
就可以了。对于VS这样的集成开发环境中,在Release
版本中,编译器会直接帮我们将assert
给优化掉。
结语
今天的补充知识点咱们就全部介绍完了,接下来我们将会继续对指针的内容进行深入的探讨,大家记得关注哦!
最后感谢各位的翻阅,咱们下一篇再见!!!