C内存管理

目录

一、简介

二、内存分区

1.、代码区

2、文字常量区

3、栈区

4、堆区

5、全局/静态区

①全局变量

②静态变量

三、内存分区运行前后的区别

1.运行前

2.运行后

四、函数调用中内存分配

1、宏函数

2、函数调用流程

3、栈的生长方向和内存存储方式


一、简介

一个由C/C++编译的程序占用的内存分为以下几个部分 :堆区、栈区、全局区(静态区)、文字常量区、代码区五部分。

 在执行一个C/C++语言程序时,此程序将拥有唯一的内存四: 区栈区、堆区、全局区、代码区。每个程序都有唯一的四个内存区域。

①程序运行前:代码段、静态区和文字常量区三部分

②程序运行后:栈区和堆区(多出来的)

二、内存分区

1.、代码区

作用:存放程序的编译后的可执行二进制代码,CPU执行的机器指令,并且是只读的。
特点:

1. 只读
2. 共享(每次打开exe文件,都会指向一个地址空间)

2、文字常量区

作用:存放数值常量、字符常量、字符串常量、符号常量,只读的,程序结束后由系统释放

字符串常量是可以共享的
 

void test01()
{
	char * p1= "hello world";
	char * p2 = "hello world";
	char * p3 = "hello world";
	printf("%d\n",&"hello world");
	printf("%d\n", p1);  
	printf("%d\n", p2);
	printf("%d\n", p3);
}

这四个打印出来的地址可以是一样的。

 注意:

char   *p  =  "hello world";   //字符串常量

char   p[]  = "hello world";   //字符串变量,可以进行修改,是把常量区拷贝到栈区了

①ANSI C中规定:修改字符串常量,结果是未定义的
②有些编译器把多个相同的字符串常量看成一个(节省空间),有些则不进行此优化
③有些编译器可修改字符串常量,有些编译器则不可修改字符串常量

一般的,尽量不要去修改字符串常量。

3、栈区

作用: 由编译器自动分配释放,存放函数的参数值,局部变量的值 。

特点:

①栈是一种先进后出的内存结构,由编译器自动分配释放数据。
②主要存放函数的形式参数值、局部变量等。
函数运行结束相应栈变量会被自动释放
④栈空间较小,不适合将大量数据存放在栈中

 注意:不要返回局部变量的地址

//栈区上开辟的数据由系统进行管理,不需要程序员管理开辟和释放
int * func()
{
	int a = 10;
	return &a;
}
//不管结果是否正确,这个值已经被释放了,不可以操作一块非法的内存空间
void test01()
{
	int * p = func();

	printf("a = %d\n", *p);
	printf("a = %d\n", *p); 

}

在调用func后,局部变量a已经被释放了,a的地址被销毁,在对指针p进行访问,属于非法访问内存

char * getString()
{
	char str[] = "hello world";
	return str;
}

void test02()
{
	char * p = NULL;
	p = getString();

	printf("p = %s\n", p);
}

这里也是一样的,str指向字符串的首地址,调用函数后,地址被销毁,访问p属于非法访问内存。

4、堆区

由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间.使用malloc进行堆的申请,堆的总大小为机器的虚拟内存的大小。

特点:

①堆区由开发人员手动申请和释放,在释放之前,该块堆空间可一直使用
②由程序员分配和释放,若程序员不释放,程序结束时由系统回收内存
③堆空间一般没有软限制,只受限于硬件。会比栈空间更大,适宜存放较大数据。

 申请/释放堆区空间

int * p = malloc(sizeof(int)* 5); //4 byte  * 5 

free(p);

 注意:主调函数中没有给指针分配内存被调函数需要利用高级指针进行分配

void allocateSpace(char * pp)
{
	char * temp = malloc(100);
	memset(temp, 0, 100);
	strcpy(temp, "hello world");
	pp = temp;
}

void test02()
{
	char * p = NULL;
	allocateSpace(p);
	printf("%s\n", p);
}

内存管理

函数中局部变量存放在栈中,malloc分配的空间存放在堆中,temp指针指向这点内存空间。主调函数test02中指针没有分配内存,被调函数allocateSpace中用同级指针接收是修饰不了实参p,所以这里p还是为null。 这个和传入变量,是否修改实参的值是一个意思。

//1.利用高级指针
void allocateSpac2(char ** pp)
{
	char * temp = malloc(100);
	memset(temp, 0, 100);
	strcpy(temp, "hello world");
	*pp = temp;
	printf("aaa%s\n", *pp);
}

void test03()
{
	char * p = NULL;
	allocateSpac2(&p);
	printf("%s\n", p);
}


//2.利用返回值
char* allocateSpace3()
{
	char *temp = malloc(100);
	memset(temp, 0, 100);

	strcpy(temp, "hello world");
	return temp;
}
void test04()
{
	char *p = NULL;
	p = allocateSpace3();
	printf("%s\n", p);
}

主调函数中没有分配内存的空指针,被调函数中要利用高级指针分配内存

内存申请可使用三个函数来完成:malloc、calloc、realloc内存释放只需要使用 free 函数
 

①malloc 函数
原型: void *malloc(unsigned int num_bytes)
用法:分配长度为 num_bytes 字节的内存块
说明:如果分配成功则返回指向被分配内存的指针,否则返回 NULL

②calloc 函数

原型: void *calloc(int num_elems, int elem_size)
用法:为具有 num_elems 个长度为 elem_size 元素的数组分配内存。
说明:如果分配成功则返回指向被分配内存的指针,否则返回 NULL

③realloc 函数

原型: void *realloc(void *mem_address, unsigned int newsize)
作用:改变 mem_address 所指内存区域的大小为 newsize 长度
说明:如果重新分配成功则返回指向被分配内存的指针,否则返回 NULL

④free 函数

原型: void free(void *p);
作用:释放指针 p 所指向的的内存空间。
说明:p所指向的内存空间必须是用 calloc,malloc,realloc 所分配的内存。如果 p 为 NULL则不做任何操作

//1. calloc
void test01()
{
	int *p =  calloc(10,sizeof(int));  //10 * 4 字节
	// int *p = malloc(sizeof(int) * 10);
	for (int i = 0; i < 10; ++i)
	{
		p[i] = i + 1;
	}
	for (int i = 0; i < 10; ++i)
	{
		printf("%d\n", p[i]);
	}
	if (p != NULL)
	{
		free(p);
		p = NULL;
	}
}


//2.realloc
void test02()
{
	int *p = malloc(sizeof(int)* 10);
	for (int i = 0; i < 10; ++i)
	{
		p[i] = i + 1;
	}
	for (int i = 0; i < 10; ++i)
	{
		printf("%d ", p[i]);
	}
	printf("%d\n", p);
    
	p = realloc(p, sizeof(int)* 200);  //重新分配内存大小
	printf("%d\n",p);

	for (int i = 0; i < 15; ++i)
	{
		printf("%d ", p[i]);
	}
}

利用realloc重新分配内存,如果比原有的内存空间大,若后面的空闲足够大,直接在后面继续扩展空间,指针指向的内存空间首地址不变。

如果后面的空闲空间不够大,系统会重新找一个足够大的内存空间将原来的空间下的内存拷贝到新空间,将新空间的首地址交付给用户。

5、全局/静态区

全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放

特点:

①全局/静态区存储全局变量静态变量常量,该区变量在程序运行期间一直存在
②程序结束由系统回收。
已初始化的数据放在data段未初始化的数据放到bss段
④该区变量当未初始化时,会有有默认值初始化

①全局变量

在其他文件调用全局变量

//其他文件中声明第二行代码
extern int g_a = 10; //c语言中 默认全局变量前 加了关键字 extern

void test01()
{
	extern int g_a;  //说明引用全局变量,在其他文件中去寻找
	printf("g_a = %d\n", g_a);
}

c语言, 默认全局变量前加了关键字 extern。若在其他文件中 extern 变量,可以调用该变量。全局变量未初始化时,默认初始化为0。

全局变量特点:

① 作用域:全局可见。

全局变量(外部变量)是在函数外部定义的,它的作用域为从变量的定义处开始,到本程序文件的末尾。
 注:通常把超出一个函数的作用域称为全局作用域,其他几种(如块作用域)不超出一个函数的作用域称为局部作用域

② 存储空间:静态存储区
 系统会在编译时将全局变量分配在静态存储区,在程序执行期间,对应的存储空间不会释放,一直到程序结束才会释放。
 注:一个程序在内存中占用的存储空间可以分为3个部分:程序区(存放可执行程序的代码)、静态存储区(存放静态变量)、动态存储区(存放动态变量)
 

优先度:全局变量优先度低于局部变量。当全局变量和局部变量重名时,会屏蔽全局变量,局部优先

 

优点 :使用全局变量程序运行时速度会快一点,因为内存不需要再分配
缺点 :使用全局变量会占用更多的内存,因为其生命期长

全局变量作用域的扩展和限制

① 扩展:使用extern关键字可以对全局变量的作用域进行扩展

        全局变量的作用域为从变量的定义处开始,到本程序文件的末尾。若想在本文件全局变量定义之前引用该全局变量,可以在引用之前用extern关键字对该变量进行说明,有了此说明,就可以从说明之处起,合法地引用该变量。
        若想在一个文件(设为a.cpp)中引用另一个文件(设为b.cpp)中已定义的全局变量,可以在a.cpp中extern关键字对该全局变量进行说明,在编译和连接时,系统就会知道该全局变量已经在其他文件(b.cpp)中定义过了
       注:在编译时遇到extern,系统会现在本文件中查找全局变量的定义,如果找到,就在本文件中扩展作用域;如果找不到,就在连接时在其他文件中查找全局变量的定义,如果找到,就将作用域扩展到本文件;如果还找不到,按出错处理

 

② 限制:使用static关键字可以限制全局变量的作用域
       全局变量默认是有外部链接性的,作用域是整个工程,在一个文件内定义的全局变量,在另一个文件中,通过extern对全局变量进行声明,就可以使用全局变量。
       如果希望全局变量仅限本文件引用,而不能被其他文件引用,可以在定义全局变量时在前面加一个static关键字
 

注:即 static(限制) 和 extern(扩展) 不可同时出现

②静态变量

静态局部变量具有局部作用域。它只被初始化一次生命周期是全局,静态局部变量只对定义自己的函数体始终可见。

静态全局变量也具有全局作用域,作用于定义它的文件里,不能作用到其他文件里。

void func()
{
	static int s_a = 10; //静态变量只初始化一次
	s_a++;
	printf("%d\n", s_a);
}

void test03()
{
	func(); //11
	func(); //12
	func(); //13
}

静态变量只会初始化一次。静态变量的存放地址,在整个程序运行期间,都是固定不变的。非静态变量(一定是局部变量)地址每次函数调用时都可能不同,只在函数的一次执行期间不变。如果静态局部未初始化,默认为0

 全局变量和全局静态变量的区别:

不管全局变量加不加static,全局变量都是存储在静态存储区的,都是在编译时分配存储空间的,两者只是作用域不同全局变量默认具有外部链接性,作用域是整个工程全局静态变量的作用域仅限本文件,不能在其他文件中引用

const修饰的变量

//全局常量
const int a = 10; //全局常量存放到常量区,收到常量区的保护

void test01()
{
    //a = 20; //直接修改失败
	int * p = &a;
	*p = 30;  //间接修改 语法通过,运行失败
	printf("a = %d ", a); 


	//局部常量
	const int b = 10; //b分配到了栈上,可以通过间接方式对其进行修改
	//b = 30; //直接修改失败
	int * p2 = &b;
	*p2 = 30;
	printf("b = %d\n", b); //间接修改成功,C语言下const修饰的局部常量为伪常量
}

const修饰全局常量存放到常量区,收到常量区的保护,不可以修改const修饰局部变量,变量会被分配到了栈上,可以通过间接方式对其进行修。
 

小结:

①堆区(heap) :允许程序在运行时动态地申请某个大小的内存空间, 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收

②栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等

③全局区(静态区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放

④文字常量区:常量字符串就是放在这里的,只读的。程序结束后由系统释放

⑤程序代码区:存放程序的编译后的可执行二进制代码,CPU执行的机器指令,并且是只读的

 

三、内存分区运行前后的区别

1.运行前

在没有运行程序前(程序没有加载到内存前),分别为代码区(text)数据区(data)未初始化数据区(bss)3 个部分(把 data 和 bss 合起来叫做静态区或全局区)。

①代码区
        存放 CPU 执行的机器指令。通常代码区是可“共享”的,即另外的执行程序可以调用它,使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可(节约内存

        代码区是只读的,使其只读的原因是防止程序意外的修改了它的指令。另外,代码区还规划了局部变量的相关信息。

②全局初始化数据区/静态数据区(data段)
        该区包含了在程序中(1)被初始化的全局变量(2)已经初始化的静态变量,包括全局静态变量(3)常量数据(如字符串常量)

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

程序源代码被编译之后主要分成两种段:程序指令(代码区)和程序数据(数据区)。代码段属于程序指令,而数据域段和 bss 段属于程序数据。

程序的指令和程序数据分开原因:

  • 程序被加载到内存中之后,可以将数据和代码分别映射到两个内存区域。由于数据区域对进程来说是可读可写的,而指令区域对程序来讲是只读的,所以分区之后呢,可以将程序指令区域和数据区域分别设置成只读或可读可写。这样可以防止程序的指令有意或者无意被修改
  • 当系统中运行着多个同样的程序的时候,这些程序执行的指令都是一样的,所以只需要内存中保存一份程序的指令就可以了,只是每一个程序运行中数据不一样而已,这样可以节省大量的内存

2.运行后

 程序在加载到内存前,代码区和全局区(data+ bss)的大小就是固定的,程序运行期间不能改变。运行可执行程序,操作系统把物理硬盘程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区

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

②未初始化数据区(BSS)
加载的是可执行文件 BSS 段,位置可以分开也可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期是整个程序运行过程

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

④栈区(stack)
栈是由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,局部变量的生存周期为申请到释放该段栈空间。

⑤堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于 BSS 区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时可能会由操作系统回收。
 

关键字修饰

类型作用生命周期存储位置
auto变量一对{}内当前函数栈区
static局部变量一对{}内整个程序运行期初始化在data段,未初始化在BSS段
extern变量整个程序整个程序运行期初始化在data段,未初始化在BSS段
static全局变量当前文件整个程序运行期初始化在data段,未初始化在BSS段
extern函数整个程序整个程序运行期代码区
static函数当前文件整个程序运行期代码区
register变量一对{}内当前函数运行时存储在CPU寄存器
字符串常量当前文件整个程序运行期data段

 内存分配图:

 

四、函数调用中内存分配

1、宏函数

宏函数:

①宏函数和宏常量都是利用#define定义出来的内容

②在项目中,经常把一些短小而又频繁使用的函数写成宏函数

③这是由于宏函数没有普通函数参数压栈、跳转、返回等时间上的开销,可以调高程序的效率

注意:宏函数通常需要加括号,保证运算的完整

宏函数将频繁短小的函数可以封装为宏函数,以空间换时间。和内联函数的作用相同。

#define MYADD(x,y)  ((x) + (y)) //不是函数 ,宏函数

//普通函数下的a、b都要进行入栈,函数执行后出栈
int  myAdd(int a ,int b)
{
	return a + b;
}
//宏函数  在一定的场景下  要比普通的函数效率高,把频繁使用并且短小的函数 可以写成宏函数
//宏函数在编译阶段就替换源码
//而没有普通函数入栈出栈的开销,以空间换时间
void test01()
{
	int a = 10;
	int b = 20;
	printf("a + b = %d\n", MYADD(a, b)); //  ((a) + (b))
}

注意:这里只是宏展开替换,是整体替换。

#define MYADD(x,y)  x+y

MYADD(a,b)*20 :宏展开----> a+b*20

2、函数调用流程

如下,在mian函数中调用func函数

int func(int a,int b){
	int t_a = a;
	int t_b = b;
	return t_a + t_b;
}

int main(){
	int ret = 0;
	ret = func(10, 20);
	return EXIT_SUCCESS;
}

函数调用会进行入栈和出栈,那么被调函数func中的a、b入栈的顺序是从左到右,还是从右到左当被调函数执行完毕后,a、b这两个参数是由主调函数mian去管理释放还是被调函数func管理释放

调用惯例

函数的调用方(主调函数)和被调用方()对于函数是如何调用的必须有一个明确的约定,
只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为调用惯例

C/C++语言中存在多个调用惯例,默认使用的调用惯例为 cdecl

注: cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字。

所以,上面的两个问题就解决了。

3、栈的生长方向和内存存储方式

栈的生长方向:自上而下 ,栈底高地址 ,栈顶低地址

 验证方式

void test01()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int d = 40;

	printf("%d\n", &a); 
	printf("%d\n", &b);
	printf("%d\n", &c);
	printf("%d\n", &d);
}

 

内存中的多字节数据相对于内存地址有大端和小端之分。

小端模式位字节数据保存在内存的地址中,位字节数据保存在内存的地址中

大端模式位字节数据保存在内存的地址中,位字节数据保存在内存的地址中

 

验证方式:

	int a = 0x11223344;
	char * p = &a; //char * 改变指针步长,一次跳一个字节 

	printf("%x\n", *p);      // 44  低地址  -- 低位字节
	printf("%x\n", *(p+1));
	printf("%x\n", *(p+2));
	printf("%x\n", *(p+3));  // 11  高地址  --  高位字节

注意:无论是小端模式还是大端模式。每个字节内部都是按顺序排列

常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Super.Bear

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值