C++的核心理念之一是RAII,Resource Acquisition Is Initialization,资源获取即初始化。资源有很多种,内存、互斥锁、文件、套接字等;RAII可以用来实现一种与作用域绑定的资源管理方法(如std::lock_guard);这些都不在本文的讨论范围之内。

内存是一种资源。从字面上来看,“资源获取”是指在栈或堆上开辟空间,“初始化”是指调用构造函数,“即”的意思是两者是绑定起来的。对应地,资源销毁即释放。这种机制保证了任何函数访问参数对象时不会访问到非法地址,除了构造和析构函数以外的任何函数的参数都不会是未初始化的。

然而,C++作为能够面向底层的语言,允许我们把内存获取与初始化分开来:std::mallocstd::free用于分配和释放内存,定位new表达式和析构函数的显式调用用于初始化和销毁对象。

malloc与free

std::malloc用于分配内存,std::free用于释放内存,两者都定义在<cstdlib>中:

void* malloc(std::size_t size);
void free(void* ptr);

比如,我想要分配一个int的动态内存,就应该给size填上sizeof(int),返回值就是指向那个int的指针。如果是数组,就给size乘上元素个数。注意在C++中,从void*T*的转换不能是隐式的。

要回收它,把指针传给free,不需要size

#include <iostream>
#include <cstdlib> int main()
{
auto p = static_cast<int*>(std::malloc(sizeof(int) * 10));
p[0] = 1;
p[1] = 2;
std::cout << p[0] << ' ' << p[1] << std::endl;
std::free(p);
}

如果std::malloc过程中发生了错误,比如内存不足,std::malloc会返回NULLnullptr的前身。实际使用时都应该考虑这种情况。

std::malloc得到的内存必须用free释放。std::malloc/std::free的内存分配与new/delete不互通。

定位new表达式

std::malloc分配的内存是未经初始化的,对于int等内置类型可以直接使用,而类类型则未必,需要先初始化才能使用。

我们知道,类的非静态成员函数都有一个隐式的this指针作为参数,构造函数也不例外,在未初始化的内存上构造对象,就是把这块内存的指针作为this传入构造函数。不幸的是,没有显式调用构造函数这种语法:

auto ptr = static_cast<A*>(std::malloc(sizeof(A)));
ptr->A("hello"); // error

(在MSVC中,ptr->A::A("hello");是合法的,但这是非标准的。)

我们需要定位new,它定义在<new>中,需要#include以后才能使用:

void* operator new(std::size_t, void*);

new运算符是可以重载的,new运算符的功能是为new表达式中的构造函数提供this指针。但是定位new不行,它总是忠实地返回它的第二个参数。

定位new表达式有以下形式:

new (ptr) Type;
new (ptr) Type(args);
new (ptr) Type[size];
new (ptr) Type[size]{list};

ptr为要当作this的指针,Type为要构造的类型,args为可能为空的参数列表,size为数组大小(可以动态),list为数组元素列表。

利用定位new,把ptr作为this的构造函数可以这样调用:

#include <iostream>
#include <cstdlib>
#include <string>
#include <utility> class A
{
public:
A(std::string s) : string(std::move(s))
{
std::cout << "A::A(std::string)" << std::endl;
}
std::string& get()
{
return string;
}
private:
std::string string;
}; int main()
{
auto ptr = static_cast<A*>(std::malloc(sizeof(A)));
// std::cout << ptr->get() << std::endl; // disaster
// ptr->A("hello"); // error
new (ptr) A("hello");
std::cout << ptr->get() << std::endl;
// delete ptr; // disaster
// what's next?
}

不要因为ptr简单就不加括号,括号不是为了什么运算符优先级,而是定位new表达式的一部分。

刚才不是说std::mallocnew不互通吗?怎么在std::malloc来的ptr上用定位new了呢?因为定位new根本不插手内存分配,和std::malloc是两回事。

定位new不止可以用于std::malloc来的动态内存,甚至可以是局部变量:

char buffer[sizeof(A)];
auto ptr = new (buffer) A("hello");
std::cout << ptr->get() << std::endl;

与常规的new一样,定位new表达式也返回一个指针,就是Type*类型的ptr

显式调用析构函数

上面通过std::malloc得到的ptr,为什么不能直接std::free呢?因为std::string里面的资源还没释放,正确的做法是调用A的析构函数。

不能对一个对象调用构造函数,那么析构函数呢?答案是肯定的。只是~在形式上有点怪,尤其是当你把模板参数T换成int后得到ptr->~int()时,后者直接写是不合法的。

所以对于上面的ptr,要这样才能释放所有资源:

ptr->~A();
std::free(ptr);

显式调用析构函数,就像在帮编译器做事情一样。编译器会不会领情呢?写个代码测试一下吧:

#include <iostream>
#include <string>
#include <utility> class A
{
public:
A(std::string s) : string(std::move(s))
{
std::cout << "A::A(std::string)" << std::endl;
}
std::string& get()
{
std::cout << "A::get()" << std::endl;
return string;
}
~A()
{
std::cout << "A::~A()" << std::endl;
}
private:
std::string string;
}; int main()
{
{
A a("");
a.~A();
}
std::cout << "I'm OK" << std::endl;
}

程序输出:

A::A(std::string)
A::~A()
A::~A()
I'm OK

看来编译器并不领情,即使我们调用了析构函数,变量离开作用域时还会再调用一次。尽管在MSVC和GCC中,I'm OK都成功输出了,std::string都挺住了错误的析构,但是这个程序的行为仍然是未定义的。

因此,定位new语句与析构函数的显式调用必须配对。

定位new表达式与显式调用析构函数的更多相关文章

  1. C++不能显式调用构造函数,会生成匿名对象,这点与Java完全不一样!

    Java可以直接调用同名构造函数,仅仅起初始化的功能,并不构造新的对象,但是C++里面没有.看一下这段代码: class A { public: A() { printf("A() \n&q ...

  2. linux下动态链接库(.so)的显式调用和隐式调用

    进入主题前,先看看两点预备知识. 一.显式调用和隐式调用的区别 我们知道,动态库相比静态库的区别是:静态库是编译时就加载到可执行文件中的,而动态库是在程序运行时完成加载的,所以使用动态库的程序的体积要 ...

  3. C++构造函数详解及显式调用构造函数

    来源:http://www.cnblogs.com/xkfz007/archive/2012/05/11/2496447.html       c++类的构造函数详解                  ...

  4. C++中构造函数详解及显式调用构造函数

    C++构造函数详解及显式调用构造函数                                         c++类的构造函数详解                        一. 构造函 ...

  5. C++如何显式调用常成员函数

    C++的常成员函数与同名成员函数重载时,该如何显式调用常成员函数? 具体的一个小例子: #include <iostream> using namespace std; class C1 ...

  6. 循环引擎 greenlet 没有显式调度的微线程,换言之 协程

    小结: 1. micro-thread with no implicit scheduling; coroutines, in other words. 没有显式调度的微线程,换言之 协程 2. 一个 ...

  7. based on Greenlets (via Eventlet and Gevent) fork 孙子worker 比较 gevent不是异步 协程原理 占位符 placeholder (Future, Promise, Deferred) 循环引擎 greenlet 没有显式调度的微线程,换言之 协程

    gevent GitHub - gevent/gevent: Coroutine-based concurrency library for Python https://github.com/gev ...

  8. simplest_dll 最简dll的创建与隐式调用(显式调用太麻烦,个人不建议使用)

    首先需要有个头文件,名字随便写  假设test.h //test.h #ifndef _TEST_H #define _TEST_H #ifdef TEST_EXPORTS //通过宏定义控制是输入还 ...

  9. el表达式不显示值

    1.场景是自己搭建一个ssm的项目,登录页面跳转到首页,首页显示登录用户的信息,用request传递的值,用el表达式在jsp页面中没有显示 2.解决办法 早jsp的代码中添加头<%@ page ...

随机推荐

  1. 使用 Vue.js 改写 React 的官方教程井字棋

    React 的官方教程井字棋很好的引导初学者一步步走进 React 的世界,我想类似的教程对 Vue.js 的初学者应该也会有启发,于是使用 Vue.js 进行了改写 可以先查看最终的结果,尝试点击体 ...

  2. vue基础指令学习

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. Mybatis在xml配置文件中处理SQL中的大于小于号的方法

    之前在项目中遇到了在SQL中<=不识别的问题,在Navicat中语句正常,在xml中不识别,想起来就记录一下 项目用的是Mybatis,经过网上多次查询,验证,总结方法如下: 一.使用<! ...

  4. Visdom 介绍 | 一

    用于创建,组织和共享实时丰富数据可视化的灵活工具.支持Python. 概述 概念 设置 用法 API 待办事项 贡献 概述 Visdom旨在促进(远程)数据的可视化,重点是支持科学实验. 为你自己和你 ...

  5. Serpent.AI - 游戏代理框架(Python)

    Serpent.AI - 游戏代理框架(Python) Serpent.AI是一个简单而强大的新颖框架,可帮助开发人员创建游戏代理.将您拥有的任何视频游戏变成一个成熟的实验的沙箱环境,所有这些都是熟悉 ...

  6. Centos 8 安装 Consul-Template

    1. 下载安装包( consul-template_0.23.0_linux_amd64.zip 文件 ) 下载地址: https://releases.hashicorp.com/consul-te ...

  7. .NET 5.0 Preview 2发布

    2020年4月2日微软.NET 团队的项目经理 Richard 在博客上 发布了.NET 5 Preview 2:https://devblogs.microsoft.com/dotnet/annou ...

  8. docker 本地镜像导入导出 compose安装

    docker 本地镜像导入导出 1.Docker导入本地gz镜像 [root@rocketmq-nameserver4 dev]# cat alibaba-rocketmq-3.2.6.tar.gz ...

  9. vue-cli 引入axios及跨域使用

    使用 cnpm 安装 axios cnpm install axios --save-dev 安装其他插件的时候,可以直接在 main.js 中引入并 Vue.use(),但是 axios 并不能 u ...

  10. 使用docsify 写开源文档

    使用docsify 写开源文档 官网:https://docsify.js.org/#/ docsify 是一个动态生成文档网站的工具.不同于 GitBook.Hexo 的地方是它不会生成将 .md ...