文章目录
- static静态变量和全局变量的区别
- volatile
- 主要作用
- malloc
- 1. 内存分配器的作用
- 2. 内存分配过程
- (1) 查找空闲内存块
- (2) 扩展堆空间
- (3) 元数据
- 3. 内存释放过程
- (1) 标记为可用
- (2) 合并相邻空闲块
- (3) 延迟释放
- 4. 内存管理策略
- (1) 分配缓存(Allocation Caching)
- (2) 延迟释放(Lazy Freeing)
- (3) 内存对齐
- (4) 分配器锁(Allocator Locking)
- 使用宏函数进行数据交换(不使用中间变量)
- 宏函数:
- 原理分析:
- 为什么使用 `do...while(0)`:
static静态变量和全局变量的区别
区别 | 全局变量 | static静态变量 |
---|---|---|
作用域 | 整个源文件,任何地方都可以访问到。 | 仅限于声明它的源文件,不能被其他文件访问。 |
链接性 | 外部链接性,可以被其他文件访问,所有文件中的实例相同。 | 内部链接性,只能在声明它的源文件中访问。 |
初始化 | 可以显式初始化,未显式初始化时,默认为零值(静态存储期)或未定义值(动态存储期)。(静0动未知) | 可以显式初始化,未显式初始化时,默认为零值或用户指定的初始值。 |
生命周期 | 具有静态存储期,生命周期从程序启动到结束,值在函数调用之间保持。 | 同左 |
volatile
它的主要作用是告诉编译器,这个变量的值可能会被程序之外的因素修改,因此编译器不应对它进行优化。这样,编译器会确保每次访问该变量时都从内存中读取其最新值,而不是使用寄存器中的缓存值。
主要作用
-
防止优化:
编译器会在代码中进行一些优化,例如将某些变量的值缓存到寄存器中,以提高性能。但是,volatile
告诉编译器不要这么做,因为这个变量的值可能会被外部事件或硬件等修改。这样,编译器每次访问该变量时都会直接从内存中读取值。 -
适用于硬件寄存器:
在嵌入式编程中,volatile
常常用于表示与硬件寄存器或传感器相关的变量。由于硬件的状态会随时发生变化,编译器不应该对这些变量进行优化。 -
适用于多线程编程:
在多线程编程中,volatile
可以用于共享变量,确保线程之间读取的是最新的值。尽管如此,在现代多线程编程中,volatile
不能代替更强的同步机制(如互斥锁或原子操作),因为它不能保证变量访问的原子性。
malloc
malloc
是 C 语言中用于动态分配内存的标准库函数,它通过内存分配器来管理程序的堆内存。
1. 内存分配器的作用
malloc
函数背后依赖的内存分配器负责管理程序的堆内存。内存分配器维护了一个可用内存块的列表,并负责动态分配和回收内存。常见的内存分配器有:
- 基于链表的分配器:如
ptmalloc
(在 Linux 中广泛使用)使用链表来管理内存块。 - 堆管理器:堆内存管理器通过多种方式维护堆空间,以优化内存分配的效率。
内存分配器的目标是尽量避免内存碎片化,并提高内存分配与回收的速度。
2. 内存分配过程
当你调用 malloc(size)
请求分配一块指定大小的内存时,内存分配器会按照以下步骤进行操作:
(1) 查找空闲内存块
- 内存分配器会在已分配的内存块列表中查找是否有一个足够大的空闲内存块,满足要求的大小 (
size
)。 - 如果找到合适的内存块,分配器将其从空闲列表中移除,并返回该内存块的起始地址。
(2) 扩展堆空间
-
如果没有找到合适的空闲块,内存分配器会向操作系统请求更多的内存。常见的系统调用有:
brk()
:通过改变程序的堆顶指针来扩展堆。通常只适用于较早的 UNIX 系统。mmap()
:通过映射新的内存区域来扩展堆,适用于更现代的操作系统。
扩展堆的过程会将一块新的内存区域分配给程序,用来满足
malloc
的请求。
(3) 元数据
分配的内存块通常包含一些额外的元数据,通常存储如下信息:
- 内存块的大小:用于在释放时检查该内存块的大小。
- 分配状态:标记该内存块是已分配还是空闲。
- 内存对齐:为了提高内存访问效率,内存块的起始地址通常是对齐的。
3. 内存释放过程
当调用 free(ptr)
时,内存分配器将释放之前通过 malloc
分配的内存块。释放的过程大致如下:
(1) 标记为可用
free
会将内存块标记为“空闲”,即这块内存可以被重新分配给其他请求。
(2) 合并相邻空闲块
- 内存分配器通常会检查相邻的内存块,如果相邻的内存块也是空闲的,它们会被合并成一个较大的空闲块,从而减少内存碎片化现象。
(3) 延迟释放
- 对于常见的内存分配器,内存释放并不会立即返回给操作系统,而是留在堆中以备将来使用。这样可以提高后续内存分配的效率。
4. 内存管理策略
内存分配器通常会实现一系列策略来优化内存分配和释放的性能,主要包括:
(1) 分配缓存(Allocation Caching)
- 内存分配器通常会为小块内存分配设置缓存池(例如
malloc
用于小对象时会使用缓存),减少频繁的操作系统调用,提高内存分配的效率。
(2) 延迟释放(Lazy Freeing)
- 内存分配器并不会在每次
free
后立即返回内存给操作系统,而是将内存块保留在堆中。这有助于避免频繁的系统调用,从而提高性能。
(3) 内存对齐
- 为了提高 CPU 访问效率,内存分配器会确保分配的内存块是对齐的。例如,分配的内存块可能会对齐到 8 字节或 16 字节边界。
(4) 分配器锁(Allocator Locking)
- 在多线程环境下,为了防止多个线程同时访问内存分配器而导致竞争条件,内存分配器通常会使用锁机制(如互斥锁)来保证内存分配的线程安全性。
使用宏函数进行数据交换(不使用中间变量)
在 C 语言中,我们可以通过宏函数来实现一些操作,甚至可以通过运用位运算来优化代码,避免使用额外的变量。下面介绍如何利用**异或(XOR)**操作来交换两个变量的值,而不使用中间变量。
宏函数:
#define SWAP_NO_TEMP(a, b) do { \(a) ^= (b); \(b) ^= (a); \(a) ^= (b); \
} while (0)
原理分析:
我们知道异或操作的一个重要性质:
- A ⊕ A = 0 A \oplus A = 0 A⊕A=0(同样的数进行异或得到0)
- A ⊕ 0 = A A \oplus 0 = A A⊕0=A(任何数与0异或还是它本身)
基于这一性质,异或操作可以用来交换两个数的值。下面逐步分析这个宏函数的工作原理:
-
第一步:
a ^= b;
- 这一步是将
a
和b
进行异或运算,结果保存在a
中。将a和b的不同位设为1。
- 这一步是将
-
第二步:
b ^= a;
- 现在
b
被更新为b ^ (a ^ b)
,可以根据异或的结合性简化为a
,即b
变成了原来的a
值。任何数异或1都为相反的值,任何数异或0都为它自身。
- 现在
-
第三步:
a ^= b;
- 最后,此时b为原来的a值,a为原来的a和b,不相同为1,相同为0,
a
被更新为(a ^ b) ^ a
,这个运算结果为原来的b
值。
- 最后,此时b为原来的a值,a为原来的a和b,不相同为1,相同为0,
经过这三步操作,a
和b
的值就成功交换了。
为什么使用 do...while(0)
:
- 这确保了宏函数的语法可以作为一个单独的语句块存在。
do...while(0)
结构是为了确保宏可以像函数一样正常工作,避免由于宏展开带来的潜在问题。这样即使在if
语句中使用宏,也不会引发意外的错误(比如漏写花括号)。