深入理解指针(二)
前言:
一、const修饰指针
1.const修饰变量
在C语言中,const修饰的变量是为常变量,常变量具有常属性,所以不能随意改变值。
c
#include<stdio.h>
int main()
{
const int a = 10;//这时候表示a的本质是不希望被改变的
//这时候a被const修饰,a变成了常变量,但是本质还是变量,所以不能改变。
a = 1;
printf("%d\n", a);
int*p = &a;
*p = 20;
printf("%d ", a);
//通过解引用操作改变了赋值
在C++中:const int n = 0;这里的n是常量,不是常变量。
return 0;
}
所以我们可以用解引用操作去修改常变量的值
2.const修饰的指针变量
const可以放在*左边和右边,都是可以去限制p的变化
c
//左边:
const int* p = &a;
int const* p = &a;
//这两个是一样
c
//右边:
int* const p = &a;
c
#include<stdio.h>
int main()
{
int a = 100;
int* pa = &a;//pa是一个指针变量,里面存入的是地址。
*pa = 0;//*pa是pa的指向对象(a)
printf("%d\n", a);
printf("%d\n", *pa);
return 0;
}

下面是我对于它们的理解:
代码示例:
理解1:在左边时,const限制的是*p,即指向的内容无法改变,但是通过改变指针变量本身内容去进行改变
c
#include<stdio.h>
int main()
{
int a = 100;
int b = 1000;
const int* p = &a;
//*p = 0;//err
p = &b;//ok
printf("%d",*p);
return 0;
}
理解2:在右边时,修饰的是指针变量本身p,所以指针变量本身不能被修改,指针指向的内容是可以根据指针来进行改变
注意:此时一定要对指针变量进行初始化
c
#include<stdio.h>
int main()
{
int a = 100;
int b = 1000;
int* const p = &a;
*p = 0;//ok
//p = &b;//err
printf("%d\n", a);
return 0;
}

二、野指针
野指针的概念:野指针就是指针所指向的位置是随机的(没有明确的限制)。
1.野指针的成因
(1).指针的未初始化
c
#include<stdio.h>
int main()
{
int* p;//局部变量不初始化的时候,里面存放的是随机值
*p = 20;//这时候属于非法访问。
return 0;
}
(2).指针的越界访问
c
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i <= sz; i++)
{
//因为这个循环本身能循环11次,当循环第11次的时候
//p指向了数组最后一个元素后面的空间,这时候就存在非法访问的问题
*p = i;//*p是指向的对象,即i;
p++;//p本身在移动,这时候的p是指针变量的本身
//*(p++) = i;也可以这么去写。
}
return 0;
}

便会出现如上的这种错误。
(3).指针的空间释放
在这同时p既得到了地址,又是野指针
c
#include<stdio.h>
int* test()
{
int n = 100;//n所占的是4个字节,但是因为它是局部变量,所以出了作用域会销毁。
//所以也表明它的空间也会还给操作系统
return &n;
}
int main()
{
int* p = test();//但是它还给了操作系统的同时,指针变量的p也接收到了n的地址。
printf("%d\n", *p);//这时候虽然会打印出结果,但是会形成了非法访问内存的结果。
return 0;
}
2.如何规避野指针
(1).指针初始化
NULL为空指针
如果指针有明确的指向,那就直接赋给明确的地址。
c
int a = 10;
int* p = &a;
如果指针变量,当前还不知道该指向哪里,这个时候应初始化为NULL;
当然这个时候也是无法进行使用的,比如通过解引用。
c
#include<stdio.h>
int main()
{
int* p = NULL;//这个时候就是NULL为空指针
//*p = 200;//想要使用它,就必须绕过NULL,即当它不等于NULL时候可以去使用,否则会非法访问内存。
if (p != NULL)
{
*p = 200;
}
return 0;
}

(2).小心访问越界
一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
防止指针在使用的时候访问而超过原有值的边界。
(3)指针不再使用的时候,及时置NULL,指针使用之前检查有效性
如果NULL指针就可以不去访问,并且在用指针时还可以起到判断指针是否为NULL
c
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
for (i = 0; i < 10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使用p的时候,判断p不为NULL的时候再使用
//...
p = &arr[0];//重新让p获得地址
if (p != NULL) //判断
{
//...
}
return 0;
}
理解: 指针在用完这块区域的时候,后期我们不用这块区域(空间)的时候,我们可以把指针置为NULL。
在下次还要使用这个指针,再把地址赋值后,需要判断这个指针还是不是NULL类型即可。
(4)避免返回局部变量的地址(函数)(栈空间)
c
#include<stdio.h>
int* test()
{
int n = 100;
return &n;//局部变量的地址,容易出现野指针
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
三、assert断言
作用:判断指针是否为空
要想使用assert必须要使用assert头文件
#defeine NDEBUG是可以在assert头文件前使用,可以起到关闭assert的作用。
当已经确认了程序已经没有了问题,便可以用这个宏定义
想要开启时,把这个宏定义注释掉即可
c
//#define NDEBUG
#include<assert.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
//int* p = arr;
int* p = NULL;
assert(p != NULL);
//验证指针变量是否为NULL,若不是,则为程序继续运行,
//若是,则会终止运行,并且给出信息错误的提示
for (int i = 0; i < 5; i++)
{
printf("%d ", *(p + i));
}
return 0;
}

c
#include<stdio.h>
#include<assert.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* p = arr;
//int* p = NULL;
assert(p != NULL);
//验证指针变量是否为NULL,若不是,则为程序继续运行,
//若是,则会终止运行,并且给出信息错误的提示
for (int i = 0; i < 5; i++)
{
printf("%d ", *(p + i));
}
return 0;
}

assert()的使用是对于程序员是有很大的好处的
好处:能够精确快速发现错误,并找到问题所在位置。可以用于测试和调试,并且可以增强代码的维护性。
劣势:
在Debug版本中assert能帮程序员在代码运行时检查错误,发现代码里的问题,方便修改和优化。但这些功能会让程序运行速度变慢,占用更多资源。
Release版本是面向用户的最终版本,关闭了调试功能,把 assert 优化掉了。它更注重程序运行效率和性能,运行速度更快,占用资源更少。
四、指针的使用和传址调用
1.strlen的模拟实现
上一节我们已经写过了strlen函数的模拟实现,现在用这节知识优化一下
代码如下:
c
#include<stdio.h>
#include<assert.h>
int my_strlen(const char* s)//const防止被修改
{
int count = 0;
assert(s != NULL);//防止被为空指针
while (*s != '\0')
{
count++;
s++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t c = my_strlen(arr);
printf("%zd\n", c);
return 0;
}
c
#include<stdio.h>
#include<assert.h>
int my_strlen(const char* str)
{
const char* start = str;
assert(str != NULL);
while(*str)
{
str++;
}
return str - start;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d ", len);
return 0;
}

2.指针的传址调用与传值调用
这应该是一份交换两个变量的值的函数:
c
#include<stdio.h>
int Swap(int x, int y)
{
int temp = 0;
temp = x;
x = y;
y = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}

但是结果令人出乎意料:打印完的值是没有交换。
通过调试我们可以看到:
Swap函数中x,y接收了a,b的值,两组的内存地址也分别不同,那么就也说明了两组是不同的且独立的空间,所以在Swap函数中交换x和y的值,再回到main函数中,那么不会对main函数的a和b的值会造成影响。
这种是把变量的本身传递给函数,叫做传值调用
原因:实参传递给形参时,形参是实参的一份临时拷贝,因为形参会再创造一个独立的空间,所以对于形参的修改不会影响到实参。
既然这么做不能交换,那么我们把a和b的地址传递给这个函数,去试一试。
c
#include<stdio.h>
int Swap2(int* px, int* py)
{
//*py等于b;*px等于a
int temp = 0;
temp = *px;
*px = *py;
*py = temp;
}
int main()
{
int a = 0;
int b = 0;
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;
}

这时候我们再通过调试看一看:
这时候把地址传递给了函数,变量储存的是地址,我们可以去进行交换了。
这种把地址传递给函数,再去调用的方式叫做传址调用。
传址调用是把函数与主调函数构成真正的联系,在函数内部可以修改主调函数的变量。
一般来说只是去调用变量的值去进行计算的这种用传值调用;
需要改变主调函数中的变量等,这时候需要传址调用。