C语言:指针与内存

C语言:指针与内存

    • 内存与地址
    • 指针变量
      • 取地址
      • 指针变量
      • 解引用
      • 指针的大小
    • 指针运算
      • 指针 + - 整数
      • 指针 - 指针
      • 指针关系运算
    • const修饰指针
    • 字符指针
    • 野指针
    • assert断言
    • 传址调用


内存与地址

计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那这些内存空间如何⾼效的管理呢?
其实计算机会把内存划分为⼀个个的内存单元,以字节为一个基础的内存单元来进行管理。

每个内存单元也都有⼀个编号,有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。⽣活中我们把⻔牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起了新的名字叫:指针

计算机内是有很多的硬件单元,而硬件单元是要互相协同⼯作的。所谓的协同,⾄少相互之间要能够进⾏数据传递。但是硬件与硬件之间是互相独⽴的,那么如何通信呢?

CPU与内存是计算机中分别独立的硬件,如果想要让不同的硬件实现数据交互,那么就需要用电线连起来。而当CPU要访问内存的数据,就需要通过地址来确定访问的内存单元,这个传输地址的数据线就叫做地址总线。

每根电线有两种形态:有电/没电,此时32根地址线就可以表示 2 ^ 32种地址,64根地址线就可以表示2 ^ 64种地址。


指针变量

取地址

理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间,⽐如:

int a = 10;

上述的代码就是创建了整型变量a,内存中申请4个字节,⽤于存放整数10,其中每个字节都有地址。

那我们如何能得到a的地址呢?
这⾥就得学习⼀个操作符:& 取地址操作符

int a = 10;
&a;//取出a的地址

&a取出的是a所占4个字节中编址最小的字节的地址。


指针变量

那我们通过取地址操作符拿到的地址是⼀个数值,⽐如:0x006FFD70,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?
C语言提供了指针变量,用于存储地址。

int a = 10;
int * pa = &a;

这里的pa就是一个指针变量,其内部存储了a的地址。

这⾥pa左边写的是 int** 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型int类型的对象。

当一个指针指向类型 xxx,这个指针的类型就是 xxx*

比如一个指向char类型变量的指针:

char ch = 'w';
char* pc = &ch;

由于指针pc指向的类型是char,所以pc的类型就是char*


解引用

我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?
在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。
C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象。
获取指针指向对象的值的操作符叫解引⽤操作符*

int a = 100;
int* pa = &a;
*pa = 0;

上⾯代码中第3⾏就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间,*pa其实就是a变量了;所以*pa = 0这个操作相当于a = 0


指针的大小

地址是通过地址线传输的,对于32位机器,其有32根地址总线,那么就需要32个比特位来标识每根地址线的状态,需要4字节来存储指针;对于64位的机器,那么就需要64个比特位来标识每根地址线的状态,需要8字节来存储指针

32位计算机指针大小为4字节
64位计算机指针大小为8字节

注意指针内部只存储地址,与指针的类型无关,只要是指针,大小就是4/8字节


指针运算

指针也是可以进行运算的,指针有以下运算:

指针 + - 整数

指针对整数的 + - 用于到达下一个指针的位置。

示例:

int n = 10;int* pi = &n;printf("%p\n", pi);
printf("%p\n", pi + 1);

输出结果:

001EF868
001EF86C

通过16进制运算,可以发现两个地址之间相差4,而int类型刚好占用4字节。

再试试char*指针:

char c = 'w';char* pc = &c;printf("%p\n", pc);
printf("%p\n", pc + 1);

输出结果:

00FFFE77
00FFFE78

通过16进制运算,可以发现两个地址之间相差1,而char类型刚好占用1字节。

指针指向的类型占用多少个字节,那么指针+ -整数时就以多少个字节为单位

指针 - 指针

指针 - 指针得到两个指针之间的距离。

示例:

int arr[10] = { 0 };int* p1 = &arr[0];
int* p2 = &arr[5];
int x = p2 - p1;printf("%d", x);

以上示例中,p1指向了数组的第1个元素,p2指向了数组的6个元素,此时两个指针相减得到多少?是指针之间的元素个数5,还是指针之间的字节数4 * 5 = 20

输出结果:

5

可以看到,指针之间的减法也与类型是有关的,指针相减得到的是两个指针有几个指向的元素,而不是单纯的地址相减

再看一个案例:

int arr[10] = { 0 };int* p1 = &arr[0];
int* p2 = &arr[5];
int x = (char*)p2 - (char*)p1;printf("%d", x);

与刚才代码的唯一区别就是,我们在(char*)p2 - (char*)p1做指针减法的时候,将两个指针转化为了char*类型,此时输出结果是多少?

输出结果:

20

因为char*的指针指向的类型占用1字节,所以5个int类型的空间可以存储20个char,而我们将指针从int*转化为了char*,计算规则就从原来计算可以存放几个int,变成了可以存放几个char了。

指针关系运算

指针关系运算

< 和 > 比两个指针的地址大小
== 和 != 判断两个指针是否相等


const修饰指针

当一个变量被const修饰,那么这个变量就不能被修改。指针也可以被const修饰,但是它的修饰规则不太一样。

怎么样才算修改指针呢?
当我们获得一个指针变量,我们可以修改指针指向内容的值,比如这样:

int a = 10;
int* pa = &a;
*pa = 5;

我们通过指针把变量a的值修改了。

我们也可以修改指针的指向:

int a = 10;
int* pa = &a;
pa = &b;

此时pa这个指针从指向变量a,变成了指向变量b

那么const修饰时,到底时禁止哪一项不能修改?
这就需要讲解指针的特殊的const修饰规则了:

const放在 * 左边,指针指向的内容不能被指针修改

示例1:

const int* pa = &a;pa = &b;//允许修改
*pa = 5;//不允许修改

示例2:

int const* pa = &a;pa = &b;//允许修改
*pa = 5;//不允许修改

对指针来说,const可以放在int左边,也可以放在int右边,效果是一样的。

当const放在 * 右边,指针的指向不能改

示例:

int a = 10;
int b = 5;
int* const pa = &a;pa = &b;//不允许修改
*pa = 5;//允许修改

字符指针

看到一串代码:

const char* p = "hello";

char*类型的指针用于存放char类型的数据,但是以上代码却可以把一个字符串存进char*类型的指针中,这是为什么?

所有常量字符串做表达式时,本质都是首个字符的地址

因此我们也可以把字符串当作指针来输出:

printf("%p", "hello");

输出结果:

00C57BD8

可以看出, "hello"这个字符串整体,代表了一个指针。

所以const char* p = "hello";的本质其实是把字符串“hello”的第一个元素‘h’的地址交给了p指针。

为什么要加const修饰指针?
用双引号引起来的字符串叫做常量字符串,存储在静态区不可以修改,所以在用指针接收时,需要const修饰常量字符串做表达式时,本质都是首个字符的地址。


野指针

野指针的概念
内存中的一块空间在使用前,是需要申请的。而指针作为直接访问内存的一种手段,当指针指向到、没有申请的空间,那么这就是一个野指针。

野指针的成因

指针没有初始化

int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;

此处的p指针,没有设置初始值,就直接解引用了。此时指针的值就是一个随机值,而随机的地址,很有可能就访问到了没有向内存申请的空间,故p是一个野指针。

指针越界访问

int arr[10] = {0};
int *p = &arr[0];for(int i = 0; i <= 11; i++)
{*(p++) = i;
}

以上代码中,我们用指针对数组进行了遍历,但是数组的下标是从0-9的,而我们访问到了10与11的位置,此时超过10的空间没被数组申请,访问到了没有申请的空间,p就是野指针了。

指针指向的空间被释放

int* test()
{int n = 100;return &n;
}
int main()
{int*p = test();printf("%d\n", *p);return 0;
}

此时的指针看似指向了&n,即n的地址。但是函数test调用时,会创建独立的栈帧,当函数调用结束,函数内部的变量n也会一起销毁,所以此时的n已经被释放了。p指向了被释放的空间,变成一个野指针。


如果我们的指针一开始没有想好赋什么值,那我们就得到了一个野指针,我们有没有办法让一个没有想好值的指针不是野指针呢?此时就需要空指针了。

NULL是C语言中的空指针,它本质上是数值为0的地址。

当我们没想好一个指针给什么值的时候,就可以给一个空指针:

int* ptr = NULL:

assert断言

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

assert(p != NULL);

上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。

assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:
它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG

#define NDEBUG
#include <assert.h>

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

assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。⼀般我们可以在debug中使⽤,在release版本中选择禁⽤assert就⾏,这样在debug版本写有利于程序员排查问题,在release版本不影响⽤⼾使⽤时程序的效率。


传址调用

写⼀个函数,交换两个整型变量的值:

思考后,你可能会给出这样的答案。

void Swap(int x, int y)
{int tmp = x;x = y;y = tmp;
}

在main函数中调用试试:

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;
}

输出结果:

10 20
交换前:a=10 b=20
交换后:a=10 b=20

我们发现其实没产⽣交换的效果,这是为什么呢?

main函数内部,创建了ab,在调⽤Swap函数时,将ab传递给了Swap函数,在Swap函数内部创建了形参xy接收ab的值。xy确实接收到了ab的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于xy是独⽴的空间,它们只得到了实参的值
那么在Swap函数内部交换xy的值,⾃然不会影响ab。当Swap函数调⽤结束后回到main函数,ab其实没交换。
Swap函数在调用的时候,是把变量本⾝的值传递给了函数,这种叫传值调⽤。

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

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

修改后代码:

void Swap2(int*px, int*py)
{int tmp = *px;*px = *py;*py = tmp;
}

输出结果:

10 20
交换前:a=10 b=20
交换后:a=20 b=10

我们可以看到实现成传递地址的的⽅式,顺利完成了任务,这⾥调⽤Swap函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤


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

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

相关文章

rk3568 WDG

rk3568 WDG 在嵌入式系统中,看门狗(Watchdog)是一种用于监控系统运行状态并在系统出现故障或死锁时自动重启系统的机制。 监控系统运行状态:看门狗定时器会周期性地计数,在规定的时间内如果系统未能对看门狗进行喂狗操作(即重置看门狗计数器),就会认为系统出现故障或死…

探索JDK5的新特性:革新编程体验的里程碑

引言 Java Development Kit (JDK) 5&#xff0c;于2004年发布&#xff0c;是Java发展历程中具有里程碑意义的一个版本。它不仅引入了一系列关键的编程新特性&#xff0c;还大大提升了开发者的生产力和代码质量。本文将带领大家深入了解JDK 5带来的变革性创新&#xff0c;包括泛…

自定义异常处理演示

​ 为了防止黑客从前台异常信息&#xff0c;对系统进行攻击。同时&#xff0c;为了提高用户体验&#xff0c;我们都会都抛出的异常进行拦截处理。 一、全局异常处理 编写一个异常拦截类&#xff0c;如下&#xff1a;ControllerAdvice&#xff0c;很多初学者可能都没有听说过…

《最新出炉》系列初窥篇-Python+Playwright自动化测试-19-处理鼠标拖拽-中篇

1.简介 上一篇中&#xff0c;主要是介绍了拖拽的各种方法的理论知识以及实践&#xff0c;今天宏哥讲解和分享一下划取字段操作。例如&#xff1a;需要在一堆log字符中随机划取一段文字&#xff0c;然后右键选择摘取功能。 2.划取字段操作 划取字段操作就是在一段文字中随机选…

Linux系统——http协议介绍

目录 引言——Internet起源 一、http协议——超文本传输协议 1.http相关概念 2.访问浏览器的过程 3.http协议通信过程 4.http相关技术 4.1WEB开发语言 4.2html 4.3CSS 4.4JS 5.MIME——Multipurpose Internet Mail Extensions 多用途互联网邮件扩展 6.URI URN URL的…

ubuntu配置pip

windows的pip镜像 在C:\Users\当前用户\下创建.pip文件夹在.pip下创建pip.ini文件编辑pip.ini添加如下内容 [global] index-url https://mirror.baidu.com/pypi/simple [install] trusted-host https://mirror.baidu.com/pypi ubuntu的pip镜像 当前用户的pip镜像 1. 进入用…

申请 Segfault 的免费 VPS

打开 Segfault 的官网&#xff1a;https://shell.segfault.net/&#xff0c;然后点击“I’m new here”在弹出的窗口中&#xff0c;复制以保存访问密钥点击 continue&#xff0c;进入主页&#xff0c;然后敲回车以创建服务器如需重返回自己的服务器&#xff0c;可以在官网点击“…

【Redis,Java】Redis的两种序列化方式—nosql数据库

redis和mysql的区别&#xff1a; redis是属于nosql的数据库&#xff0c;而mysql是属于sql数据库&#xff0c;redis是属于nosql数据库。mysql是存储在磁盘中的&#xff0c;redis是存储在内存中的&#xff0c;所以redis的读取书读快。这里所说的redis代表nosql&#xff0c;而mysq…

【wails】(1):使用go做桌面应用开发,wails框架入门学习,在Linux上搭建环境,运行demo项目,并打包测试

1&#xff0c;视频地址 https://www.bilibili.com/video/BV1fK421b7QC/ 【wails】&#xff08;1&#xff09;&#xff1a;使用go做桌面应用开发&#xff0c;wails框架入门学习&#xff0c;在Linux上搭建环境&#xff0c;运行demo项目&#xff0c;并打包测试 2&#xff0c;参考…

在Ubuntu中使用python

目录 一、利用vim使用python 1、下载vim 2、使用vim创建python文件 3、编辑完成后的vim操作 4、如何运行 5、vim常见操作 二、安装Jupyter 1、更新系统 2、安装pip 注&#xff1a;pip无法应用的原因及解决方案 3、安装Jupyter 4、打开Jupyter 三、安装其他Python模…

ActiveMQ高可用架构涉及常用功能整理

ActiveMQ高可用架构涉及常用功能整理 1. activemq的集群模式2. 镜像模式高可用系统架构和相关组件2.1 架构说明2.2 相关概念说明2.3 消息模型2.3.1 点对点2.3.2 发布订阅 3. activemq常用命令4. activemq配置集群5. 疑问和思考5.1 activemq的数据删除策略是怎样的&#xff1f;5…

C++ 中的单例模式singleton

C 中的单例模式 引言 在面向对象编程中&#xff0c;设计模式是解决常见问题的最佳实践。单例模式是其中之一&#xff0c;它确保一个类只有一个实例&#xff0c;并提供一个全局访问点来获取该实例。在本文中&#xff0c;我们将详细介绍 C 中的单例模式。 什么是单例模式&…

【软考问题】-- 1 - IT知识 - 信息化发展

一、基础问题 问题1:信息系统的生命周期可以简化为哪5个阶段? (1)系统规划(可行性分析与项目开发计划)(2)系统分析(需求分析) (3)系统设计(概要设计、 详细设计) (4)系统实施(编码、 测试) (5)系统运行和维护问题2:国家信息化体系六要素分别是什么? 1信息…

ChatGPT-用ChatGPT指令,自学任何领域的系统知识

1. 指令位置 Github仓库&#xff1a;Mr Ranedeer AI Tutor 但是需要开通chatgtp plus版本&#xff0c;并且打开代码解释器 2 使用 学习内容 开始学习 GPT甚至可以给你思考题&#xff0c;给出的答案还能进行评价 配置 通过配置表修改 深度 学习风格 沟通风格 语气风格 …

探索Nginx:一款高效、稳定的Web服务器和反向代理工具

在网站性能优化和架构设计中&#xff0c;Nginx以其高性能、低资源消耗和良好的扩展性成为了许多开发者和服务器管理员的首选。本文将为您详细介绍Nginx的概念、特点、安装、配置和使用&#xff0c;帮助您更好地了解并运用这款优秀的工具。 一、Nginx简介 Nginx&#xff08;发…

基于RWKV架构推理成本大降:Eagle 7B模型的十倍效能提升

前言 在今天这个数据驱动的时代&#xff0c;大型语言模型&#xff08;LLM&#xff09;在处理自然语言处理&#xff08;NLP&#xff09;任务时的效能和效率成为了众多研究者和工程师关注的焦点。尤其是在推理成本日益攀升的背景下&#xff0c;如何在保持甚至提升模型性能的同时…

【Java】数据类型与变量

1.数据类型 在Java中数据类型主要分为两类&#xff1a;基本数据类型和引用数据类型。 基本数据类型有四类八种&#xff1a; 四类&#xff1a;整型、浮点型、字符型以及布尔型八种&#xff1a; 注意&#xff1a;不论是在16位系统还是32位系统&#xff0c;int都占用4个字节&am…

js设计模式:原型模式

作用: 使用js特有的原型链机制,可以通过Object.create方法创建新对象,将一个对象作为另外一个对象的原型 也可以通过修改原型链上的属性,影响新对象的行为 可以更方便的创建一些对象 示例: let obj {getName: function(){return this.name},getAge:function(){return this…

VINS-FUSION 在Opencv4下编译报错,且ceres也报错

Opencv报错 /home/monica/Documents/code/vins-fusion-old-ws/src/VINS-Fusion-master/camera_models/src/chessboard/Chessboard.cc:20:38: error: ‘CV_GRAY2BGR’ was not declared in this scope 20 | cv::cvtColor(image, mSketch, CV_GRAY2BGR); | …

代码随想录算法训练营29期|day55 任务以及具体安排

第九章 动态规划part12 309.最佳买卖股票时机含冷冻期 class Solution {public int maxProfit(int[] prices) {//0代表持股票&#xff0c;1代表保持卖出状态&#xff0c;2代表卖出股票。3代表冷冻int[][] dp new int[prices.length][4];dp[0][0] -prices[0];for(int i 1 ; …