跳至主要內容

new/delete关键字

张威大约 15 分钟c/c++c++基础

new/delete关键字

1681303112683-41ec0262-0bfb-46e8-a571-0e4cd626492a
1681303112683-41ec0262-0bfb-46e8-a571-0e4cd626492a

在C语言中,我们写程序时,总是会有动态开辟内存的需求,每到这个时候我们就会想到用malloc/free 去从堆里面动态申请出来一段内存给我们用。但对这一块申请出来的内存,往往还需要我们对它进行稍许的“加工”后即初始化 才能为我们所用,虽然C语言为我们提供了calloc来开辟一段初始化好(0)的一段内存,但,它同样束手无策。同时,为了保持良好的编程习惯,我们也都应该对申请出来的内存作手动进行初始化。于是到了C++中就有了new/delete, new []/delete[] 。用它们便可实现动态的内存管理。

开辟一个元素的空间

int *p = new int(1);
cout << *p << endl;
delete p;
p = nullptr;	//无论是c还是c++释放内存后要把指针制空,养成好的习惯

有几种new的方式

情况1:最常用操作,,抛异常处理内存开辟失败

int *p1 = new int(20);//最常用操作,通过抛出异常判断内存开辟失败

情况2:

int *p2 = new (nothrow) int;//不抛出异常,开辟失败判断情况与malloc一样

情况3:堆上开辟常量,

const int *p3 = new const int(40);//在堆上开辟了一个常量

情况4:

//定位new
int data = 0;
// 在&data位置分配内存并初始化
int *p4 = new (&data) int(50);//在指定的内存上划分了一块初值为4字节大小的内存,初值为50

开辟一个数组的空间

int *p = new int[10]();//开辟数组时,要记得采用[]
for(int idx = 0; idx != 10; ++idx)
{
    p[idx] = idx;
}
delete []p;//回收时,也要采用[],1.[]写在数组名前,2.[]不要写数组的大小,编译器会自动推断大小,写了反而会报错(和编译器有关)
p = nullptr;

常考题:new/delete表达式与malloc/free的区别是?

相同:1、都是用来申请堆空间2、malloc与free,new与delete要成对出现,否则可能造成内存泄漏

不同:

  1. malloc与new的区别

    ①malloc按字节开辟内存的;new开辟内存时需要指定类型②malloc开辟内存返回的都是void *** ,new相当于运算符重载函数,返回值自动转为指定的类型的指针。**③malloc只负责开辟内存空间,new不仅仅也有malloc功能,还可以进行数据的初始化④malloc开辟内存失败返回nullptr指针;new抛出的是bad_alloc类型的异常⑤malloc开辟单个元素内存数组内存都是给字节数;new开辟时对单个元素内存后面不需要[],而组需要[]并给上元素个数

  2. free和delete的区别:

    ①free不管释放单个元素内存还是数组内存,只需要传入内存的起始地址即可②delete释放单个元素内存,不需要加中括号,但释放数据内存时需要加中括号③delete执行其实有两步先调用析构,再释放;free只有一步

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

  • :因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

课堂代码

#include <stdlib.h>	//memset()的头文件
#include <iostream>

using std::cout;
using std::endl;

void test2()
{
    int number = 10;
    printf("sizeof(number) = %lu", sizeof(number));
    printf("sizeof number = %lu", sizeof number);//sizeof是一个运算符,不是一个函数,因为函数不能这么写
}
//面试中常问的
//内存溢出?踩内存?内存越界?野指针
//
//
//面试题
//malloc/free与new/delete异同点?
//1、都是用来申请堆空间的
//2、malloc与free,new与delete要成对出现,否则可能造成内存泄漏
//
//不同点:
//1、malloc/free是C里面的库函数,new/delete是C++中的表达式
//2、malloc申请的是未初始化的堆空间,new申请是已经初始化的堆空间

void test()
{
    int *pInt = (int *)malloc(sizeof(int));//1、申请堆空间
    memset(pInt, 0, sizeof(int));//2、初始化
    *pInt = 10;//3、赋值
    
    free(pInt);//4、释放堆空间
    /* pInt = NULL;//0 */
    pInt = nullptr;//void *

    int *pArray = (int *)(malloc(sizeof(int) * 10));
    memset(pArray, 0, sizeof(int) * 10);

    free(pArray);
    pArray = nullptr;
}

void test3()
{
    int *pInt = new int(10);//1、申请堆空间,并初始化,还可以进行赋值
    cout << "*pInt = " << *pInt << endl;

    delete pInt;//2、释放堆空间
    pInt = nullptr;

    int *pArray = new int[10]();
    pArray[0] = 120;

    delete [] pArray;
}
int main(int argc, char **argv)
{
    cout << "Hello world" << endl;
    return 0;
}

new和deletea原理

    class T{  
    public:  
        T(){  
            cout << "构造函数。" << endl;  
        }  

        ~T(){  
            cout << "析构函数。" << endl;  
        }  

        void * operator new(size_t sz){  

            T * t = (T*)malloc(sizeof(T));  //operator new就是简单的分配内存即可
            cout << "内存分配。" << endl;  

            return t;  
        }  

        void operator delete(void *p){  

            free(p);  
            cout << "内存释放。" << endl;  

            return;  
        }  
    };  

    int main()  
    {  
        T * t = new T(); // 先 内存分配 ,再 构造函数  

        delete t; // 先 析构函数, 再 内存释放  

        return 0;  
    }  
  • error: ‘nullptr’ was not declared in this scope 解决办法 编译语句中添加-std=c++11,在链接语句不需要添加
g++ -g main.cc -std=c++11
objdump -M intel -S -C  a.out #-C 选项用于进行名称修饰
image-20240202222822996
image-20240202222822996

new[] 和delete 或者说new和delete[]能混用吗?C++如何区分单个元素和数组内存分配和释放

不能混用,数组new[]时,编译器会在开头多分配4B空间记录new对象的数量

class Test
{
public:
	Test(int data = 10) { cout << "Test()" << endl; }
	~Test() {  cout << "~Test()" << endl; }
private:
	int ma;
};

int main()
{
	Test* p1 = new Test[5];
	delete[] p1; // 从operator delete(p1-4),即从最顶上存储对象的数量的内存起始地址开始释放
}
在这里插入图片描述
在这里插入图片描述

注意返回的地址不是最顶上的地址!!

若混用new[]和delete:

Test* p1 = new Test[5];  // 输出operator new[] addr:0157F6C0
cout << "p1:" << p1 << endl;  // p1:0157F6C4
delete p1;

可以看出返回的是p1[0]对象的地址,调用delete p1也只是析构p1[0]对象,不合法,抛出异常

若混用new 和delete[]更好理解,delete[]会传入【new返回的地址-4】来调用析构,这块空间根本就没分配,必然就出错

可以通过重载new和delete来解决内存泄漏

[深入理解C++ new/delete, new ]/delete[]动态内存管理 open in new window

在C++ Primer书中有提到说: new/delete的表达式与标准库函数同名了,所以系统并没有重载new或delete表达式。new/delete真正的实现其实是依赖下面这几个内存管理接口的。c++中称之为“placement版”内存管理接口接口原型:

void * operator new (size_t size);  
void operator delete (size_t size);

void * operator new [](size_t size);  
void operator delete[] (size_t size);

探究它,不妨从这样一个类AA开始

class AA
{
public:
    AA(size_t count = 1)
    {
        _a = new int[count];
        cout<<"AA()"<<endl;
    }
    
    ~AA()
    {
        delete[] _a;
        cout<<"~AA()"<<endl;
    }

private:
    int* _a;
};

用AA* pA = new AA[10]创建对象,VS下通过调试进入new表达式内部系统函数,得到下面两个图:

img
img
img
img

通过上面两个图,大致可以看出来new表达式并不直接开辟内存出来,而是通过调用operator new来获得的内存,而operator new获得的内存实质上还是用malloc开辟出来的。这便证实了前面所述的:开空间出来还是得 malloc来。

同样的道理,delete表达式也不是直接去释放掉内存。比如对上面的对象数组进行delete

AA* pA = new AA[10];
delete[] pa;

delete[]实际做了这样几件事情:

  • 依次调用pA指向对象数组每个对象的析构函数,共10次

  • 调用operator delete[](),它将再调用operator delete

  • 底层用free执行operator delete表达式,依次释放内存

小结operator new/ operator delete

  1. operator new/operator delete operator new[]/operator delete[] 和 malloc/free用法一样。

  2. 他们只负责分配空间/释放空间,不会调用对象构造函数/析构函数来初始化/清理对象。

  3. 实际operator new和operator delete只是malloc和free的一层封装

这其实是因为编译器用相差的这4个字节用来保存一个东西——对象个数,即AA* p = new AA[10] 中的‘10’。这也就不难解释 为什么在delete[] 的时候,不用传给它对象个数。

img

delete[] 删除时,将new[] 返回的地址再往前移4个字节便可以拿到要析构的对象个数了。

但是注意:new type[] ,只有type显示定义析构函数时,编译器才会多开4字节来保存对象个数。所以像new int、char这样的内置类型编译器不会多开这4字节,编译器自行优化。

它们之间可用下面的图展示:

img
img

new/delete, new []/delete[], malloc/free配套使用!

class AA
{
public:
    AA(size_t count = 1)
    {
        _a = new int[count];
        cout<<"AA()"<<endl;
    }    
    ~AA()
    {
        delete[] _a;
        cout<<"~AA()"<<endl;
    }

private:
    int* _a;
};

malloc/delete的组合

void Test1()
{

    AA* p1 = (AA*)malloc(sizeof(AA));   //没有报错,但不建议采用,容易引起混淆
    delete p1;                       
    AA* p2 = (AA*)malloc(sizeof(AA));   //报错,同上,释放位置也不对
    delete[] p2;
}

void Test2()
{
    AA* p3 = new AA;         //不报错,但未清理干净。p3的构造函数开辟的空间没有被释放
    free(p3);
    AA* p4 = new AA[10];   //崩溃卡死,存在问题,释放位置被后移了4字节。同时只调用了一次析构函数
   delete p4; ,
   AA* p5 = new AA;     //报错 非法访问内存
   delete[] p5; 
}

①delete p4错误在于释放位置不对(和编译器实现new []的机制有关),导致内存泄漏

img
img

②delete[] p5 直接就崩了,这次new AA的时候并未多开4字节保存对象个数,编译器便无法知道要调用多少次析构函数(这里仅仅调用一次析构函数就好了)但编译器内部还是试图去访问p5前4字节的内存,以此获得对象个数;这便非法内存访问了,所以程序就挂了。

针对内置类型

void Test3()
{
    int* p6 = new int[10];  //没问题
    delete[] p6;
    int* p7 = new int[10];  //没问题
    delete p7;
    int* p8 = new int[10];  //没问题
    free(p8);
           
}

内存管理内置类型,它们的析构函数其实上是可调可不调的,所以它的实现机制不像前面的new []/delete[],编译器会自行对处理的数据做记录,然后处理;所以即便是不匹配的使用,它们也没出现什么问题。不仅仅这种内置类型如此,那种无自定义类型析构函数的类对象,这样的用法同样不会表现出什么问题。但即便如此,

NULL、0、nullptr 区别分析open in new window

C的NULL

C语言中,我们使用NULL表示

int *i = NULL;
foo_t *f = NULL;

实际上在C语言中,NULL通常被定义为如下:

#define NULL ((void *)0)	//void *指针赋值给int *和foo_t *的指针的时候,隐式转换成相应的类型
,所以通常情况下,编译器提供的头文件会这样定义NULL:
#ifdef __cplusplus ---简称:cpp c++ 文件
#define NULL 0
#else
#define NULL ((void *)0)
#endif

C++的0

因为C++中不能将void *类型的指针隐式转换成其他指针类型,而又为了解决空指针的问题,所以C++中引入0来表示空指针(注:0表示,还是有缺陷不完美),这样就有了类似上面的代码来定义NULL。实际上C++的书都会推荐说C++中更习惯使用0来表示空指针而不是NULL,尽管NULL在C++编译器下就是0。

为什么C++的书都推荐使用0而不是NULL来表示空指针呢?

我们看一个例子:

在foo.h文件中声明了一个函数:

void bar(sometype1 a, sometype2 *b);

这个函数在a.cpp、b.cpp中调用了,分别是:

a.cpp:

bar(a, b);

b.cpp:

bar(a, 0);

都是正常完美的编译运行。但是突然在某个时候我们功能扩展,需要对bar函数进行扩展,我们使用了,现在foo.h的声明如下:

void bar(sometype1 a, sometype2 *b);
void bar(sometype1 a, int i);

这个时候危险了,a.cpp和b.cpp中的调用代码这个时候就不能按照期望的运行了。但是我们很快就会发现b.cpp中的0是整数,也就是在overload resolution的时候,我们知道它调用的是void bar(sometype1 a, int i)这个重载函数,于是我们可以做出如下修改让代码按照期望运行:

bar(a, static_cast<sometype2 *>(0));  --- 我们的游戏项目就遇到这个问题,这样用开起来别扭

我知道,如果我们一开始就有bar的这两个重载函数的话,我们会在一开始就想办法避免这个问题(不使用重载)或者我们写出正确的调用代码,然而后面的这个重载函数或许是我们几个月或者很长一段时间后加上的话,那我们出错的可能性就会加大了不少。貌似我们现在说道的这些跟C++通常使用0来表示空指针没什么关系,好吧,假设我们的调用代码是这样的:

foo.h

void bar(sometype1 a, sometype2 *b);

a.cpp

bar(a, b);

b.cpp

bar(a, NULL);

当bar的重载函数在后面加上来了之后,我们会发现出错了,但是出错的时候,我们找到b.cpp中的调用代码也很快可能忽略过去了,因为我们用的是NULL空指针啊,应该是调用的void bar(sometype1 a, sometype2 *b)这个重载函数啊。实际上NULL在C++中就是0,写NULL这个反而会让你没那么警觉,因为NULL不够“明显”,而这里如果是使用0来表示空指针,那就会够“明显”,因为0是空指针,它更是一个整形常量

在C++中,使用0来做为空指针会比使用NULL来做空指针会让你更加警觉。

C++ 11的nullptr

虽然上面我们说明了0比NULL可以让我们更加警觉,但是我们并没有避免这个问题。这个时候C++ 11的nullptr就很好的解决了这个问题,我们在C++ 11中使用nullptr来表示空指针,这样最早的代码是这样的,

foo.h

void bar(sometype1 a, sometype2 *b);

a.cpp

bar(a, b);

b.cpp

bar(a, nullptr);

在我们后来把bar的重载加上了之后,代码是这样:

foo.h

void bar(sometype1 a, sometype2 *b);
void bar(sometype1 a, int i);

a.cpp

bar(a, b);

b.cpp

bar(a, nullptr);

这时候,我们的代码还是能够如预期的一样正确运行。

在没有C++ 11的nullptr的时候,我们怎么解决避免这个问题呢?

// 定义一个名为 nullptr_t 的类,用于模拟 C++11 中的空指针常量 nullptr
class nullptr_t
{
public:
    // 转换操作符,将 nullptr_t 转换为任意指针类型 T*
    template<class T>
    inline operator T*() const
        { return 0; }

    // 转换操作符,将 nullptr_t 转换为类 C 中成员指针类型 T C::*
    template<class C, class T>
    inline operator T C::*() const
        { return 0; }
 
private:
    // 私有成员函数,禁止取地址操作符&的使用,以避免与普通指针混淆。
    void operator&() const;
} nullptr = {};  // 创建一个名为 nullptr 的 nullptr_t 类型的实例并初始化为一个匿名对象

结论:。 cocos2d-x3.0 采用 c++11的新特性了... c++ 二春来了...

内存溢出、踩内存、内存越界、野指针、悬空指针

  • "内存溢出"(Memory Overflow)是指程序尝试写入超出其所分配的内存范围的区域,导致数据覆盖或程序崩溃。也可以是指系统中可用内存不足,无法满足程序的内存需求,导致程序运行异常或崩溃。

  • "踩内存"(Memory Leak)指的是程序在动态分配内存时,没有正确释放已经不需要的内存,导致内存占用不断增加,最终导致程序崩溃或系统资源不足。

  • "内存越界"(Array Out of Bounds)指的是程序试图访问数组的越界元素(即访问数组中不存在的元素),导致程序异常或崩溃。

  • 野指针:指针定义时未被初始化,它的默认值是随机的。(因为任何指针变量(除了static修饰的指针变量)在被定义的时候是不会被置空的)

  • 悬空指针:指针指向的内存空间已被释放或不再有效,没有及时置空