外观
07.内存
约 3724 字大约 12 分钟
内存C个人随笔
2021-04-06
一、动态内存分配
1、动态内存分配的意义
- C语言中的一切操作都是基于内存的
- 变量和数组都是内存的别名
- 内存分配由编译器在编译期间决定
- 定义数组的时候必须指定数组长度
- 数组长度是在编译期就必须确定的
需求∶程序运行的过程中,可能需要使用一些额外的内存空间。
2、malloc 和 free
注意事项:
- malloc和free是库函数,而不是系统调用
- malloc 实际分配的内存可能会比请求的多
- 库函数的实现是需要操作系统的支持,不同的操作系统对于内存池的管理是不同的,eg:有些操作系统为了高效,内存池里面的空闲的内存总是4字节的整数倍,这就意味着,如果我们通过malloc函数申请3字节,有可能给我们可用的内存空间是4字节,当然这是可能性,不可以依赖这个特性。
- 不能依赖于不同平台下的malloc行为
- 当请求的动态内存无法满足时malloc返回NULL
- 当free 的参数为NULL时,函数直接返回
实例分析:内存泄露检测模块
问题1、malloc(0);将返回什么?
- 正常返回地址。是合法的。但是无法正常使用,因为长度是0;
#include <stdio.h>
#include <malloc.h>
int main()
{
int* p = (int*)malloc(0);
printf("p = 0x%p", p);
free(p);
return 0;
}问题2、如果一直申请malloc(0),不释放,会产生内存泄露吗?
- 会的,对于现代大多数编译器来讲,往往实际的比申请的要大。一般来说编译器是4字节对齐的, 申请0字节,也可能实际是4字节。malloc一定要对应free。
问题3:内存泄露检测模块
利用结构体和static申请一段内存来保存一些信息:用malloc申请的这段内存的地址、大小、文件夹名、行数。当使用malloc时,将这些信息保存下来,使用free时,将这些信息去除。
#include "mleak.h"
#define SIZE 256
/* 动态内存申请参数结构体 */
typedef struct
{
void* pointer;
int size;
const char* file;
int line;
} MItem;
static MItem g_record[SIZE]; /* 记录动态内存申请的操作 */
void* mallocEx(size_t n, const char* file, const line)
{
void* ret = malloc(n); /* 动态内存申请 */
if( ret != NULL )
{
int i = 0;
/* 遍历全局数组,记录此次操作 */
for(i=0; i<SIZE; i++)
{
/* 查找位置 */
if( g_record[i].pointer == NULL )
{
g_record[i].pointer = ret;
g_record[i].size = n;
g_record[i].file = file;
g_record[i].line = line;
break;
}
}
}
return ret;
}
void freeEx(void* p)
{
if( p != NULL )
{
int i = 0;
/* 遍历全局数组,释放内存空间,并清除操作记录 */
for(i=0; i<SIZE; i++)
{
if( g_record[i].pointer == p )
{
g_record[i].pointer = NULL;
g_record[i].size = 0;
g_record[i].file = NULL;
g_record[i].line = 0;
free(p);
break;
}
}
}
}
void PRINT_LEAK_INFO()
{
int i = 0;
printf("Potential Memory Leak Info:\n");
/* 遍历全局数组,打印未释放的空间记录 */
for(i=0; i<SIZE; i++)
{
if( g_record[i].pointer != NULL )
{
printf("Address: %p, size:%d, Location: %s:%d\n", g_record[i].pointer, g_record[i].size, g_record[i].file, g_record[i].line);
}
}
}#ifndef _MLEAK_H_
#define _MLEAK_H_
#include <malloc.h>
#define MALLOC(n) mallocEx(n, __FILE__, __LINE__)
#define FREE(p) freeEx(p)
void* mallocEx(size_t n, const char* file, const line);
void freeEx(void* p);
void PRINT_LEAK_INFO();
#endif3、calloc和realloc
- malloc的同胞兄弟
void* calloc(size_t num, size_t size);
void* realloc(void* pointer, size_t new_size);- calloc的参数代表所返回内存的类型信息
- calloc会将返回的内存初始化为0
- realloc用于修改一个原先已经分配的内存块大小
- 在使用realloc之后应该使用其返回值
- 当pointer的第一个参数为NULL时,等价于malloc
实例分析:calloc和realloc的使用
#include <stdio.h>
#include <malloc.h>
#define SIZE 5
int main()
{
int i = 0;
int* pI = (int*)malloc(SIZE * sizeof(int));
short* pS = (short*)calloc(SIZE, sizeof(short));
for(i=0; i<SIZE; i++)
{
printf("pI[%d] = %d, pS[%d] = %d\n", i, pI[i], i, pS[i]);
}
printf("Before: pI = %p\n", pI);
pI = (int*)realloc(pI, 2 * SIZE * sizeof(int));
printf("After: pI = %p\n", pI);
for(i=0; i<10; i++)
{
printf("pI[%d] = %d\n", i, pI[i]);
}
free(pI);
free(pS);
return 0;
}
4、小结
- 动态内存分配是C语言中的强大功能
- 程序能够在需要的时候有机会使用更多的内存
- malloc单纯的从系统中申请固定字节大小的内存
- calloc能以类型大小为单位申请内存并初始化为0
- realloc用于重置内存大小
二、程序中的三国天下:栈、堆、静态存储区
1、程序中的栈
(1)ESP:栈顶指针,指向当前的栈顶
(2)EBP:用于指向函数调用结束之后的返回地址
===================================================================================================
在嵌入式的寄存器中:
在通常情况下ESP是可变的,随着栈的生产而逐渐变小;而ESB寄存器是固定的,只有当函数的调用后,发生入栈操作而改变。
pop ebp:出栈 栈扩大4byte 因为ebp为32位(32位系统)
push ebp:入栈,栈减少4byte
(1)函数调用的栈变化一
- 从main()开始运行

(2)函数调用的栈变化二
- 当main()调用f()

(3)函数调用的栈变化三
- 当从f()调用中返回main()

2、函数调用栈上的数据
- 函数调用时,对应的栈空间在函数返回前是专用的
- 函数调用结束后,栈空间将被释放,数据不再有效(虽然保存,但是可能不在了)
- 栈空间里面的数据不会因为函数的返回而改变,但是可能会被其他数据覆盖,导致数据错误。

编程实验:指向栈数据的指针
#include <stdio.h>
int* g()
{
int a[10] = {9,8,7,6,5,4,3,2,1,0};
return a;
}
void f()
{
int i = 0;
int b[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* pointer = g();
for (i = 0; i < 10; i++)
{
b[i] = pointer[i];
}
for (i = 0; i < 10; i++)
{
printf("%d\n", pointer[i]);
}
}
int main()
{
f();
return 0;
}在VCC中,发生错误。

3、程序中的堆
堆是程序中一块预留的内存空间,可由程序自由使用
堆中被程序申请使用的内存在被主动释放前将一直有效
为什么有了栈还需要堆?
- 栈上的数据在函数返回后就会被释放掉,无法传递到函数外部,如:局部数组
C语言程序中通过库函数的调用获得堆空间
- 头文件: malloc.h
- malloc -- 以字节的方式动态申请堆空间
- free --将堆空间归还给系统
系统对堆空间的管理方式
空闲链表法,位图法,对象池法等等

- 空闲链表管理法:遍历空闲内存大小与需要申请内存大小最接近的。
4、程序中的静态存储区
- 静态存储区随着程序的运行而分配空间
- 静态存储区的生命周期直到程序运行结束
- 在程序的编译期静态存储区的大小就已经确定
- 静态存储区主要用于保存全局变量和静态局部变量
- 静态存储区的信息最终会保存到可执行程序中
编程实验:静态存储区的验证
#include <stdio.h>
int g_v = 1;
static int g_vs = 2;
void f()
{
static int g_vl = 3;
printf("%p\n", &g_vl);
}
int main()
{
printf("%p\n", &g_v);
printf("%p\n", &g_vs);
f();
return 0;
}
8、小结
栈,堆和静态存储区是程序中的三个基本数据区
- 栈区主要用于函数调用的使用
- 堆区主要是用于内存的动态申请和归还
- 静态存储区用于保存全局变量和静态变量
三、程序的内存布局
1、程序文件的一般布局
- 不同代码在可执行程序中的对应关系

2、程序与进程
程序和进程不同
- 程序是静态的概念,表现形式为一个可执行文件
- 进程是动态的概念,程序由操作系统加载运行后得到进程
- 每个程序可以对应多个进程(QQ多开)
- 每个进程只能对应一个程序
面试中的小问题:包含脚本代码的文本文件是一种类型的可执行程序吗?如果是,对应什么样的进程呢?

3、程序文件的一般布局
- 文件布局在内存中的映射

4、程序的内存布局
- 各个段的作用
- 堆栈段在程序运行后才正式存在,是程序运行的基础
- .bss段存放的是未初始化的全局变量和静态变量
- .text段存放的是程序中的可执行代码
- .data段保存的是已经初始化了的全局变量和静态变量
- .rodata段存放程序中的常量值,如字符串常量
5、程序术语的对应关系
- 静态存储区通常指程序中的.bss 和.data段
- 只读存储区通常指程序中的.rodata段
- 局部变量所占空间为栈上的空间
- 动态空间为堆中的空间
- 程序可执行代码存放于.text段
面试中的小问题:同是全局变量和静态变量,为什么初始化的和未初始化的保存在不同段中?
(从效率角度考虑,C语言以高效著名)
- 对于没有初始化的全局变量和静态变量,在加载的时候,这片内存区域就全部加载为0;
- 对于已经初始化的全局变量和静态变量,在程序中必然要保存初始值,并且要一一对应。
6、小结
- 程序源码在编译后对应可执行程序中的不同存储区
- 程序和进程不同,程序静态概念,进程是动态概念
- 堆栈段是程序运行的基础,只存在于进程空间中
- 程序可执行代码存放于.text段,是只读的
- .bss和.data段用于保存全局变量和静态变量
四、内存操作经典问题分析一
1、野指针
- 指针变量中的值是非法的内存地址,进而形成野指针
- 野指针不是NULL 指针,是指向不可用内存地址的指针
- NULL指针并无危害,很好判断,也很好调式
- C语言中无法判断一个指针所保存的地址是否合法
2、野指针的由来
- 局部指针变量没有被初始化
- 指针所指向的变量在指针之前被销毁
- 使用已经释放过的指针
- 进行了错误的指针运算
- 进行了错误的强制类型转换
实例分析:野指针初探
#include <stdio.h>
#include <malloc.h>
int main()
{
int* p1 = (int*)malloc(40);
int* p2 = (int*)1234567; // 进行了错误的强制类型转换
int i = 0;
for(i=0; i<40; i++)
{
*(p1 + i) = 40 - i; // 内存越界,从第11个开始,就是野指针了
}
free(p1); // 该行以下,p1都是野指针
for(i=0; i<40; i++)
{
p1[i] = p2[i]; // p1和p2都是野指针
}
return 0;
}程序运行报错。
3、基本原则
- 绝不返回局部变量和局部数组的地址
- 任何变量在定义后必须初始化为0
- 字符数组必须确认0结束符后才能成为字符串
- 任何使用与内存操作相关的函数必须指定长度信息
实例分析:无处不在的野指针
#include <stdio.h>
#include <string.h>
#include <malloc.h>
struct Student
{
char* name;
int number;
};
char* func()
{
char p[] = "D.T.Software";
return p; // 绝不返回局部变量和局部数组的地址
}
void del(char* p)
{
printf("%s\n", p);
free(p);
}
int main()
{
struct Student s; // 任何变量在定义后必须0初始化
char* p = func(); // 产生野指针
strcpy(s.name, p); // 使用野指针
s.number = 99;
p = (char*)malloc(5);
strcpy(p, "D.T.Software"); // 产生内存越界,操作了野指针所指向的内存空间
del(p);
return 0;
}程序运行报错。
4、小结
内存错误是实际产品开发中最常见的问题,然而绝大多数的bug 都可以通过遵循基本的编程原则和规范来避免。因此,在学习的时候要牢记和理解内存操作的基本原则、目的和意义。
五、内存操作经典问题分析二
1、常见内存错误
- 结构体成员指针未初始化
- 结构体成员指针未分配足够的内存
- 内存分配成功,但并未初始化
- 内存操作越界
实例分析:常见内存错误①
#include <stdio.h>
#include <malloc.h>
void test(int* p, int size)
{
int i = 0;
for(i=0; i<size; i++)
{
printf("%d\n", p[i]);
}
free(p); // 重复释放了两次
}
void func(unsigned int size)
{
int* p = (int*)malloc(size * sizeof(int));
int i = 0;
if( size % 2 != 0 )
{
return; // 程序如果运行到这里,p指针就会变成野指针,因为也没释放。
}
for(i=0; i<size; i++)
{
p[i] = i;
printf("%d\n", p[i]);
}
free(p);
}
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
test(p, 5);
free(p);
func(9);
func(10);
return 0;
}程序运行报错。
实例分析:常见内存错误②
#include <stdio.h>
#include <malloc.h>
struct Demo
{
char* p;
};
int main()
{
struct Demo d1; // 结构体变量未初始化
struct Demo d2; // 结构体变量未初始化
char i = 0;
for(i='a'; i<'z'; i++)
{
d1.p[i] = 0; // 操作野指针
}
d2.p = (char*)calloc(5, sizeof(char)); // calloc自动初始化为0
printf("%s\n", d2.p);
for(i='a'; i<'z'; i++) // 内存越界
{
d2.p[i] = i;
}
free(d2.p);
return 0;
}程序运行报错。
2、内存操作的交通规则
- 动态内存申请之后,应该立即检查指针值是否为NULL,防止使用NULL指针。

- free指针之后必须立即赋值为NULL。

- 任何与内存操作相关的函数都必须带长度信息。

- malloc操作和free操作必须匹配,防止内存泄露和多次释放。

3、小结
- 内存错误的本质源于指针保存的地址为非法值
- 指针变量未初始化,保存随机值
- 指针运算导致内存越界
- 内存泄漏源于malloc和free不匹配
- 当malloc次数多于free时,产生内存泄漏
- 当malloc 次数少于free时,程序可能崩溃
- 不要跨函数释放内存。
