复习
- 指针和数组非常相似
- 字符串知识字符指针/数组,末尾有一个空终止符
- 指针算法按指针指向的事物大小移动指针
- 指针式许多Cbug的根源
C的内存布局
C语言程序的地址空间通常包括以下4个区域:
1. 代码区(Code Segment):也称为文本区,存储程序的指令(机器代码)。该区域是只读的,通常是可共享的。程序在运行时,指令被复制到处理器的指令缓存中执行。
2. 数据区(Data Segment):也称为静态数据区,存储程序中已经初始化的全局变量和静态变量。该区域通常是可读写的,但是只能被程序本身修改。
3. BSS区(Block Started by Symbol):存储未初始化的全局变量和静态变量,通常在程序启动时自动清零。该区域也是可读写的。
4. 堆区:用于动态内存分配,通常使用malloc()和free()等函数,并向上增长
5. 栈区(Stack Segment):存储程序的局部变量、函数参数、返回地址等信息。该区域通常是以“先进后出”的方式存储数据,即栈结构。栈的大小在程序运行时动态地增长和收缩。
变量一般存储在哪里呢?
- 在函数外部声明:Static Data
- 在函数内部声明:Stack
- 动态分配:Heap
堆栈Stack
遵循LIFO原则
- 每个堆栈帧都是一个连续的内存块,保存单个过程的局部变量:堆栈帧是一种内存布局的概念,用于保存函数调用期间的信息,包括函数的参数、局部变量和其他状态信息。每个堆栈帧通常被分配在内存中的一个连续区域,并且其大小取决于该函数所需的空间。
- 堆栈帧包括:调用方函数的位置,函数参数,局部变量的空间:堆栈帧通常包括以下内容:
- 返回地址:指向调用方函数中下一条要执行的指令的地址。
- 参数:传递给函数的参数值。
- 局部变量:在函数内部定义的变量。
- 其他状态信息:例如保存的寄存器值、异常处理信息等。
- 堆栈指针(SP)告诉最低(当前)堆栈帧的位置:堆栈指针是一个指向当前堆栈顶部的指针。当一个新的堆栈帧被创建时,堆栈指针向下移动以分配新的空间。当函数返回时,堆栈指针向上移动以释放已分配的空间。
- 当过程结束时,堆栈指针被移回(但数据仍然存在,为将来的堆栈帧释放内存):当函数返回时,堆栈指针向上移动以回收该函数所需的空间,但是在该空间中存储的数据仍然存在。这些数据实际上成为“垃圾”,并且可能会在将来的函数调用中被覆盖。堆栈指针的移动并不是为了删除这些数据,而是为了将堆栈顶部的位置向上移动,以为将来的堆栈帧留出空间。释放这些垃圾数据的任务通常由垃圾回收器完成。
Static Data
Static Data(静态数据)是指在程序运行期间一直存在于内存中的数据,不随函数的调用而创建或销毁。它通常包括全局变量、静态变量和常量等。
全局变量是定义在函数外部的变量,其作用域在整个程序中都是可见的。全局变量的内存分配在程序启动时就已经完成,它们通常存储在静态数据区中。全局变量的值在程序运行期间可以被修改,但其内存地址和大小在程序生命周期内都是不变的。
静态变量是在函数内部定义的变量,但其生命周期与全局变量相似,也是在程序运行期间一直存在于内存中。静态变量的作用域只限于定义它的函数内部,但它们与全局变量一样,都存储在静态数据区中。
常量是指在程序中定义的不可修改的值,通常用于存储程序中的常量、字符串和其他不需要修改的数据。常量也存储在静态数据区中,并且在程序运行期间一直存在于内存中。
静态数据区是位于程序的地址空间中的一块固定区域,通常位于堆和栈之外。在程序启动时,静态数据区被初始化,并分配给全局变量、静态变量和常量等静态数据。静态数据区的大小是在编译时确定的,并且在程序运行期间不会发生变化。
持久变量通常指在程序运行期间一直存在于内存中,并且不会受到函数调用等影响的变量。这些变量通常是全局变量、静态变量和字符串文字等。
全局变量和静态变量在程序启动时就已经分配了内存空间,并且在整个程序运行期间一直存在于内存中。它们的值可以被程序中的任何函数访问和修改,而且其作用域范围也非常广泛。
字符串文字是指在程序中直接使用双引号括起来的字符串,例如:char * str = “hello world”;。字符串文字是在程序启动时分配内存空间的,并且其值也不会受到函数调用等影响。它们通常存储在静态数据区中,并且在程序运行期间一直存在于内存中。但需要注意的是,字符串文字是不可修改的,任何对其进行修改的操作都会导致程序崩溃。
需要注意的是,在使用字符串文字时,不能将其赋值给一个指向栈上分配内存的指针,因为栈上的内存在函数返回时会被释放,这会导致指针指向的内存区域无效。正确的做法是将字符串文字赋值给一个指向堆上分配内存的指针,或者使用字符数组来保存字符串。
寻址和字节序
寻址是指访问和组织计算机内存的方式。计算机中的每个内存字节都有一个唯一的地址,用于访问和操作存储在该字节中的数据。
计算机系统中有两种常用的寻址方式: 大端和小端。这些术语指的是字节在内存中存储的顺序。在 big-endian 系统中,最高有效字节(即具有最高数值的字节)首先被存储,其次是降序排列的次高有效字节。相比之下,little-endian 系统首先存储最低有效字节,然后按升序存储更高有效字节。
字节顺序的选择影响数据从内存中存储和检索的方式,并且它可能影响跨不同计算机体系结构的软件的可移植性。例如,如果软件是在大端系统上开发的,但后来在小端系统上运行,那么多字节数据类型(如整数)中的字节顺序可能会颠倒,从而导致错误和意外行为。
为了解决这些问题,一些软件开发人员使用与平台无关的数据格式,比如网络字节顺序(network byte order) ,它为网络数据传输指定了一个标准的字节顺序。此外,现代编译器和编程语言通常提供内置函数或库,用于在不同字节顺序之间转换数据。
Addresses
- 地址的大小和指针的大小,以字节byte为单位,取决于体系结构(32-bit or 64-bit)
- 如果一台机器是字节寻址的,那么它的每个地址都指向一个唯一的字节
Endianness
- Big Endian
- 带有升序内存地址的降序数字意义
- 最高有效字节(MSB)存储在最低内存地址,而最低有效字节(LSB)存储在最高内存地址。
- Little Endian
- 带有升序内存地址的升序数字意义
- 最低有效字节(LSB)存储在最低内存地址,而最高有效字节(MSB)存储在最高内存地址。
在字节寻址机器上,整数的字节排序方式取决于机器的字节序(endianness)。在大端字节序(big-endian)机器上,整数的最高字节被存储在最低的地址中,而最低字节被存储在最高的地址中。在小端字节序(little-endian)机器上,情况正好相反:整数的最低字节被存储在最低的地址中,而最高字节被存储在最高的地址中。
例如,考虑一个4字节的整数0x12345678,其在大端字节序和小端字节序机器上的存储方式如下所示:
大端字节序:0x12 0x34 0x56 0x78
小端字节序:0x78 0x56 0x34 0x12
当处理跨越多个字节的整数时,了解机器的字节序非常重要。在许多情况下,程序员可以通过使用特定的字节序函数(例如htonl()和ntohl())来保证数据在不同机器上的正确传输。
Common Mistakes
- 字节序仅适用于占用多个字节的值
- 字节序是指内存中的存储,而不是数字表示
- 数组和指针仍然具有相同的顺序
动态内存分配
- 需要持久内存(如静态)
- 动态分配的内存比堆栈的更大更持久
- 动态内存分配允许在程序运行时根据需要分配和释放内存。动态内存分配通常使用堆来分配内存,因此分配的内存大小可以非常灵活。动态内存分配的另一个重要优势是,它可以让程序员手动控制内存的生命周期和使用方式,避免了内存泄漏和其他内存相关的问题。
- 堆栈适用于分配存储局部变量和函数参数等较小的内存空间,而动态内存分配则适用于需要分配较大的、动态变化的内存空间的情况。
分配内存
malloc()
1. 用于在运行时从堆(heap)中分配指定大小的内存块,返回指向该内存块起始地址的指针。
2. 可以根据需要分配任意大小的内存,但是需要注意内存的大小不能超出操作系统或硬件的限制。
3. 分配的内存块不会自动初始化,其值将是未定义的。如果需要初始化,可以使用calloc()
函数。
4. 分配的内存块在使用完成后必须被释放,否则会造成内存泄漏,可以使用free()
函数释放。
5. 如果分配的内存块大小为0,则行为是未定义的。因此,应该避免使用0作为参数调用malloc()
函数。
6. 如果没有足够的内存可以分配,malloc()
函数将返回NULL指针。
- 几乎总是用于数组和结构的内存分配
- 使用sizeof()和类型转换有很好的作用
- sizeof() 使代码更具可移植性 – malloc() 返回 void * ;
- 类型转换将帮助您在指针类型不匹配时捕获编码错误
- 可以使用数组或指针语法进行访问
释放内存
free()
是 C 语言中用于释放动态分配内存的函数。它的原型定义在 <stdlib.h>
头文件中:
void free(void *ptr);
该函数接收一个指向要释放的内存块的指针作为参数。释放的内存块必须是之前使用 malloc()
、calloc()
或 realloc()
动态分配的,并且参数指针必须指向该内存块的起始位置。
free()
函数将之前动态分配的内存块返回给系统,以便其他程序或本程序再次使用。注意,一旦内存块被释放,就不能再对它进行访问,因为其内容可能已经被其他程序覆盖。
以下是 free()
函数的几个注意事项:
- 只能释放之前动态分配的内存块,对于静态分配或栈分配的内存块不能使用
free()
函数。 - 每次动态分配内存时,都需要对其调用
free()
函数来避免内存泄漏。 - 释放的内存块必须是合法的指针,不能是空指针或已经被释放的指针。否则会导致未定义行为,甚至崩溃。
使用 free()
函数可以避免动态分配的内存空间浪费和内存泄漏,从而提高程序的稳定性和性能。
Calloc
calloc()
函数也是用于动态内存分配的函数,但其与 malloc()
有所不同。calloc()
可以在分配内存空间的同时,将其初始化为 0。它接受两个参数:所需的内存块数和每个内存块的大小(以字节为单位),并返回一个指向分配内存的指针。语法如下:
void *calloc(size_t num, size_t size);
其中,num
为所需的内存块数,size
为每个内存块的大小。函数返回一个指向分配内存的指针,如果分配失败则返回 NULL
。
calloc()
与 malloc()
的区别在于,calloc()
会在分配内存空间后将其初始化为 0。这使得在使用指针变量之前不需要手动将其初始化为 0,也可以保证分配的内存空间中不会有任何未知值。这是一种安全的编程实践,因为未初始化的变量可能包含不可预测的值,导致程序出现难以追踪的错误。
需要注意的是,calloc()
分配的内存空间是连续的,就像 malloc()
一样,可以使用指针操作来访问和修改其内容。
Realloc
realloc()
函数用于重新分配已分配的内存块的大小。它接受两个参数:原来分配的内存块指针和新的内存块大小。如果新的大小小于或等于原来的大小,则它只是截断内存块。如果新的大小大于原来的大小,则它将分配一个新的内存块,并复制旧的数据到新的内存块中,并且释放旧的内存块。
realloc()
函数的一般语法如下:
ptr = realloc(ptr, size);
其中,ptr
是原先已经分配的内存块的指针,size
是新的内存块的大小。realloc()
函数会返回一个指向重新分配的内存块的指针,这个指针可能与原来的指针相同,也可能是一个新的指针。如果realloc()
函数无法分配新的内存块,则返回NULL
。
需要注意的是,realloc()
函数的使用有一些需要注意的细节,如:
- 重新分配的内存块大小必须大于或等于0。
- 如果
ptr
是NULL
,则realloc()
的行为就像是malloc()
一样。 - 如果
size
是0,则realloc()
的行为就像是free()
一样,即释放原先分配的内存块。 - 在调用
realloc()
函数之后,必须始终检查返回的指针是否为NULL
,以确保成功分配了新的内存块。 - 由于
realloc()
函数可能会移动内存块,因此在重新分配内存块时,不要保存原先指向内存块的指针,而应该使用realloc()
返回的新指针。
realloc()
函数是一个非常有用的函数,能够帮助我们更好地管理内存,尤其是在需要动态调整内存块大小时。
常见内存问题
- 分段错误 “正在运行的 Unix 程序尝试访问未分配给它的内存并终止并终止并出现分段冲突错误(通常是核心转储)的错误。
- 总线错误 ”由于处理器在其总线上检测到异常情况而导致机器语言指令执行时出现致命故障。此类条件包括无效的地址对齐(访问奇数地址的多字节数字),访问与任何设备不对应的物理地址,或其他特定于设备的硬件错误。
常见内存问题:
1. 使用未初始化的值:如果您声明变量但未初始化它们,则变量的值将是未知的。如果您尝试使用这些未初始化的值,则会出现意外行为和错误。
2. 使用不属于您的内存:这种情况通常发生在使用指针时,尤其是在使用未初始化的指针或在指针上进行算术运算时。如果指针包含错误的地址或者它指向的内存不属于您的程序,那么您的程序将崩溃或者产生未定义的行为。
3. 释放无效内存:如果您试图释放已经释放或者不属于您的内存,那么您的程序将会崩溃或者产生未定义的行为。这通常发生在使用不正确的指针或者多次释放相同的内存时
4. 内存泄漏:如果您分配内存但没有释放它,那么就会发生内存泄漏。如果您的程序重复执行这个操作,那么程序将使用越来越多的内存,最终可能导致系统资源耗尽。内存泄漏是一种常见的问题,尤其是在长时间运行的程序中。
以上简单记录lec04的笔记,主要涉及的C语言内容应该在学习C语言的过程中就有所了解且掌握。C语言的核心就是指针和内存管理,而这两者是和体系结构有紧密的联系的。更多详细C语言知识可以学习 C primer Plus等书籍和及时查找互联网。