深入理解指针(2)

一、const修饰指针

1.const修饰变量

变量是可以被修改的,若把变量的地址赋值给指针变量,通过指针变量也可以修改变量的值。那有没有一种方法能限制修改变量的值?这就是const的作用了,const是常属性,是不能改变的属性。

代码1:

#include <stdio.h>
int main()
{
	int m = 10;
	m = 20;//变量m的值可以被修改

	const int n = 30;
	n = 0;//变量n的值不能被修改,会报错误
	return 0;
}

在这里插入图片描述
上述代码中,其实n本质上还是变量,只不过被const修饰后,在语法上加了限制,下面的例子可以说明:在数组那一节中,提到了变长数组的概念,在VS2022上是不支持变长数组语法的,下图所示
在这里插入图片描述
但是在C++中,const int n;这里的n就是常量。

既然变量n的值不能直接被改变,那我们绕过n,通过指针变量的解引用能不能改变呢?如下列代码所示,n是可以被改变的:

#include <stdio.h>
int main()
{
	const int n = 10;
	printf("n = %d\n", n);
	int* p = &n;
	*p = 0;
	printf("n = %d\n", n);
	return 0;
}

在这里插入图片描述

但是既然使用了const修饰变量nn的值肯定是不能被改变的,使用指针变量p改变n的值是在打破语法规则,那有没有方法能限制指针变量的使用呢?

2.const修饰指针变量

结论:

  1. 当const在*的左边时,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可以改变。
  2. 当const在*的右边时,修饰的是指针变量本身,保证了指针变量的内容不能改变,但是指针变量指向的内容可以通过指针改变。

以下两个const表示的意思都一样,都是在*的左边,限制的是*p的改变:

const int* p = &a;
int const * p = &a;

const*的右边,限制的是p的改变:

int* const p = &a;

分析具体代码:

#include <stdio.h>

void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;//ok
	p = &m;//ok
}

void test2()
{
	int n = 10;
	int m = 20;
	int const* p = &n;
	*p = 20;//err
	p = &m;//ok
}

void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20;//ok
	p = &m;//err
}

void test4()
{
	int n = 10;
	int m = 20;
	int const * const p = &n;
	*p = 20;//err
	p = &m;//err
}

int main()
{
	test1();//测试无const修饰的情况
	test2();//测试const在*的左边的情况
	test3();//测试const在*的右边的情况
	test4();//测试const在*的两边的情况
	return 0;
}

二、野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

1.野指针成因

(1)指针未初始化

#include <stdio.h>
int main()
{
	int* p;//局部变量指针未初始化,默认是随机值
	*p = 20;//非法访问,p就是野指针
	return 0;
}

(2)指针越界访问

当指针指向的范围超出数组arr的范围时,这时就是非法访问了,指针p就是野指针:

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	int i = 0;
	for (i = 0; i <= sz; i++)
	{
		*(p++) = i + 1;//后置++,先使用,后++
		//上述写法与*p = i + 1;
		//           p++;相同
	}
	return 0;
}

(3)指针指向的空间释放

p得到的n的地址的那一刻就是野指针了:

#include <stdio.h>
int* test()
{
	int n = 10;
	return &n;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

n所占的4个字节空间,在函数test()返回时,n的空间就还给了操作系统。

2.如何规避野指针

(1)指针初始化

如果明确知道指针指向哪里,就直接给指针变量赋值地址,若不知道指针指向哪里,就给指针赋值为NULLNULL是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址无法使用,读写该地址会报错:

#include <stdio.h>
int main()
{
	int* p = NULL;
	*p = 200;
	return 0;
}

在这里插入图片描述

所以记住一点:指针为空,不去使用;指针不为空,可以使用。

C++里NULL是0,C语言里NULL是0强制类型转换成void*类型的地址,本质都是0:

 #ifdef __cplusplus
 	#define NULL 0
 #else
 	#define NULL ((void *)0)
 #endif

指针初始化如下:

#include <stdio.h>
int main()
{
	int n = 30;
	int* p1 = &n;
	int* p2 = NULL;
	return 0;
}

(2)小心指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

(3)指针变量不再使用时,及时置NULL,指针使用之前检查有效性

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

#include <stdio.h>
int main()
{
	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	int i = 0;
	for (i = 0; i <= sz; i++)
	{
		*(p++) = i;
	}
	//此时p已经越界了,可以把p置为NULL
	p = NULL;
	//下次使用p之前,判断p不为NULL的时候再使用
	//...
	p = &arr[0];//重新让p获得地址
	if (p != NULL)//判断
	{
		//...
	}
	return 0;
}

在这里插入图片描述

(4)避免返回局部变量的地址

如造成野指针的第3个例子,不要返回局部变量的地址。

三、assert()断言

头文件assert.h定义了宏assert(),确保在运行时程序符合指定条件,若不符合,就报错终止运行,这个宏常被称为断言

assert(p != NULL);

上面代码中,程序运行到这一行时,判断p是否为NULL,若确实不为NULL,则程序继续运行,否则就会终止运行,并且给出报错信息。

assert()宏接收一个表达式作为参数。若该表达式为真(返回值为非0),assert()不会产生任何作用,程序继续运行;若表达式为假(返回值为0),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

例如:

#include <stdio.h>
#include <assert.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = NULL;
	assert(p != NULL);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

在这里插入图片描述

报错出现的图标点击“中止”即可:
在这里插入图片描述

assert()的使用对程序员是非常友好的,使用assert()的好处:不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。若已经确认程序没有问题,不需要断言,也就是想要关闭assert()语句,就在#include <assert.h>语句前面,定义一个宏#define NDEBUG

#define NDEBUG
#include <assert.h>

代码1:

#define NDEBUG//关掉断言
#include <stdio.h>
#include <assert.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	assert(p != NULL);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

在这里插入图片描述

代码2:

#define NDEBUG//关掉断言
#include <stdio.h>
#include <assert.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	int* p = NULL;
	assert(p != NULL);
	return 0;
}

运行结果,关掉断言,程序不会报错了:

在这里插入图片描述

然后重新编译程序,编译器就会禁用文件中所有的assert()语句。若程序又出现了问题,可以移除这条#define NDEBUG指令(或者把它注释掉),再次编译就重新启用了assert()语句,也就是说没有(注释)#define NDEBUG表示打开断言。

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

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

1.strlen()函数的模拟实现

在《深入理解指针(1)》“指针运算”里已经写过了strlen()函数的模拟实现,现在用一下本节学的内容对上一节的strlen()函数进行优化。

库函数strlen()的功能是求字符串的长度的,统计的是字符串中\0之前的字符个数,函数原型如下:

size_t strlen ( const char * str );

模拟实现strlen()函数的优化:

#include <assert.h>
#include <stdio.h>
size_t my_strlen(const char* str)//用const修饰,防止在函数内部修改*str
{
	assert(str != NULL);
	size_t count = 0;
	while (*str)//\0对应的ASCII码值为0
	{
		str++;
		count++;
	}
	return count;
}
int main()
{
	size_t len = my_strlen("abcdef");
	printf("%zd\n", len);
	return 0;
}
#include <assert.h>
#include <stdio.h>
size_t my_strlen(const char* str)
{
	assert(str != NULL);
	const char* start = str;
	while (*str)
	{
		str++;
	}
	return str - start;
}
int main()
{
	char arr[] = "abcdef";
	size_t len = my_strlen(arr);
	printf("%zd\n", len);
	return 0;
}
#include <assert.h>
#include <stdio.h>
size_t my_strlen(const char* str)
{
	assert(str != NULL);
	const char* p = str;
	while (*p)
	{
		p++;
	}
	return p - str;
}
int main()
{
	char arr[] = "abcdef";
	size_t len = my_strlen(arr);
	printf("%zd\n", len);
	return 0;
}

在这里插入图片描述

2.传值调用和传址调用

学习指针的目的是使用指针解决问题,那什么问题非指针不可呢?

例如:写一个函数,交换两个整型变量的值。

可能会写出下面的代码:

#include <stdio.h>
void Swap(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);
	Swap(a, b);
	printf("交换后:a = %d, b = %d\n", a, b);
	return 0;
}

我们发现结果并没有交换:

在这里插入图片描述

这是怎么一回事呢?我们可以借助调试查看问题所在:
在这里插入图片描述
main()函数内部创建变量ab,调用Swap()函数时,a、b作为参数传递给Swap()函数,函数内部创建形参x、y分别接收a、b的值,但是如上图所示,x的地址和a的地址不一样,y的地址也和b的地址不一样,相当于x和y是独立的空间,那么在Swap()函数内部交换x和y的值,自然就不会影响a和b的值,当Swap()函数调用结束后回到main()函数,a和b的值没法交换。上述Swap()函数在使用的时候,是把变量本身传递给了函数,这种叫传值调用

结论:实参传递给形参时,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。

传值调用例子:

代码1

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int r = Add(a, b);//传值调用
	printf("%d\n", r);
	return 0;
}

代码2

#include <stdio.h>
int Max(int x, int y)
{
	return (x > y ? x : y);
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int r = Max(a, b);//传值调用
	printf("%d\n", r);
	return 0;
}

那怎么才能做到在Swap()函数内部交换a、b的值呢?可以尝试把a、b的地址作为参数传给函数:

#include <stdio.h>
void Swap(int* pa, int* pb)
{
	int tmp = *pa;//tmp = a
	*pa = *pb;//a = b
	*pb = tmp;//b = tmp
}
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);
}

在这里插入图片描述
将地址作为参数传递给函数,相当于指针变量pa指向变量a、指针变量pb指向变量b:
在这里插入图片描述
上述调用Swap()函数时是将变量的地址传递给函数,这种调用函数的方式是传址调用

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值