C++ memory primitives
分配 | 释放 | 类属 | 可否重载 |
---|---|---|---|
malloc() | free() | C函数 | 不可 |
new | delete | C++表达式 | 不可 |
::operator new | ::operator delete | C++函数 | 可 |
allocate<T>::allocate() | allocate<T>::deallocate() | C++标准库 | 可自由设计并搭配容器 |
void *p = malloc(512);
free(p);
obj *p = new obj(1);
delete(p);
void *p = ::operator new(512);
::operator delete(p);
new expression
当通过new创建对象的时候,编译器将new转为三个步骤
obj *p = new obj(1);
void *men = operator new(sizeof(obj));
p = static_cast<obj*>(men);
p->obj::obj(1);
delete expression
当通过delete释放对象的时候,先析构,在释放内存
delete p;
p->~obj();
operator delete(p);
placement new
c++允许我们从对象构造中分离内存分配。例如,我们可以使用malloc()分配一个字节数组,并在该内存区域中构造一个新的对象。
auto memory = std::malloc(sizeof(obj));
auto obj_ = new (memory) obj("...");
为了析构对象并释放内存,我们需要显式地调用析构函数,然后释放内存
obj_->~obj();
std::free(memory);
Memory alignment
CPU每次会读取一个字的内存到寄存器。
32位 | 64位 | |
---|---|---|
字 | 32 bits | 64 bits |
为了让CPU在处理不同数据类型时高效工作,它对不同类型的对象的地址会进行限制。
C++中的每种类型都有一个对齐要求,它定义了特定类型的对象在内存中的地址。如果类型的对齐方式为1,则意味着该类型的对象可以位于任何字节地址。
如果一个类型的对齐是2,这意味着该类型的对象只能定位在是2的倍数的地址上,以此类推。
我们可以使用 alignof 来找出一个类型的对齐要求
std::cout << alignof(int) << std::endl;// 4
当我们使用new(),malloc()时,返回的内存是与指定的类型正确对齐的。
auto p = new int();
auto address = reinterpret_cast<std::uintptr_t>(p);
std::cout << (address % 4ul) << std::endl; // 0
在头文件 <cstddef>中为我们提供了 std::max_align_t 类型。从new返回的内存都会与 std::max_align_t对齐。即使是char。
auto* p = new char{};
auto address = reinterpret_cast<std::uintptr_t>(p);
auto max_alignment = alignof(std::max_align_t);
std::cout << (address % max_alignment) << std::endl; // 0
Padding
在我们自己定义的数据类型中,编译器可能会添加一些额外的bytes。(padding)。当我们在类或者结构体中定义数据成员时,编译器会按照成员的定义顺序来对他们进行放置。编译器为了确保类中的数据成员有正确的对齐方式,可能会在成员之间进行填充(padding)。
class Test1{
bool a{};
double b{};
int c{};
};
std::cout << sizeof(Test1) << std::endl;// 24
padding
class Test1{
bool a{};
char padding1[7];
double b{};
int c{};
char padding2[4];
};
具有最大对齐需求的成员决定整个数据结构的对齐需求。在例中,最大的是double 8-bytes。所以整个类的大小应该是8的倍数。
class Test2{
double b{};
int c{};
bool a{};
};
std::cout << sizeof(Test2) << std::endl; // 16
class Test2{
double b{};
int c{};
bool a{};
char padding[3];
};
作为一般规则,可以将最大的数据成员放在开头,将最小的成员放在末尾。通过这种方式,可以最小化由填充引起的内存开销.
从性能的角度来看,也可能需要将对象与缓存线(cache lines)对齐,以最小化对象跨越的缓存线的数量。当我们谈到 cache friendliness 的问题时,还应该提到,将经常使用的多个数据成员放在一起可能是有好处的。
Smart pointer
在C++中使用动态内存很容易出现问题,可能会产生内存泄漏,或者引用已经释放的指针。C++ 11标准库提供了智能指针来管理动态对象。使用智能指针我们不用关心对象是否已经释放。标准库提供了三种智能指针 std::shared_ptr, std::unique_ptr, std::weak_ptr。
unique_ptr | 独占对象,只有一个拥有者 |
---|---|
shared_ptr | 允许多个指针指向同一个对象 |
weak_ptr | 如果对象存在则使用,但不控制对象的生存期 |
Unique pointer
最安全、最简单、所有权唯一的智能指针。某时刻只有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,该对象也被销毁。unique_ptr非常高效,与普通指针相比,不会增加任何性能开销。
初始化unique_ptr必须采用直接初始化形式。不支持普通的拷贝和赋值操作。可从函数中返回。
std::unique_ptr<int> p(new int(1));
auto p = std::make_unique<int>(1);// C++14
auto p1 = std::move(p) //交换所有权
unique_ptr<T, D> u | 会调用D类型可调用对象来进行释放操作 |
---|---|
unique_ptr<T, D> u(d) | 用类型为D 的对象d代替 delete |
u.release() | u放弃指针的控制权并返回指针 |
u.reset() | 释放u对象 |
u.reset(p) | 令u指向p原对象被释放 |
Shared pointer
多个shared_ptr可以指向一个对象,当最后一个shared_ptr不存在后,对象将被释放。shared_ptr使用引用计数来记录对象所有者的数量。当计数器达到0时,对象将被删除。计数器需要存储在某个地方,因此与unique_ptr相比,它确实有一些内存开销。另外,std::shared_ptr是线程安全的,因此计数器需要自动更新,以防止竞争条件。
创建shared_ptr的推荐方法是使用std::make_shared()。它比使用new手动创建对象并将其传递给std::shared_ptr构造函数更安全,也更有效。
std::shared_ptr<int> p = std::make_shared<int>(32);
auto operator new(size_t size) -> void* {
void* p = std::malloc(size);
std::cout << "allocated " << size << " byte(s)" << std::endl;
return p;
}
auto operator delete(void* p) noexcept -> void {
std::cout << "deleted memory\n";
return std::free(p);
}
使用make_shared
auto main() -> int {
auto i = std::make_shared<double>(42.0);
return 0;
}
// ouput
allocated 32 byte(s)
deleted memory
使用new
auto main() -> int {
auto i = std::shared_ptr<double>(new double{42.0});
return 0;
}
//output
allocated 4 byte(s)
allocated 32 byte(s)
deleted memory
deleted memory
可以看出使用new需要进行两次分配。 double shared_ptr。
shared_ptr<T> sp | 空智能指针 |
---|---|
sp | 可作为判断条件,若指向对象则为true |
*sp | 解引用 |
sp.get() | 获得指针 |
swap(sp,sp1)\sp.swap(sp1) | 交换指针 |
sp.unique() | 若sp.use_count()为1返回true |
shared_ptr<T> sp(u) | 从unique_ptr接管对象,将u置空 |
shared_ptr<T>sp(p,d) | 使用可调用对象d代替delete |
sp.reset(p,d) | 指向p,替换delete |
智能指针的构造函数是explict的,必须直接初始化。
std::shared_ptr<int> clone(int p){
return new int (p);
}//错误的
不能进行内置指针到智能指针的隐式转换。
不要使用get来初始化另一个智能指针或赋值。不会增加引用计数,各自的计数都为1。
Weak pointer
weak_ptr不控制指向对象的生存期,它指向由shared_ptr管理的对象。将一个weak_ptr绑定到shared_ptr不会改变其引用计数。
使用weak_ptr的常见原因是避免shared_ptr的循环引。.当两个或多个对象使用shared_ptr相互引用时,就会发生引用循环。即使所有的外部shared_ptr都析构了,这些对象仍然通过引用它们自己而不被释放。
class B;
class A{
std::shared<B> ptr;
};
class B{
std::shared<A> ptr;
}
auto main() -> int{
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->ptr = b;
b->ptr = a;
}
a,b所指的对象互相引用,此时引用计数为2。在程序结束a,b被释放后,其对象中的ptr的引用计数为1,导致内存泄漏。
weak_ptr<T> w | 空对象 |
---|---|
weak_ptr<T> w(sp) | 绑定到shared_ptr |
w = p | p 可为weak或shared |
w.reset() | 置空 |
w.use_count() | 对象sp的数量 |
w.expired() | 若w.use_count()为0 返回true |
w.lock() | 如果w.expired()为true返回一个空sp,否则返回该对象sp |
Small size optimization
像std::vector这样的容器的优点之一是它们在需要时自动分配动态内存。但是,有时只包含几个小元素的容器对象使用动态内存会损害性能。将元素保存在容器本身中,并且只使用堆栈内存,而不是在堆上分配小区域的内存,这样效率会更高。大多数std::string的现代实现都利用了这样一个事实:普通程序中的很多字符串都很短,短字符串在不使用堆的情况下处理效率更高。
一种替代方法是在string类本身中保留一个小的单独缓冲区,当字符串内容较短时可以使用它。这将增加字符串类的大小,即使没有使用短缓冲区。因此,一个更节省内存的解决方案是使用union,当字符串处于short模式时,union可以保存一个短缓冲区,否则,它保存处理动态分配的缓冲区所需的数据成员。为处理小数据而优化容器的技术通常称为针对字符串的小字符串优化.