C++学习笔记(四)--new与delete


前言


提示:以下是本篇文章正文内容,下面案例可供参考

一、内存四区

在C++和许多其他编程语言中,内存通常被划分为四个主要区域,通常称为内存四区或内存管理区域。这些区域在程序运行时用于不同的目的,了解它们对于有效地管理内存非常重要。这些四个区域是:

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的
  • 全局区:存放全局变量和静态变量以及常量
  • 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

其中,C++中在程序运行前分为全局区和代码区,这两个区域在程序运行前就已经存在,而不是在程序运行前创建的。程序运行后分为堆区和栈区,这两个区域在程序运行时动态地分配和释放内存。

二、new与delete的基本用法

在C++中,我们使用 new 运算符来在堆上分配内存,并使用 delete 运算符来释放它。如果不适当地释放堆内存,可能会导致内存泄漏。接下来介绍这两个关键字的常见用法。

1.动态分配内存和数组

使用new操作符可以在堆区分配一个新的对象,并且返回一个对象指向该对象的指针;使用delete操作符可以释放一个之前用new分配的对象已。基本操作如下:

int* ptr = new int;    //分配一个整数对象
int* arr = new int[10];   //分配一个包含5个整型数据的数组

delete ptr;   //释放ptr指向的整数对象
delete[] arr;  //释放arr指向的整数数组

上述代码展示了new和delete的基本操作,需要注意的是,使用new分配的对象和数组必须使用delete 操作符释放,否则会导致内存泄漏。而且在使用delete释放数组时,必须在用delete[]而不是 delete,否则会导致未定义的行为

2.动态分配对象

C++分配对象可以分为以下几个步骤。

2.1分配对象内存

  • 当使用 new 运算符来分配对象内存时,首先需要计算所需的内存大小,这通常是通过对象类型的 sizeof 运算符来获取的。
  • 然后,new 运算符会在堆区中找到足够大小的空闲内存块,以存储该对象。堆是一块动态分配内存的区域,用于存储生存期不确定的对象。
  • new 运算符返回一个指向新分配内存的指针,这个指针指向对象的起始地址。

2.2调用对象的构造函数

在对象的内存分配完成后,会自动调用对象的构造函数来初始化对象。构造函数是类的特殊成员函数,用于设置对象的初始状态。

2.3使用对象

一旦对象被分配和构造,你可以使用指针来访问和操作对象的成员函数和数据。

2.4释放对象内存

当不再需要对象时,需要负责释放分配的内存,以避免内存泄漏。这是通过 delete 运算符来完成的。
delete 运算符接受对象的指针作为参数,并释放与对象关联的内存。对象的析构函数也会在内存释放之前自动调用,用于清理对象。
下面是按照以上步骤执行的示例代码:

class Student
{
public:
	Student():age(10),name("kkkkk")    //初始化列表
	{
		cout << "调用构造函数" << endl;
	};
	~Student()
	{
		cout << "调用析构函数" << endl;
	};
	void getName()
	{
		cout << this->name << endl;
	};

private:
	int age; 
	string name;

};

int main()
{
	Student* stu1 = new Student; //分配对象内存
	stu1->getName();			//使用指针访问成员函数
	delete stu1;			//释放对象内存
	return 0;
}

三、new与delete的注意事项

1.使用new和delete采用相同形式

当使用new时候,会发生两件事:首先内存被分配(通过operator new函数),之后针对此内存会有一个构造函数被调用。delete与new的过程相似,但是顺序相反:首先针对此内存会有一个析构函数被调用,然后内存才被释放(通过operator delete函数)。所以delete就会出现一个大问题:即将被删除的内存之内到底有多少对象,从而决定调用多少析构函数。我们可以将问题简化为:即将被删除的那个指针,所指的是单一对象还是对象数组
通常情况下,我们有一个规定:如果调用new时使用[],那么必须在对应调用delete时也使用[]。如果第哦啊用new时没有使用[],那么也不该在对应调用delete时使用[]。
接下来我们会思考,如果违反了这个规定,会怎么样呢?

string* stringPtr1 = new string;
string* stringPtr2 = new string[100];
...
delete stringPtr1;	//删除一个对象
delete stringPtr2;	//删除一个由对象组成的数组
  • 其一,如果对stringPtr1使用了 delete[] 形式,结果会未有定义。假设内存布如上,delete会读取若干内存并将它解释为 数组大小,然后开始多次调用析构函数,浑然不知它所处理的那块内存不但不是数组,也或许并未持有它正忙着销毁的那种类型的对象。
  • 其二,如果没有对strinfPtr2使用 delete[] 形式,结果也未有定义,但可以猜想可能导致太少的析构函数被调用,这对内置类型同样有危害。
    总结来说,我们需要谨记,使用new和delete采用相同形式。

2.new和delete必须匹配使用

四、operator new

《More Effetive C++》的条款Item M8开头说,人们有时好像喜欢故意使C++语言的术语难以理解。比如说new 操作符(new operator)和 new操作( operator new)的区别。
这确实令人有些无语,但是了解这些区别有时候也很重要。先看下面一行代码:

string *ps = new string("Memory Management");

上述代码中使用的new是new操作符。这个操作符就象sizeof一样是语言内置的,你不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new 操作符总是做这两件事情,我们不能以任何方式改变它的行为。
我们所能改变的是第一个步骤:如何为对象分配内存。new 操作符调用一个函数来完成必需的内存分配,你能够重写或重载这个函数来改变它的行为。new操作符为分配内存所调用函数的名字就叫做operator new。
函数operator new通常这样声明:

void * operator new(size_t size) ;

返回值类型是void*,因为这个函数返回一个未经处理(raw)的指针,未初始化的内存。参数size_t确定分配多少内存。我们可以增加额外的参宿重载operator new,但是第一个参数类型必须是size_t。
我们先看一下编译器中的operator new和operator delete函数是如何实现的,接下来我将给出他们的简化实现:

#include <cstdlib> // 包含malloc和free函数的头文件

void* operator new(std::size_t size) {
    void* ptr = std::malloc(size); // 使用malloc分配内存
    if (!ptr) {
        throw std::bad_alloc(); // 如果分配失败,抛出bad_alloc异常
    }
    return ptr;
}

void operator delete(void* ptr) noexcept {
    std::free(ptr); // 使用free释放内存
}

可以看到,上述代码主要分为两个步骤,首先使用malloc函数分配内存,之后判断内存是否分配成功。实际的函数更加复杂,抛出异常后,会调用一个new-handling函数,以及一系列复杂的过程。。。
接下来我们要思考的问题是:我们为什么会想要自己替换编译器提供的operator new或operator delete函数呢?这种行为是不是太过自大了,居然想以一己之力去挑战编译器!但通过下面的分析,我们可能就会对这个问题更加清晰了。

  • 首先可以用来检测运用上的错误。如果将所new所得内存delete掉却不幸失败,会导致内存泄漏,如果将new所得内存多次delete会导致不确定性为。但是如果operator new持有一串动态分配所得地址,而operator delete将地址从中移走,就很容易检测出上述错误的用法。
  • 其次是为了强化性能。编译器所带的operator new和operator delete主要用于一般目的,他们不但可被长时间执行的程序接受,也可被执行时间少于疫苗的程序接受。他们必须处理一系列的需求,包括不同大小的内存。他们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配与归还。。。。所以,为了满足这么多的需求,编译器所带的operator news和operator deletes只能采取“中庸之道”——他们的工作对每人的态度都是适当的好,但不对特定任何人有最佳表现。所以,如果-我们需要完成特定的任务,定制版的operator news和operator deletes会大大提高程序性能。

1.operator new的三种形式

C++ 中 operator new 的不同形式,它们用于满足不同的内存分配需求。以下是对这三种形式的详细解释:

throwing形式
这是最常见的 operator new 形式。它用于在分配内存时,如果分配失败,则抛出 std::bad_alloc 异常,通常用于需要内存分配的操作。

void* operator new(std::size_t size) throw (std::bad_alloc);

nothrow形式
这种形式的 operator new 不会抛出异常,而是返回一个空指针 (nullptr),如果内存分配失败。这对于需要处理内存分配失败但不希望引发异常的情况非常有用。

void* operator new(std::size_t size, const std::nothrow_t& nothrow_value) throw();
MyClass* ptr = new (std::nothrow) MyClass;
if (!ptr) {
    // 处理内存分配失败
}

placement形式

这种形式的 operator new 用于在指定的内存地址上构造对象,而不分配新的内存。它通常与定位 new 运算符一起使用,允许您在预分配的内存块上创建对象。

void* operator new(std::size_t size, void* ptr) throw();
void* memory = operator new(sizeof(MyClass));
MyClass* obj = new (memory) MyClass; // 在指定的内存地址上构造对象

这三种形式的 operator new 可以根据需要在不同情况下使用,以满足内存管理和异常处理的特定要求。下面是三种用法的示例代码:

#include <iostream>
#include<new>
class MyClass {
public:
    int data;

    MyClass(int val) : data(val) {
        std::cout << "MyClass constructor called with data: " << data << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destructor called with data: " << data << std::endl;
    }

    static void* operator new(std::size_t size) throw (std::bad_alloc) {
        std::cout << "Using throwing operator new" << std::endl;
        void* memory = std::malloc(size);

        if (!memory) {
            throw std::bad_alloc();
        }

        return memory;
    }

    static void operator delete(void* ptr) noexcept {
        std::cout << "Using operator delete" << std::endl;
        std::free(ptr);
    }

    static void* operator new(std::size_t size, const std::nothrow_t& nothrow_value) throw() {
        std::cout << "Using nothrow operator new" << std::endl;
        void* memory = std::malloc(size);

        return memory;
    }

    static void operator delete(void* ptr, const std::nothrow_t& nothrow_value) noexcept {
        std::cout << "Using nothrow operator delete" << std::endl;
        std::free(ptr);
    }

    static void* operator new(std::size_t size, void* ptr) throw() {
        std::cout << "Using placement operator new" << std::endl;
        return ptr;
    }
};

int main() {
    // 使用 throwing operator new
    try {
        MyClass* obj1 = new MyClass(1);
        delete obj1;
    }
    catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    }

    // 使用 nothrow operator new
    MyClass* obj2 = new (std::nothrow) MyClass(2);
    if (!obj2) {
        std::cerr << "Memory allocation failed." << std::endl;
    }
    else {
        delete obj2;
    }

    // 使用 placement operator new
    void* memory = operator new(sizeof(MyClass));
    MyClass* obj3 = new (memory) MyClass(3);

    // 手动调用析构函数
    obj3->~MyClass();

    return 0;
}

总结

通常情况下我们只会使用new和delete的基本用法,但是了解他们的实现过程,并且知道定制new和delete之后,我们就会耳目一新,发现一片新的天地。
参考书籍:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值