C语言的灵魂——指针(2)

前言:上期我们介绍了如何理解地址,内存,以及指针的一些基础知识和运算;这期我们来介绍一下const修饰指针,野指针,assert断言,指针的传址调用。
上一篇指针(1)

一,const修饰指针

1,const修饰变量

我们先从const修饰变量说起,在学函数的时候我们知道被const修饰后的变量在C语言中变成了常变量,它是一个变量但具有常量属性(不变的属性)是不能被修改的。

#include<stdio.h>
int main()
{
    
    
	int n=0;
	n=100;//没有加上const之前n可以修改
	const int m=0;
	m=100;//加上了const之后m就不能被修改了
	return 0;
}

在这里插入图片描述
我们可以看到被const修饰后的m修改时会报错,那 我们怎么验证它是一个变量呢?还记得在数组篇说过:数组定义变量的大小时必须是一个常量而不能是一个变量吗?这样我们就可以用数组来检验,如果数组报错说明是变量,不报错说明是常量。

#include<stdio.h>
int main()
{
    
    
	const int n=10;
	int arr[n]={
    
    0};
	return 0;
}

在这里插入图片描述
我们们看到编译器报错就说明被const修饰的n就是一个变量。那么既然const修饰变量会导致变量不能被修改,那么const修饰指针会有什么样的结果呢?接下来就来看看const修饰指针会有什么样的效果。

延用上面的例子分析一下为什么m不能被修改?假如想修改该怎么做呢?

#include<stdio.h>
int main()
{
    
    
	int n=0;
	n=100;//没有加上const之前n可以修改
	const int m=0;
	m=100;//加上了const之后m就不能被修改了
	return 0;
}

上述代码中m是不能被修改的,其实m本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对m就⾏修改,就不符合语法规则,就报错致使没法直接修改n。

我们说指针的好处就是可以间接访问内存,但如果我们绕过m取得m的地址然后再去修改m就可以做到了,虽然这是在打破语法规则但确实能够达到我们的目的。

#include<stdio.h>
int main()
{
    
    
	const int m=0;
	m=100;//加上了const之后m就不能被修改了
	int *p=&m;
	*p=100;
	printf("%d\n",*p);
	return 0;
}

在这里插入图片描述
显然m被修改了,但是思考一下我们加const的用意是什么?我们其实是要固定m的值使他不能被修改,但是我们却有方法让他修改,这就打破了const的限制所以这与我们的初衷是违背的。这是一个漏洞要避免这个漏洞要怎么做呢?
答案是用const来修饰指针,使指针变量p拿不到m的地址从而无法间接修改m的值。

2,const修饰指针变量

首先const修饰指针变量有三种情况:

int * p;//没有const修饰 
int const * p;//const 放在*的左边做修饰 
int * const p;//const 放在*的右边做修饰
const int *const p;//const 放在*左右两边的修饰

第一种const在*左边

我们先来看一段代码:

#include<stdio.h>
int main()
{
    
    
	int n=0;
	int *p1=&n;
	*p1=100;
	printf("%d\n",*p1);//没加const 打印的结果是100
	
	int m=0;
	const int *p2=&m;
	*p2=100printf("%d\n",*p2);//加了const *p2这行代码报错
	return 0;
}

在这里插入图片描述

由此我们可以知道在 *号 左边加上const是限制解引用这个操作,即限制修改指针变量p所指向的变量的内容(即 *p ),但能否修改指针变量本身( p )来改变p所储存的地址从而再解引用改变所想要改变的值呢?我们不妨写个代码来验证一下:

#include<stdio.h>
int main()
{
    
    
	int n = 0;
	const int* p = &n;
	int m = 100;//创建第三变量
	int* x = &n;//要想改变n的值只能再重新创建一个指针变量
	p = &m;
	
	//*p = 100;//指针变量p在*左边加了const 所以解引用已经被禁用
	*x = 10;
	printf("*p=m=%d\n", *p);
	printf("*x=n=%d\n", *x);
	printf("n=%d\n", n);
	return 0;
}

在这里插入图片描述
从运行结果我们可以得出3个结论: 1,要想改变*p 必须借助第三变量m才能改变,但是是不会改变n的值的!2,指针变量p被const修饰后要想改变n的值只能通过再创建一个指针变量来改变!3,指针变量被const修饰后,*p 是无论如何都不能使用了,即使是改变了p也依然不能解引用!

第二种const在*右边

来看一段代码
还是上面的代码我们改一下

#include<stdio.h>
int main()
{
    
    
	int n = 0;
	int* const p = &n;//在*右边加const
	//p = &m;//指针变量p在*右边加了const p所存的地址就已经固定不能更改了
	*p = 100;
	printf("*p=%d\n", *p);
	printf("n=%d\n", n);
	return 0;
}

在这里插入图片描述

从运行结果上来看我们可以知道在*右边加上const 是限制了指针变量p,*p 是可以使用的。上面的代码中由于 *p 可以使用所以改变*p 就相当于改变了n。

第三种const在*左和右边

显然我们知道const放在*的左右两边会导致*p 和p都无法使用,下面来看代码:

#include<stdio.h>
int main() 
{
    
     
	int n = 10; 
	int m = 20; 
	int const * const p = &n; 
	*p = 20; //ok? no
	p = &m; //ok? no
}

将代码复制到vs里就会发现报错,结果当然与我们猜想的一样。下面给出一个图来进行总结:
在这里插入图片描述

结论:const修饰指针变量的时候
• const如果放在的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
• const如果放在
的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。

二,野指针

我们先来了解一下它的概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

1,野指针的成因

野指针有几种成因分别是:

  1. 指针未初始化
    int *p; *p=20 *p没有指向的对象所以默认为随机值
  2. 指针访问越界
#include <stdio.h> 
int main() 
{
    
     
	int arr[10] = {
    
    0}; 
	int *p = &arr[0]; 
	int i = 0; 
	for(i = 0; i <= 11; i++) 
	{
    
     
		//当指针指向的范围超出数组arr的范围时,p就是野指针 
		*(p++) = i; 
	}
	return 0; 
} 
  1. 指针指向的空间被回收
#include <stdio.h> 
int* test() 
{
    
     
	int n = 100; 
	return &n; 
}
int main() 
{
    
     
	int*p = test(); 
	printf("%d\n", *p); 
	return 0; 
}

由于n是一个局部变量,在函数调用完后就已经被回收了 (还给操作系统了),所以返回n的地址是一个随机值。
指针指向的内存空间不属于当前程序,这个时候就是野指针。

2,如何规避野指针

了解了野指针的成因后我们自然有办法去规避它。

1. 及时初始化
如果我们不明确指针指向的对象就及时给指针初始化:int *p=NULL

  1. NULL 是C语言中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址
    会报错。

如果我们明确指针所指向的对象就要及时给指针初始化:int n=0; int *p=&n;
2.小心指针越界
以上面的代码来举例:

#include <stdio.h> 
int main() 
{
    
     
   int arr[10] = {
    
    0}; 
   int *p = &arr[0]; 
   int i = 0; 
   for(i = 0; i <= 10; i++) //i必须小于等于10防止指针越界
   {
    
     
   	*(p++) = i; 
   }
   //程序走到这指针就越界了要即使置为空指针
   *p=NULL;
   int m=0;
   *p=&m;
   //下次使用该指针的时候再进行判断
   if(*p!=NULL)
   {
    
    
   	
   }
   return 0; 
} 

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。

为了更加深入的理解我们举一个例子:

我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来, 就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起来。
不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我 们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去 使⽤。

3. 不要返回局部变量的地址
当局部变量的作用域与该指针的作用域不同时,给指针返回局部变量的地址就相当于没有初始化指针变成了野指针。

三,assert断言

assert.h 头文件定义了宏 assert() ,用于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏。这个宏常常被称为“断言”。

举个例子我们运行下面的代码看看会有什么结果:

#include<stdio.h>
#include<assert.h>//assert.h 头文件定义了宏 assert() 所以要包含assert.h
int main()
{
    
    
	int n=0;
	int *p=NULL;
	assert(p!=NULL);
}

在这里插入图片描述

上⾯代码在程序运行到这一行语句 assert(p!=NULL) 时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序 继续运行,否则就会终止运行,并且给出报错信息提示。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣ 任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误 流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的文件名和行号。

当然assert不仅仅能用来判断指针,还可以判断非指针的问题,上代码:

#include<stdio.h>
#include<assert.h>
int main()
{
    
    
	int a=10;
	scanf("%d",&a);
	assert(a==10);
}

我们输入15看看有什么结果:
在这里插入图片描述
我们看到编译器直接报错,其原因是assert括号内表达式的值为假返回0所以编译器直接报错,那如果我们输入10呢?
在这里插入图片描述
我们可以看到输入10编译器就不会报错,因为assert括号内表达式值为真返回非0所以不报错。

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:

1. 它不仅能⾃动标识文件和 出问题的行号

*2.还有⼀种无需更改代码就能开启或关闭 assert() 的机制

该机制是如果已经确认程序没有问 题,不需要再做断,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。 例如(以上面的代码来举例):

#define NDEBUG
#include<stdio.h>
#include<assert.h>
int main()
{
    
    
	int a=10;
	scanf("%d",&a);
	assert(a==10);
}

我们刚刚输入15编译器会报错,现在我们在 #include<assert.h> 语句前面加了 #define NDEBUG 这句话后输入15看看会不会报错:
在这里插入图片描述
发现没有报错,但前提是要在 #include<assert.h> 这句话前 加上 #define NDEBUG 这句话才行!

然后,重新编译程序,编译器就会禁用文件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。

这么好用的assert当然也有缺点:

> assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。 ⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题, 在 Release 版本不影响⽤⼾使⽤时程序的效率。

介绍完了assert我们就来看看指针的使用和传址调用

四,指针的使用和传址调用

我们学习指针就是为了使用指针来解决问题,但有什么问题是非指针不可得呢?
举个例子,写一个函数完成两个数得交换(我们先用函数的传值调用)看看能不能实现:

1,传值调用

#include<stdio.h>
void swap1(int a,int b)
{
    
    
	int z=0;
	z=a;
	a=b;
	b=z;
}
int main()
{
    
    
	int a=10;
	int b=20;
	printf("交换前a=%d b=%d\n",a,b);
	swap1(a,b);
	printf("交换后a=%d b=%d\n",a,b);
	return 0;
}

在这里插入图片描述

我们看到并没有交换,因为这是传值调用形参只是实参的一份零时拷贝,形参的改变不影响实参。所以Swap1是失败的了。


如果对传值调用还不理解也可以看看我在函数篇讲的形参和实参就知道了
传送门:函数(上)


那怎么办呢?

我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接 将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap 函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

2,传址调用

还是上面的代码我们修改一下:

#include<stdio.h>
void swap2(int *pa,int *pb)
{
    
    
	int z=0;
	z=*pa;//z=a
	*pa=*pb;//a=b
	*pb=z;//b=z
}
int main()
{
    
    
	int a=10;
	int b=20;
	int *pa=&a;
	int *pb=&b;
	printf("交换前a=%d b=%d\n",a,b);
	swap2(&a,&b);
	printf("交换后a=%d b=%d\n",a,b);
	return 0;
}

在这里插入图片描述

在这里插入图片描述

我们将a和b的地址传给形参pa和pb这样形参和实参就共用一块内存空间,所以形参的改变会影响实参。这就是传址调用!

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改 主调函数中的变量的值,就需要传址调用。

五,strlen的模拟实现

学完了const和assert断言后我们对上次模拟strlen的代码进行修改:

1. 加上const修饰
2. 加上assert断言

#include <assert.h>
size_t my_strlen(const char* p)//在*左边加const防止arr内容被修改
{
    
    
	size_t count = 0;
	assert(p != NULL);//加上assert断言避免传入的是空指针!
	while (*p)
	{
    
    
		count++;//计数器
		p++;
	}
	return count;
}

int main()
{
    
    
	char arr[] = "abcdef";
	//a b c d e f \0
	size_t len = my_strlen(NULL);
	printf("%zd\n", len);
	return 0;
}

好了以上就是本章的全部内容啦!
感谢能够看到这里的读者,如果我的文章能够帮到你那我甚是荣幸,文章有任何问题都欢迎指出!制作不易还望给一个免费的三连,你们的支持就是我最大的动力!

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/2403_86779341/article/details/145469025
今日推荐