【指针(2)-- 使用指针的技巧】
作者主页:lightqjx
本文专栏:C语言
目录
一、const修饰指针
1.const修饰变量
(1)常规操作
(2)特殊操作
2.const修饰指针变量
(1)const放在*号的左边
(2)const放在*号的右边
(3)const放在*号的左边和右边
二、野指针
1.野指针的形成
(1)指针未初始化
(2)指针越界
(3)指向的空间已释放
2.野指针的规避
(1)指针初始化
(2)避免指针越界
(3)指针变量不再使用时,及时置NULL
(4)避免返回局部变量的地址
三、arrert断言
1.assert的作用
2.如何取消使用assert
2.assert的缺点
一、const修饰指针
const的修饰是我们要讲的第一个使用技巧。
const关键字用于声明常量或标识不可修改的数据,一般的作用为:替代宏定义的常量,防止函数内部修改参数,区分指针常量和常量指针,保护指针指向的数据。
被const修饰的变量,是无法被修改的。若当你要修改它时,编译器就会报错。
1.const修饰变量
(1)常规操作
一般来说,变量是可以被修改的,如:
#include<stdio.h>
int main()
{
int n = 10;
n = 20;
printf("n = %d\n", n);
return 0;
}
运行结果:
n原本为10,但通过n=20让n改为了20。
若在上述代码中让const来修饰n,即:
const int n = 10;
则会出现错误:
说明这时的 n 的值是无法被修改的。
(2)特殊操作
但是如果运用指针变量来操作,就可以绕过n,通过n的地址来修改,却能够改变了。如:
#include<stdio.h>
int main()
{
const int n = 10;
int* p = &n;
*p = 20;//使用指针进行修改
printf("n = %d\n", n);
return 0;
}
运行结果:
虽然这里可以绕过n修改变量n,但是要记住:这样做并不合理。正确的做法应让指针p也无法进行修改。
2.const修饰指针变量
当const用来修饰指针变量时,需要分三种情况,每种情况都不相同。即:
const int n = 10;
int* p = &n;//无const修饰
const int* p = &n;//const放在*左边
int* const p = &n;//const放在*右边
(1)const放在*号的左边
- const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本身的内容可变。
(2)const放在*号的右边
- const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
(3)const放在*号的左边和右边
- const如果同时放在*的左边和右边,修饰的既有指针变量本身,也有指针指向的内容。保证指针指向的内容不能通过指针来改变,也保证了指针变量的内容不能修改。
当然,一般来说我们并不会这样将const同时放在*的左边和右边。因为,被这样修饰的指针其实就没有什么用处了,这样不如不创建它呢。
二、野指针
野指针就是我们要理解的第二个使用技巧
在 C 语言中,野指针是指指向无效内存地址的指针(例如指向已被释放的内存,或未经初始化的随机地址)。使用野指针会导致严重的后果,如程序崩溃、数据损坏、调试困难等问题,所以一般当我们要在使用指针时,应该要正确识别野指针以及避免使用到野指针。
简单来说,野指针指向了不可知的的位置,而这个位置又是随机的、不正确的、没有明确限制的。
1.野指针的形成
要识别野指针,就要知道它是怎么形成的?也就是也野指针的成因是什么?
一般来说,野指针的成因有三种:
(1)指针未初始化
当我们声明指针变量时,有时会忘记对指针变量赋值就去使用了它,就会产生野指针。如:
#include <stdio.h>
int main()
{
int* p;
*p = 10;//未对指针变量赋初值就使用了它
return 0;
}
这里的p就是一个野指针,在编译器中这样的程序也会报错,即:
导致了程序无法运行。
(2)指针越界
当我们用指针来访问一个数组时,有时就会出现指针的越界访问,这是也会产生野指针。因为当指针越界之后,该指针所指向的地址时不确定的,如果这个地址有我们存的数据,那么使用越界的指针进行访问时就会覆盖这个数据,造成数据丢失。
如:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int* p = arr;
for (i = 0; i <= 10; i++)
{
//循环运行了11次,在第11次时p已经越界
*p = 1;
p++;
}
return 0;
}
当p指向越界的数组地址进行操作时,这时p就是野指针,调试上述代码会出现以下错误异常:
(3)指向的空间已释放
比如当指向局部变量的指针,函数返回局部变量的地址后,该变量超出作用域,导致指针失效。如:
#include <stdio.h>
int* test()
{
int x = 10;
return &x; // 返回后,x的内存被回收
}
int main()
{
int* p = test(); // p为野指针
return 0;
}
p接收到了一个已释放的内存的空间地址,这是p就是野指针。
2.野指针的规避
有产生野指针的形成,当然也有规避野指针的方法,这种方法一般有4种。
(1)指针初始化
我们知道指针未初始化会产生野指针,所以每当我们声明一个指针变量时,就应该将它进行赋值。
如果知道它指向哪里,就直接对它赋值这个地址;
如果不知道它指向哪里,那么就对它赋值空指针(NULL)。
NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
在C语言的定义中:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
指针初始化:
#include <stdio.h>
int main()
{
int* p = NULL;
*p = 10;
return 0;
}
(2)避免指针越界
之前我们知道,当我们访问数组时,如果指针指向越界的数组,就会产生野指针。所以我们需要注意避免这种情况。
当我们在空间中申请的哪些空间,那么通过指针也就只能访问哪些空间,不能超出范围进行访问,超出了就是越界访问。
(3)指针变量不再使用时,及时置NULL
我们知道只要是NULL指针就不去访问。
所以当指针变量指向一块空间区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。
如果我们后续再使用这个指针变量时,就需要在使用指针之前可以判断指针是否为NULL。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int* p = arr;
for (i = 0; i < 10; i++)
{
*p = 1;
p++;
}
//跳出循环时,p就已经越界,可以把p置为空指针(NULL)
p = NULL;
//在下次如果要使用p时,需要判断p是否为NULL
p = &arr[0];//让p重新获得地址
if (p != NULL)//判断
{
//判断到p不为空(NULL),可以使用p
*p = 2;
}
return 0;
}
(4)避免返回局部变量的地址
在上述理解野指针形成时,我们知道指针指向空间已经释放的地址时,就是野指针。
所以我们在使用函数传递返回值时,需要注意一般不要将返回值设做地址(即不要返回地址)。
三、assert断言
assert的断言使用便是我们要理解的第三个使用技巧
assert()是头文件assert.h中定义的一个宏。这个宏常常被称为 “ 断言 ”。 它1使用方法如下:
assert( 表达式 );
1.assert的作用
当assert的括号中的表达式中的结果为真(返回值非零)时,则这条代码啥也不干,程序继续正常执行。
若assert的括号中的表达式中的结果为假(返回值为零)时,则程序立即终止,并显示出错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号,帮助我们快速定位问题。
如:
#include <stdio.h>
#include <assert.h>
void test(int* p)
{
assert(p!= NULL);//表达式为假,程序终止
}
int main()
{
int n = 10;
int* p = NULL;
test(p);//传递空指针NULL
printf("%d\n", n);
return 0;
}
运行结果:
因此,我们在判断空指针时,就可以使用它来代替 if(p != NULL) 的条件判断语句:
if (p != NULL)
{
//......
}
不仅仅只局限与指针,当然还可以判断许许多多的表达式。
2.如何取消使用assert
在有一个程序中,若存在 “ assert(表达式) ” 的语句,当你已经确定在该语句中的表达式没有问题,已经不需要再做断言时,这是就将要屏蔽它了。
你可以对它进行注释的方法来屏蔽,也可以通过删除它的方法来屏蔽它。
但是,如果在该程序中的assert语句较多时,就不能用注释或删除的方法来屏蔽它了。这就需要另一种方法:在 “ #include<assert.h> ” 的前面 定义一个宏NDEBUG 就可以了。即:
#define NDEBUG //该宏要在“#include <assert.h>”之前定义
#include <assert.h>
这样就可以让该程序中的所有assert(表达式) 无效了。
若程序又出现了问题,则可以再将 “ #define NDEBUG ” 注释或删除,这样就可以重启程序中的所有 “ assert(表达式) ” 语句了。
2.assert的缺点
知道了assert的使用以及屏蔽操作,当然也必须要知道它的缺点:
- 当我们在运用assert()时,就引入的额外的检查,增加了程序的运行时间。
- 一般我们只能在Debug版本中使用,而不能在Release版本中使用。比如,在VS编译器环境中,在 Release 版本中,assert直接就是被优化掉了,所以无法使用。
- 在次感谢各位读者的欣赏!