avatar

标准库源码

分配器

关于c++中new

new.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void* operator new(std::size_t) _GLIBCXX_THROW (std::bad_alloc)
__attribute__((__externally_visible__));
void* operator new[](std::size_t) _GLIBCXX_THROW (std::bad_alloc)
__attribute__((__externally_visible__));
void operator delete(void*) _GLIBCXX_USE_NOEXCEPT
__attribute__((__externally_visible__));
void operator delete[](void*) _GLIBCXX_USE_NOEXCEPT
__attribute__((__externally_visible__));
void* operator new(std::size_t, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
__attribute__((__externally_visible__));
void* operator new[](std::size_t, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
__attribute__((__externally_visible__));
void operator delete(void*, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
__attribute__((__externally_visible__));
void operator delete[](void*, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
__attribute__((__externally_visible__));

// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
inline void* operator new[](std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }

// Default placement versions of operator delete.
inline void operator delete (void*, void*) _GLIBCXX_USE_NOEXCEPT { }
inline void operator delete[](void*, void*) _GLIBCXX_USE_NOEXCEPT { }

这是minggw中定义new的文件头,具体实现要看linux下的源码
规则:
1.new 表达式形式

  • 普通的new int(2) / 数组new new int[10]
  • 调用new ClassType()
  • new § obj() p表示分配了空间的指针,obj()表示一个对象的构造
    补充:
    new为 new operator 用户不能重载,该操作符就是平时使用的,它会调用operator new 即标准库定义的,并且调用对应的构造方式,无论是一般类型还是类类型; place new 仅仅对分配了的空间进行赋值.
    new()和new[] 问题,看看allocator.allocate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  pointer
allocate(size_type __n, const void* = 0)
{
if (__n > this->max_size())
std::__throw_bad_alloc();

return static_cast<_Tp*>(::operator new(__n * sizeof(_Tp))); //明显根据默认类型大小*n来进行分配,假设你要分配数组,那么n>1,也就是说new 和new [] 编译器帮程序员做过了计算类型大小的步骤
}
void
deallocate(pointer __p, size_type)
{ ::operator delete(__p); } //由于分配是new所以销毁也就是delete,并且这样不会主动调用析构函数

template<typename _Up, typename... _Args>
void
construct(_Up* __p, _Args&&... __args) //分配器分配函数
{ ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }

template<typename _Up>
void
destroy(_Up* __p) { __p->~_Up(); } //析构函数的调用

2.如果是对普通类型进行new操作,编译器首先会对该类型进行匹配,如果程序员没有定义一个重载的operator new运算符(运算符在c++中视为函数),那么编译器就会直接去调用new.h定义的.
当new表达式进行的是类,那么编译器也会先对类内部进行匹配,如果你没有重写,那么此时就会用调用new.h中的,并且会在空间分配之后调用类的构造器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//测试关于类的new 操作符相关
class F {
public:
int a;
int b;
//构造器初始化顺序时按照变量位置进行的
F(int c, int d) : F(c, d) {
this->p = p;
}
F(int c, int d) : a(c), b(d) {
std::cout<<"构造函数"<<std::endl;
}
F(const F &f) {
a = f.a;
b = f.b;
std::cout << "调用了拷贝构造" << std::endl;
}
~F() {
std::cout << "析构" << std::endl;
delete p;
}
//重写一个new操作符,此处的代码实际就是new.h定义的一般new 的源码形式,仅仅是分配了空间
void *operator new(size_t sz) { //此处的参数是给编译器使用的,并非是给程序员使用的
std::cout<<"调用op new"<<std::endl;
void *p;
// new_handler 以后说明,但是可以看出首先我们根据入口参数 sz的大小分配内存,
// 如果sz为0 则令其为1 ,然后在while循环中调用malloc申请内存
// 直到 申请成功 或者 抛出异常或者 abort
/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;

while (__builtin_expect ((p = malloc (sz)) == 0, false))
{
auto handler = std::new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(std::bad_alloc());
handler ();
}
return p;
}
};
//重写new操作符
void opNew() {
F *f=new F(1,2); //次数的参数则是当作了汇编器调用构造器时使用的形参
PRINT(f->a); //打印宏
}
// 调用op new
// 构造函数
// 1

输出的结果说明了编译器new 的实质过程,如果你喜欢可以去看一眼反汇编代码
3.关于palce new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//使用定位new
void test3() {
F *f;
F f2(1, 2);
std::allocator<F> allocator;
f = allocator.allocate(1); //f指针必须先是一块分配了空间的指针才行
::new ((void*)f) F(f2); //表示直接使用编译器关键字new
new ((void*)f) F(f2); //如果改成这样,至少在minGW编译器下会提示没有匹配到 F::operator new(sizetype, void*)'
PRINT(f->a); //打印宏
}
//构造函数 --->这是f2创建时调用的和palce new 无关
//调用了拷贝构造 -->这个拷贝构造则时临时对象的创建,最后该临时对象也没有发生析构,也就是说将该临时对象的内存给了p
//1
//析构

对于palce new 在mingGW环境下,编译器会去从类内部匹配是否重写了place new, 如果没有就并没有去调用标准库定义的全局,此处就要主动使用
输出结果说明了palce new 的调用过程 就是将后者的内存使用拷贝拷贝,并且在palce new 函数的调用过程不会进行析构函数的调用
4.关于转发和移动语义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include "../marco.h"
namespace cp13{
class MoveTest {
int a;
int*p;
public:
MoveTest(int a,int*p):a(a),p(p){

}
//拷贝保持值行为
MoveTest(const MoveTest& moveTest){
a=moveTest.a;
p=new int(*moveTest.p);
PRINT("拷贝")
}
//
MoveTest&operator=(const MoveTest& moveTest){
a=moveTest.a;
p=new int(*moveTest.p);
PRINT("拷贝运算")
}
~MoveTest(){
if(p)
delete p;
PRINT("析构")
}
//移动保持控制转移
MoveTest( MoveTest&& moveTest){
a=moveTest.a;
p=moveTest.p;
moveTest.p= nullptr;
PRINT("移动")
}
MoveTest&operator=( MoveTest&& moveTest){
a=moveTest.a;
p=moveTest.p;
moveTest.p= nullptr;
PRINT("移动运算")
}
};

MoveTest test(MoveTest&&move){ //只有模板的情况该函数才能接受左值,具体看看constur()函数
MoveTest moveTest(std::forward<MoveTest>(move));
return moveTest;
}
Test("移动fun"){
auto p=test(MoveTest(1,new int(1)));
PRINT("main");
}
}

1.形参 返回值 局部变量,如果直接返回形参,test函数中相当生成了两个变量;如果返回局部变量也是两个变量,也就是说编译器将结果在汇编语句每条直接放到跟eax相关寄存器中了,也就是说如果返回值是一个局部变量那么它就不在栈中分配.
2.如果返回值赋给了一个句柄,那么该返回值不会在函数中析构,从而延长了声明周期
3.转移语句的实质就是控制权限的转移,如果要返回一个参数,那么就说明该参数的声明周期并不是该函数中结束,那么为啥你不将参数设为引用呢?如果使用了形参值传递,那么返回形参的时候只要保证形参->返回值是移动构造,就能把形参的控制权移交给返回值,那么也没有任何问题
4.只有当要使用权限转移的时候才应该使用转移语句,也就是说将A中的堆内存转移到B中,如局部变量值的返回.
5.我认为只要保证拷贝函数是值拷贝行为,移动是控制转移行为,就ok了. —此条是完全正确的
6.关于allocate.construct(),函数其形参即使是拷贝构造,也不会调用析构,是什么鬼.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//再次说明返回值问题
//1.形参如果是值,那么不论如何调用时要进行拷贝构造,即使该形参时&&类型,你创造这个形参的方式除了临时产生的右值,就是通过std::move(),那么对于值类型形参和之前的值没有关系
//2.返回值问题,移动语义可以发生在返回值到调用这过程

//情况1 返回临时对象
A test(){
A a; //构造,rax存放该对象首地址
return a; //a为临时右值
//若没有返回赋值语句,那么析构返回值
}
A a=test();// 赋值行为,test中a返回值,进行一次拷贝构造函数,若存在移动构造函数,那么就进行移动构造
//析构返回值
//析构a

//情况2 返回形参
A test2(A a){ //形参a存在于rdi寄存器中
return a; //返回值rax 进行移动/拷贝构造
//若无赋值语句,析构rax
//析构形参a
}
A a;
A a2=test2(a); //拷贝构造复制形参
//返回值进行拷贝/移动构造
//析构返回值
//析构a2
//析构a

F test6(F f){
return f;
}
/**
* 这种情况就保留了 返回值到调用者的移动赋值函数/或者移动赋值函数
*/
// Test("参数返回赋值情况"){
// F f(1,2);
// f=test6(f);
// PRINT("MAIN")
// }
/**
* 由于返回值优化的情况存在,test6函数中形参到返回值f的移动/拷贝构造过程创造的对象就直接时调用者f2,不存在中间的test6()返回值到f2的拷贝或者移动构造
*/
Test("参数返回构造情况"){
F f(1,2);
F f2=test6(f);
PRINT("MAIN")
}

上边这些代码我觉得很能说明问题了, 移动语义,返回值优化,返回值RAX,形参寄存器这些都构成了C++这部分难以理解.

  1. 参照上边的5,移动和复制保持原意
  2. 移动语义适用的场合 将不需要的内存转到另一个身上,避免新对象构造的开销
  • 具体来说 如果上例子中值返回情况,既然是值返回,那么该值就是无用对象,那么为何不使用移动构造/赋值呢
  • 将指针指向的堆函数移动都另一个指针上,这个问题在C++ perimer中13.5中realloca函数中 这里我重点说明,移动转移的是string*(栈内存A)–>string实际占用的堆内存B—>string内部的指针指向的堆内存C,实际上转移的是C内存
文章作者: fancylight
文章链接: https://www.fancylight.top/2019/01/04/%E6%A0%87%E5%87%86%E5%BA%93%E6%BA%90%E7%A0%81/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 博客
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论