内存管理

1、作用域

C 语言变量的作用域主要有三种:

  • 代码块作用域(代码块是 {} 之间的一段代码)
  • 函数作用域
  • 文件作用域
1.1、局部变量

局部变量也叫 auto 自动变量(auto 可以不写),一般情况下代码块 {} 内部定义的变量都是自动变量,具有如下特点:

  • 在一个函数内定义,只在函数范围内有效
  • 在复合语句中定义,只在复合语句中有效
  • 随着函数调用的结束或复合语句的结束,局部变量的生命周期也就结束了
int  main()
{
    //定义变量,局部变量,只在main函数内有效
    //作用域:main函数之内
    //生命周期:从创建到函数结束
    auto int a=10;		//auto可以省略不写
    return 0;
}
1.2、全局变量
  • 在函数外定义,可被本文件及其他文件中的函数所共用,若其他文件中的函数调用此变量,须用 extern 声明
  • 全局变量的生命周期和程序运行周期一样
  • 不同文件的全局变量不可重名
  • 全局变量存储在数据区
  • 全局变量可以和局部变量同名,使用的时候采用就近原则
  • 不同文件中的全局变量不可以重名

main.c

#include
//全局变量:在函数外部定义的变量
//作用域,整个项目中所有文件,如果在其他文件中使用,需要声明
//生命周期,从程序创建到程序销毁
int a=10;
void func()
{
    a=100;
    printf("%d\n",a);
}
int main()
{
    printf("%d\n",a);	//10
    int a=123;		//数据在操作时,采用就近原则
    
    //匿名内部函数
    {
        int a=456;
        printf("%d\n",a);		//456
    }
    
     //匿名内部函数
    {
        a=456;
        printf("%d\n",a);		//456
    }
    
    printf("%d\n",a);	//456
 
    func();				//100
    func2();			//1000
    return 0;
}

test.c

#include
//声明全局变量
extern int a;

//定义全局变量
int b=10;	//全局变量不可以重名
int b=10;	//err,全局变量不可以重名
void func2()
{
    a=1000;
    printf("%d\n",a);
}
1.3、静态局部变量
  • static 局部变量的作用域也是在定义的函数内有效
  • static 局部变量的生命周期和程序的运行周期一样,同时 static 局部变量的值只能初始化一次,但可以赋值多次
  • static 局部变量若未赋初值,则由系统自动赋值,数值型变量自动赋初值 0,字符型变量赋空字符
  • static 局部变量只能在其作用域内使用,即当前函数中使用
  • static 局部变量在数据区存储,不会随着栈区的变化而变化
//静态局部变量
void func()
{
	printf("%d\n",b);	//err,未定义变量b
 
}
int main()
{
	//静态局部变量
    static int b=10;
    printf("%d\n",b);		//10
    return 0;
    
}
//静态局部变量
void func()
{
   // printf("%d\n",b);	//err,未定义变量b
    
    static int b=10;		//静态局部变量只会初始化一次,可以多次赋值,作用域只在函数内,只能在当前函数中使用,生命周期与程序生命周期相同
    b++;
    printf("%d\n",b);
}

int main()
{
    //如果在 func() 中定义的是局部变量,会输出十次 11
    for(int i=0;i<10;i++)
    {
        func();		//十次输出,分别输出:11,12,13,14,15,16,17,18,19,20
    }
    return 0;
}
1.4、静态全局变量
  • 静态全局变量,作用域为本文件,只能在本文件中使用,不能在其他文件中使用
  • 生命周期:从程序创建到程序销毁
  • 存储在数据区

main.c

//静态全局变量,作用域为本文件,只能在本文件中使用,不能在其他文件中使用
//生命周期:从程序创建到程序销毁
//存储在数据区
static int a=10;
void func()
{
    a=20;
     printf("%d\n",a);		//20
}
int main()
{
    printf("%d\n",a);		//10
    func();
    func06();
    return 0;
}

test.c

extern int a;	//err,静态全局变量的作用域为本文件,因此这里无法使用
void func06()
{
	 printf("%d\n",a);		//20
}
  • 变量作用域
变量类型 作用域 生命周期 存储位置
局部变量 函数内部 从局部变量创建到函数结束 栈区
全局变量 项目中所有文件 从程序创建到程序销毁 数据区
静态局部变量 函数内部 从程序创建到程序销毁 数据区
静态全局变量 定义所在的文件中 从程序创建到程序销毁 数据区
1.5、未初始化的数据
  • 局部变量未初始化,值为任意值(乱码)
  • 未初始化的全局变量,值为0
  • 未初始化的静态局部变量,值为0
  • 未初始化的静态全局变量,值为0
//未初始化的全局变量,值为0
int abc;
int main()
{
    /*
    //局部变量未初始化,值为任意值(乱码)
    int abc;
    printf("%d\n",abc);		//在 vs 中 err,在其他环境中可以输出,但是为乱码
    */
    printf("%d\n",abc);	
}
1.6、全局函数和静态函数
  • 在 C 语言中,函数默认都是全局的,使用 static 关键字可以使函数变成静态函数
  • static 静态函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数也无法使用
  • 对于不同文件的 static 函数的名字可以相同
  • 在 C 语言中,不支持函数的重载
  • 全局函数的名称在作用域中是惟一的
  • 全局函数的作用域:在这各个项目的所有文件中都可以使用
  • 函数可以调用自己:递归函数。递归函数一定要有出口
  • 静态函数可以和全局函数重名,在当前文件中,使用的是当前文件中定义的函数
  • 静态函数和全局函数的生命周期相同,从程序创建到程序销毁

main.c

//全局函数声明
void buble();
int main()
{
    buble();	//err,静态函数,作用域仅限于定义这个函数的文件
    return 0;
}

test.c

//静态函数,作用域仅限于定义这个函数的文件
static void buble()
{
    printf("buble\n");
}
  • 函数作用域
函数类型 作用域 生命周期 存储位置
全局函数 项目中所有文件 从程序创建到程序销毁 代码区(未唤醒),栈区(唤醒之后)
静态函数 定义所在的文件 从程序创建到程序销毁 代码区
1.7、extern 全局变量声明
  • extern 用于声明一个在别的文件中已经定义的全局变量,这里只是声明,不是定义

注意:

  • 语序在不同的函数中使用相同的变量名,它们代表了不同的对象,分配不同的单眼,互不干扰
  • 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用
  • 所有的函数默认是全局的,那么函数名不能重名;不同文件的 static 函数是可以重名的

2、内存布局

2.1、内存分区
  • 局部常量不安全,全局常量是安全的
//未初始化全局变量
int a1;

//初始化全局变量
int b1=10;

//未初始化静态全局变量
static int c1;

//初始化静态全局变量
static int d1=10;

int main()
{
    //初始化局部变量
    int e1=10;
    
    //未初始化静态局部变量
    static int f1;
    
    //初始化静态局部变量
    static int g1=10;
    
    //常量字符串
    char* p="hello world";
    
    //数组
    int arr[]={1,2,3,4};
    
    //指针变量
    int *pp=arr;
    //指针地址 &pp
    return 0;
}
2.2、内存模型

应用程序的内存四区模型:

  • 代码区:程序执行的二进制码(程序指令)
    • 共享的,形同内容只存放一份
    • 只读的
  • 数据区(与程序同生共死)
    • 初始化数据区,data 段
    • 未初始化数据区,bss 段
    • 常量区
  • 栈区:系统为每一个程序分配一个临时的空间,要用就分配,用完就销毁
    • 局部变量,函数信息,函数参数,数组
    • 栈区大小:1MB,在 windows 中可以拓展到 10MB,在 Linux 中,可以拓展到 16MB
  • 堆区:
    • 堆区大小:没有上限限制
    • 存储大数据,图片,音频视频文件
    • 需要手动开辟,手动释放。开辟方式:malloc,colloc,realloc,free
2.3、内存分区概述
  • 在程序没有运行前,即程序没有加载到内存前,可执行程序内部已经分好了 3 段信息:代码区(text)、数据区(data)、未初始化数据区(bss),其中 data 和 bss 合起来统称静态区或者全局区。
  • 程序在加载到内存前,代码区和全局区(data 段和 bss 区)的大小是固定的,程序运行期间是不能改变的。
  • 运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出的代码区(text)、数据区(data)、未初始化数据区(bss)之外,还额外增加了栈区、堆区。
  • 内存中,从低地址区到高地址区的方向,依次分别是:代码区、初始化数据区(离低地址区较近的部分是常量区,离高地址区较近的部分是 data 段)、未初始化数据区(bss)、堆区、栈区(栈区内部从高地址区往低地址区)。

程序运行之前:

1)代码区

  • 存放 CPU 执行的机器指令。
  • 代码区通常是可共享的(即其他的可执行程序也可以调用它)。使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。
  • 代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。
  • 另外,代码区还规划了局部变量的相关信息。

2)全局初始化数据区 / 静态数据区(data 段)

  • data 段包含了在程序中明确被初始化的全局变量、已经被初始化的静态变量(包括全局静态变量和局部静态变量)、常量数据(例如字符串常量)

3)未初始化数据区(bss 区)

  • 存入的是全局未初始化变量和外初始化静态变量
  • 未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者是空(NULL)

程序运行之后:

1)代码区(text segment)

  • 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的

2)未初始化数据区(bss)

  • 加载的是可执行文件 bss 段,位置可以分开亦可以紧靠数据段,存储在数据段的数据(全局未初始化、静态未初始化数据)的生存周期为整个程序运行过程

3)全局初始化数据区 / 静态数据区(data segment)

  • 加载的是可执行文件的数据段,存储于数据段(全局初始化、静态初始化的数据,文字常量(只读))的数据的生存周期为整个程序运行过程

4)栈区(stack)

  • 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。
  • 在程序运行过程中,实时加载和释放,因此局部变量的生存周期为申请到释放该段栈空间

5)堆区(heap)

  • 堆是一个大容器,其容量远远大于栈,但没有栈那样先进后出的顺序。
  • 用于动态内存分配。
  • 堆在内存中位于 bss 区和栈区之间。
  • 一般由程序员分配和释放,若程序员未释放,程序结束时由操作系统回收。
2.4、栈区存储模型

1)普通变量存储:从高地址区往低地址区存储

2)数组:开辟一段连续的内存,数组从前往后是从低地址区往高地址区存储

3)栈区:从高地址区向低地址区生长。先进后出,后进先出

4)堆区:从低地址区向高地址区生长

3、堆空间开辟和使用

3.1、栈区的大小
int main()
{
    //栈区的大小
	int arr[820000]={0};	//err,数组太大了,栈区内存不够	820000*4/1024/1024>31MB,远大于栈区的大小,栈区大小为1MB
}

  • 栈区的大小可以拓展,windows 中可以拓展至10 MB,在 Linux 中,可以拓展到 16MB
3.2、开辟堆空间存储数据

1)malloc() 函数

#include
void* malloc(size_t size);
  • 功能:在内存的动态存储区(堆区)中分配一块长度为 size 字节的连续区域,用来存放类型说明符指定的类型,分配的内存空间内容不确定,需要使用 memset 进行初始化
  • 参数:size,需要分配的内存大小(单位,字节)
  • 返回值:
    • 成功:分配空间的起始地址
    • 失败:NULL

2)free() 函数

#include
void free(void* ptr);
  • 功能:释放 ptr 所指向的一块内存空间,ptr 是一个任意类型的指针变量,指向被释放区域的首地址,对同一内存空间多次释放会出错
  • 参数:ptr,需要释放空间的首地址,被释放区应该是由 maclloc 函数所分配的区域
  • 返回值:无
int main()
{
	//开辟堆空间存储数据
    int* p=(int*)malloc(sizeof(int));
    printf("%p\n",p);
    printf("%d\n",*p);	//乱码
    
    //使用堆空间
    *p=123;
    printf("%d\n",*p);	//123
    
    //释放堆空间
    free(p);
    printf("%p\n",p);	//还是输出 p 的地址,但是此时 p 指向的是一个未知空间,p 为野指针
    *p=456;
    printf("%d\n",*p);	//456
    p=NULL;
    
    return 0;
}

为了防止所使用的堆空间指针释放后成为野指针,应该让其指向空指针。

3)可以开辟多大的堆空间

  • 理论上没有上限,不超过最大剩余内存即可。但是由于开辟的是连续空间,因此开辟空间过大,当内存不存在这么大的连续空间,会开辟失败。
3.3、堆空间存储数据
  • 在堆空间的开辟和释放的时候,应该保证释放的指针和开辟时的指针相同
#define MAX 10

int* p=(int*)malloc(sizeof(int)*10);
for(int i=0;i<10;i++)
{
	p[i]=i;
}
for(int i=0;i<10;i++)
{
    printf("%d\n",*(p+i));
}
free(p);


srand((size_t)time(NULL));
int* p=(int*)malloc(sizeof(int)*MAX);
for(int i=0;i<MAX;i++)
{
    p[i]=rand()%100;
    printf("%d\n",*(p+i));
}

for(int i=0;i<MAX;i++)
{
    printf("%d\n",*p);
    p++;
}
free(p);	//err,这里会报错,因为指针在叠加过程中,正在不断改变,最后释放的指针和开辟的空间首地址不同,会导致前面开辟的堆空间成为无主空间,只有等程序结束才能释放
//在堆空间的开辟和释放的时候,应该保证释放的指针和开辟时的指针相同
3.4、内存操作函数

1)memset()

#include
void* memset(void* s,int c,size_t,n);
  • 功能:将 s 的内存区域的前 n 个字节以参数 c 填入
  • 参数:
    • s:需要操作的内存的首地址
    • c:填充的字符,c 为 int 型,但必须是 unsigned char,范围为 0-255
    • n:指定需要设置的大小
  • 返回值:s 的首地址
int* p=(int*)malloc(sizeof(int)*10);
//未初始化时,原始数据,乱码
for(int i=0;i<10;i++)
{
    printf("%d\n",*(p+i));
}

//memset() 重置内存空间的值
memset(p,0,40);
//memset(p,1,40),这样无法使内部每个整型都变成 1,实际上是将四十个字节分别设置为 00000001,最后再将 0000001000000100000010000001 以整型读出,就和预想的不一样了
//字符数组可以使用 memset() 重置,但是不能用于字符串形式输出
for(int i=0;i<10;i++)
{
    printf("%d\n",*(p+i));		//输出十个 0 
}

free(p);

2)memcpy()

#include
void* memcpy(void* dest,const void* src,size_t,n);
  • 功能:拷贝 src 所指的内存内容的前 n 个字节到 dest 所指的内存地址上
  • 参数:
    • dest:目的内存首地址
    • src:源内存首地址
    • n:需要拷贝的字节数
  • 返回值:dest的首地址

注:dest 和 src 所指的内存空间不可以重叠,否则可能会导致程序报错。当他们重叠的时候,使用 memmove

int arr[]={1,2,3,4,5,6,7,8,9,10};
int* p=(int*)malloc(sizeof(int)*10);

memcpy(p,arr,sizeof(int)*10);
for(int i=0;i<10;i++)
{
    printf("%d\n",*(p+i));		//输出:1,2,3,4,5,6,7,8,9,10 
}

free(p);

//dest 和 src 所指的内存空间重叠,可能会导致报错
memcpy(&arr[5],&arr[3],20);

strcpy() 和 memcpy() 的区别:

  • strcpy():遇到 \0 停止
  • memcpy():拷贝的内容和字节数有关,和拷贝内容无关

3)memmove()

memmove() 功能用法和 memcpy() 一样,区别在于:/dest 和 src 所指的内存空间重叠时,memmove() 仍然能够处理,不过执行效率比 memcpy() 低。

int arr[]={1,2,3,4,5,6,7,8,9,10};

//dest 和 src 所指的内存空间重叠,使用 memmove() 更安全,不会报错
memmove(&arr[5],&arr[3],20);

4)memcmp()

#include
int memcmp(const void* s1,const void* s2,size_t n);
  • 功能:比较 s1 和 s2 所指向内存区域的前 n 个字节(逐字节比较)
  • 参数:
    • s1:内存地址1
    • s2:内存地址2
    • n:需要比较的前 n 个字节
  • 返回值:
    • 相等:0
    • 大于:>0
    • 小于:<0
int arr1[]={1,2,3,4,5,6,7,8,9,10};
int arr2[]={1,2,3,4,5};

memcmp(arr1,arr2,20);	// 0,相同

char str1[]="hello\0 world";
char str1[]="hello\0 world";
memcmp(str1,str2,13);	// 0,相同

strcmp() 和 memcmp() 的区别:

  • strcmp():遇到 \0 停止
  • memcmp():拷贝的内容和字节数有关,和拷贝内容无关
3.5、内存常见问题

1)数组下标越界

//数组下标越界
char ch[11]="hello world";

//堆空间越界
char* p=(char*)malloc(sizeof(char)*11);
strcpy(p,"hello world");
printf("%s\n",p);		//输出:hello world
free(p);				//err,开辟了11字节,使用和释放了12个

2)开辟内存大小和释放内存大小不一致

int* p=(int*)malloc(0);
printf("%p\n",p);	//会输出一个地址
*p=100;
printf("%d\n",*p);	//输出:100
free(p);			//err,由于分配了 0 字节空间,p 相当于是野指针,但是释放了 4 字节,开辟内存大小和释放内存大小不一致

3)开辟空间和类型不对应

int* p=(int*)malloc(10);		
p[0]=123;
p[1]=456;
//p[2]=789;
printf("%p\n",p);	//会输出一个地址
printf("%d\n",*p);	//输出:123
printf("%d\n",*(p+1));	//输出:456
//printf("%d\n",*(p+2));	//err,堆空间越界了

4)堆空间不允许多次释放

int* p=(int*)malloc(sizeof(int)*10);		

free(p);	//释放 p,没问题
//free(p);	//此时 p 是野指针,再次释放会报错

//在释放完之后,应该让 p 指向空指针
p=NULL;

//空指针允许多次释放
free(p);
free(p);
free(p);
free(p);
free(p);

5)释放的指针应该和开辟空间时得到的指针相同

int* p=(int*)malloc(sizeof(int)*10);	
for(int i=0;i<10;i++)
{
    *p=0;
    //指针叠加,不断改变指针方向,释放会出错
    p++;
}
free(p);	//err,指针叠加,不断改变指针方向,释放会出错
p=NULL;

4)值传递和地址传递

void func(int* p)		//本质上还是值传递,传递的是指针变量
{
    p=(int*)malloc(sizeof(int)*10);	
}

voidfunc1(int** p)	//这个才是地址传递
{
    *p=(int*)malloc(sizeof(int)*10);	
}

int main()
{
    int *p=NULL;
    func(p);
    
    //func1(&p);
    
    for(int i=0;i<10;i++)
    {
        p[i]=i;			//err,这里 p 依然是空指针,操作空指针会报错
    }
    free(p);
}

//解决这里的问题可以使用地址传递,或者是使用函数的返回值
3.6、二级指针对应的堆空间
//开辟二级指针对应的堆空间,实际上就是一个 5*3 的二维整型数组,int arr[5][3]
int** p=(int**)malloc(sizeof(int*)*5);		//在堆空间中开辟
p[0]=(int*)malloc(sizeof(int)*3);			
p[1]=(int*)malloc(sizeof(int)*3);

//开辟一级指针对应的堆空间
for(int i=0;i<5;i++)
{
    p[i]=(int*)malloc(sizeof(int)*3);		//在堆空间中开辟,但是 p 和 p[i]之间不一定是连续的
}

//内存使用
for(int i=0;i<5;i++)
{
    for(int j=0;j<3;j++)
    {
        scanf("%d",&p[i][j]);
    }
}
for(int i=0;i<5;i++)
{
    for(int j=0;j<3;j++)
    {
        printf("%d ",p[i][j]);
    }
    printf("\n");
}

//空间释放,需要一级一级地释放,从内层向外层释放
for(int i=0;i<5;i++)
{
    free(p[i]);
}
f

你可能感兴趣的:(C,开发语言,c语言,笔记)