C语言指针(中)

目录

1.前言

2.指针运算 

2.1 指针加减整数

 2.2 指针减指针

2.3 指针关系运算 

3.野指针

3.1 野指针的成因

3.1.1 指针未初始化

3.1.2 指针越界访问 

3.1.3 指针指向的空间释放

3.2如何规避野指针

3.2.1 指针初始化

3.2.2 小心越界的指针

3.2.3 指针变量使用前检查有效性,使用后及时置NULL

3.2.3  不要返回局部变量的地址

4.assert断言

5.传值调用和传址调用

6.总结 


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 指针初始化
  如果明确知道指针指向哪里,就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL。 NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,但这个地址是无法使用的,读写该地址会报错
示例:
#include <stdio.h>
int main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;//随手置NULL
 
 return 0;
}
3.2.2 小心越界的指针
  ⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间。指针越界了,就别再访问了。否则就会那啥,你懂得。
3.2.3 指针变量使用前检查有效性,使用后及时置NULL
  当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为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

  assert.h 头文件定义了宏 assert,用于在运行时确保程序符合指定条件,如果不符合,就报
错终止运行。这个宏常常被称为“断⾔”。
assert接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert不会产⽣ 任何作用,程序继续运行。如果该表达式为假(返回值为零), assert就会报错,在标准错误 流 stderr 中写入⼀条错误信息,显示没有通过的表达式,以及这个表达式的文件名和行号。
  assert很像一个守卫,当满足它的条件时才能通过,否则将会被它拦下,并将信息上报。出现特殊情况时,我们只需要下达“放行”的指令即可。
  assert 的使用对程序员是非常友好的,使用 assert 有几个好处:
①能自动标识文件和出问题的行号,便于bug的修改。
②有⼀种无需更改代码就能开启或关闭 assert的机制。如果已经确认程序没有问题,不需要再做断⾔了,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG NDEBUG 好似assert的开关,被定义时,编译器会禁用掉所有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函数只是交换形参的值而对实参没有产生任何影响呢?

 确实如此,在调用exchange函数时,将a和b传递给了exchange函数,在exchange函数内部创建了形参x和y接收a与b的值,但是x的地址和a的地址不⼀样,y的地址和b的地址也不相同,相当于x和y是独立的空间,那么在exchange函数内部交换x和y的值,自然不会影响a和b,当exchange函数调用结束后回到main函数,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.总结 

 我们先后梳理了,指针的运算,野指针,如何规避野指针,以及最后的函数调用方式。那么指针与其他板块的深度结合又将碰撞出怎样的火花呢,我们下期再见。

 

  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值