C语言指针·高级用法超详解(指针运算、野指针、悬空指针、void类型指针、二级以及多级指针)

news2024/9/30 3:31:01

目录

1.  指针的运算

2.  野指针和悬空指针

2.1  野指针

2.2  悬空指针

3.  void类型指针

4.  二级指针和多级指针

4.1  命名规则

4.2  作用

4.2.1  二级指针可以操作一级指针记录的地址

4.2.2  利用二级指针获取变量中记录的数据


1.  指针的运算

文章开始前可以先了解:C语言指针·入门用法超详解-CSDN博客

        通过上一章我们初步了解了指针的用法,知道指针的类型是:

        其中指针中数据类型的作用是获取字节数据的个数,正常情况下变量p只能获取到0x001的内存地址,而int型数据占4字节,因此:

        那么我们可以思考一下,若是:

int a=10;
int* p=&a;

p+1

        假设a的地址为:0x01,那么p+1指向的地址会是什么呢?

        会是0x02还是0x05呢?

        正确答案应该是:0x05,这是为什么呢?

步长:指针移动一次的字节个数

        因为,指针的加法操作实际上是将指针向后移动若干个存储单元,而不是简单的数学加法,上面我们也提到了正常情况下变量p只能获取到0x01的内存地址,而int型数据占4字节,而p+1就是指针移动一步,一步在这里是四个字节,因此p+1,最终会移动到0x05的位置。我们拿代码验证一下:

#include <stdio.h>

int main()
{
	/*
	* 指针的运算
	* 步长:指针移动一次,走了多少字节
	* char:1
	* short:2
	* int:4
	* long:4
	* long long:8
	*/

	//加法:指针往后移动了N步P+N
	//减法:指针往前移动了N步P-N

	int a = 10;
	int* p = &a;

	printf("%p\n", p);
	printf("%p\n", p + 1);
	printf("%p\n", p + 2);
	printf("%p\n", p - 1);
	printf("%p\n", p - 2);
	return 0;
}


注意:

指针有意义的操作:

        指针跟整数进行加、减操作(每次移动N个步长)

        指针跟指针进行减操作(间隔步长)

#include <stdio.h>

int main()
{
	/*
	* 有意义的操作:
		指针跟整数进行加、减操作(每次移动N个步长)
		指针跟指针进行减操作(间隔步长)
	  无意义的操作:
		指针跟整数进行乘除操作
		原因:此时指针指向不明
		指针跟指针进行加、乘、除操作

	*/

	//前提条件:保证内存空间是连续的
	//数组
	int arr[] = { 1,2,3,4,5,6,7,8,9 };

	//获取0索引的内存地址
	int* p1 = &arr[0];

	//指针跟整数进行加、减操作(每次移动N个步长)
	//通过内存地址获取数据
	printf("%d\n", *p1);
	printf("%d\n", *(p1 + 1));

	//获取5索引的内存地址
	int* p2 = &arr[5];

	//p2 - p1间隔了多少步长
	printf("%d\n", p2 - p1);
	printf("%p\n", p1);
	printf("%p\n", p2);


	return 0;
}

指针无意义的操作:

        指针跟整数进行乘除操作

        原因:此时指针指向不明

        指针跟指针进行加、乘、除操作


2.  野指针和悬空指针

2.1  野指针

        野指针:指针指向的空间未分配。

#include <stdio.h>

int main()
{
	/*
	野指针:指针指向的空间未分配
	悬空指针:指针指向的空间已分配,但是被释放了
	*/

	//野指针:指针指向的空间未分配
	int a = 10;
	int* p1 = &a;

	printf("%p\n", p1);
	printf("%d\n", *p1);

	//p2为野指针
	int* p2 = p1 + 10;
	printf("%p\n", p2);
	printf("%d\n", *p2);


	return 0;
}

        此时运行程序,虽然也能得到p2的地址,但是要知道,这块地址我们并没有给他分配,他虽然能找到地址,但是若是随意修改数据,在正常使用中,可能会修改别的函数的数据,导致最终运行错误: 

2.2  悬空指针

        悬空指针:指针指向的空间已分配,但是被释放了。

#include <stdio.h>

int* method();

int main()
{
	/*
	野指针:指针指向的空间未分配
	悬空指针:指针指向的空间已分配,但是被释放了
	*/

	//悬空指针:指针指向的空间已分配,但是被释放了
	int* p3 = method();

	printf("拖点时间\n");
	printf("拖点时间\n");
	printf("拖点时间\n");
	printf("拖点时间\n");
	printf("拖点时间\n");
	printf("拖点时间\n");

	printf("%p\n", p3);
	printf("%d\n", *p3);

	return 0;
}

int* method()
{
	int num = 10;
	int* p = &num;
	return p;
}

        会发现此时代码数据错误,正常p3应当显示10: 

        这里可以调用static来使用:

#include <stdio.h>

int* method();

int main()
{
	/*
	野指针:指针指向的空间未分配
	悬空指针:指针指向的空间已分配,但是被释放了
	*/

	//悬空指针:指针指向的空间已分配,但是被释放了
	int* p3 = method();

	printf("拖点时间\n");
	printf("拖点时间\n");
	printf("拖点时间\n");
	printf("拖点时间\n");
	printf("拖点时间\n");
	printf("拖点时间\n");

	printf("%p\n", p3);
	printf("%d\n", *p3);

	return 0;
}

int* method()
{
	static int num = 10;
	int* p = &num;
	return p;
}

3.  void类型指针

特殊类型:void* p

不表示任何类型,只能记录地址值,不能获取到变量里面的数据,也不能进行加减的计算。

特点:无法获取数据,无法计算,但是可以接收任意地址数据。

        在我们使用指针的过程中会发现,不同类型的指针之间是不能相互赋值的,否则会发生编辑错误:

#include <stdio.h>

int main()
{
	//void类型指针

	//定义两个变量
	int a = 10;
	short b = 20;

	//定义两个指针
	int* p1 = &a;
	short* p2 = &b;

	//输出打印
	printf("%d\n", *p1);
	printf("%d\n", *p2);

	//不同类型的指针之间是不能相互赋值的
	char* p3 = p1;
	printf("%d\n", *p3);

	//这里编译器为我们进行了强制类型转换,上面的过程相当于如下
	char* p4 = (char*)p1;
	printf("%d\n", *p4);

	return 0;
}

         这里的指针间进行复制那是因为编译器在编译过程中,对p1进行了强制类型转换,p3的实际流程,如p4所示:

        为了解决这一问题我们可以将数据类型定义为void,不过这也会导致,不能printf输出数据,因为void不能获取到变量里面的数据,也不能进行加减的计算,否则编译器会报错:

#include <stdio.h>

int main()
{
	//void类型指针

	//定义两个变量
	int a = 10;
	short b = 20;

	//定义两个指针
	int* p1 = &a;
	short* p2 = &b;

	//输出打印
	printf("%d\n", *p1);
	printf("%d\n", *p2);

	//不同类型的指针之间是不能相互赋值的
	char* p3 = p1;
	printf("%d\n", *p3);

	//这里编译器为我们进行了强制类型转换,上面的过程相当于如下
	char* p4 = (char*)p1;
	printf("%d\n", *p4);

	//void类型指针打破上面的观念
	//void没有任何类型,好处可以接收任意类型指针记录内存地址
	void* p5 = p1;
	void* p6 = p2;

	//缺点:不能获取到变量里面的数据,也不能进行加减的计算
	//printf("%d\n", *p5);
	//printf("%d\n", *p6);

	//printf("%d\n", *p5 - 1);
	//printf("%d\n", *p6 + 1);

	return 0;
}

        那么我们引用void有什么用呢?

        例如,我们用于调用函数进行交换数据,我们可以编写如下代码:

#include <stdio.h>

void swap(void* p1, void* p2);

int main()
{
	//void类型指针

	//定义两个变量
	int a = 10;
	short b = 20;

	//定义两个指针
	int* p1 = &a;
	short* p2 = &b;

	//输出打印
	printf("%d\n", *p1);
	printf("%d\n", *p2);

	//不同类型的指针之间是不能相互赋值的
	char* p3 = p1;
	printf("%d\n", *p3);

	//这里编译器为我们进行了强制类型转换,上面的过程相当于如下
	char* p4 = (char*)p1;
	printf("%d\n", *p4);

	//void类型指针打破上面的观念
	//void没有任何类型,好处可以接收任意类型指针记录内存地址
	void* p5 = p1;
	void* p6 = p2;

	//缺点:不能获取到变量里面的数据,也不能进行加减的计算
	//printf("%d\n", *p5);
	//printf("%d\n", *p6);

	//printf("%d\n", *p5 - 1);
	//printf("%d\n", *p6 + 1);

	//调用函数交换数据
	int c = 100;
	int d = 200;

	swap(&c, &d);

	printf("c=%d,d=%d\n", c, d);

	return 0;
}

//函数:用来交换两个变量记录数据

void swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

         此时可以实现数据的交换:

        但是若是此时我想使用long long型数据呢?数据会发生报错(我在DevC++上运行报错,在VS上运行正确,因为VS在这里编辑器给他自动强制类型转换了):

#include <stdio.h>

void swap(void* p1, void* p2);

int main()
{
	//void类型指针

	//定义两个变量
	int a = 10;
	short b = 20;

	//定义两个指针
	int* p1 = &a;
	short* p2 = &b;

	//输出打印
	printf("%d\n", *p1);
	printf("%d\n", *p2);

	//不同类型的指针之间是不能相互赋值的
	char* p3 = p1;
	printf("%d\n", *p3);

	//这里编译器为我们进行了强制类型转换,上面的过程相当于如下
	char* p4 = (char*)p1;
	printf("%d\n", *p4);

	//void类型指针打破上面的观念
	//void没有任何类型,好处可以接收任意类型指针记录内存地址
	void* p5 = p1;
	void* p6 = p2;

	//缺点:不能获取到变量里面的数据,也不能进行加减的计算
	//printf("%d\n", *p5);
	//printf("%d\n", *p6);

	//printf("%d\n", *p5 - 1);
	//printf("%d\n", *p6 + 1);

	//调用函数交换数据
	long long c = 100L;
	long long d = 200L;

	swap(&c, &d);

	printf("c=%lld,d=%lld\n", c, d);

	return 0;
}

//函数:用来交换两个变量记录数据

void swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

        那么我们如何才能进行不同类型都能调用该函数呢?可以使用void类型,

void swap(void* p1, void* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

        但是如果使用void又会出现新的问题就是:void不能获取到变量里面的数据,也不能进行加减的计算,那么要如何解决呢?

           这里将void*类型的指针p1和p2分别转换为char*类型的指针pc1和pc2。这是因为char类型是 C 语言中的基本数据类型,且大小为1字节,所以可以通过char*指针逐字节访问内存。     

        同时函数void swap(void* p1, void* p2, int len)加上一个len用于表示字节数,要是int型len就等于4,long long就是等于8(在x64环境下):

void swap(void* p1, void* p2, int len)
{
	//把void类型的指针,转换char类型的指针
	char* pc1 = p1;
	char* pc2 = p2;

	char temp = 0;

	//以字节为单位,一个字节一个字节的进行转换
	for (int i = 0; i < len; i++)
	{
		temp = *pc1;
		*pc1 = *pc2;
		*pc2 = temp;

		pc1++;
		pc2++;
	}
}

        完整程序代码: 

#include <stdio.h>

void swap(void* p1, void* p2, int len);

int main()
{
	//void类型指针

	//定义两个变量
	int a = 10;
	short b = 20;

	//定义两个指针
	int* p1 = &a;
	short* p2 = &b;

	//输出打印
	printf("%d\n", *p1);
	printf("%d\n", *p2);

	//不同类型的指针之间是不能相互赋值的
	char* p3 = p1;
	printf("%d\n", *p3);

	//这里编译器为我们进行了强制类型转换,上面的过程相当于如下
	char* p4 = (char*)p1;
	printf("%d\n", *p4);

	//void类型指针打破上面的观念
	//void没有任何类型,好处可以接收任意类型指针记录内存地址
	void* p5 = p1;
	void* p6 = p2;

	//缺点:不能获取到变量里面的数据,也不能进行加减的计算
	//printf("%d\n", *p5);
	//printf("%d\n", *p6);

	//printf("%d\n", *p5 - 1);
	//printf("%d\n", *p6 + 1);

	//调用函数交换数据
	int c = 100;
	int d = 200;

	swap(&c, &d, 4);

	printf("c=%d,d=%d\n", c, d);

	return 0;
}

//函数:用来交换两个变量记录数据
/*
void swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}
*/

//修改一下函数,更具有通用性
//因为以上函数,若是主函数调用的话,只能int类型的数据
void swap(void* p1, void* p2, int len)
{
	//把void类型的指针,转换char类型的指针
	char* pc1 = p1;
	char* pc2 = p2;

	char temp = 0;

	//以字节为单位,一个字节一个字节的进行转换
	for (int i = 0; i < len; i++)
	{
		temp = *pc1;
		*pc1 = *pc2;
		*pc2 = temp;

		pc1++;
		pc2++;
	}
}

4.  二级指针和多级指针

4.1  命名规则

        以二级指针为例,上一章最初我们了解了什么是指针?

C语言指针·入门用法超详解-CSDN博客

         那么既然普通变量就有指针,指针也是一个变量为什么不能有一个指针继续指向他呢?那么我们可以了解到:

        既然指针也有他的指针,那么指针的指针要怎么命名呢?

        其和指针的命名规则是一样的也是:

数据类型  *  变量名;

        但是需要注意的是:指针的数据类型要跟指向空间中的数据的类型保持一致。

        首先对于指针p,其指向数据a的地址,需要和a数据类型保持一致,那么指针的命名就是:

int                       *                    p;

数据类型           标记            变量名

        然后,二级指针指向的是指针的数据类型,此时指针空间内存储的不是数据,而是a的地址,他的数据类型是int*,也就是说二级指针的命名:

int*                       *                    p;

数据类型           标记            变量名

4.2  作用

4.2.1  二级指针可以操作一级指针记录的地址

        上代码:

#include <stdio.h>

int main()
{
	//定义变量
	int a = 10;
	int b = 20;

	//定义一级指针
	int* p = &a;

	printf("a的地址:%p\n", &a);
	printf("b的地址:%p\n", &b);
	printf("修改前p的地址:%p\n", p);

	//定义二级指针
	int** pp = &p;

	*pp = &b;

	printf("修改后p的地址:%p\n", p);

	return 0;
}

        可以发现p的地址被修改了,简单点来说:初始时,p指向变量a的地址。然后,通过pp这个二级指针,修改了p的指向,使其指向了变量b的地址。因此,最后输出显示p的地址发生了变化,从指向a的地址变为指向b的地址。

4.2.2  利用二级指针获取变量中记录的数据

#include <stdio.h>

int main()
{
	//定义变量
	int a = 10;
	int b = 20;

	//定义一级指针
	int* p = &a;

	//定义二级指针
	int** pp = &p;

	printf("修改前获取变量里面的数据:%d\n", **pp);

	*pp = &b;

	printf("修改后获取变量里面的数据:%d\n", **pp);

	return 0;
}

C语言指针·入门用法超详解-CSDN博客

指针_时光の尘的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1980126.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于强化学习的Deep-Qlearning网络玩cartpole游戏

1、环境准备&#xff0c;gym的版本为0.26.2 2、编写网络代码 # 导入必要的库 import gym import torch import torch.nn as nn import torch.optim as optim import numpy as np from collections import deque import random# 定义DQN网络 class DQN(nn.Module):def __init__…

《深入浅出WPF》学习笔记五.Mvvm设计模式

《深入浅出WPF》学习笔记五.Mvvm设计模式 背景 在通过视频学习wpf的过程中&#xff0c;讲师花了不少篇幅来讲Mvvm。特地在此用自己的语言总结一番,方便以后面试回答&#xff0c;如有理解不对&#xff0c;欢迎指正哈。 Mvvm结构 Mvvm指的是ModelViewViewModel 为什么要使用…

《网络安全自学教程》- MySQL匿名用户的原理分析与实战研究

《网络安全自学教程》 低版本的MySQL数据库在安装时会创建一个用户名和密码为空的账户&#xff0c;也就是匿名账户。即使升级到高版本&#xff0c;匿名账户仍然会存在。 MySQL匿名账户 1、检查是否存在匿名账户2、检查用户权限3、创建匿名账户4、使用匿名账户登录5、删除匿名账…

医院管理系统

医院管理系统 本文所涉及所有资源均在传知代码平台可获取 文章目录 医院管理系统概述使用技术核心功能1. 登录与注册2. 管理员系统3. 患者系统&#xff08;医院电子平台&#xff09;4. 医生系统&#xff08;坐诊系统&#xff09; 部署与启动适用场景 概述 本项目是一个专为大学…

读零信任网络:在不可信网络中构建安全系统09用户信任

1. 用户信任 1.1. 将设备身份和用户身份混为一谈会导致一些显而易见的问题 1.1.1. 特别是当用户拥有多台设备时&#xff0c;而这种情况很普遍 1.1.2. 应该针对不同类型的设备提供相匹配的凭证 1.1.3. 在存在共用终端设备的情况下&#xff0c;所有的这些问题将更加凸显 1.2…

打造未来交互新篇章:基于AI大模型的实时交互式流媒体数字人项目

在当今数字化浪潮中,人工智能(AI)正以前所未有的速度重塑我们的交互体验。本文将深入探讨一项前沿技术——基于AI大模型的实时交互式流媒体数字人项目,该项目不仅集成了多种先进数字人模型,还融合了声音克隆、音视频同步对话、自然打断机制及全身视频拼接等前沿功能,为用…

Python中使用正则表达式

摘要&#xff1a; 正则表达式&#xff0c;又称为规则表达式&#xff0c;它不是某种编程语言所特有的&#xff0c;而是计算机科学的一个概念&#xff0c;通常被用来检索和替换某些规则的文本。 一.正则表达式的语法 ①行定位符 行定位符就是用来描述字符串的边界。"^&qu…

第十三节、人物属性及伤害计算

一、碰撞器层级剔除 选中player和敌人&#xff0c;即可去除 若勾选触发器&#xff0c;则会取消掉碰撞效果&#xff0c;物体掉落 二、人数属性受伤计算 1、创建代码 将两个代码挂载到玩家和敌人身上 2、调用碰撞物体的方法 3、伤害值 开始&#xff1a;最大血量即为当前血量…

Arduino PID库 (6):初始化

Arduino PID库 &#xff08;6&#xff09;&#xff1a;初始化 参考&#xff1a;手把手教你看懂并理解Arduino PID控制库——初始化 Arduino PID库 &#xff08;5&#xff09;&#xff1a;开启或关闭 PID 控制的影响 问题 在上一节中&#xff0c;我们实现了关闭和打开PID的功…

最小二乘法求解线性回归问题

本文章记录通过矩阵最小二乘法&#xff0c;求解二元方程组的线性回归。 假设&#xff0c;二维平面中有三个坐标&#xff08;1&#xff0c;1&#xff09;、&#xff08;2&#xff0c;2&#xff09;、&#xff08;3&#xff0c;2&#xff09;&#xff0c;很显然该三个坐标点不是…

React(三):PDF文件在线预览(简易版)

效果 依赖下载 https://mozilla.github.io/pdf.js/getting_started/ 引入依赖 源码 注意&#xff1a;pdf文件的预览地址需要配置代理后才能显示出来 import ./index.scss;function PreviewPDF() {const PDF_VIEWER_URL new URL(./libs/pdfjs-4.5.136-dist/web/viewer.html, im…

12.SpringDataRedis

介绍 SpringData是Spring中数据操作的模块&#xff0c;包含对各种数据库的集成&#xff0c;其中redis的集成模块就叫做SpringDataRedis。 spring的思想从来都不是重新生产&#xff0c;而是整合其他技术。 SpringDataRedis的特点 1.提供了对不同redis客户端的整合&#xff08…

8.4 day bug

bug1 忘记给css变量加var 复制代码到通义千问&#xff0c;解决 bug2 这不是我的bug&#xff0c;是freecodecamp的bug 题目中“ 将 --building-color2 变量的颜色更改为 #000” “ 应改为” 将 #000 变量的颜色更改为 --building-color2 “ bug3 又忘记加var(–xxx) 还去问…

渗透小游戏,各个关卡的渗透实例

Less-1 首先&#xff0c;可以看见该界面&#xff0c;该关卡主要是SQL注入&#xff0c;由于对用户的输入没有做过滤&#xff0c;使查询语句进入到了数据库中&#xff0c;查询到了本不应该查询到的数据 首先&#xff0c;如果想要进入内部&#xff0c;就要绕过&#xff0c;首先是用…

C#中的TCP和UDP

TcpClient TCP客户端 UDP客户端 tcp和udp的区别 TCP&#xff08;传输控制协议&#xff09;和UDP&#xff08;用户数据报协议&#xff09;是两种在网络通信中常用的传输层协议&#xff0c;它们在C#或任何其他编程语言中都具有相似的特性。下面是TCP和UDP的主要区别&#xff1a;…

MySQL的基本使用

文章目录 MySQL的基本使用什么是SQLSQL学习目标SQL的SELECT语句SQL的INSERT INTO语句 SQL的UPDATE语句SQL的DELETE语句 SQL的WHERE子句可在WHERE子句中使用的运算符SQL的AND和OR运算符SQL的ORDER BY子句SQL的COUNT(*)函数 在项目中操作数据库的步骤安装mysql模块配置mysql模块测…

微服务设计原则——易维护

文章目录 1.充分必要2.单一职责3.内聚解耦4.开闭原则5.统一原则6.用户重试7.最小惊讶8.避免无效请求9.入参校验10.设计模式11.禁用 flag 标识12.分页宜小不宜大参考文献 1.充分必要 不是随便一个功能都需要开发个接口。 虽然一个接口应该只专注一件事&#xff0c;但并不是每个…

摩托罗拉刷机包和固件下载地址

发现了一个非常好的摩托罗拉刷机包和固件下载地址&#xff1a;https://firmware.center/ 里面包含了所有的摩托罗拉的刷机包和软件、电路图等等&#xff0c;非常多&#xff0c;我想镜像到本地网盘&#xff0c;但不知道怎么操作&#xff0c;有没有懂得朋友教我全部镜像到国内的…

Kafka生产者(二)

1、生产者消息发送流程 1.1 发送原理 在消息发送的过程中&#xff0c;涉及到了两个线程——main 线程和 Sender 线程。在 main 线程中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator&#xff0c;Sender 线程不断从 RecordAccumulator 中拉取…

Gamma AI:一键生成专业级PPT的智能工具

1. Gamma 简介 Gamma 是一个致力于通过非常简单的ai交互&#xff0c;制作好的视觉体验作品&#xff0c;它始终站在作者的视角新增功能&#xff0c;同时注重观众视角呈现作品。 突破了以往演示文档&#xff08;ppt、pdf、网站&#xff09;表现形式&#xff0c;能够借助ai的力量…