那些 C语言指针 你不知道的小秘密 (2)

本篇会加入个人的所谓‘鱼式疯言’
❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言
而是理解过并总结出来通俗易懂的大白话,
我会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.
可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念 !!!
在这里插入图片描述

前言

在上一篇的指针专题中我们主要学习了
1.指针变量和地址中,内存是什么以及其大小转化关系,指针和地址的区别 ,& 和 * 的运用。
2.我们还说明了,指针可以加上或减去一个整数,指针之间可以想减,指针可以关系判断,
还有一个比较特殊的指针类型 void* 的解释
3最后我们提了一嘴我们的关键字 const 的在普通变量和指针变量的用法。

原文链接:https://blog.csdn.net/mgzdwm/article/details/134691818

这次小编又带来了指针的第二篇内容的学习了,虽说 指针 真的有点小难度
但小编相信只要宝子们认真去学习并配上 小编文章 的 小小的总结
咱们一定可以拿捏 这小小指针哒
下面请宝子们移步 目录区 观赏哦
精 彩 马 上 开 始

目录

1. 野指针

2. assert 断言

3. 传值调用与传址调用

4. 二级指针

一. 野指针

<1>. 野指针 的简介

野指针 就是指针指向的位置是不可知的(随机的不正确的没有明确限制的
出现野指针的情况很多,小编的收集了以下三种情况供友友们参考

<2>. 出现野指针的可能情况

1. 指针未初始化

//野指针情况一:
// 指针未初始化
#include 
int main()
{
	//局部变量指针未初始化,默认为随机值
	int* p;
	*p = 20;
	
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第1张图片

2. 指针越界访问

//野指针情况一:
//指针越界访问
#include 
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;
}

那些 C语言指针 你不知道的小秘密 (2)_第2张图片

3. 函数调用结束后空间的释放

#include 
int* test()
{
	int n = 100;
	
	//取出 n 的地址返回
	return &n;
}

int main()
{
	//用 p 来接收
	int* p = test();
	
	printf("hehe\n");
	//观察验证
	printf("%d\n", *p);
	
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第3张图片

鱼式疯言

小编用大白话说野指针就是

用 没有存放的地址的指针,用 别人的地址,用 销毁的地址

有那么多情况会出现 野指针,友友们该怎么避免出现 野指针 呢
请 宝子们 继续往下看哦

<3>. 避免指针的对策

如果是未初始化的指针呢,那 好办啊,直接初始化不就完了,那宝子们该如果初始化呢 ? ? ?

1.指针的初始化

如果明确知道指针指向哪里就直接赋值地址

如果 不知道指针应该指向哪里,可以给指针赋值NULL.

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

//指针初始化
#include 
int main()
{

	//初始化为 NULL
	//防止出现野指针
	int* p2=NULL ;

	
	//如果为 NULL 就会报错
	printf("%d", *p2);
	
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第4张图片

如果加个判断呢就不会报错了

//指针初始化
#include 
int main()
{
	int num = 10;
	int* p1 = &num;
	
	//初始化为 NULL
	//防止出现野指针
	int* p2=NULL ;

	//当我们需要用 p2 指针时我们就判断是否为 NULL
	if (p2==NULL)
	{
		// 一旦为 NULL 就退出程序
		return 1;
	}

	//如果不为 NULL 我们就可以避免野指针了
	printf("%d", *p2);
	
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第5张图片

2. 访问到别人的空间

越界了怎么办 ? ? ?

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

3.指针变量不再使用时

及时置NULL,指针使用之前检查有效性

那些 C语言指针 你不知道的小秘密 (2)_第6张图片
鱼式疯言
我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找一棵树把野狗拴起来,就相对安全了
给指针变量及时赋值为 NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来
那些 C语言指针 你不知道的小秘密 (2)_第7张图片

不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗,有点危险;

对于指针也是,在使用之前,我们也要判断是否为 NULL

看看是不是被拴起来起来的野狗

如果是,那就不能直接使用

如果不是,我们再去使用。

4.函数返回局部变量的地址

#include 
int* test()
{
	//将原本是栈区的局部变量转化为静态区的静态变量
	static int n = 100;
	
	//取出 n 的地址返回
	return &n;
}

int main()
{
	//用 p 来接收
	int* p = test();
	
	printf("hehe\n");
	//观察验证
	printf("%d\n", *p);
	
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第8张图片

鱼式疯言

小编就简单说下我们的局部变量

我们函数内部的局部变量是放在栈区的,会被 销毁

当我们加上 static 放在静态区就不会被销毁

自然就可以继续访问我们的 n 的 地址

二. assert

小爱同学又来发问了,如果我们每次都要 if 来判断指针是否为 NULL 是不是太麻烦了 ? ? ?
我们有没有一种函数或者宏定义来给出提示呢,答案是有哒,那就只有我们的 assert 断言能办到了

<1>. assert 简介

在C语言中,assert 是一个预处理宏,用于在代码中插入断言语句。
断言用于检查程序运行过程中的一些条件是否满足
如果断言失败,即条件不满足,则会触发一个错误。
assert 宏的定义如下:

#include 

void assert(int expression);

<2>. 举个栗子

就拿上一个代码举个栗子吧

//assert 所需的头
#include
//不用该指针时置为NULL
int main()
{
	int arr[10] = { 1,2,3,4,5,67,7,8,9,10 };
	int* p = &arr[0];
	for (int i = 0; i < 10; i++)
	{
		*(p++) = i;
	}
	//此时p已经越界了,可以把p置为NULL

		p = NULL;
	//下次使用的时候,判断p不为NULL的时候再使用
	//...

	assert(p != NULL); //判断

	p = &arr[0];//重新让p获得地址
	
	//...
	
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第9张图片
如果我们要关闭所有的 assert 怎么办呢

这时有个宏定义 闪亮登场

//关掉 assert 断言的宏定义
#define NDEBUG
//assert 所需的头
#include
//不用该指针时置为NULL
int main()
{
	int arr[10] = { 1,2,3,4,5,67,7,8,9,10 };
	int* p = &arr[0];
	for (int i = 0; i < 10; i++)
	{
		*(p++) = i;
	}
	//此时p已经越界了,可以把p置为NULL

		p = NULL;
	//下次使用的时候,判断p不为NULL的时候再使用
	//...

	assert(p != NULL); //判断

	p = &arr[0];//重新让p获得地址
	
	//...
	
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第10张图片

鱼式疯言

上面的栗子充分说明了我们可以

  1. 当我们需要判断是的为 NULL 指针时,用上assert 是不错的选择
  2. 当我们选择用 assert 时,我们一定不要忘记引用
  3. 当我们用了 assert 后,如果不想发挥其作用的话,可以加上宏定义 NDEBUG
  4. 温馨提示:由于我们的 assert 是由头文件 生成的,所以我们需要用 NDEBUG 时,就要放在其上面 。

<3>. assert 的不足

assert() 的缺点是,因为引入了额外的检查,增加了程序的运行时间。
一般我们可以在Debug 中使用
在Release 版本中选择禁用 assert 就行
在VS 这样的集成开发环境中,
在Release 版本中,直接就是优化掉了。

这样在debug版本写有利于程序员排查问题

在Release 版本不影响用户使用时程序的效率。

鱼式疯言

如果大白话说就是

assert 只有在 debug 环境下才能使用,在 release 的环境下无法使用

三. 传值调用和传址调用

<1>. 两者的简介

前提 : 说到 调用,那必须是我们 函数的调用

那我们的 传值调用,顾名思义肯定是传他的值。

传址调用,当然是传他的地址,竟然是传他的地址必然和我们的指针有关

<2>. 举个栗子

1. 传值调用

//传值调用
//加法器
int add(int x,int y)
{
	return x + y;
}

int main()
{
	int a = 0, b = 0;
	scanf("%d%d", &a, &b);
	int sum=add(a, b);
	printf("%d+%d=%d",a,b, sum);
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第11张图片

可有时候我们传值调用却没有达到我们想要的预期的效果 ! !!

//传值调用
//加法器
void swap(int x,int y)
{
	//定义临时变量
	int tmp = x;
	
	x = y;
	
	y = tmp;
}

int main()
{
	int a = 0, b = 0;
	scanf("%d%d", &a, &b);
	
	swap(a, b);

	printf("交换前\n");
	printf("a=%d,b=%d\n",a,b);
	

	printf("交换后\n");
	printf("a=%d,b=%d\n", a, b);
	
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第12张图片
我们不妨观察一下他们的地址
那些 C语言指针 你不知道的小秘密 (2)_第13张图片

发现他们的地址居然没改变

是的,结果发现 a 和 b 都没有交换

我们发现在main函数内部,创建了 ab ,a的地址是 0x0093f824,b的地址是 0x0093f818

在调用 Swap 函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是x的地址是
0x0093f740,y的地址是 0x0093f744

x 和 y 确实接收到了 a 和 b 的值,不过x的地址和a的地址不一样,y的地址和b的地址不一样,相当于 x 和 y 是独立的空间,那么在
Swap 函数内部交换x和y的值

自然不会影响a和b,当 Swap 函数调用结束后回到 main 函数, a 和 b 的没法交换。
Swap 函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数的时候就知道了,这种叫传值调用
那我们试试传址调用吧 `

2. 传址调用

//传址调用
//俩数交换
void swap(int *x,int* y)
{
	//定义临时变量
	int tmp = *x;
	
	*x = *y;
	
	*y = tmp;
}

int main()
{
	int a = 0, b = 0;
	scanf("%d%d", &a, &b);
	
	

	printf("交换前\n");
	printf("a=%d,b=%d\n",a,b);
	
	swap(&a, &b);

	printf("交换后\n");
	printf("a=%d,b=%d\n", a, b);
	
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第14张图片

当我们 调试 的时候就会发现,这俩地址居然相同

那些 C语言指针 你不知道的小秘密 (2)_第15张图片

我们可以看到实现成 Swap 的方式,顺利完成了任务,这里调用 Swap 函数的时候是将变量的地址传
递给了函数,这种函数调用方式叫:传址调用
传址调用,可以让函数和主调函数之间建立真正的联系
在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算
就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要 传址调用

鱼式疯言

小编有两点总结:

  1. 当我们传值调用时,只需记住一点,形参只是实参的一份 临时拷贝,我们只能得到数据而不能真正改变其原先的数据
  1. 小编的认为的使用场景是: 当我们需要改变其原值是,我们就用 传址调用,当我们只需要他的一个数值的时候,我们只需要 传值调用即可

<3>. 传址调用的实际运用

strlen的模拟实现

//assert 需要的头 
#include
#include

//const 修饰防止 str 被修改
size_t my_strlen(const char* str)
{
	int count = 0;
	
	//assert 断言防止传NULL
	assert(str);
	
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}

int main()
{
	//返回的是无符号整型 
	//size_t = unsigned 
	size_t len = my_strlen("abcdef");
	
	printf("%zd\n", len);
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第16张图片
在这个实际运用中,我们重复让 指针右移 ,直到我们找到 ‘\0’ 就停止,从中算出我们想要的字符串的长度

鱼式疯言

  1. 指针虽好,但我们如果 不想改变原有数据 ,一定也不要忘了在其参数前加 const 修饰哦
  2. 为防止 NULL 传入,我们也可加入 assert 来判断
  3. 因我们字符串长度是个 非负数 ,所以我们返回是 无符号整型 ,记住 size_t <==> unsigned ,同时我们的占位符用 %zd 输出

四. 二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
二级指针就由此诞生了 ! ! !

<1>. 二级指针的简介

C语言中的二级指针是指指向指针的指针。在C语言中,指针是一个变量,它存储了内存地址。而一个二级指针就是存储了指针变量的地址的变量。通过使用二级指针,我们可以间接地访问或修改指针的值。

二级指针的声明需要在前面加上两个 “*” 号。例如,** “int ** pp” ** 表示一个二级指针,它指向一个指针,用两个 * 对其解引用时,指针指向一个整数

int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	return 0;
}

在这里插入图片描述

那些 C语言指针 你不知道的小秘密 (2)_第17张图片

<2>. 举个栗子

#include

int main()
{
	//整型变量
	int a = 10;
	
	//一级指针
	int* pa = &a;
	//二级指针
	int** ppa = &pa;

	
	printf("a=%d\n", a);
	//对一级指针解引用
	printf("*pa=%d\n", *pa);
	
	//对二级指针解引用
	printf("**ppa=%d\n", **ppa);
	return 0;
}

那些 C语言指针 你不知道的小秘密 (2)_第18张图片

鱼式疯言

一级指针 的指针才叫 二级指针

一级指针一颗 * ,二级指针两颗 * 。

一级指针解引用 一颗 * ,二级指针解引用 两颗 *

总结

  • 野指针:小编总结了野指针出现的情况并说明其应对对策

  • assert 断言:小编带着大家试着怎么用 assert 并体会了 assert 防止野指针的益处

  • 传值调用与传址调用: 理解了 传值传址 调用的不同,并说明分别用于哪些 使用场景 更好

  • 二级指针:二级指针的理解,同时并知晓如何 定义解引用

如果觉得小编写的还不错的咱可支持三关下,不妥当的咱评论区指正

希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大动力

在这里插入图片描述

你可能感兴趣的:(#,C语言与粉红色回忆,c语言,开发语言)