IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> 浅谈重载new操作符 -> 正文阅读

[嵌入式]浅谈重载new操作符

new是C++里非常重要的一个关键词,用于申请内存、初始化对象。俗话说“有借有还再借不难”,通过new向操作系统“借”到的内存用完后必然要“还”回去,所以对应地还有一个delete操作符与new共同管理内存,delete的作用是析构对象、释放内存。

new有什么作用?

  • 申请内存
  • 初始化对象

说到内存管理,有些同学会想到C标准库函数malloc()free()。C++是C语言的延续,那么C++一定可以丝滑地使用这两个标准库函数管理内存,那为什么还要提供关键词newdelete呢?

我们申请到内存后一般都会先做初始化,关键词new相当于用一条语句做两件事——申请内存并初始化。考虑类Test

struct Test {
    ...
};

使用new生成一个Test的对象

Test* test = new Test(...);

相当于malloc()申请内存后再进行初始化

// void initTest(Test* test);

void* testBuff = malloc(sizeof(Test));
initTest((Test*)testBuff);

同理,delete也是一条语句做了两件事——先析构对象释放相关资源,然后再释放内存。

new和malloc有什么区别?

malloc()free()是函数,只用于管理内存。

newdelete是操作符,newdelete用于管理内存,还有其他职责——new初始化对象,delete析构对象(清理资源)。

函数和操作符的使用方式存在很大差异。另外,编译后函数调用对应一条函数调用指令,操作符对应一条或多条其他汇编指令。

new有哪些用法?

  • plain new
  • operator new
  • placement new

plain newnew的常见用法,用于生成对象。会申请内存并调用构造函数

Test* test = new Test(...);

operator new用于申请内存,作用与malloc()一样,实际上是函数调用。只会申请内存,不会调用构造函数

void* buff = operator new(sizeof(Test));

placement new用于调用构造函数初始化指定内存。只会调用构造函数,不会申请内存

// void* testBuff;

Test* test = new (testBuff)Test(...);

实际上只要观察Test* test = new Test(...);对应的汇编结果就能发现plain new用法包含了两步——申请内存、调用构造函数

...
mov     edi, 4
call    operator new(unsigned long)
mov     rbx, rax
mov     rdi, rbx
call    Test::Test() [complete object constructor]
mov     QWORD PTR [rbp-24], rbx
...

第3行指令表示调用函数operator new(unsigned long)申请内存;第6行指令表示调用Test的构造函数初始化内存。所以

Test* test = new Test();

相当于

void* buff = operator new(sizeof(Test));
Test* test = new ((Test*)buff)Test;

如何重载new操作符?

重载new操作符实际上是重写函数operator new(...),对应的是申请内存的步骤,所以重载new操作符后只会影响plain new的内存分配,不影响调用构造函数。

重载new操作符的形式有两种——全局重载、局部重载。

全局重载就是直接重写全局函数operator new(...)

void* operator new(size_t size) {
    ...
}

从前面Test* test = new Test(...);对应的汇编结果就能看出operator new()是一个全局函数(汇编后函数符号没有任何改变,也没有加任何作用阈),全局重写new操作符后,所有类型的new操作都会调用自定义的operator new()申请内存。

局部重载就是在类中重写静态函数operator new(...)(即便不以static修饰,编译器也会自动转为静态函数)

struct Test {
    void* operator new(size_t size) throw() {
        ...
    }
};

局部重载new操作符后,Test* test = new Test(...);对应的汇编代码为

...
mov     edi, 4
call    Test::operator new(unsigned long)
mov     rbx, rax
mov     rdi, rbx
call    Test::Test() [complete object constructor]
mov     QWORD PTR [rbp-24], rbx
...

可以看到第3行的函数调用变为Test::operator new(unsigned long),增加了作用域Test,意味着自定义的operator new()只会对Test有效。

operator new和throw?

我们知道Test* test = new Test(...);实际上是两步——申请内存、调用构造函数,现在有一个问题,如果内存申请失败返回空指针,构造函数初始化成员函数时岂不是会出现访问空指针的问题?之前在项目上遇到过类似的问题,重载new操作符后构造函数报了空指针访问异常,代码如下

class Test {
private:
    int value;

public:
    Test() {
        printf("[Test] Constructor\n");
        value = 1;
    }

    void* operator new(size_t size) {
        printf("[Test] operator new\n");
        return NULL;    // 内存申请失败,返回空指针
    }
};

客户代码

int main() {
    printf("-------- start --------\n");
    Test* point = new Test();
    printf("[main] point is %p\n", point);
    printf("-------- done --------\n");
    return 0;
}

运行结果出现内存访问错误,错误的位置发生在Test的构造函数中

-------- start --------
[Test] operator new
[Test] Constructor
Segmentation fault: 11

new操作符申请内存失败后,在构造函数中出现了访问空指针的错误。这个问题看起来非常棘手,new操作符会先申请内存然后调用构造函数,这个流程是编译器决定的不能修改,那么要解决这个问题似乎只有两个思路——在使用new之前手动判断内存是否会申请成功;在构造函数中判断this指针是否为空。

用这两种方式解决都存在隐患——因为需要改动很多代码,而人总是会出错的。事实上,一个throw()声明(或者宏_NOEXCEPT)就可以解决这个问题。

关键字thow的是用来声明异常的,说明函数会抛出哪些类型的异常,显而易见throw()说明函数不会抛出任何类型的异常,Test类只要做以下改动即可

class Test {
...
    void* operator new(size_t size) throw() {   // 只比原来的代码多了 throw()
        ...
    }
...
};

运行结果

-------- start --------
[Test] operator new
[main] point is 0x0
-------- done --------

new操作符申请内存失败后未调用构造函数,不会出现访问空指针的问题。

为了一探究竟,我们比较一下加了thow()声明和没有throw()声明的汇编结果(右侧是无throw()的,左侧是有throw()的)

在这里插入图片描述

总的来说,加了throw()声明后会多两条汇编指令,用于在operator new的返回结果是空指针时不调用Test的构造函数。

在线工具

最后,分享一个在线汇编神器: https://godbolt.org/

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2022-05-08 08:17:54  更:2022-05-08 08:18:55 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/30 1:13:54-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码