一、野指针
话说,指针有很多种,一般指针指向的对象都是已经确定好的
but!有没有例外呢?
指针未初始化
假设我们给出这段代码
int main()
{
int *pa;
*pa = 10;
return 0;
}
你会发现我的指针变量pa并没有初始化
这个时候我再给*pa赋值,那我究竟给谁赋了10?
对此我们不清楚
因此定义:指针指向的位置不可知,且其指向的空间不属于当前这个程序,即非法访问
究竟是为什么会导致这样,除了我们之前的未初始化,还有哪些其他原因呢?
指针的越界访问
我们拿超过数组范围的情况举例
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };//是个元素,对应下标9
int sz = sizeof(arr) / sizeof(arr[0]);
int* pa = arr;
for (int i = 0; i <= sz; i++)
{
printf("%d ", *(pa + i));
}
return 0;
}
我们看到,前面几个都能按照要求打印数组对应的元素
但是最后一个发生了非法访问
当循环进行到最后时,对应数组下标为10
此时不再属于数组范围,发生越界非法访问
这个时候的我们的*(pa+i)就是一个野指针
指针指向的空间被释放
我们举个例子
int* print_arr()
{
int temp = 10;
return &temp;
}
int main()
{
int *pa = print_arr();
printf("%d", *pa);
return 0;
}
这个例子你看到就会说
哎呀函数中指针变量销毁了不还是能得到返回值吗
但是假设你再加一条语句,你就会发现为什么加了一条语句之后变了
说明你指针的返回值的函数栈帧被其他值占用了
这就说明了即使你这个指针指向的是正确的地址,但是这个地址在函数调用结束后就还给内存了
这个地址对应的值可能发生了变化,就好比如同一间教室,每隔一段时间都有不同的同学上课
int* print_arr()
{
int temp = 10;
return &temp;
}
int main()
{
int *pa = print_arr();
printf("%d ", 10);//加在这
printf("%d", *pa);
return 0;
}
如何规避野指针
指针要初始化(明确初始化成确定值)
假如不知道给什么值,就直接初始化成空指针→int *p =NULL
这样做的目的就是避免指针乱指向,空指针相当于给野指针下了紧箍咒
谨防指针越界
像之前的数组越界就是典型例子
指针变量不再使用的时候及时安置为空指针
在每次使用指针之前
我们可以使用if或者是下面要讲的assert断言来判断指针有效性(if(p==NULL))
避免使用局部变量的地址(栈空间)返回
就比如上面的打印数字10
二、Assert断言
这是一个宏,功能是在运行时判断是否符合条件,如果不符合就报错并终止程序
报错会显示文件位置,错误的代码行号等
头文件<assert.h>,是防御性编程的一种
int main()
{
int* pa = NULL;
assert(pa != NULL);
printf("不是空指针");
return 0;
}
我们可以发现,运行程序后编译器报警告
而且在cmd命令提示符中我们可以看到出错的文件位置,文件的行数,出错原因,还是很实用的
这个跟if语句有点像,跟if语句类似,条件为真即满足时,不执行assert断言
反之为假即不满足条件时,执行assert断言,但是你if语句能报错吗?很显然不行
但是如果你文章中有很多assert断言,一个个开关是不是太麻烦了,我们可以用一个“总闸”
#define NDEGUG
但是assert断言就没有缺点了吗?
肯定有,你每一次都要判断,是不是增加了程序的运行时间呀
三、指针使用与传指调用
模拟实现strlen函数效果
我们就像可不可以利用指针来传达数组首元素的地址
并在“\0”之前返回,达到统计数目的目的
int my_strlen(char *str)
{
size_t count = 0;
while (*str != 0)
{
count++;
str++;//以此寻找下一个元素
}
return count;
}
int main()
{
char arr[] = "helloworld";
size_t len = my_strlen(arr);
printf("%zu", len);
return 0;
}
我们可以看到结果符合我们的预期,但是这样代码是不是不太安全
假设我在函数中把数组元素修改了呢,又或者传过来的指针是NULL情况?
结合我们之前知识,我们可以使用assert断言和判断指针合法性
int my_strlen(const char *str)//const在前说明是对指针指向的对象arr数组的限定
{
size_t count = 0;
assert(str != NULL);//以防野指针情况出现
while (*str != 0)
{
count++;
str++;//以此寻找下一个元素
}
return count;
}
int main()
{
char arr[] = "helloworld";
size_t len = my_strlen(arr);
printf("%zu", len);
return 0;
}
如何甄别传指调用和传值调用
假设我们要交换两个变量中的值,你可能会这么写代码
void swap(int x, int y)
{
int temp = 0;
temp = x;
x = y;
y = temp;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
printf("交换后:%d %d", a, b);
return 0;
}
为什么没有交换,还记得函数传参吗
你把a和b的值传过去,形参x,y完成接收,然后对x和y进行交换
但是,函数调用结束后形参x,y销毁了,你a和b交换了吗
没有
换句话说,因为形参是实参的一份临时拷贝,它们在内存中的位置(也就是地址)不同
交换了形参并不会影响实参,我们借助调试观察,不难发现确实这样
那我们要怎么写才能实现交换效果呢?
聪明的你一定想到了,利用指针即可,这样我们形参与实参之间就可以建立起联系
void swap(int *x, int *y)
{
int temp = 0;
temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int a = 10;
int b = 20;
int* pa = &a;
int* pb = &b;
swap(pa, pb);
printf("交换后:%d %d", a, b);
return 0;
}
四、数组名进一步理解
都说数组是首元素地址,确实是这样,但是有例外:
sizeof(arr)其计算的就是数组的大小,单位是字节,这个不用过多介绍
&arr其取出的地址位整个数组的地址,虽然和首元素地址相同
但是如果进行位置偏移,效果就会不同,我们举个例子
int main()
{
int arr[] = { 0,1,2,3,4,5,6,7,8,9,10 };
printf("&arr= %p\n", &arr);
printf("arr+1= %p\n", arr + 1);
printf("&arr[0]+1=%p\n", &arr[0] + 1);
printf("&arr+1= %p\n", &arr + 1);
return 0;
}
我们发现对于&arr+1,加的是整个数组的地址,相当于跳过了整个数组
导致其直接到数组末尾元素地址之后了,因此指针类型决定了你的下个元素地址跳跃跨度
五、使用指针来访问数组
int main()
{
int arr[5] = { 0 };//这里我们默认大小输入五个数
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
scanf("%d", p);//为什么scanf不用写*,因为scanf后面参数默认就是跟地址参数,输入时自动忽略空格
p++;
}
p = arr;//如果不及时初始化指针位置,会产生越界现象
for (int i = 0; i < sz; i++)
{
printf("%d ", *p);
p++;
}
return 0;
}
当然还有其他写法,比如这样,更多的写法就不列举了
int main()
{
int arr[5] = { 0 };//这里我们默认大小输入五个数
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
scanf("%d", p+i);//为什么scanf不用写*,因为scanf后面参数默认就是跟地址参数,输入时自动忽略空格
}
p = arr;//如果不及时初始化指针位置,会产生越界现象
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
总之,使用指针访问数组,编译器会自动把arr[i]→→*(arr+i)
六、一维数组传参本质
本质就是地址(指针),传参是数组名表示首元素地址,要使用指针来接收
因此在函数中也可以用arr[i]访问数组所有元素,编译器自动转化成*(arr+i)
注意:计算数组元素只能在主函数中进行,在函数中进行传参传的是首元素地址,无法计算
七、冒泡排序
其意思为我们对两两相邻的元素进行比较,从而完成排序
比如我们要对一组有序的逆序数进行正序排序:
9 8 7 6 5 4 3 2 1 0 →→0 1 2 3 4 5 6 7 8 9
理解了原理,则我们给出代码
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
//这只是一次冒泡排序,我们继续需要在这一次冒泡排序中对数组元素进行排序
int j = 0;
for (j = 0; j < sz - 1 - i; j++)//在一次冒泡排序中,每一次排序后需要排序的元素就少一个,以此类推
{
//这是在一次冒泡排序中的内部元素的比较
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
void print_arr(int arr[],int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr [] = {10,9,8,7,6,5,4,3,2,1};
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
print_arr(arr,sz);
return 0;
}
但是代码可不可以再优化下,就是假设我有一个部分倒序的数组:
9 8 7 6 0 1 2 3 4 5 →→0 1 2 3 4 5 6 7 8 9
那我后面那些有序的部分还要排吗?显然不用啊
因此我们采用标记的形式,假设我们默认原数组有序,假设发生了数组元素交换
说明它这个部分不是有序的,那我们进行这一次的冒泡排序
假设这一次冒泡排序标记都未发生变化
说明是有序的,后面也是有序的(针对我上面那种情况,一般不会这么判断)
那我还要接着判断剩下的吗,显然不用,因此我们给出优化代码
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1;//假设是有序数组
//这只是一次冒泡排序,我们继续需要在这一次冒泡排序中对数组元素进行排序
int j = 0;
for (j = 0; j < sz - 1 - i; j++)//在一次冒泡排序中,每一次排序后需要排序的元素就少一个,以此类推
{
//这是在一次冒泡排序中的内部元素的比较
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = 0;//有交换就将标记改为0
}
}
if (flag == 1)
{
break;//假设未发生交换,后面的也不用判断了(假设有序数组的极端情况)
}
}
}
void print_arr(int arr[],int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr [] = {10,9,8,7,6,5,4,3,2,1};
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
print_arr(arr,sz);
return 0;
}
作者基础有限,难免有错,欢迎指正,我们友好交流
END