目录
前提:
程序在运行时,将需要的数据都组织存放在内存空间,以备程序使用。在软件开发过程中,常需要动态地分配和撤销内存空间,这就需要对内存进行管理。
一、变量的存储类别
1.1 内存的分区
①、内存:物理内存、虚拟内存
物理内存:实实在在存在的内存设备
虚拟内存:操作系统虚拟出来的内存。还是会和物理内存做练习。
操作系统会在物理内存和虚拟内存之间做映射。实际上,虚拟内存占用多少空间,都会映射到物理内存当中。
在32位系统下,每个进程的寻址地址范围是4G, 0x00 00 00 00 - 0xff ff ff ff
在32位操作系统中,虚拟内存被分为两个部分,3G的用户空间和1G内核空间,
其中用户空间:是当前进程所私有的,
内核空间:是一个系统中所有进程所公有的。
在写应用程序时,看见的都是虚拟地址。
②、在运行程序时,操作系统会将虚拟内存进行分区
1)堆
在动态申请内存时,在堆里开辟内存
2)栈
主要存放局部变量
3)静态全局区
1:未初始化静态全局区
静态变量(定义变量时,前面添加static修饰),或者全局变量,没有初始化,存在此区
2:初始化的静态全局区
全局变量、静态变量、赋过初值的,存放在辞去
4) 代码区
存放程序代码。
5) 文字常量区
存放常量的
二、 在每块区域中变量申请空间的特点
2.1 普通的全局变量
全局变量: 在函数外部定义的变量
作用范围:
- 程序的所有地方
- 不同.c文件用之前需要声明,extern int num;
- 声明时不要赋值;
生命周期:程序运行的整个过程,一直存在,直到程序结束。
#include <stdio.h>
//定义普通全局变量
//主要在mian,子函数外就是全局变量
//如果全局 变量没有进行初始化,则系统自动将其初始化为0
int num;
//全局变量可以在程序的任意位置 对其进行操作
void fun()
{
num = 888;
}
void main()
{
printf("%d\n", num);
fun();
printf("%d\n", num);
}
2.2 静态全局变量static
定义全局变量,在定义前加static
作用范围:
- static限定静态全局变量的作用范围
- 只能在它定义的 .c 文件中有效
生命周期:程序运行的整个过程,一直存在。
#include <stdio.h>
//定义静态全局变量
//静态全局变量只能在当前 .c文件中使用
static int num;
void fun()
{
num++;
}
void main()
{
printf("%d\n", num);
fun();
printf("%d\n", num);
}
2.3 普通的局部变量
在函数内部定义,或复合语句中定义的变量
作用范围:
- 在函数内部定义,在函数中有效
- 在复合语句定义,在复合语句有效
生命周期:
- 在函数调用前,局部变量不占空间,在函数调用时,才为局部变量开辟空间,函数结束,局部变量就释放
//定义普通局部变量
//在函数内部定义的,不加任何修饰的都是局部变量
void fun()
{
int num = 100;
num++;
printf("%d\n", num);
}
void main()
{
//局部变量只能在定义的函数内使用 ,使用周期相对较短 ,函数结束,局部变量就会释放
fun();
//printf("%d\n", num);
//num值都一样
fun();
fun();
fun();
fun();
}
2.4 静态局部变量
定义局部变量时,前面加static
作用范围:在定义函数或复合句有效
生命周期:
- 第一次调用函数时,开辟空间赋值,函数结束后,不释放
- 以后在调用该函数,不再为其开辟空间,也不赋初值,用的是之前那个变量。
静态局部变量就初始化一次
#include <stdio.h>
//定义静态局部变量
//在函数内部定义的,加static修饰的都是静态局部变量
void fun()
{
//如果普通局部变量不进行初始化,则默认是随机变量
//如果静态局部变量不进行初始化,则默认为0
int a;
static int num;
printf("a = %d\n", a);
printf("num = %d\n", num);
}
void fun1()
{
//静态变量不会随着当前函数执行结束而释放空间,下次函数使用的是之前的空间
//静态局部变量只会初始化一次
static int num1 = 100;
num1++;
printf("num1 = %d\n", num1);
}
void main()
{
fun();
fun1();
fun1();
fun1();
fun1();
fun1();
}
注意:
1、定义普通局部变量,如果不赋初值,值是随机的
定义静态局部变量,如果不赋初值,默认为0
2、普通全局变量、和静态全局变量如果不赋初值,值为0
三、内部函数和外部函数
3.1 内部函数
内部函数(静态函数):就是用static进行修饰的函数。
作用范围:只能在定义的.c中有效
static int ADD(int iNum1, int iNum2)
{
}
static char* GetString(char* pString)
{
return pString;
}
static void ShowString(char* pString)
{
printf("%s\n", pString );
}
main()
{
char* pMyString;
pMyString = GetString("MingRi");
ShowString(pMyString);
printf("\n");
}
3.2 外部函数
外部函数:可以在程序任何一个文件中调用
在分文件编程中,只需要将函数的实现过程写在指定的.c文件中,然后将其声明写在指定的.h文件中,其他文件主要包含了头文件,就可以使用外部函数
定义外部函数可用extern 进行修饰。在使用一个外部函数时,需先用extern声明所用函数是外部函数
extern int Add(int iNum1, int iNum2);
extern char* GetString(char* pString);
extern void ShowString(char* pString);
char* GetString(char* pString)
{
return pString;
}
void ShowString(char* pString)
{
printf("%s\n", pString );
}
main()
{
char* pMyString;
pMyString = GetString("MingRi");
ShowString(pMyString);
printf("\n");
}
3.3 内部函数与外部函数的区别
外部函数:在所有地方都可以调用
内部函数:只能在所定义的.c中函数调用
3.4 扩展
在同一作用范围内,不允许变量重名
作用范围不同的可以重名
局部范围内,重名的全局变量不起作用(就近原则)
int num = 100; //全局
int main()
{
//如果出现可以重名的情况,使用的时候满足向上就近原则
int num = 999; //局部
return 0;
}
四、内存组织方式(参考)
程序员将程序编写完成,需要先将程序装载到计算机内核或半导体内存中,然后在运行。
程序被分成4个逻辑段:
注意:根据操作平台和编辑器不同,堆和栈可使用被所有同时运行的程序共享的操作系统资源,也可使用程序独占局部资源。
4.1 堆与栈
🍖堆:存放动态分配和释放内存块的存储空间
malloc()和free()从堆中动态分配和释放内存。
main()
{
int *pInt;
pInt = (int*)malloc(sizeof(int));
*pInt = 100;
printf("数值是: %d\n", *pInt);
free(pInt);
printf("\n");
}
🍖栈
程序调用函数和声明局部变量时,系统将自动分配内存。栈——后进先出的压弹式数据结构。
后进先出:在程序运行时,每次向栈中压入一个对象,然后栈指针向下移动一个位置。当系统从栈中弹出一个对象时,最近进栈的对象将被弹出,然后栈指针向上移动一个位置。如果栈指针位于栈顶——栈是空的;如果栈指针指向最下面的数据项的后一个位置——栈为满。
栈如何工作:当一个函数A调用另一个函数B时。
void DisplayB(char* string)
{
printf("%s\n",string);
}
void DisplayA(char* string)
{
char String[20] = "LoveWorld";
printf("%s\n",string);
DisplayB(String);
}
main()
{
char String[20] = "LoveChina!";
DisplayA(String);
printf("\n");
}
五、动态内存申请
5.1 动态分配内存的概述
所谓动态分配内存就是早堆区开辟空间,就是在堆区开辟空间,堆区空间手动申请,手动释放,相对比较灵活
内存中动态分配内存。
在数组章中,介绍过数组的长度是预先定义好的,在整个程序中固定不变,但是在实际的编程中,往往会发生这种情况,即所需的内存空间取决于实际输入的数据,而无法预先确定。为了解决上问题,C语言提供了一些内存管理函数, 这些内存管理函数可以按需要动态的分配内存空间,也可把不再使用的空间回收再次利用。如想用这40字节,就申请使用,不想使用释放。若40个字节不够使用,可以在40字节之后多开辟些,这样也是允许的。
固定不变:int a[10]; //当程序运行到此处就会给其开辟40字节的空间,并且最多只能放10个整型的数据,不可超过这个范围,如果超过这个范围,除非重新定义数组,否则无法对其进行操作。
所需的内存空间取决于实际输入的数据:实际需要用多少空间就用多少空间,不像数组直接开辟定长的,
5.2 静态分配、动态分配
①、静态分配
- 在程序编译或运行过程中,按事先规定大小分配内存空间的分配方式。int a[10]
- 必须事先知道所需空间的大小
- 分配在栈区或全局变量区,一般以数组的形式
- 按计划分配
②、动态分配
- 在程序运行过程中,根据需要大小自由分配所需空间
- 按需分配
- 分配在堆区,一般使用特定函数进行分配。
5.3 动态分配函数
①、malloc():
malloc:
头文件: #include<stdlib.h>
函数原型: void *malloc(unsigmed int size);
功能说明:
在内存的动态存储器(堆区)中分配一块size字节大小连续区域,用来存放类型说明符指定的类型。
malloc 函数会返回void*指针,使用时必须做相应的强制类型转换,分配的内存空间内容不确定,
一般使用memset初始化。
返回值:
分配空间的起始地址(分配成功)
Null(分配失败)
main()
{
int *pInt; //定义指针pInt,来保存分配内存地址
pInt = (int*)malloc(sizeof(int)); //指定大小sizeof,返回指针时也相对应int型指针(强制转换)
*pInt = 100;
printf("数值是: %d\n", *pInt);
printf("\n");
}
#include <stdio.h>
#include <stdlib.h>
char *fun()
{
//char ch[100] = "hello world"; //因为被释放了,F返回值为NULL
//静态 全局区的空间只要开辟号,除非程序结束,否则不会释放,所以
//如果是临时使用,不建议使用静态全局区的空间
//static char ch[100] = "hello world";
//堆区开辟空间,手动申请,手动释放,更加灵活
//使用malloc函数的时候一般要进行强制转换,如果不加会警告
char *str = (char *)malloc(100 * sizeof(char));
str[0] = 'h';
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';
str[5] = '\0';
return str;
}
void main()
{
char *p;
p = fun();
printf("p = %s\n", p);
}
注意:
- 在调用malloc之后,一定要判断,是否申请内存成功。
- 如果多次malloc申请内存,第一次和第二次申请的内存不一定是连续的
- 使用malloc开辟空间需要保存开辟好的地址空间的首地址,但是由于不确定空间用于做什么,所以本身返回值类型为void*,所以在调用函数时根据接收者的类型对其进行强制转换
②、calloc():
在内存中动态分配n个长度为size的连续内存空间数组,calloc函数会返回一个指针,该指针指向动态分配的连续内存空间地址,如果分配空间错误时,返回NULL。
calloc:
头文件: #include<stdlib.h>
函数原型: void *calloc(size_t nmemb, size_t size);
nmemb:要申请的空间的块数
size:每块的字节数
功能说明:
在堆区申请指定大小的空间,在内存堆中,申请nmemb块,每块的大小为size个字节的连续区域
返回值:
申请的内存首地址(申请成功)
Null(申请失败)
main()
{
int *pArray;
int i; //定义指针pArray
pArray = (int*)calloc(3, sizeof(int)); //指定大小sizeof和数组个数,pArray指向首地址
for(i = 0; i < 3; i++)
{
*pArray = 10 * i;
printf("NO :%d 数值是: %d\n", i, *pArray);
pArray +=1;
}
printf("\n");
}
注意:malloc和calloc函数都是用来申请内存的
区别:
- 函数名不一样
- 参数个数不一样
- malloc申请的内存,内存中存放的内容是随机的,不确定的,而calloc函数申请的内存中的内容为0
③、realloc():(重新申请内存)
存在问题:调用malloc和calloc函数单词申请内存是连续的,两次申请的两块内存不一定连续。
还存在,即先用malloc或者calloc申请了一块内存,还想在原先内存的基础上接着申请内存;或开始使用malloc或calloc申请了一块内存,释放后边的一部分内存。
为了解决这个问题,发明了realloc函数
改变ptr指针指向空间大小。size可以是任意大小,返回值是一个指向新地址的指针,如果出现错误,则返回NULL
realloc:
头文件: #include<stdlib.h>
函数原型: void *realloc(void*s, unsigned int newsize);
功能说明:
在原本申请好的堆区空间的基础上重新申请内存,新的空间大小为函数的第二个参数,
如果原本申请好的空间后面不足以增加指定的大小,系统会重新找一个足够大的位置靠谱指定的空间,然后将原本空间的数据拷贝过来,然后释放原本的空间。
s:原本开辟好的空间的首地址
newsize;重新开辟的空间的大小
返回值:
新申请的内存的首地址
//增加空间
void test1()
{
char *p;
p = (char *)malloc(100);
//在100个字节后面追加50个字节
p =(char *)realloc(p, 150); //p指向的内存的新的大小为 150个字节
}
//减少空间
void test2()
{
char *p;
p = (char *)malloc(100);
//想重新申请内存,新的大小为50个字节
p =(char *)realloc(p, 50); //p指向的内存的新的大小为 50个字节 , 100个字节后面的50字节的空间被释放
}
void main()
{
test1();
test2();
}
注意:malloc、calloc、realloc动态申请的内存,只有在free或程序结束时才释放
④、free()——释放内存函数:
头文件: #include<stdlib.h>
函数原型: void *free(void *ptr); //ptr:开辟后使用完毕的堆区空间的首地址
功能说明:
free函数释放ptr指向的内存。只能释放堆区空间
注意:
ptr指向的内存必须是malloc calloc relloc动态申请的内存
使用由指针ptr指向的内存区,使部分内存区能被其他变量使用,ptr是最近一次调用calloc或malloc函数时返回的值。
main()
{
int *pInt;
pInt = (int*)malloc(sizeof(int));
*pInt = 100;
printf("数值是: %d\n", *pInt);
free(pInt);
printf("数值是: %d\n", *pInt);
printf("\n");
printf("\n");
}
char *fun()
{
//char ch[100] = "hello world"; //因为被释放了,F返回值为NULL
//静态 全局区的空间只要开辟号,除非程序结束,否则不会释放,所以
//如果是临时使用,不建议使用静态全局区的空间
//static char ch[100] = "hello world";
//堆区开辟空间,手动申请,手动释放,更加灵活
//使用malloc函数的时候一般要进行强制转换,如果不加会警告
char *str = (char *)malloc(100 * sizeof(char));
str[0] = 'h';
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';
str[5] = '\0';
return str;
}
void main()
{
char *p;
p = fun();
printf("p = %s\n", p);
//使用free函数释放空间
free(p);
//防止野指针
p = NULL;
}
注意:
- free函数只能释放堆区的空间,其他区域的空间无法使用free
- free释放空间必须释放malloc或calloc或rello的返回值对应空间,不能说只释放一部分。
char *p = (char *) malloc(100); free(p);
- free后,因为没有给p赋值,所以p还是指向原先动态申请的内存,但内存已经不能再使用了,p变成野指针。解释:释放空间并不是把内存直接删除,而是告诉系统本人不在使用这个空间了,而别人可以使用这个空间。还会出现一种情况,虽然告知别人这块空间不用了,但该变量p还知道该空间在哪,因为p还保存原先地址,假设一个A同学在班级坐在x1的座位,在班级中这个x1的座位就属于他,但如果这个A同学要换位置呢,这个x1的位置还是存在,但里这个位置坐的内容却不确定,因此,只要你释放内容,尽可能不要去操作了‘。因为不一定这块空间内容是什么。一般为了防止野指针,会在free完毕之后对p赋为NULL。
- 一块动态申请的内存,只能free一次,不能多次free.
六、内存泄露
内存泄漏:首地址丢了,找不到,再也没法使用了 ,也没法释放了,这块内存就被泄露了。
使用malloc()等函数分配内存后,需要使用free()函数进行释放。若不释放,会造成内存泄漏,从而导致系统崩溃。
易造成漏删除的:
pOld = (int*)malloc(sizeof(int)); pNew = (int*)malloc(sizeof(int)); pOld = pNew; free(pOld);
free(pOld)释放的是pOld所指向的内存空间即原来pNew,但pOld原来指向的内存空间还没有被释放。
6.1 内存泄漏案例
内存泄漏案例1:
int main()
{
char *p;
p = (char *)malloc(100);
p = "hello world"; // p指向其他地方,把字符串的首地址赋给p;这样子导致原本堆区的空间找不到了
//从此刻起,再也找不到申请的100个字节的空间,动态申请的100个字节被泄漏了
return 0;
}
内存泄漏案例2:
void test1()
{
char *p;
p =(char *)malloc(100);
//接下来,可以用p指向内存了 ,
//...
//没有释放,没有返回,这样导致这个函数执行完后不知道这个申请的内存空间在哪了
}
void main()
{
//每次用一次fun泄露100个字节
test1();
test1();
}
如果在代码中出现这2种情况,一定要及时解决,否则后面内存就不够用了
6.2 解决方式
解决方式1
void test1()
{
char *p;
p =(char *)malloc(100);
//接下来,可以用p指向内存了 ,
//...
free(p);
}
void main()
{
//每次用一次fun泄露100个字节
test1();
test1();
}
解决方式2:
char *test1()
{
char *p;
p =(char *)malloc(100);
//接下来,可以用p指向内存了 ,
//...
return p;
}
void main()
{
char *q;
//可以通过q调用动态申请的100个字节的内容
q = test1();
释放
free(q);
//防止野指针
p = NULL;
}
总结:申请的内存,一定不要把首地址给丢了,在不用的时候一定要释放内存。也要防止野指针