C语言学习七:内存
进程的内存布局
- 程序:就是我们写好的代码并编译完成的那个二进制文件,它被存放与磁盘中,它是死的。
- 进程:把磁盘中的二进制文件"拷贝"到内存中取执行它,让运行起来,它是活的。
所有的程序被执行起来之后,系统会为他分配各种资源内存,用来存放该进程中用到的各种变量、常量、代码等等。这些不同的内容将会被存放到内存中不同的位置(区域),不同的内存区域他的特性有差别
每一个进程所拥有的内存都是一个虚拟的内存,所谓的虚拟内存是用物理内存中映射(投影)而来的,对于每一个进程而言所有的虚拟内存布局都是一样的。让每个进程都以为自己独自拥有了完整的内存空间
物理内存(Physical Memory)-----------------------------虚拟内存(Virtual Memory)

虚拟内存的布局(区域)
- 栈 (stack)
- 堆(heap)
- 数据段
- 代码段


栈空间

特点:
- 空间非常有限,尤其在嵌入式的环境下,因此应该尽可能少去使用栈空间内存,特别是要存放比较大的数据。
- 每当一个函数被调用的时候,栈空间会向下增长一段,用来存放该函数的局部变量
- 当一个函数退出的时候,栈空间会向上回缩一段,该空间的所有权将归还系统
- 栈空间的分配与释放,用户是无法干预的,全部由系统来完成。
1 | ulimit ‐a //查看系统栈空间内存 |
静态变量
在 C 语言中有两种静态变量:
- 全局变量: 定义在函数体之外的变量
- 静态的局部变量: 定义在函数体内部而且被
static修饰的变量
1 | int c ; // 在函数体之外,属于全局变量 ‐‐> 静态变量 |
static 修饰的作用:
- 修饰局部变量: 使得局部变量的存放位置从栈空间改为数据段
- 修饰全局变量: 可以缩小全局变量的可见范围,由原本的拷文件可见变为本文可见,可以使得重名概率降低
- 修饰函数: 使得函数为本文将可见
为什么会有静态变量:
- 当我们需要把一个变量引用到不同的函数内部甚至不在同一个.c 文件中,可以使用全局变量来实现。
- 当我们需要一个局部变量用来记录某个值,并希望这个值不会被重新初始化的情况下可以使用静态的局部变量。
1 | int k = 1000 ; |
数据段
-
内容
.bss未初始化的静态数据,会被自动初始化为0.data已初始化的静态数据.rodata存放常量数据 ,比如"Hello" , 是不允许修改的(只读)

-
特点
- 没有初始化则自动初始化为0
- 初始化语句只会被执行一次(在程序被加载的过程中已经初始化结束)
- 静态数据的内存从程序运行之初就存在,直到程序退出才会被释放(与进程共生死)
代码段
-
内容:
- 用户的代码 (比如我们自己写函数 func…main)
- 系统初始化代码,由编译器为我们添加的

堆内存
基础知识
-
概念:堆内存,又称为动态内存、自由内存、简称堆。唯一一个由开发者随意分配与释放的内存空间。具体的申请大小,使用的时常都是由我们自己来决定。
-
堆内存空间的基本特性
- 相对与栈空间来说,堆空间大很多(堆的大小受限于物理内存),系统不会对对空间进行限制。
- 相对与栈空间来说,堆内存是从下往上增长的。
- 堆空间的内存称为匿名内存,不像栈空间那样有个名字,只能通过指针来访问
- 堆空间内存的申请与释放都是由用户自己完成,用户申请之后需要手动去释放,直到程序退出。
申请堆空间内存
方法:
- malloc
- calloc
- realloc
-
malloc (只是申请内存而已,并不会清空)
1
2
3
4
5
6
7
8
9
10malloc (向系统申请内存)
头文件:
#include <stdlib.h>
函数原型:
void *malloc(size_t size);
参数分析:
size ‐‐> 需要申请的内存 (字节为单位)
返回值:
成功 返回一个指向成功申请到内存的指针(入口地址)
失败 返回 NULL示例 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int *p = (int *)malloc(10 * sizeof(int)); // 申请一个可以存放10个整型的 堆空间
for (int i = 0; i < 10; i++)
{
// 重新赋值
*(p + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("*(p+%d):%d\n", i, *(p + i));
}
// &p 打印的是栈空间地址 p中所存放的地址为堆空间地址
printf("&p:%p‐‐‐>%p\n", &p, p);
free(p); // 不再使用该堆空间时需要释放 -
calloc (会把内存进行清空)
1
2
3
4
5
6
7
8
9
10
11calloc (向系统申请内存)
头文件:
#include <stdlib.h>
函数原型:
void *calloc(size_t nmemb, size_t size);
参数分析:
nmemb ‐‐ > N 块内存(连续的)
size ‐‐ > 每一块内存的大小尺寸
返回值:
成功 返回一个指向成功申请到内存的指针(入口地址)
失败 返回 NULL示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 申请10块连续的内存 ,每一块为 整型大小
int * p = calloc( 10 , sizeof(int));
for (size_t i = 0; i < 10; i++)
{
*(p+i) = i+ 998 ;
}
for (int i = 0; i < 10; i++)
{
printf("*(p+%d):%d\n" , i , *(p+i));
}
/不再使用该堆空间时需要释放
free(p); -
realloc
1
2
3
4
5
6
7
8
9
10realloc (重新申请空间)
头文件:
#include <stdlib.h>
函数原型:
void *realloc(void *ptr, size_t size);
参数分析:
ptr ‐‐> 需要 扩容/缩小 的内存的入口地址8 size ‐‐> 目前需要的大小
返回值:
成功 返回修改后的地址
失败 NULL示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// 申请10块连续的内存 ,每一块为 整型大小
int * p = calloc( 10 , sizeof(int));
for (size_t i = 0; i < 10; i++)
{
*(p+i) = i+ 998 ;
}
for (int i = 0; i < 10; i++)
{
printf("*(p+%d):%d\n" , i , *(p+i));
}
printf("重新分配前p:%p\n" , p );
// 如果重新申请内存需要另找宝地 , 那么 原本的地址会被释放掉
int * p1 = realloc( p , 128);
p = NULL ; // 让p不要成为野指针, 需要让它指向NULL
printf("重新分配后p1:%p\n" , p1 );
//realloc 会帮我们把原有的数进行搬运
for (int i = 0; i < 10; i++)
{
printf("*(p1+%d):%d\n" , i , *(p1+i));
}
//不再使用该堆空间时需要释放
free(p1);
清空与释放
-
清空
方法:
- bzero
- memset
-
释放
方法:
- free
1
2
3
4
5
6
7
8
9free(释放堆内存)
头文件:
#include <stdlib.h>
函数原型:
void free(void *ptr);
参数分析:
ptr ‐‐> 需要释放的内存的入口地址
返回值:
无释放内存的含义:
- 释放内存仅仅意为着,当前内存的所有权交回给系统
- 释放内存并不会清空内存的内容
- 也不会改变指针的指向,需要手动把指针指向NULL ,不然就成野指针了
总结
malloc申请内存时,内存中的值时随机值,可以使用bzero清空calloc申请内存时,内存中的值会被初始化为0free只能释放堆空间的内存,不能释放其它区域的内存
补充:
1 | char * p = malloc(32); |
c
拓展练习
-
TEST1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 把p修改为二级指针, 用来接收 指针str的地址
void GetMemory1(char **p) // p ==== NULL //原本为char *p
{
*p = (char *)malloc(100); // 向系统申请100个字节的堆空间, 让p 指向该区域
}
void Test1(void)
{
char *str = NULL;
GetMemory1(&str); // str ==== NULL //原本为GetMemory1(str);
// 经过GetMemory1 的操作后 str的指向依然没有变换还是指向NULL
strcpy(str, "hello world");
// 提示: 拷贝字符串, 把"hello world" 拷贝到str 所指向的内存空间中
//原本拷贝函数会出现段错误
printf("%s\n", str);
free(str) // 释放申请的堆空间
}
int main(int argc, char const *argv[])
{
Test1();
return 0;
}
在原始代码中:
GetMemory1(str);:str是Test1函数中定义的一个指向字符的指针变量。通过将str作为参数传递给GetMemory1函数,实际上是将str的值(即指向字符的指针)复制给了GetMemory1函数中的参数p。他只改了p的内存分配,没改到str更改后的代码中:
GetMemory1(&str);: 将指针str的地址作为参数传递给GetMemory1函数,是指针str的地址而不是str的值。
把GetMemory1函数中的p修改为二级指针,用来接收指针str的地址。**p就是&str,*p = (char *)malloc(100);中,*p实际上就是str -
test 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21char *GetMemory2(void)
{
// 数组 p 所存放的位置为 栈空间, 当函数 GetMemory2 退出返回时,该区域会被系统回收
// 不应该返回该内存中的地址
// 可以使用 static 来修饰该数组, 使其的内存区域改为数据段
static char p[] = "hello world"; // 原本为char p[] = "hello world";
return p;
}
void Test2(void)
{
char *str = NULL;
str = GetMemory2();
printf("TEST‐2:%s\n", str);
}
int main(int argc, char const *argv[])
{
Test2();
return 0;
}
-
test 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18char *GetMemory3(void)
{
// 直接返回 常量区的内存地址 , 注意该区域只读
return "hello world";
}
void Test3(void)
{
char *str = NULL;
str = GetMemory3();
printf("TEST‐3:%s\n", str);
}
int main(int argc, char const *argv[])
{
Test3();
return 0;
} -
test 4(第一个 test1的改版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void GetMemory4(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test4(void)
{
char *str = NULL;
GetMemory4(&str, 100);
strcpy(str, "hello");
printf("TES‐04:%s\n", str);
free(str); // 释放申请的堆空间
}
int main(int argc, char const *argv[])
{
Test4();
return 0;
} -
test 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void Test5(void)
{
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str); // 把堆空间进行释放, 但是str依然指向堆空间的位置
if (str != NULL)
{
strcpy(str, "world"); // 虽然可以拷贝, 但是属于非法访问
printf("TEST‐5:%s\n",str);
}
}
int main(int argc, char const *argv[])
{
Test5();
return 0;
} -
test 6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void Test6()
{
char *str = (char *)malloc(100);
strcpy(str, "hello");
str += 6; //str=str+6
if (str != NULL)
{
strcpy(str, "world");
printf("TEST‐6:%s\n", str); // 输出 wrold
printf("TEST‐6:%s\n", str‐ = 6); // 输出 Hello
}
free(str); // 释放的时候必须使用 最初申请得到的那个首地址(入口地址)
}
int main(int argc, char const *argv[])
{
Test6();
return 0;
}





