首页 > 系统相关 >C++内存分配方法new与placement new使用方法详解

C++内存分配方法new与placement new使用方法详解

时间:2023-01-22 10:02:00浏览次数:81  
标签:delete placement void C++ operator new Foo size



tags: C++

写在前面

总结一下C++内存分配中的​​new​​​/​​delete​​​方法, 以及一个很有意思的工具: ​​placement new​​.

参考:

  1. cppprimer5ed, pp409, pp726(19.1).
  2. 侯捷C++ video

new的基本使用

编译器角度

在使用​​new​​分配内存的时候, 例如下面这样:

string *sp = new string("abc"); // 分配并初始化一个string对象
string *sa = new string[10]; // 分配10个默认初始化的string对象

上面的​​new​​内存分配, 本质上进行了三个步骤:

  1. ​new​​​表达式调用一个名为​​operator new​​​(或​​operator new[]​​)的标准库函数, 这个函数分配了一块足够大的/原始的/未命名的内存空间, 以便存储特定类型的对象(或对象构成的数组);
  2. 编译器运行相应的构造函数以构造这些对象, 传入初始值;
  3. 对象被分配空间, 并且构造完成, 返回指向该对象的指针.

对于​​delete​​, 同理:(两个步骤)

delete sp;   // 销毁*sp, 释放sp所指向的内存空间
delete[] sa; // 销毁数组中的元素, 然后释放对应的内存空间

步骤:

  1. 对​​sp​​​所指向的对象或者​​arr​​所指的数组中的元素执行相应的析构函数;
  2. 编译器调用​​operator delete​​​(或​​operator delete[]​​)释放内存.

operator new/operator delete调用规则

当重载了全局的operator new和operator delete之后, 内存分配就不是系统默认的了, 所以这两个函数一定要保证正确.

分配内存/析构内存时, 编译器首先在被分配内存类(及其基类)的作用域中查找, 是否有定义operator new和operator delete成员函数. 若未找到, 则在全局作用域内查找, 最后, 会调用标准库定义的版本.

其中, 若在定义了/重载了operator new和operator delete之后, 还想使用全局的operator new和operator delete, 那么应该使用​​作用域运算符​​, 即:

::new
::delete

标准库定义的版本

书上写错了, ​​operator delete​​​返回值类型应该是​​void​​​而不是​​void*​​.

下面的版本可能会抛出​​std::bad_alloc​​异常:

void *operator new(size_t);   // 分配一个对象
void *operator new[](size_t); // 分配一个数组
void operator delete(void*) noexcept; // 释放一个对象
void operator delete[](void*) noexcept; // 释放一个数组

下面的版本承诺不会抛出异常:

#include <new> // struct nothrow_t, nothrow
void *operator new(size_t, nothrow_t&) noexcept; // 分配一个对象
void *operator new[](size_t, nothrow_t&) noexcept; // 分配一个数组
void operator delete(void*, nothrow_t&) noexcept; // 释放一个对象
void operator delete[](void*, nothrow_t&) noexcept; // 释放一个数组

注意:

  1. 重载上述运算符函数时, 必须使用​​noexcept​​异常说明符指定其不抛出异常.
  2. 应用程序可以自定义上述函数中任意一个, 前提是自定义的版本必须位于全局作用域或者类作用域中.
  3. 将上述运算符函数定义成类的成员函数时, 其默认就是隐式静态的(implicit, static), 这是因为​​operator new​​用在对象构造之前, 而​​operator delete​​用在对象销毁之后, 所以这两个成员必须静态, 并且不能操纵类的任何数据成员.
  4. 在类中定义时, 无需显式声明​​static​​.(声明了也不会报错)
  5. C++内存分配方法new与placement new使用方法详解_数组对​​operator new​​或​​operator new[]​​来说, 其返回值类型必须是​​void*​​, 第一个形参必须是​​size_t​​且该形参不能含有默认实参.
  6. 当编译器调用​​operator new​​时, 把存储指定类型对象所需的字节数传给​​size_t​​形参; 当调用​​operator new[]​​时, 传入函数的是存储数组中所有元素所需的空间.
  7. 如果要自定义​​operator new​​, 可以为其提供额外形参, 这就用到了后面会提到的​​placement new​​, 将实参传给新增的形参.
  8. 下面这个全局函数不能被重载: (在类中作为成员函数可以被重载)

void *operator new(size_t, void*);

这个函数只能供标准库使用.

下面是一个测试:

// error: redefinition of 'void* operator new(size_t, void*)' void* operator new(size_t size, void* start) { cout << "operator new(size_t size, void* start), size=" << size << ", start=" << start << endl; return start; }


重载operator new/operator delete

#include <iostream>
using namespace std;

class Foo {
public:
int _id;
long _data;
string _str;

public:
Foo() : _id(0) {
cout << "default ctor.this=" << this << " id=" << _id << endl;
}
Foo(int i) : _id(i) {
cout << "ctor.this=" << this << " id=" << _id << endl;
}

// virtual or not
// ~Foo() { cout << "dtor.this=" << this << " id=" << _id << endl; }
virtual ~Foo() { cout << "dtor.this=" << this << " id=" << _id << endl; }

static void* operator new(size_t size);
static void operator delete(void* pdead, size_t size);
static void* operator new[](size_t size);
static void operator delete[](void* pdead, size_t size);
};

void* Foo::operator new(size_t size) {
Foo* p = (Foo*)malloc(size);
cout << "Foo::new, size=" << size << endl;
return p;
}

void Foo::operator delete(void* pdead, size_t size) {
cout << "Foo::delete" << endl;
free(pdead);
}

void* Foo::operator new[](size_t size) {
Foo* p = (Foo*)malloc(size);
cout << "Foo::new[], size=" << size << endl;
return p;
}

void Foo::operator delete[](void* pdead, size_t size) {
cout << "Foo::delete[]" << endl;
free(pdead);
}

接下来是测试函数:

  1. 调用全局的new/delete:
void t1() {
Foo* pf = new Foo;
delete pf;
/*
Foo::new, size=48
default ctor.this=0x600003388270 id=0
dtor.this=0x600003388270 id=0
Foo::delete, size=48
*/

Foo* pf1 = ::new Foo;
::delete pf1;
// if not overload new and delete: call system new and delete
/*
default ctor.this=0x600003388270 id=0
dtor.this=0x600003388270 id=0
*/
}
  1. 调用自定义的new/delete:
void t2() { // 验证内存大小
cout << "sizeof(int)=" << sizeof(int) << endl;
cout << "sizeof(long)=" << sizeof(long) << endl;
cout << "sizeof(string)=" << sizeof(string) << endl;
cout << "sizeof(Foo)=" << sizeof(Foo) << endl;
/*clang++
sizeof(int)=4
sizeof(long)=8
sizeof(string)=24
sizeof(Foo)=40
*/
/*g++
sizeof(int)=4
sizeof(long)=8
sizeof(string)=32
sizeof(Foo)=48
*/
}
// 然后是内存分配测试:
void t3() {
cout << "sizeof(Foo)=" << sizeof(Foo) << endl; // g++: 48
Foo* p = new Foo(7);
cout << "sizeof(new Foo)=" << sizeof(*p) << endl;
delete p;

Foo* pArr = new Foo[5];
cout << "sizeof(new Foo[5])=" << sizeof(*pArr) << endl;
delete[] pArr;
/*
sizeof(Foo)=48
Foo::new, size=48
ctor.this=0x600000f18000 id=7
sizeof(new Foo)=48
dtor.this=0x600000f18000 id=7
Foo::delete
Foo::new[], size=248
default ctor.this=0x600003d1c008 id=0
default ctor.this=0x600003d1c038 id=0
default ctor.this=0x600003d1c068 id=0
default ctor.this=0x600003d1c098 id=0
default ctor.this=0x600003d1c0c8 id=0
sizeof(new Foo[5])=48
dtor.this=0x600003d1c0c8 id=0
dtor.this=0x600003d1c098 id=0
dtor.this=0x600003d1c068 id=0
dtor.this=0x600003d1c038 id=0
dtor.this=0x600003d1c008 id=0
Foo::delete[]
*/
}

这里要注意, 为什么对象数组的大小不是​​48*5=240​​, 反而还多了一个8呢? 248=48*5+8

因为8就是size_t也就是unsigned long在64位机器下的大小, 保存了对象数组长度.

下面是分别在64位机器和32位(使用​​g++ -m32​​选项, 仅支持x86_64)机器下测试的情况:

// 64bit: cout << sizeof(size_t) << endl; // 8 cout << sizeof(long) << endl; // 8 cout << sizeof(unsigned long) << endl; // 8 cout << sizeof(unsigned) << endl; // 4 cout << typeid(unsigned).name() << endl; // j cout << typeid(unsigned int).name() << endl; // j cout << typeid(unsigned long).name() << endl; // m cout << typeid(size_t).name() << endl; // m // 32bit: 4 4 4 4 j j m j

  1. 类中含有虚析构函数的情况:
// 析构函数为:
virtual ~Foo() {cout << "dtor.this=" << this << " id=" << _id << endl;}

此时​​t3()​​函数执行情况如下:

sizeof(Foo)=56
Foo::new, size=56 // has a vptr: 48+8
ctor.this=0x6000039701c0 id=7
sizeof(new Foo)=56
dtor.this=0x6000039701c0 id=7
Foo::delete
Foo::new[], size=288 // 288=56*5+8
default ctor.this=0x139e04588 id=0
default ctor.this=0x139e045c0 id=0
default ctor.this=0x139e045f8 id=0
default ctor.this=0x139e04630 id=0
default ctor.this=0x139e04668 id=0
sizeof(new Foo[5])=56
dtor.this=0x139e04668 id=0
dtor.this=0x139e04630 id=0
dtor.this=0x139e045f8 id=0
dtor.this=0x139e045c0 id=0
dtor.this=0x139e04588 id=0
Foo::delete[]

一个小坑

来说一个奇怪的情况:

首先看下面的例子:(算是上面例子的一个简化)

#include <iostream>
using namespace std;

class P {
public:
int a; // 4bytes
void* operator new(size_t size) {
P* p = (P*)malloc(size);
cout << "P::new, size=" << size << endl;
return p;
}
void* operator new[](size_t size) {
P* p = (P*)malloc(size);
cout << "P::new[], size=" << size << endl;
return p;
}
// virtual
// ~P() {}
};

这里面为了省事我直接把成员函数定义也放在类中了.

然后是测试函数:

void t1() {
P* p = new P;
// 4 (无析构函数)
// 4 (无虚析构, 仅有析构)
// 16 (虚析构)
delete p;
P* p1 = new P[5]();
// 20 (无析构函数) 因为没设置析构函数,
// 这时候就不会记录之后delete时候要释放的大小, 所以没有size_t信息,
// 也就只有4*5=20大小 28 (无虚析构, 仅有析构) 88 (虚析构)
delete[] p1;
}
int main(int argc, char const* argv[]) {
t1();
return 0;
}

思考一下输出是多少(运行环境:64bit机器, g+±12)

如果你的答案是:

P::new, size=4
P::new[], size=20

并且你知道为什么, 那么就要恭喜你, 可以不用往后看了.

想知道为什么注释掉析构函数之后就不会多出来​​size_t​​类型的记录数组大小的cookie的话, 就接着往下看吧.

下面我做了一个表格, 就是关于上面这个例子有析构函数, 没有析构函数以及有虚析构函数三种情况得到的内存大小数据.

析构行为

单个对象大小

对象数组大小

无析构函数

4

20=4*5

有非虚析构函数

4

28=4*5+8

有虚析构函数

16=4+8+4

88=16*5+8

并且, ​​clang++ -cc1 -fdump-record-layouts test.cpp​​查看内存布局发现:

*** Dumping AST Record Layout
0 | class P
0 | (P vtable pointer)
8 | int a
| [sizeof=16, dsize=12, align=8,
| nvsize=12, nvalign=8]

发现存在内存对齐, 所以就变成了16而不是12了.

并且, 没有析构函数的时候并不会占用多余的内存来记录数组大小, 这是因为没有析构动作, 那么也不会去记录析构的数组大小, 换句话说, 只有在调用用户的析构函数的时候, 才会申请额外的内存​​size_t​​​来存储数组大小, 这就是​​8​​的由来.

但是, 类中包含指针对象(例如​​string​​)的话就一定会产生​​size_t​​大小的一个变量来保存数组长度信息.

[c++ - 成员运算符 new​​] 的参数“大小”增加,如果类具有析构函数/删除[] - 堆栈溢出 (stackoverflow.com)​​;

placement new(定位 new)

定义

cppprimer的定义是:

通过改变使用​​new​​的方式来阻止其抛出异常.

int *p1 = new int; //分配失败, new throw `std::bad_alloc` int *p2 = new (nothorw) int; //分配失败, new返回空指针

这种形式(第二行)的​​new​​​称为​​placement new​​​(定位​​new​​​)表达式, 这种写法允许我们向​​new​​传递额外的参数.

侯捷老师的PPT:

placement-new 允许我们将对象建构在allocated memory(已分配好的内存)中,
但是没有placement-delete, 因为并没有额外分配内存空间,
或者可以称呼与placement-new对应的是placement-delete.
placement-new:等同于调用构造函数.

基本使用

#include <complex>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <new>
using namespace std;

void t1() {
char* buf = new char[sizeof(complex<int>) * 3];
complex<int>* pc = new (buf) complex<int>(1, 2);
// 这里其实调用了下面的:
// static void* operator new(size_t size, void* start);
printf("buf=%p\n", buf);
printf("pc=%p\n", pc);
/* buf=0x600001bc1100 */
/* pc=0x600001bc1100 */

cout << pc->real() << " " << pc->imag() << endl; // 1 2
// 标准库提供的placement new()重载
delete[] pc;
}
int main(int argc, char const* argv[]) {
t1();
return 0;
}

直接使用, 原地重新分配, 可以看到地址是相同的.

重载placement new/placement delete

#include <iostream>

using namespace std;
/*placement-new*/
class Bad {}; // 稍后会作为异常抛出
class Foo {
public:
Foo() { cout << "Foo::Foo()" << endl; }
Foo(int) {
cout << "Foo::Foo(int)" << endl;
throw Bad(); // 这里给出一个异常的例子
}
// new
static void* operator new(size_t size); // 一般的重载
static void* operator new(size_t size,
void* start); // 标准库提供的placement new()重载
static void* operator new(size_t size, long extra); // 新的placement new()
static void* operator new(size_t size, long extra,
char init); // 另一个新的placement new()
// static void* operator new(long extra, char init);
// error, 第一参数必须是size_t类型

/*可以重载对应版本的placement delete(), 但是不会被调用,
只有当placement new()调用的ctor抛出异常时候, 才会调用这些重载版本的delete(),
也只可能被这样调用, 用途是归还未能完全创建成功的object占用的内存memory*/
static void operator delete(void*, size_t);
static void operator delete(void*, void*);
static void operator delete(void*, long);
static void operator delete(void*, long, char);

private:
int m_i;
};


void* Foo::operator new(size_t size) {
cout << "operator new(size_t size), size=" << size << endl;
return malloc(size);
}

void* Foo::operator new(size_t size, void* start) {
cout << "operator new(size_t size, void* start), size=" << size
<< ", start=" << start << endl;
return start;
}

void* Foo::operator new(size_t size, long extra) {
cout << "operator new(size_t size, long extra), size=" << size
<< ", extra=" << extra << endl;
return malloc(size + extra);
}

void* Foo::operator new(size_t size, long extra, char init) {
cout << "operator new(size_t size, long extra, char init), size=" << size
<< ", extra=" << extra << ", init=" << init << endl;
return malloc(size + extra);
}

// void* Foo::operator new(long extra, char init) {
// //error: 'operator new' takes type size_t ('unsigned long') as first
// parameter return malloc(extra);
// }

void Foo::operator delete(void*, size_t) {
cout << "operator delete(void*, size_t)" << endl;
}

void Foo::operator delete(void*, void*) {
cout << "operator delete(void*, size_t)" << endl;
}

void Foo::operator delete(void*, long) {
cout << "operator delete(void*, long)" << endl;
}

void Foo::operator delete(void*, long, char) {
cout << "operator delete(void*, long, char)" << endl;
}

下面是测试函数:

// 基本调用方式, 与析构
void t1() {
Foo* pf = new (300, 'c') Foo;
delete pf;
/*
Foo::Foo()
operator delete(void*, size_t)
*/
}
// 全部调用方式
void t2() {
Foo start;
Foo* p1 = new Foo;
Foo* p2 = new (&start) Foo;
Foo* p3 = new (100) Foo;
Foo* p4 = new (100, 'a') Foo;
delete p1;
delete p2;
delete p3;
delete p4;
/*
Foo::Foo()
operator new(size_t size), size=4
Foo::Foo()
operator new(size_t size, void* start), size=4, start=0x16f35b04c
Foo::Foo()
operator new(size_t size, long extra), size=4, extra=100
Foo::Foo()
operator new(size_t size, long extra, char init), size=4, extra=100, init=a
Foo::Foo()
operator delete(void*, size_t)
operator delete(void*, size_t)
operator delete(void*, size_t)
operator delete(void*, size_t)
*/
}
// 调用重载的operator delete的情况(bad_alloc)
void t3() {
Foo start;

Foo* p5 = new (100) Foo(1);
delete p5;
/*
Foo::Foo()
operator new(size_t size, long extra), size=4, extra=100
Foo::Foo(int)
libc++abi: terminating with uncaught exception of type Bad
[1] 10066 abort
*/
// Foo* p6 = new(100, 'a') Foo(1);
// Foo* p7 = new(&start) Foo(1);
// Foo* p8 = new Foo(1);
}
int main(int argc, char const* argv[]) {
// t1();
// t2();
t3();
return 0;
}

应用1: 委托构造

在C++11的委托构造函数出现之前, 可以使用placement new原地调用构造函数来完成构造.

#include <iostream>
using namespace std;

class P {
public:
P() {
new (this) P(1, 1.2);
cout << "call P()\n";
}
P(int a) {
new (this) P(a, 1.2);
cout << "call P(int)\n";
}
// 使用委托构造
// P() : P(1, 1.2) { cout << "call P()\n"; }
// P(int a) : P(a, 1.2) { cout << "call P(int)\n"; }

private:
// target ctor
P(int a, double b) : m_a(a), m_b(b) { cout << "call P(int, double)\n"; }

int m_a;
double m_b;
};

void t1() {
// 使用placement new
P p1;
// call P(int, double)
// call P()

P p2(10);
// call P(int, double)
// call P(int)
}

void t2() {
// C++11: 使用委托构造
P p1;
// call P(int, double)
// call P()

P p2(10);
// call P(int, double)
// call P(int)
}

int main(int argc, char const *argv[]) {
t1();
// t2();
return 0;
}

应用2: 显式调用构造函数

还可以这样在实例化的对象上通过placement new显式调用构造函数: (C++黑科技)

#include <iostream>
#include <string>
using namespace std;

void t1() {
string* ps = new string;
cout << "strs: " << *ps << endl;
// ps->string::string("1");//error,不能直接调用, 但是编译器可以
// error: 'class std::__cxx11::basic_string<char>' has no member named
// 'string'
// ps->string::~string;
}
class A {
public:
int id;
A(int i) : id(i) { cout << "ctor. this=" << this << ' ' << id << endl; }
~A() { cout << "dtor. this=" << this << endl; }
};

void t2() {
A* pA = new A(1);
// vc can build successfully
// pA->A::A(3); // error: cannot call constructor 'A::A' directly
delete pA;
// ctor. this=0x600001d14040 1
// dtor. this=0x600001d14040
}

void t3() {
// 采用placement new 调用构造函数
A* pA = new A(1);
new (pA) A(2);
// ctor. this=0x600000578040 1
// ctor. this=0x600000578040 2
}

int main(int argc, char const* argv[]) {
// t1();
t2();
// t3();
return 0;
}


标签:delete,placement,void,C++,operator,new,Foo,size
From: https://blog.51cto.com/u_15366127/6021431

相关文章

  • 验证二叉搜索树的C++实现多种解法
    tags:C++DSABinaryTree写在前面给你一个二叉树的根节点root,判断其是否是一个有效的二叉搜索树。有效二叉搜索树定义如下:节点的左子树只包含小于当前节点的数。节点......
  • 详细实例说明+典型案例实现 对迭代法进行全面分析 | C++
    第四章迭代法:::hljs-center目录第四章迭代法●前言●一、迭代法是什么?1.简要介绍2.代码示例(简单理解)3.生活实例●二、迭代法的典型案例——开平方&帕......
  • C++的宏利用include和undef来重复使用
    如dll导出函数,需要定义以及QueryInterface,其中函数有多个,如果想代码尽量简洁,只有这个方法定义#defineNVIDIA_API_DEF(_fun) decltype(_fun) *_##_fun=NUL......
  • happy new year
    <!DOCTYPEhtml><htmllang="en"><head>  <metacharset="UTF-8">  <title>Fireworks</title><style>body{ background:black; overflow:hidden; margin:0;}......
  • c++中运算符重载总结
    运算符重载的本质是函数重载。语法格式重载函数的一般格式如下:返值类型operator运算符名称(形参表列){    重载实体;}operator运算符名称在一起构成了新的函......
  • [C/C++] 简单实现按字符分割字符串split函数
    记录一下/***字符串str通过字符target进行分割*/vector<string>split(stringstr,chartarget){vector<string>res;intpos=0;while(po......
  • c/c++ mysql api函数
    一、常用APImysql_affected_rows()返回上次UPDATE、DELETE或INSERT查询更改/删除/插入的行数。mysql_autocommit()切换autocommit模式,ON/OFFmysql_change_user()......
  • C++概述、选择结构、循环结构
    目录1C++概述1.1计算两个整数相加之和1.2计算三个整数相加之和2选择结构2.1小老鼠走迷宫1(if语句)2.2小老鼠走迷宫1(if语句)(多个单分支结构)2.3小老鼠走迷宫2(switch语句)2......
  • C++实战笔记(三)异常处理
    tags:C++Interview写在前面简单总结一下C++异常处理部分(Exception).异常只是C++为了处理错误提出的一种解决方案,并不是唯一的一种.异常处理特点异常处理的流程完全独立......
  • c++ return
    return本是上是一个拷贝过程,不过是右值拷贝,也就是无标记变量的拷贝。不管是返回指针还是返回值,return首先将要return的值存到eax寄存器中,回到父函数再将返回的值赋给变量......