当前位置: 首页 > news >正文

C语言指针篇

要想学好C语言,作为灵魂的指针那是必须要掌握的,而要想搞定指针,就不得不讲一下内存和地址之间的关系

内存和地址

计算机上的CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中!那么内存空间又是如何管理和使用的呢?

其实内存空间也是划分为一个一个的内存单元的,每个内存单元的大小取一个字节!

补充(计算机中的单位)

计算机中的单位大致有以下几种:

bit(比特位)、Byte(字节)、KB、MB、GB、TB、PB

他们的转换关系为:

1Byte = 8bit

1KB = 1024Byte

1MB = 1024KB

1GB = 1024MB

1TB = 1024GB

1PB = 1024TB

其中一个比特位(bit)的大小是存放一个二进制位的0或1所占空间的大小。

每个内存单元的大小是1个字节,即8个比特位,而且每个内存单元都有一个编号,有了这个编号,CPU就可以快速的找到这个内存空间。

b41cc783bc2e42aa9f3c7599e4662b53.png

在计算机中,我们把内存单元的编号也叫地址,而在C语言中,地址也叫指针。

如何编址?

因为CPU访问内存的某块空间时是通过地址来完成的,所以需要对内存进行编址。计算机中的编址,是硬件在设计的时候就完成的,跟钢琴的键位差不多,所以并不是把每一个内存的地址都给记录下来。

那具体是如何编址的呢?

首先补充一点:CPU计算机内部是由很多硬件单元相互协同工作的,而硬件与硬件又是相互独立的,要把这些硬件相互联系起来,就需要用线(就是物理意义上的线)连起来。那么内存和CPU之间也是通过这些线来完成大量的数据交互的,硬件编址要用到其中一组线,叫地址总线。

假设32位机器有32根地址总线,每根地址总线都有两种状态表示0和1(电脉冲的有无),那么一根线就能表示两种含义,两根就能表示4中含义,32根地址线就能表示2^32中含义,每一种含义代表一种地址。

地址具体如何使用?

首先再补充两点

1:CPU与内存或其他器件之间的数据传输是通过数据总线来进行的

2:CPU对外部器件的控制是通过控制总线进行的

假设CPU要对内存进行一次读操作

首先控制总线先发一条读的命令(R),当地址总线传过来一个信息的时候,把地址信息下达给内存,在内存上,就可以找到该地址对应的那块空间,然后将里面的数据通过数据总线传入CPU内的寄存器,这样就完成了一次读操作。

注:找内存单元是地址来做的,地址总线是用来传递地址信息的。

指针变量和地址

变量创建的本质

我们在写代码的时候会创建一些变量,而创建变量的本质其实是向内存申请了一块空间

int n = 5;

因为int类型的变量所占空间的大小是4个字节,所以上面这句代码就是向内存申请了4个字节的空间,存放5这个数字,而这4个字节每一个都有一个地址

需要注意的是:变量名其实是给我们看的,编译器是通过地址来找内存单元的

取地址操作符&

这个操作符可以取出变量在内存中的地址,例如:

#include <stdio.h>
int main()
{int n = 8;int * p = &n;//p是指针变量,类型是int*printf("%p\n", p);//%p是用来打印n的地址的return 0;
}

在这个代码中,定义了一个int类型的变量n,值是8,&n就可以得到变量n的地址(需要注意的是:一个int类型有4个字节,每一个字节都有一个地址,而&n取出的是4个字节中地址较小的字节的地址)这里用int*类型的指针变量p来存放n的地址,因为在C语言中,地址就是指针,要用指针变量来接收,至于为什么是int*类型,下文讲指针变量类型的意义时会有解释

int * p = &n;这句代码中,p是指针变量的名字,int *是指针变量p的类型,*表示p是一个指针变量,int表示p指向的变量n的类型是int

解引用操作符*

解引用操作符也叫间接访问操作符,对指针变量进行解引用就能找到指针变量所指向的那块空间

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

在这段代码中,指针变量p中存放了n的地址,然后*p = 70;这句代码对p进行了解引用操作找到了变量n所在的空间,并把n的值改变成了70,最后打印n的值。

指针变量的大小

首先,指针变量是用来存放地址的,一个地址的存放需要多大空间,那么指针变量的大小就是多大空间,所以指针变量的大小与类型无关。

比如在32位机器上,有32根地址总线,每一根地址线上都会产生一个0或1的数字信号,将这些数字信号组成的二进制序列作为一个地址就需要32个比特位(bit),即4个字节的空间才能存储。

同理,在64位机器上,有64根地址总线,那么一个地址就需要64个比特位(bit),即8个字节。

指针变量类型的意义

指针的解引用

指针的类型决定了对指针进行解引用时候的权限,即一次能访问几个字节比如指针类型是int*,那么解引用操作就能访问4个字节的空间

如果指针类型是char*,那么解引用操作就能访问一个字节的空间

指针加减整数

指针的类型决定了指针加减整数的时候向前或向后走一步有多大距离,例如:

p1是int*类型的指针,+1跳过一个整型,即4个字节;p2是char*类型的指针,+1跳过一个char类型的空间,即一个字节

void*指针

void*类型的指针是无具体类型的指针(也叫泛型指针),所以它可以接收任意类型的地址,但是void*类型的指针不能直接进行解引用操作和指针加减整数的操作

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

编译结果如下:指针变量的运算更其指向的对象无关,而是取决于指针变量的类型的

从这段代码中可以看出:

void*指针的作用

一般void*类型的指针是使用在函数的参数部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得一个函数可以处理多种类型的数据

以后想使用的时候,强制类型转换成想要的类型就行了

const修饰指针

const关键字

一个变量的值本来是可以被修改的,要想变量的值不被修改,就可以用const关键字

const修饰的变量具有常量的性质,叫做常变量

这个被修饰的变量还是变量,但是不能被修改,例如:

#include <stdio.h>
int main()
{const int n = 7;n = 24;//会出错return 0;
}

变量n被const修饰,具有了常属性,如果被修改就不符合语法规则,编译时会报错

但是要修改变量n的值,还有别的方法,那就是指针

const修饰指针变量

先说结论:

1:const放在*的左边,限制的是指针指向的内容,即不能通过指针变量来改变它所指向的内容,但是指针变量本身是可以修改的

int n = 24;
int m = 15;
int const *p = &n;//const int *p = &n;只要const在*的左边就行
p = &m;

上面这段代码中,const限制的就是*p,即*p不能被修改,但p本身是可以修改的,所以p = &m;这句代码就是对的

2:const放在*的右边,限制的是指针变量本身,即指针变量不能改变它的指向,但是可以通过指针变量修改它所指向的内容

int n = 45;
int * const p = &n;
*p = 56;

上面这段代码中,const限制的就是p,即p变量本身不能被修改,但是*p是可以被修改的,所以*p = 56;这句代码就是对的

指针运算

指针加减整数

前面已经说过了指针变量类型的意义,即指针的类型决定了指针解引用的权限,也决定了指针加减整数的时候向前或向后走一步有多大距离,例如用指针打印数组的元素

这里需要补充一些数组和指针的知识

数组名是数组首元素的地址,比如一个整型数组arr,那么arr就是数组首元素的地址,而&arr[0]也是数组首元素的地址,因为是整型数组,所以该地址的类型是int*

#include <stdio.h>
int main()
{int arr[] = { 1,2,3,4,5,6,7,8,9,0 };int len = sizeof(arr) / sizeof(arr[0]);int* parr = arr;int i = 0;for (i = 0; i < len; i++){printf("%d ", *(parr + i));}return 0;
}

上面这段代码中,将数组名即数组首元素的地址赋给了指针变量parr,通过对指针变量的解引用操作来访问数组的每一个元素,parr + i就是下标为i的数组元素

运行结果:

指针减指针

指针-指针的绝对值是指针和指针之间的元素个数,但计算的前提条件是两个指针指向的是同一块空间,例如:

#include <stdio.h>
int main()
{int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int len = sizeof(arr) / sizeof(arr[0]);printf("%d\n", &arr[len - 1] - &arr[0]);return 0;
}

这段代码中,&arr[len - 1]是数组的最后一个元素,&arr[0]是数组的首元素,二者之间的元素个数如下图

运行结果:

指针的关系运算

主要指比大小,还以打印数组元素为例

#include <stdio.h>
int main()
{int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int len = sizeof(arr) / sizeof(arr[0]);int* p = arr;while (p <= &arr[len - 1]){printf("%d ", *p);p++;}return 0;
}

p中存放着数组首元素的地址,从p地址开始向后访问,直到将数组所有元素全部打印完,即到数组的最后一个元素为止

运行结果:

野指针

野指针就是只想未知空间的指针,这中指针是不安全的

野指针的成因

1:指针未初始化

一个局部变量不初始化,它的值是随机的,那指针变量不初始化,而是直接将指针变量的值当做地址,解引用操作就会非法访问

2:指针越界访问

3:指针指向的空间释放(如返回局部变量的地址)

如何避免野指针

1:指针初始化

一般情况下,可以给指针变量赋某个变量的地址,但如果不知道要给指针变量赋什么值,可以给指针赋值为NULL

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

2:小心指针越界

3:指针变量不在使用时,及时置为NULL;在指针使用之前,检查指针的有效性(在使用之前判断指针变量是否为NULL)因为约定俗成的规则是只要是NULL指针就不去访问

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

assert断言

assert断言常用来检测指针的有效性

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

assert( p != NULL );

当程序执行到上面这条语句时,验证变量p是否等于NULL,如果不等于NULL,则程序继续执行,否则会终止运行,并给出报错信息提示,比如下面这段代码

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

运行结果:

在程序终止运行的同时给出了报错信息的提示

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

使用assert()的好处

assert()不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做判断,就在语句前面定义一个宏NDEBUG,即

#define NDEBUG
#include <assert.h>

就像一个开关一样,然后重新编译程序,编译器就会禁用文件中所有的assert()语句

传值调用和传址调用

在函数传参的时候,有时会传变量的值,有时会传变量的地址

传值调用

当实参传递给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参

传址调用

将实参的地址传递给形参,形参通过地址可以找到地址所对应的变量,传址调用可以让函数和主调函数之间建立真正的联系

指针的使用(strlen函数的模拟实现)

strlen是一个库函数,作用是计算字符串中\0之前字符的个数,函数原型如下

size_t strlen ( const char * str );

参数str接收一个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回长度。如果要模拟实现可以从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直 到 \0 就停止,代码如下

//模拟实现库函数strlen
#include <stdio.h>
size_t mystrlen(const char* str)
{size_t count = 0;while (*str){count++;str++;}return count;
}
int main()
{char arr[] = "abcdef";size_t len = mystrlen(arr);printf("%zd\n", len);return 0;
}

在实现strlen函数的时候,参数部分str指向了数组的第一个元素(或者第一个字符)并向后开始计算字符串的长度,我们并不希望str指向的内容被修改,所以用const对*srt进行了限制,这样也提升了函数的健壮性(也叫鲁棒性,指的是在异常情况下系统生存的能力)

相关文章:

C语言指针篇

要想学好C语言&#xff0c;作为灵魂的指针那是必须要掌握的&#xff0c;而要想搞定指针&#xff0c;就不得不讲一下内存和地址之间的关系 内存和地址 计算机上的CPU&#xff08;中央处理器&#xff09;在处理数据的时候&#xff0c;需要的数据是在内存中读取的&#xff0c;处…...

Unity 使用Editor工具查找 Prefab 中的指定脚本

在 Unity 项目中&#xff0c;随着项目规模的扩大和 Prefab 数量的增加&#xff0c;管理和定位 Prefab 中的脚本变得更加复杂。为了提高开发效率&#xff0c;所以需要编写一个自定义的 Unity Editor 工具&#xff0c;帮助查找某个 Prefab 中是否使用了指定的脚本。本文将介绍如何…...

Frida-JSAPI:Interceptor使用

拦截器 Interceptor.attach(target, callbacks[, data]) 参数分析 target &#xff1a;target是一个NativePointer&#xff0c;用于指定想要拦截的函数的地址。callbacks &#xff1a;参数是一个包含一个或多个回调函数的对象。 onEnter(args) 回调函数&#xff0c;接收一个参…...

【深度学习】(3)--损失函数

文章目录 损失函数一、L1Loss损失函数1. 定义2. 优缺点3. 应用 二、NLLLoss损失函数1. 定义与原理2. 优点与注意3. 应用 三、MSELoss损失函数1. 定义与原理2. 优点与注意3. 应用 四、BCELoss损失函数1. 定义与原理2. 优点与注意3. 应用 五、CrossEntropyLoss损失函数1. 定义与原…...

git学习报告

文章目录 git学习报告如何配置vscode终端安装PowerShell安装 Microsoft.Powershell.Preview使用 git的使用关于团队合作 git指令本地命令&#xff1a;云端指令 git学习报告 如何配置vscode 安装powershell调教window终端&#xff0c;使其像Linux一样&#xff0c;通过Linux命令…...

Spring MVC的应用

目录 1、创建项目与maven坐标配置 2、核心配置 3、启动项目测试 4、不同请求参数在controller的配置 4.1 servlet API 4.2 简单类型 4.3 pojo类型 4.4 日期类型 4.5 restful风格4种操作类型 4.5.1 GET&#xff1a;获取资源 4.5.2 POST&#xff1a;新建资源 4.5.3 P…...

JavaEE: 深入探索TCP网络编程的奇妙世界(六)

文章目录 TCP核心机制TCP核心机制九: 面向字节流TCP核心机制十: 异常处理 小小的补充(URG 和 PSH)~TCP小结TCP/UDP 对比用UDP实现可靠传输(经典面试题) 结尾 TCP核心机制 上一篇文章JavaEE: 深入探索TCP网络编程的奇妙世界(五) 书接上文~ TCP核心机制九: 面向字节流 TCP是面…...

探秘 Web Bluetooth API:连接蓝牙设备的新利器

引言 随着物联网技术的快速发展&#xff0c;蓝牙设备在日常生活中扮演着越来越重要的角色。而在 Web 开发领域&#xff0c;Web Bluetooth API 的出现为我们提供了一种全新的方式来连接和控制蓝牙设备。本文将深入探讨 Web Bluetooth API 的使用方法和原理&#xff0c;帮助开发…...

Kubernetes Pod调度基础(kubernetes)

实验环境依旧是k8s快照&#xff0c;拉取本次实验所需的镜像文件&#xff1b; 然后在master节点上传已经编写好的yaml文件&#xff1b; 然后同步会话&#xff0c;导入镜像&#xff1b; pod控制器&#xff1a; 标签选择器--》标签&#xff1a; 标签&#xff1a; 在Kubernetes&…...

Angular由一个bug说起之十:npm Unsupported engine

我们在用npm下载包的时候&#xff0c;有时候会碰到这样的提示 这是npm的警告&#xff0c;说我们使用的nodejs版本与下载的包所要求的nodejs版本不一致。 这是因为有些包它对nodejs的版本有要求&#xff0c;然后就会在package.json文件里的engines字段里声明它所要求的nodejs版本…...

Android 开发高频面试题之——Flutter

Android开发高频面试题之——Java基础篇 flutter高频面试题记录 Flutter1. dart中的作用域与了解吗2. dart中. .. ...分别是什么意思?3. Dart 是不是单线程模型?如何运行的?4. Dart既然是单线程模型支持多线程吗?5. Future是什么6. Stream是什么7. Flutter 如何和原生交互…...

视频单目标跟踪研究

由于对视频单目标跟踪并不是很熟悉&#xff0c;所以首先得对该领域有个大致的了解。 视频目标跟踪是计算机视觉领域重要的基础性研究问题之一&#xff0c;是指在视频序列第一帧指定目标 后&#xff0c;在后续帧持续跟踪目标&#xff0c;即利用边界框&#xff08;通常用矩形框表…...

若依vue3.0表格的增删改查文件封装

一、因若依生成的文件没进行封装&#xff0c;维护起来比较麻烦。所以自己简单的进行封装了一下 gitee代码&#xff08;文件&#xff09;地址&#xff1a;https://gitee.com/liu_yu_ting09/ruo_yi.git 二、封装的方法&#xff08;下面绿色按钮进行全局封装一个JeecgListMixin.js…...

【已解决】如何使用JAVA 语言实现二分查找-二分搜索折半查找【算法】手把手学会二分查找【数据结构与算法】

文章目录 前言任务描述编程要求 输出样例:未查找到11元素&#xff01; 二、代码实现总结理解不了考试的时候直接背下来就好了。 前言 [TOC]二分搜索 任务描述 折半查找&#xff08;二分搜索&#xff09; 设a[low..high]是当前的查找区间&#xff0c;首先确定该区间的中点位置…...

ERROR 1524 (HY000): Plugin ‘mysql_native_password‘ is not loaded

你遇到的错误是由于 MySQL 版本不再默认支持 mysql_native_password 认证插件导致的。从 MySQL 8.0 开始&#xff0c;默认的认证插件是 caching_sha2_password&#xff0c;而不是 mysql_native_password。 解释&#xff1a; 错误 ERROR 1524 (HY000): Plugin mysql_native_pa…...

.NET 6.0 WebAPI 使用JWT生成Token的验证授权

1.引入相关程序包JwtBearer注意版本: 2.配置文件appsettings.json写相关配置参数(也可不写&#xff0c;写在程序里面&#xff0c;数据库读取也是一样的) , //JWT加密"JWTToken": {"SecretKey": "jsaduwqe6asdjewejdue7dfmsdfu0sdfmwmsd8wfsd6",…...

M9410A VXT PXI 矢量收发信机,300/600/1200MHz带宽

M9410A PXI 矢量收发信机 -300/600/1200MHz带宽- M9410A VXT PXI 矢量收发信机&#xff0c;300/600/1200MHz带宽支持 5G 的 PXI 矢量收发信机&#xff08;VXT&#xff09;是一个 2 插槽模块&#xff0c;具有 1.2 GHz 的瞬时带宽 主要特点 Keysight M9410A VXT PXIe 矢量收发…...

用工厂模式演示springboot三种注入方式 | @Autowired

背景&#xff1a;看了个demo工厂模式&#xff0c;示例代码的工厂类是new出来的&#xff0c;但是实际项目中都是用springboot框架、bean都是会给容器管理的&#xff0c;所以在思考这个工厂类要交给springboot托管要怎么改。以下是总结笔记 依赖注入 1.工厂类用new实现2.工厂类用…...

es查询语法

查询关键词的含义&#xff1a; match: 用于进行全文搜索&#xff0c;分析查询文本并与倒排索引中的词项进行匹配。 term: 精确匹配&#xff0c;适用于非分析字段&#xff0c;如 keyword 类型。用于查找字段值完全相等的文档。 bool: 组合多个查询条件。可以使用 must&#xf…...

LabVIEW提高开发效率技巧----合理使用数据流与内存管理

理使用数据流和内存管理是LabVIEW开发中提高性能和稳定性的关键&#xff0c;特别是在处理大数据或高频率信号时&#xff0c;优化可以避免内存消耗过大、程序卡顿甚至崩溃。 1. 使用 Shift Register 进行内存管理 Shift Register&#xff08;移位寄存器&#xff09; 是 LabVIE…...

【Linux】shell脚本忽略错误继续执行

在 shell 脚本中&#xff0c;可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行&#xff0c;可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令&#xff0c;并忽略错误 rm somefile…...

SCAU期末笔记 - 数据分析与数据挖掘题库解析

这门怎么题库答案不全啊日 来简单学一下子来 一、选择题&#xff08;可多选&#xff09; 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘&#xff1a;专注于发现数据中…...

sqlserver 根据指定字符 解析拼接字符串

DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...

反射获取方法和属性

Java反射获取方法 在Java中&#xff0c;反射&#xff08;Reflection&#xff09;是一种强大的机制&#xff0c;允许程序在运行时访问和操作类的内部属性和方法。通过反射&#xff0c;可以动态地创建对象、调用方法、改变属性值&#xff0c;这在很多Java框架中如Spring和Hiberna…...

Module Federation 和 Native Federation 的比较

前言 Module Federation 是 Webpack 5 引入的微前端架构方案&#xff0c;允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...

用机器学习破解新能源领域的“弃风”难题

音乐发烧友深有体会&#xff0c;玩音乐的本质就是玩电网。火电声音偏暖&#xff0c;水电偏冷&#xff0c;风电偏空旷。至于太阳能发的电&#xff0c;则略显朦胧和单薄。 不知你是否有感觉&#xff0c;近两年家里的音响声音越来越冷&#xff0c;听起来越来越单薄&#xff1f; —…...

代码随想录刷题day30

1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币&#xff0c;另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额&#xff0c;返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...

AGain DB和倍数增益的关系

我在设置一款索尼CMOS芯片时&#xff0c;Again增益0db变化为6DB&#xff0c;画面的变化只有2倍DN的增益&#xff0c;比如10变为20。 这与dB和线性增益的关系以及传感器处理流程有关。以下是具体原因分析&#xff1a; 1. dB与线性增益的换算关系 6dB对应的理论线性增益应为&…...

Linux 内存管理实战精讲:核心原理与面试常考点全解析

Linux 内存管理实战精讲&#xff1a;核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用&#xff0c;还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...

Go 语言并发编程基础:无缓冲与有缓冲通道

在上一章节中&#xff0c;我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道&#xff0c;它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好&#xff0…...