内存四区 & malloc/free与new/delete的区别

前言

之前写了一篇关于《快速排序的4种优化》的博文,当时在验证各种情况的时候忽略内存分配的问题,导致所得到的结果分析的不全面。因为在刚开始写程序的时候将数组声明在 main() 里面,这样数组占用的栈空间,影响了递归的深度,也影响了程序处理的数据量(即使不用尾递归,处理的数据量也能超过 4 万)。在了解内存分配问题之前,先复习一下进程的概念。进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位(引入线程后,调度单位为线程)。简而言之,进程是程序的基本执行实体,是活的程序。程序和进程最大的差别为:是否获得了系统资源。进程会占用一定数量的内存,它可能是用来存放从磁盘载入的程序代码,也可能是存放取自用户输入的数据等。不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内存是事先静态分配和统一回收的,而有些却是按需要动态分配和回收的。

 

对任何一个普通进程来讲,它都会涉及到不同的数据段(如代码段,数据段,bss 段,堆段,栈段)

代码段:代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作。其通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的变量,例如字符串常量等。

初始化数据段:通常将此段称为数据段,它用来存放可执行文件中需要明确赋初值的变量,换句话说就是存放程序静态分配的变量和全局变量。例如:int a = 100; 。

未初始化数据段:通常将此段称为 bss 段,它包含了程序中未初始化的全局变量。bss 是英文 Block Started by Symbol 的简称,bss 段属于静态内存分配。在程序开始执行之前,内核将此段中的数据初始化为 0 或空指针。例如:int sum[100]; 。// 此时,sum数组内的所有元素都为 0。

堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用 malloc() 或 new 分配内存时,新分配的内存就被动态添加到堆上;当利用 free() 或 delete 释放内存时,被释放的内存从堆中被剔除。堆区由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收 。注意:它与数据结构中的堆是两回事,分配方式类似于链表。( C++ Primer Plus 中文版 356 页说 new 创建的对象将驻留在栈内存是翻译者的错。)

栈:栈区(不同于数据结构中的栈)是用户存放程序临时创建的局部变量,也就是说我们函数括弧 "{}" 中定义的变量(但不包括 static 声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的后进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。栈区由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。它是由操作系统分配的,内存的申请与回收都由 OS 管理。

内存四区图:

注意:栈的分配是由高向低,而堆是由低向高。

 

在进程被载入内存中时,基本上被分裂成许多小的节(section)。需要关注的是6个主要的节:

(1) .text 节:基本上相当于二进制可执行文件的 .text 部分,它包含了完成程序任务的机器指令。该节标记为只读,如果发生写操作,会造成 segmentation fault。在进程最初被加载到内存中开始,该节的大小就被固定。

(2) .data 节:用来存储初始化过的变量,如:int a = 0; 该节的大小在运行时固定的。

(3) .bss 节:用来存储未初始化的变量,如:int a; 该节的大小在运行时固定的。

(4) 堆节(heapsection):用来存储动态分配的变量,位置从内存的低地址向高地址增长。内存的分配和释放通过 malloc() 和 free() 函数控制。

(5) 栈节(stacksection):用来跟踪函数调用(可能是递归的),在大多数系统上从内存的高地址向低地址增长。同时,栈这种增长方式,导致了缓冲区溢出的可能性。

(6) 环境/参数节(environment/argumentssection):用来存储系统环境变量的一份复制文件,进程在运行时可能需要。例如,运行中的进程,可以通过环境变量来访问路径、shell 名称、主机名等信息。该节是可写的,因此在格式串(format string)和缓冲区溢出(buffer overflow)攻击中都可以使用该节。另外,命令行参数也保持在该区域中。

之前,我对这类知识不太关注,只是知道个大概,但是,最近的写的快排优化,让我意识到这类问题的重要性(也是对知识盲区的学习)。在哪一篇博文中,我习惯性的将数组声明写在了 main() 函数里。从而得出了在 Codeblocks 里使用固定基准方式(没有尾递归的情况)处理升序数组时只能处理 4 万个数组元素的结论(这一结论并不能说错,但忽略了内存分配的问题)。因为我之前并不太清楚 main 函数中的数组(不是动态分配)占用的是栈的空间。所以这就影响了递归的深度。准确的说,要是将数组声明成全局的,那个算法就可以处理更多的数据元素。上面的概念我们已经学习了,简化其复杂的内容,可以将内存的分配当做只有 4 个部分(代码区、数据区、堆和栈)。现在就用一个简单的例子来看看各种变量在内存中的分配情况:

#include <stdio.h>

int a = 0;    //a在全局已初始化数据区  
int asd[10];  //asd[]bss段
char *p1;     //p1在bss段(未初始化全局变量)  
int main()  
{
    int b;                   //b在栈区
    char s[] = "abc";        //s为数组变量,内容存储在栈区
    char *p1,p2;            //p1、p2在栈区
    char *p3 = "123456";     //123456\0是字符串常量,而p3在栈区  
    static int c = 0;       //C为静态数据,存在于已初始化数据区,另外,静态数据会自动初始化
    p1 = (char*)malloc(10);  //分配得来的10个字节的区域在堆区
    p2 = (char*)malloc(20);  //分配得来的20个字节的区域在堆区
    free(p1);
    free(p2);
    return 0;
}

 

2019.4.30补充

今天使用了一个函数 char *strtok_r(char *str, const char *delim, char **saveptr);,它的功能是切割字符串。传参的时候,我习惯性的创建char * str = " abc def "; 然后使用这个函数,结果程序报段错误。尝试修改为char str[ ] = " abc def "; 就可以解决问题。

这是因为这两种方式的操作对象不同。使用 char * str = "abc def" 后,编译器在内存的常量区分配一块内存,保存 "abc def" 这一字符串字面值,然后在栈上分配内存保存 str, str 的内容为 "abc def" 的地址。str 试图修改常量 "abc def" 的内容时(例如str[0] = 'g'),程序就崩了。而 char str[] = "abc def" 定义了一个数组,编译器为其在栈上分配了内存空间,因而可以进行修改操作。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
	char source[] = "hello, world! welcome to china!";  //内容在栈区,可修改内容。
	char *input1 = source;                              //input1指向source,内容在栈区,所以input1可修改内容
	char *input2 = "hello, world! welcome to china!";   //input2所指内容在常量区,而input2本身在栈区
	//input2 = input1;
	(input2)[1] = 'a';                                  //尝试修改常量区的内容,程序出错
	printf("%c\n", (input1)[1]);                         
	printf("%c\n", (input2)[0]);                        //若只是读相关内容,程序能正常运行
	return 0;
}

 

 

malloc/free与new/delete的区别

相同点:都可用于申请动态内存和释放内存。

不同点:

(1) 操作对象有所不同。
malloc 与 free 是 C/C++ 的标准库函数,new/delete 是 C++ 的运算符对于非内部数据类的对象而言,光用 malloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数对象消亡之前要自动执行析构函数。由于 malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加 malloc/free 。

(2) 用法上也有所不同。
函数 malloc 的原型如下:

void* malloc(size_t size);

用 malloc 申请一块长度为 length 的整数类型的内存,程序如下:

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

注意:

① malloc 返回值的类型是 void *,所以在调用 malloc 时要显式地进行类型转换,将 void * 转换成所需要的指针类型。
② malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。

 

函数 free 的原型如下:

void free(void* memblock);

为什么 free 函数不象 malloc 函数那样复杂呢?原因是指针 p 的类型以及它所指的内存的容量事先都是知道的,语句 free(p) 能正确地释放内存。如果 p 是 NULL 指针,那么 free 对 p 无论操作多少次都不会出问题。如果 p 不是 NULL 指针,那么 free 对 p 连续操作两次就会导致程序运行错误。

 

 

new/delete 的使用要点

运算符 new 使用起来要比函数 malloc 简单得多,例如:

int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];

这是因为 new 内置了 sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么 new 的语句也可以有多种形式。
如果用 new 创建对象数组,那么只能使用对象的无参数构造函数。例如:

Obj *objects = new Obj[100];     // 创建100 个动态对象
Obj *objects = new Obj[100](1);  // 错误写法,VS下直接报无参数构造函数

在用 delete 释放对象数组时,留意不要忘了符号'[ ]'。例如

delete []objects; // 正确的用法
delete objects;   // 错误的用法

后者相当于 delete objects[0],漏掉了另外 99 个对象。

(1) new 自动计算需要分配的空间,而 malloc 需要手工计算字节数。
(2) new 是类型安全的,而 malloc 不是,比如:

int*p = new float[2];              //编译时指出错误
int*p = malloc(2*sizeof(float));   //编译时无法指出错误

new operator 由两步构成,分别是 operator new 和 construct 。
(3) operator new 对应于 malloc,但 operator new 可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而 malloc 无能为力。
(4) new 将调用 constructor,而 malloc 不能;delete 将调用 destructor,而 free 不能。
(5) malloc/free 要库文件支持,new/delete 则不要。

 

 

本质区别

malloc/free 是 C/C++ 语言的标准库函数,new/delete 是 C++ 的运算符。对于用户自定义的对象而言,用 maloc/free 无法满足动态管理对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于 malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free。因此C++需要一个能完成动态内存分配和初始化工作的运算符 new,以及一个能完成清理与释放内存工作的运算符 delete。

#include <iostream>
#include <malloc.h>
using namespace std;

class Obj
{
public:
	Obj( )
	{ cout  <<  "Initialization"  <<  endl; }
	~ Obj( )
	{ cout  <<  "Destroy" <<  endl; }
	void Initialize( )
	{ cout  <<  "Initialization"  <<  endl; }
	void  Destroy( )
	{ cout  <<  "Destroy"  <<  endl; }
}obj;

int main()
{
    Obj *objects = new Obj[10];
    cout <<endl;

    //use malloc & free
    Obj*a = (Obj*)malloc(sizeof(obj));// allocate memory
    a->Initialize();                // initialization
    a->Destroy();                   // deconstruction
    free(a);                        // release memory

    //use new & delete
    Obj*b = new Obj;
    delete b;
    return 0;
}

 

问题:既然 new/delete 的功能完全覆盖了 malloc/free,为什么 C++ 还保留 malloc/free 呢?

答:因为 C++ 程序经常要调用 C 函数,而 C 程序只能用 malloc/free 管理动态内存。如果用 free 释放 "new创建的动态对象",那么该对象因无法执行析构函数而可能导致程序出错。如果用 delete 释放 "malloc申请的动态内存",理论上讲程序不会出错,但是该程序的可读性很差。所以 new/delete、malloc/free 必须配对使用。

 

参考:https://blog.csdn.net/hackbuteer1/article/details/6789164

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Tyler_Zx

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

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

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

打赏作者

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

抵扣说明:

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

余额充值