C语言之内存和位操作
Posted 阿C_C
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言之内存和位操作相关的知识,希望对你有一定的参考价值。
内存和程序运行
程序运行的目的是为了得到特定的结果,计算机本质上是用于计算的,既然是用于计算,就需要参与计算的数据,那这些数据就存储在内存中,计算之前参与运算的数据以及运算之后得到的数据,都存储在内存中。
程序运行无外乎两种目的,一种是为了得到某种结果,另外一种是为了执行某一种过程,在C语言中返回值void类型的函数就是为了执行某一种过程,有具体返回值的函数就是为了得到某种结果。
计算机程序的运行过程,就是很多函数相继执行的过程,程序的本质就是函数,是由很多函数组成的,函数本质上是加工数据的动作。
哈佛和冯诺依曼结构
哈佛结构和冯诺依曼结构是CPU程序运行的两种结构,分别表示数据和代码存放规则。代码指的就是操作数据的动作,也就是函数,而数据就指的是全局变量以及函数中的局部变量。
哈佛结构
数据和代码分开存放
冯诺依曼结构
数据和代码放在一起
动态内存和静态内存
动态内存需要初始化之后才能使用,静态内存不需要初始化就可以使用,读写速度较快。
程序的内存管理
内存用于存储可变数据,在程序中数据就是程序的全局变量和局部变量,内存是程序的本质性需求,所以内存管理是写程序中很重要的话题,堆积计算器来说内存容量越大,可操作任务就越多,所以理论来说内存越大越好,在写程序时对内存的管理就成了大问题,如果管理不善可能造成程序消耗过多内存,当内存消耗殆尽程序就会崩溃,所以内存管理对程序来说是一个重要的技术和话题。
操作系统掌管所有的硬件内存,操作系统把内存分成一个一个内存块,一般是4K的块大小,也叫作页,页内用更细小的字节单位来管理。操作系统管理内存的原理过于晦涩和难以理解,操作系统提供了内存管理的接口,我们只需要调用这些API即可管理这些内存。
在没有操作系统的裸机程序上,程序需要直接操作内存,编程人员需要自己计算内存的使用和安排,所以如果不小心计算和使用错误,则程序就会变得很麻烦
不同的编程语言提供不同的内存操作接口,例如汇编中根本没有任何内存管理,需要程序员自己直接使用内存地址来操作,C语言中编译器管理直接的内存地址,程序员通过编译器提供的变量名来访问内存,如果需要大块的内存可以通过API(malloc和free)来访问内存,但是如果在裸机程序中需要使用大块内存,需要定义基本的数组来解决,在更高级的面向对象编程语言中,可以使用new关键字创建对象来分配内存,在Java语言中甚至有垃圾回收器回收不再使用的对象来自动释放内存
位,字节,半字,字以及内存位宽
从硬件来讲,内存实际上是计算机上的一个配件,内存还可以分为SRAM和DRAM,DRAM又可以分为很多代,例如SDRAM,DDR,以及LPDDR等,从逻辑上说,内存可以随机访问,并且可以读写,由于内存可以随机访问,所以在编程中最适合用于存放变量,可以说有了内存C语言才可以定义变量,一般C语言中的一个变量,对应内存中的一个存储单元
内存的编程模型
内存可以想象成一个个排列的单元格,每个单元格都有其特定的地址,且永久绑定,内存地址理论上可以有无限大,但是受制于CPU的位数,32位的CPU的内存地址范围最大为2的32次方,也就是4G的内存,64位的CPU则可以有2的64次方的内存地址范围
内存位宽
硬件内存的实现是有宽度限制的,有8位宽的内存,也有16位宽的内存,内存芯片之间可以并联,并联之后即使是8位的内存芯片,也可以做出16位或者32位的硬件内存,逻辑上,内存位宽是任意的,不论内存位宽是多少,对内存的操作不构成影响,但是由于操作需要硬件去执行,所以对内存的操作是受限于内存硬件的。
数据大小
- 位:指的是一个bit位
- 字节:byte,8个bit的大小
- 字:一般是32bit
- 半字:一般是16bit
字和半字根据平台是有所不同的,在编程时用到的较少。
内存编址和寻址
内存地址的编排,和内存中的单元格相关,每个单元格都有一个唯一的编号,这个编号就是内存地址,该内存地址和单元格的空间是一一对应且永久绑定的,程序运行的时候CPU只认识内存地址,不会去关心这个地址对应的单元空间在哪里,如何分布等实际问题,因为硬件设计时就保证了根据该地址就可以找到该单元格,所以内存单元有地址和空间两个重要概念,每个内存单元的地址和空间是对应的
内存编址是以字节为单位的,也就是说,任意一个内存地址,都指的是位于该地址的内存单元格空间,比如0x00211100这个内存地址,代表的是位于硬件内存上的第0x00211100个这个单元格,它的容量是一个字节,也就是8bit
内存和数据类型
C语言的基本数据类型,都和占用的内存长度有相对应的关系
- int属于整数类型,和CPU本身的数据位宽是一样的,32位的CPU,一个int数据类型就是32bit,
- 数据类型用于定义变量,这些变量运行在内存中,所以数据类型必须和内存相匹配才能获取最好的性能,否则可能不工作或者效率低下,例如在32位系统中使用int定义变量效率最高,因为此时的int本身就是32位的。
内存对齐
内存对齐是一个硬件问题,不同位宽内存硬件在逻辑上有相关性,例如32位的内存,则每4个字节之间是具有逻辑相关性,内存访问时如果跨逻辑相关空间来访问,会额外的消耗资源,如果把访问对齐,每次都按照逻辑相关空间的方式访问,则效率会发挥的最好,非对齐的访问会导致效率大打折扣。
C语言操作内存
C语言对内存的操作做了封装,在C语言中使用变量名代表了某一块内存地址,C语言中的数据类型,表示存储该数据需要的内存单元格的长度,以及解析方式,C语言中的函数,就是一段代码的封装,实质是这段代码的首地址,所以说,函数名本身就是一个内存地址。
使用指针来访问内存,其实指针也是一种数据类型,所以说指针类型有自身要求的内存单元格长度和解析方式,使用指针来访问内存,实际上是使用指针类型的解析方式来解析指针值开始的那段内存地址,解析的长度就是指针类型的长度,所以从本质来说,使用指针来访问内存和用变量访问内存是一样的,都代表一段内存地址,只是对该内存地址的解析方法和解析长度不一样而已。
使用数组管理内存和使用变量使用内存其实本质上也是一样的,只是对内存地址的解析方式和长度不一样。解析数组时,从数组首元素的地址开始,按照数组中的元素类型开始解析,连续解析数组长度个元素,就得到了该数组的全部数据。
内存和数据结构
数据结构本质上指的是数据在内存中如何排布,如何加工的问题。
数组
数组是最基本的数据结构,用于存储一系列类型相同,意义相关的变量,数组在内存中是连续排布的,占用了一块连续的内存空间。
数组比较简单,可以使用下标进行随机访问,但是数组需要其中的元素的数据类型都是一样的,并且在定义数组时必须给出该数组的大小,并且大小一旦确定之后就不能更改,没有伸缩性。
结构体
结构体可以看成是一个复杂的数组,目的是为了解决数组中元素的数据类型必须一致的限制,在存储一系列元素数据类型不一致的变量时,就必须使用结构体来存储。
结构体内嵌指针
在结构体定义的时候,包含一些变量指针和函数指针,指向某个变量或者函数,则就可以实现出类,成员变量以及成员方法等面向对象的特性。
栈
栈是在C语言中的一种数据结构,用于管理内存,可以自动的帮助我们分配一些小块内存,栈具有先入后出的特性,栈低指针始终指向栈的开始,栈顶指针可以进行上下移动,向栈中压栈数据时栈顶指针向上移动一位
栈和局部变量
在C语言中,局部变量就是使用栈来实现的,在C中定义一个局部变量,编译器会在栈中寻找一块内存,将该局部变量压入栈对应的内存中,这个动作是栈自动完成的,不需要写代码来操作这一个过程,在函数退出的时候,栈将其中的元素从栈顶开始依次弹出,所有的局部变量就自动销毁了。
使用栈来管理内存,不需要额外写代码来操作,内存分配和回收由C语言自动完成,在定义局部变量时,由于变量所在的内存空间在栈中,栈里边的内存空间是进进出出反复使用的,如果没有进行初始化,则该局部变量在栈中的内存空间可能存放的还是上一次弹出去的变量的值,所以这次新的变量的值就是一个随机的未知数,这样就是局部变量未初始化值是随机数的原因。
栈的约束
栈是有固定大小的,栈的大小不好设置,太小怕溢出,太大怕浪费,所以栈不够灵活,在局部变量定义过多或者过大时,或者递归层级过多时,就有可能造成栈溢出
堆
堆是内存的另外一种管理方式,堆管理没存比较自由,可以随时申请和释放,内存的大小可以根据需要自定义,操作系统通常把内存划分区块,把一部分内存区域分配给堆来管理,堆内存并不属于某一个进程,由堆管理器进行管理,进程调用堆管理器提供的内存管理API(malloc和free)来分配和释放内存。
堆内存通常适用于内存容量较大,需要反复使用和释放的情况,很多数据结构也是使用堆来实现的。
堆内存特点
- 堆内存通常不限于容量
- 申请和释放都需要程序员手工进行,在程序代码中调用malloc以及free函数进行内存分配和释放,如果申请了内存没有释放,则该段内存丢失,会造成内存泄露
- 堆内存比较灵活,空间大小可以随意控制,但是由于过于灵活,需要程序员处理的细节较多,所以容易出错。
堆管理器接口
- free:堆内存释放使用free函数,free(void *ptr)
- malloc:分配内存,单位为字节,void *malloc(size_t size)
- calloc:批量分配内存,void *calloc(size_t nmemb,size_t size)
- realloc:重新分配内存,改变原来申请内存的空间大小,void *realloc(void *ptr,size_t size)
其他数据结构
除了一些基本的数据结构之外,还有一些其他的数据结构。
链表
链表使用之处比较多,在linux驱动和应用编程时中就常见到链表这种结构。
链表中有一个一个的节点,每个节点通常包含三部分:前指针,数据域和后指针,前指针指向该节点之前的节点,后指针指向该节点之后的节点,数据域表示当前节点的数据。
哈希表
哈希表和数组类似,不同之处在于,哈希表不使用数字作为索引,而是用该元素的哈希值作为该元素的索引,元素查找的时候根据哈希值查找该元素,每个元素和哈希值是一一对应的映射关系,由于哈希值的单向性,所以哈希表中的索引,是不会出现重复的。
数据结构和算法
现实生活中的问题是多种多样的,问题的复杂度不同,解决问题使用的算法和数据结构不同,所以产生了多种不同的数据结构,每个数据结构的发明都是为了配合特定的算法,每个算法是为了树立特定的问题,算法的实现,依赖于对应的数据结构
位操作
常用的位操作符号
常用的位操作符有大概6种
- 位与&:只有两个位都为1时,结果为才1
- 位或 |:只要两个位中有一个为1,结果就为1
- 取反 ~:对于特定位,如果位等于0,则结果为1,如果位等于1,则结果为0
- 异或 ^:两个位同为1或者同为0时,结果为0,否则结果1
- 移位 <<或者>>:对于一个数,将其二进制位向左统一向左边移动n位,称为左移<<,右边空出来的位补0,统一向右边移动n位,称为右移>>,左边空出来的位,无符号数补0,有符号数补符号位。
C语言中还有逻辑与(&&),逻辑或(||)或者逻辑取反(!),和位操作不同的是,位操作是把操作数按照二进制位按位操作,逻辑操作是将操作数作为整体来操作。
操作寄存器
寄存器的操作适合使用位运算来执行,所以常用的寄存器操作都可以使用位操作来实现:
- 特定位清0,使用&操作,将该数的特定位和0相&,就可以把这些位置为0,
- 特定位置1,使用|操作,将该数的特定位和1相|,就可以把这些位置为1
- 特定位取反,使用^操作,将该数的特定位和1相^,就可以把这些位置为相反的值
构建特定二进制数
为了给特定位设置特定的值,需要构造一个特定的二进制数,这个二进制数的特定位要符合要修改的数的位要求,在这种情况下,可以使用左移或者右移运算,来得到一个特定的二进制数,例如要得到一个0000 0000 1111 0000的二进制的数,我们可以先获取一个0000 0000 0000 1111的二进制数,也就是0xFF,然后将其进行左移4位,就得到了0xFF00,也就是0000 0000 1111 0000。
宏定义实现位运算
在宏定义中使用位运算,可以使用宏定义来进行置为和复位,例如:
- #define SET_NTH_BIT(x,n) (x | ((1U) << (n - 1))),将变量x的n位置1
- #define CLR_NTH_BIT(x , n) (x & ~((1U) << (n - 1))),将变量x的n位清零
还可以截取变量的部分连续位:
- #define GET_BITS(x,m,n,) ((x & ~(~(0U)<<(m - n + 1)<<(n - 1))>>(n - 1))),截取变量x的从m开始到第n位截取出来
以上是关于C语言之内存和位操作的主要内容,如果未能解决你的问题,请参考以下文章
C语言中,使用一个结构体之前,要用memset把各个位清零???