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. leetcode 签到 面试题40. 最小的k个数

    题目 输入整数数组 arr ,找出其中最小的 k 个数.例如,输入4.5.1.6.2.7.3.8这8个数字,则最小的4个数字是1.2.3.4. 示例 1: 输入:arr = [3,2,1], k = ...

  2. 2.Django与Vue的结合

    Django与Vue的结合 在django项目中创建vue项目 首先,进去django项目的项目目录中,执行: vue-init webpack firstvue ## firstvue为前端项目的名 ...

  3. JavaScript超越了Java,c,python等等成为Stack Overflow上最热门的

    JavaScript超越了Java,c,python等等成为Stack Overflow上最热门的标签 在2015年6月至今,JavaScript超越了Java,c,python等等成为Stack O ...

  4. 如何利用python实现为每行添加行数编号

    可能还有更好的方法,在这里我是这么写的,针对小文件可以,但是如果文件内容太多,这种方法感觉不太好 先把所有的数据读取出来,然后利用W覆盖写入模式打开文件进行写入 遍历枚举类型数据后,默认是从0开始,然 ...

  5. 算法修炼之路——【链表】Leetcode24 两两交换链表中的节点

    题目描述 给定一单链表,两两交换其中相邻的节点,并返回交换后的链表. 你不能只是简单的改变节点内部的值,而是需要实际的进行节点交换. 示例: 输入:head = [1, 2, 3, 4] 输出:hea ...

  6. Feign客户端实现RPC 调用

    1,springcloud 中支持http调用的两种方式,RestTemplate,Feign客户端 2,Feign 客户端是一个声明式(注解方式)http 远程调用工具 3,实现方式如下: 第一步: ...

  7. linux 中的页缓存和文件 IO

    本文所述是针对 linux 引入了虚拟内存管理机制以后所涉及的知识点.linux 中页缓存的本质就是对于磁盘中的部分数据在内存中保留一定的副本,使得应用程序能够快速的读取到磁盘中相应的数据,并实现不同 ...

  8. SpringBoot系列(二)入门知识

    SpringBoot系列(二)入门知识 往期推荐 SpringBoot系列(一)idea新建springboot项目 引言 本来新建springboot项目应该放在入门知识这一章的,但是由于新建spr ...

  9. 透过 ReentrantLock 分析 AQS 的实现原理

    对于 Java 开发者来说,都会碰到多线程访问公共资源的情况,这时候,往往都是通过加锁来保证访问资源结果的正确性.在 java 中通常采用下面两种方式来解决加锁得问题: synchronized 关键 ...

  10. css过渡和2d详解及案例

    css过渡和2d详解及案例(案例在下方,不要着急) 本文重点: 1.在2D变化使用过程中,有些需求需要两种或两种以上的变化同时使用, 值得注意的是尽量把位移变化放在最前面,把其他变化放在最后面,属性值 ...