《嵌入式Linux与物联网软件开发——C语言内核深度解析》一1.5 C语言如何操作内存

简介: 本节书摘来自华章出版社《嵌入式Linux与物联网软件开发——C语言内核深度解析》一书中的第1章,第1.5节,作者朱有鹏 , 张先凤,更多章节内容可以访问云栖社区“华章计算机”公众号查看。 1.5 C语言如何操作内存 1.5.1 C语言对内存地址的封装 前面一直谈内存,其间虽然穿插一些C语言的内容,但还不够细致、深入。

本节书摘来自异步社区《嵌入式Linux与物联网软件开发——C语言内核深度解析》一书中的第1章,第1.5节,作者朱有鹏 , 张先凤,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

1.5 C语言如何操作内存

1.5.1 C语言对内存地址的封装

前面一直谈内存,其间虽然穿插一些C语言的内容,但还不够细致、深入。本节将深入分析C语言的内存地址封装,如用变量名来访问内存、数据类型的含义、函数名的含义。下面以C代码实例分析。

int a;
a = 5;
a += 4;            // 结果a等于9

下面结合内存来解析C语言语句的本质。

int a:编译器帮我们申请了一个int类型的内存格子(长度是4字节,地址是确定的,但是只有编译器知道,我们是不知道的,也不需要知道),并且把符号a和这个格子绑定。

a = 5:编译器发现我们要给a赋值,就会把这个值5丢到符号a绑定的那个内存格子中。

a += 4:编译器发现我们要给a加值,a += 4等效于a = a + 4,编译器会先把a原来的值读出来,然后给这个值加4,再把加之后的和写入a里面去,最后这个格子里面存储的内容就是9。

C语言中数据类型的本质含义,是表示一个内存格子的长度和解析方法。

数据类型决定长度的含义,如一个内存地址(0x30000000),本来只代表一个字节的长度,但是实际上我们可以通过给它一个类型(int),让它有了长度(4),这样这个代表内存地址的数字0x30000000,就能表示从这个数字开头的连续n(4)个字节的内存格子了,即0x30000000 + 0x30000001 + 0x30000002 + 0x30000003。

数据类型决定解析方法的含义是:假如有一个内存地址(0x30000000),我们可以通过给这个内存地址不同的类型来指定这个内存单元格子中二进制数的解析方法。如int的含义就是0x30000000 + 0x30000001 + 0x30000002 + 0x30000003这四个字节连起来共同存储的是一个int型数据;那float的含义就是0x30000000 + 0x30000001 + 0x30000002 + 0x30000003这四个字节连起来共同存储的是一个float型数据。

int a;时,编译器会自动分配一块内存出来,假设这里是32位操作系统,那么int就是4个字节,如果这块内存的第一个字节(首字节)地址为0x12345678,编译器会将变量名a与这个首字节地址绑定,对a进行存取与操作,实际上就是向0x12345678开始的4个字节空间进行读写操作。

float a;     
(int *)a;         // 等价于分配一块指针类型的空间,并且把地址和变量名关联

最后我们谈谈C语言中的函数,不知道你是否思考过C语言中函数调用是如何实现的,主调函数是如何找到那些被调函数的。在C语言中,函数就是一段代码的封装。函数名的实质就是这一段代码的首地址,所以说函数名的本质也是一个内存地址。有了函数名(指针),也就是有了地址,我们才实现了函数的调用。

1.5.2 用指针来间接访问内存

我们常常把C语言中的指针列为难点对象,很多人始终搞不清楚指针,其实这就需要你掌握我们前面讲的那些东西。下面我们就以前面的知识为基础,啃下这根难啃的骨头。

指针是什么?我们的回答是指针就是地址。说得再全面一点,指针是一个变量,且这个变量是专门用来存放地址的。这就好比你想给A打电话,但你不知道A的电话号码,但你知道C有A的电话号码,而且你也有C的电话,这样你就可以间接地通过C来找到A。指针也是如此。通过下面的例子我们就可以看出用指针变量p来间接地获取了变量a的内容。

# include <stdio.h>
   int main (void)
   {
       int a=5;            // 开辟一块整型类型的内存空间,里面放入数据5
                              并且把该内存空间的地址和变量名a相关联
       int *p=&a;          // 开辟一块指针类型的内存空间,里面放入a的
                           // 地址数据,并且把空间地址和变量名p相关联
       printf("%d",*p);    // *是取内容符,也就是取一个地址所对应空
                           // 间里面的内容,*p就是*(a的地址)也就是取a地
                           // 址所对应空间里的内容,那就是5
       return 0;
   }

1.5.3 指针类型的含义

C语言中的指针,全名叫指针变量,指针变量其实和普通变量没有任何区别(不管int float等,还是指针类型int 或者float 等)。只要记住:类型只是对其所修饰的数字或者符号所代表内存空间的长度和解析方法的规定。如int a和int p其实没有任何区别,a和p都代表一个内存地址(如0x20000000),但是这个内存地址0x20000000的长度和解析方法不同。a和b的空间大小虽然都是4个字节(碰巧),但是解析方法是截然不同的,前者解析方法是按照int的规定来的;后者按照int 方式解析。对于int *p来说,表示变量p里面存放的是一个地址,这个地址指向的空间用于存放一个int型的整数。

1.5.4 用数组来管理内存
数组和变量其实没有本质区别,只是符号的解析方法不同(普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,只是解析方法不一样)。我们知道数组定义就是在内存中开辟一块连续的内存空间,并且数组名就是其开辟的内存空间的首地址。也就是说,数组名就相当于一个指针,它里面放着这个数组的首元素地址(如&a[0])。由此定义我们可以知道,定义数组就是一次性定义一堆变量,并且这些变量在内存中开辟的空间地址是连续的,第一个变量a[0]的地址记录在数组名a中。

int a;         // 编译器分配4字节长度给a,并且把首地址和符号a绑定起来
  int a[10];     // 编译器分配40个字节长度给a,并且把首元素首地址和符号a绑定起来

下面我们做个实验:为了大家更加容易理解,我们把一个数组中的元素地址输出看看。

#include <stdio.h>
  int main (void)
  {
      int a[5]={5,4,3,2,1};//定义数组
      printf("%p,%p,%p,%p,%p,%p\n",a,&a[0],&a[1],&a[2],&a[3],&a[4]);
      // %p是以地址形式输出(也就是16进制)。在我的计算机中运行
      // 后输出的结果是:
      // 0018FF34,0018FF34,0018FF38,0018FF3C,0018FF40,0018FF44
      // 我们可以看出,在当前编译器下,编译器为每一个元素分配4
      // 个字节的空间
      // 并且它们的地址是连续的。也可以看出数组名就是首元素
      // 的地址
      return 0;
  }

下面我们给出对应上面程序的内存逻辑图,前面为了便于大家理解,内存地址全部是十进制,实际上,我们一般用十六进制表示内存地址。


2b54f606a39d8dd6a44b6f673b04c771eb35e537

8 位内存逻辑模型

数组中第一个元素(a[0])称为首元素;每一个元素类型都是int,所以长度都是4,其中第一个字节的地址称为首地址;首元素a[0]的地址称为首元素地址。由上面的图可以看出,第三个元素的地址也就是a[2]的地址,可以由第一个元素a[0]的地址推出,因为数组元素的地址是连续的。也就是a[0]的地址(a)加1就是a[1]的地址,加2就是a[2]的地址。可能此时你就有疑问了,不对啊,a[0]的地址加1不是0x0018FF35吗?其实不是的,我们一直强调数据类型的重要性,其中一点就是解析方法。int*的数据类型,编译器解析的时候就知道是以4个字节的跨度来依次分配。你写的a+1,编译器就会明白是指a[1]的地址,也就是首地址加4。你写a+2,编译器就会明白是a[2]的地址,也就是首地址加8,依次类推。如果你还是不够明白,我们举个例子。当你确定我们是一个人的时候,那我们就是一个完整的人,此时对于我们的划分就是一个人、两个人, 不能是1/2个、1/3个。当然我们不是单纯地为了研究数组中元素的地址,我们的最终目的是想通过元素中的地址找到该元素的值。

#include <stdio.h>
int main (void)
{
      int a[5]={5,4,3,2,1};    // 定义数组
      printf("%p,%p,%p,%p\n",a,a+1,a+2,a+3);
      // 输出结果是:0018FF34,0018FF38,0018FF3C,0018FF40
      // 以首地址为基础,依次加1,加2,加3,也就是a[1],a[2],a[3]的地址
      printf("%d,%d,%d,%d\n",a[0],*a,*(a+1),*(a+3));
      // 第一个应该输出5,也就是首元素a[0]
      // 第二个也是5,原因是a相当于指针变量,里面放着a[0]的地址
      // 所以取该地址所对应的内容,也就是a[0]的值
      // 第三个值是4,*(a+1)取(a+1)地址里面的内容,也就是a[1]的值4
      // 第四个输出是2,取a+3后的地址所对应的内容,a+3后的地址也就
      // 是a[3]的地址。取其内容就是a[3]的值:2
      return 0;}
相关文章
|
6天前
|
安全 Linux 测试技术
Intel Linux 内核测试套件-LKVS介绍 | 龙蜥大讲堂104期
《Intel Linux内核测试套件-LKVS介绍》(龙蜥大讲堂104期)主要介绍了LKVS的定义、使用方法、测试范围、典型案例及其优势。LKVS是轻量级、低耦合且高代码覆盖率的测试工具,涵盖20多个硬件和内核属性,已开源并集成到多个社区CICD系统中。课程详细讲解了如何使用LKVS进行CPU、电源管理和安全特性(如TDX、CET)的测试,并展示了其在实际应用中的价值。
|
19天前
|
Ubuntu Linux 开发者
Ubuntu20.04搭建嵌入式linux网络加载内核、设备树和根文件系统
使用上述U-Boot命令配置并启动嵌入式设备。如果配置正确,设备将通过TFTP加载内核和设备树,并通过NFS挂载根文件系统。
68 15
|
1月前
|
存储 网络协议 编译器
【C语言】深入解析C语言结构体:定义、声明与高级应用实践
通过根据需求合理选择结构体定义和声明的放置位置,并灵活结合动态内存分配、内存优化和数据结构设计,可以显著提高代码的可维护性和运行效率。在实际开发中,建议遵循以下原则: - **模块化设计**:尽可能封装实现细节,减少模块间的耦合。 - **内存管理**:明确动态分配与释放的责任,防止资源泄漏。 - **优化顺序**:合理排列结构体成员以减少内存占用。
167 14
|
1月前
|
存储 编译器 C语言
【C语言】数据类型全解析:编程效率提升的秘诀
在C语言中,合理选择和使用数据类型是编程的关键。通过深入理解基本数据类型和派生数据类型,掌握类型限定符和扩展技巧,可以编写出高效、稳定、可维护的代码。无论是在普通应用还是嵌入式系统中,数据类型的合理使用都能显著提升程序的性能和可靠性。
63 8
|
1月前
|
算法 Linux
深入探索Linux内核的内存管理机制
本文旨在为读者提供对Linux操作系统内核中内存管理机制的深入理解。通过探讨Linux内核如何高效地分配、回收和优化内存资源,我们揭示了这一复杂系统背后的原理及其对系统性能的影响。不同于常规的摘要,本文将直接进入主题,不包含背景信息或研究目的等标准部分,而是专注于技术细节和实际操作。
|
1月前
|
存储 算法 C语言
【C语言】深入浅出:C语言链表的全面解析
链表是一种重要的基础数据结构,适用于频繁的插入和删除操作。通过本篇详细讲解了单链表、双向链表和循环链表的概念和实现,以及各类常用操作的示例代码。掌握链表的使用对于理解更复杂的数据结构和算法具有重要意义。
665 6
|
1月前
|
存储 缓存 网络协议
Linux操作系统的内核优化与性能调优####
本文深入探讨了Linux操作系统内核的优化策略与性能调优方法,旨在为系统管理员和高级用户提供一套实用的指南。通过分析内核参数调整、文件系统选择、内存管理及网络配置等关键方面,本文揭示了如何有效提升Linux系统的稳定性和运行效率。不同于常规摘要仅概述内容的做法,本摘要直接指出文章的核心价值——提供具体可行的优化措施,助力读者实现系统性能的飞跃。 ####
|
1月前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####
|
1月前
|
存储 网络协议 算法
【C语言】进制转换无难事:二进制、十进制、八进制与十六进制的全解析与实例
进制转换是计算机编程中常见的操作。在C语言中,了解如何在不同进制之间转换数据对于处理和显示数据非常重要。本文将详细介绍如何在二进制、十进制、八进制和十六进制之间进行转换。
60 5
|
1月前
|
C语言 开发者
【C语言】断言函数 -《深入解析C语言调试利器 !》
断言(assert)是一种调试工具,用于在程序运行时检查某些条件是否成立。如果条件不成立,断言会触发错误,并通常会终止程序的执行。断言有助于在开发和测试阶段捕捉逻辑错误。
52 5

相关产品

  • 物联网平台