目录
1.前言
寒星动有芒,又是一个冬夜,我横竖睡不着,无奈翻开C语言指针的下一章。看着纸上密密麻麻的字,我的内心久久不能平息。那下一章到底写的什么呢?别急,听我娓娓道来。
2.指针运算
指针的基本运算共有三种,分别是 ①指针加减整数 ②指针减指针 ③指针的关系运算。那么他们到底有何作用和运用场景呢?下面我们就逐一地进行剖析。
2.1 指针加减整数
指针加减整数一般用在数组中。探其原因,是因为数组中的元素在在内存中是连续存放的,所以,只要我们知道第一个元素的地址,我们就可以蔓引株求地找到数组中所有的元素。
口说无凭,我们来写个代码运行看看:
#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;
int sz = sizeof(arr) / sizeof(arr[0]);
//求数组中元素的个数
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//p+i 这⾥就是指针加整数
}
return 0;
}
我们先来分析看看:
如图所示,随着数组下标的增长,每个元素对应的地址也逐步增加。所以,我们解引用每个元素 的指针就是找到该元素,之后再将其打印出来。代码中那么随着变量i的增加,地址不断增加,打印的元素的下标也不断增加。由此,代码的运行结果就是打印整个数组。
2.2 指针减指针
指针减指针,听到这个标题有没有感到讶异,两个地址竟然也能相减?相减的结果又有什么意义呢?
随便人给我两个毫不相干的指针相减当然没有意义。倘若是同一个对象的两个指针,那相减的意义可就大着呢。以字符串为例,我们将它一头一尾的两个指针相减,得到的就是字符串的元素个数。这就好比是两个日期相减,得到的自然是他们之间的天数。
我们来看段代码:
#include <stdio.h>
//模拟实现strlen函数
int my_strlen(char* s)
{//s中存放的是字符串首元素的地址
char* p = s;
while (*p != '\0')
p++;
return p - s;//此时的p存放的是'\0'的地址
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
按照这个逻辑我们最后计算的就是字符串除'\0'的元素个数:
2.3 指针关系运算
指针的关系运算,简单来说就是指针比较大小。依旧同指针减指针,只有对于同一对象的连个指针比较才有意义。
我们直接上代码:
#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;
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素个数
while (p < arr + sz) //指针的大小比较
{//arr是数组首元素的地址,再加上元素个数,就是跳过整个数组后的地址
printf("%d ", *p);
p++;
}
return 0;
}
使用while循环语句,只要指针p小于指针arr+sz就打印指针对应的元素。这样我们就可以打印出整个数组。
怎么样,有了指针后,是不是又多了一种处理数组的手段 。
3.野指针
金无足赤,人无完人,指针也是这样。它在带给程序员方便的同时,也增加了出错的概率,一不留神就是系统崩溃。而野指针就是这样的一类,它们就像无鞘利刃,伤人伤己。
那什么是野指针呢? 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)一类指针
那么到底是什么把我们的好汉逼上梁山呢?
3.1 野指针的成因
3.1.1 指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
这里的指针p由于未初始化,编译器就不知道它里面存放的谁的地址(指针指向的位置是随机的),就会把它当成随机值。这个时候再解引用后赋值,编译器就会报错。
3.1.2 指针越界访问
#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++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = 0;
}
return 0;
}
由于数组arr的元素个数是10,而*(p+10) 是访问第十一个元素,这就属于越界访问了,显然是不正确的,所以这时的指针p也是野指针。
如果我们此时运行代码,编译器就会崩溃:
这个时候不知道各位有没有疑问 :2.3中的指针arr+sz同样越界了,为什么编译器没有报错呢?
此时的指针arr+sz的确越界了,但是我没有解引用访问它所指向的空间,当然不会报错了。这就好比我动了杀人越货的念头,但是我还没有采取行动,当然不算违法犯罪了。
3.1.3 指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;//返回n的地址
}
int main()
{
int* p = test();//创建指针p接收n的地址
printf("%d\n", *p);//解引用后打印
return 0;
}
我们知道,为了节省空间,函数调用时开辟的空间,在调用结束后会归还给内存。而我们在函数调用时创建的变量n的指针指向的空间在调用结束后就会释放。所以此时指针p指向的空间是未知的。它最终也难逃野指针的命运。
3.2如何规避野指针
简单来说,就是将问题扼杀在摇篮里——避免出现野指针,或者出现野指针时,让编译器报错。而不是等到系统崩溃时,才后知后觉。
3.2.1 指针初始化
#include <stdio.h>
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;//随手置NULL
return 0;
}
3.2.2 小心越界的指针
3.2.3 指针变量使用前检查有效性,使用后及时置NULL
如:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i;
for (i = 0; i < 10; i++)
{
*(p++) = i;
}
//此时p已经越界了
p = NULL;//将其置为NULL
p = &arr[0];//重新让p获得地址
if (p != NULL) //判断指针p是否为NULL
{
//...满足条件,继续使用
}
return 0;
}
3.2.3 不要返回局部变量的地址
局部变量在生命周期结束后会被销毁,如果返回它的地址,就很容易出现野指针,如3.1.3,增加了代码出错的几率。所以宁缺毋滥,我们就不要返回局部变量的地址。
4.assert断言
下面给大家介绍一个非常好用的东西——assert
#define NDEBUG
#include <assert.h>
int main()
{
int* p = NULL;
int a = 3;
assert(p != NULL);//此时表达式为假
printf("%d\n", a);
return 0;
}
取消NDEBUG的注释后就一切安好 :
5.传值调用和传址调用
void exchange(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);//输入:3 4
printf("交换前:a=%d b=%d\n", a, b);
exchange(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
即直接将变量本身最为参数传递,然后在函数内部创建临时变量交换两个变量。
我们来看看运行结果:
结果并不是我们预想的那样,这到底是什么原因呢?别急,我们先来调试看看:
我们发现运行到函数末尾时,形参x,y的确是交换了值,但是实参a,b却无动于衷。而同时参x,y和实参a,b的地址并不相同。这是不是意味着,我们的exchange函数只是交换形参的值而对实参没有产生任何影响呢?
void exchange(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n",a, b);
exchange(&a,&b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
这种调用函数时,将函数的地址传递给函数的方式称作:传址调用
6.总结
我们先后梳理了,指针的运算,野指针,如何规避野指针,以及最后的函数调用方式。那么指针与其他板块的深度结合又将碰撞出怎样的火花呢,我们下期再见。