《Hands-On System Programming with C++》读书笔记之七
关于new()和delete()函数
程序运行的基础
当一个程序运行时,有几种内存空间可供使用:
- 全局内存
- 栈内存
- 堆内存
全局内存存在于程序自身,由操作系统分配,分布于2个内存段内:
- .bss:未初始化或初始化为0的变量空间;
- .data:初始化为其他值的变量空间;
例如下面的代码
#include <iostream>
int bss_mem = 0;
int data_mem = 42;
int main()
{
std::cout << bss_mem << '\n';
std::cout << data_mem << '\n';
}
过多的使用全局变量会导致编译后的程序体积过大,并且延长加载时间(除了初始化为0的变量)。
当变量只存在于特定的作用域时(如上例的main()函数),它们被存储在栈空间。离开作用域后,存储空间被出栈释放。栈空间是有限的,而且大多数时候,栈空间溢出后没有错误返回,程序只是直接崩溃。
#include <iostream>
int main()
{
int stack_mem[268435456];
std::cout << stack_mem[4] << '\n';
}
上面的程序不能正确执行,直接打印Segmentation fault(结果视实验平台而异)。
堆内存也称为动态内存,只要硬件和操作系统支持,堆内存可以变得非常大。但是它是根据申请而分配的,而且这个过程相比全局内存和栈内存来说要慢。堆内存使用结束后要释放。
#include <iostream>
class myclass
{
public:
~myclass() {
std::cout << "my delete\n";}
};
int main()
{
auto ptr = new myclass[2];
std::cout << ptr << '\n';
delete [] ptr;
}
输出结果
0x559219278e78
my delete
my delete
从上面看出,delete []会调用队列里每个对象的析构函数。注意new()和delete()配合使用,new ()[]和delete 配合使用,不可混用。
空间地址对齐
物理设备和API往往要求使用内存空间时要对齐于一定的地址单位。对全局空间和栈空间的空间地址对齐,在C++中可以使用alignas()实现。
#include <iostream>
alignas(0x1000) int ptr[42]; // 4K align in global memory
int main()
{
// alignas(0x1000) int ptr[42];
// 4K align in stack memory
std::cout << ptr << '\n';
}
在栈空间的对齐会导致额外空间占用,加快耗尽栈内存。
也可以用手工计算并指定的方式对齐空间
#include <iostream>
int main()
{
char buffer[0x2000];
auto ptr1 = reinterpret_cast<uintptr_t>(buffer);
auto ptr2 = ptr1-(ptr1 % 0x1000)+0x1000;
std::cout << std::hex << std::showbase;
std::cout << ptr1 << '\n'; // 0x7ffdc6c51980
std::cout << ptr2 << '\n'; // 0x7ffdc6c52000
}
注意上例中对reinterpret_cast的使用和对指针的计算都是C++ Core Guide不推荐的。
对动态空间(堆空间)的空间地址对齐,最早使用的是posix_memalign();在C++11出现后,使用align_alloc()。前者书中给了例子,但这种用法已经不再推荐了。后者的用法如下
#include <iostream>
int main()
{
if (auto ptr = aligned_alloc(0x1000, 42 * sizeof(int)))
{
std::cout << ptr << '\n';
free(ptr);
}
}
C++17中引入了更新的方式。
#include <iostream>
using aligned_int alignas(0x1000) = int;
int main()
{
auto ptr = new aligned_int[42];
std::cout << ptr << '\n';
delete [] ptr;
}
这里aligned_int被定义成具有地址对齐属性的整数的别名,然后通过new()和delete()创建了并释放了其对象数组。
nothrow
new()和delete()执行失败时会抛出异常,并不会返回nullptr。如果不希望它抛出异常而是返回nullptr,只需将上例中的 new aligned_int[42]变为new(std::nothrow) aligned_int[42]即可。
Placement of new
C++还允许在用户已有的空间分配空间。
#include <iostream>
char buf[0x1000];
int main()
{
auto ptr1 = new (buf) int;
auto ptr2 = new (buf) int;
std::cout << ptr1 << '\n';
std::cout << ptr2 << '\n';
std::cout << (void *)buf << '\n';
}
可以看到三行输出的结果完全相同。注意这里不用使用delete(),因为分配的空间在已有buf上,而不是在动态空间中新申请的内存。实际上这里的new()并没有实际管理这片内存,需要用户格外仔细,自行操作管理。
也正由于这样,当需要地址对齐时,应该在buf空间定义时使用alignas()。
重载
当默认的new()和delete()不能产生希望的结果时,可以重载它们。
#include <iostream>
void *operator new (std::size_t count)
{
// WARNING: Do not use std::cout here
return malloc(count);
}
void operator delete (void *ptr)
{
// WARNING: Do not use std::cout here
return free(ptr);
}
int main()
{
auto ptr = new int;
std::cout << ptr << '\n';
}
由于std::cout使用了new(),因此在重载的new()中使用它会造成无限的递归调用。类似问题在std::vector和std::list中也存在。
调试和统计中常用到重载new(),如下面的例子。
#include <iostream>
std::size_t allocations = 0;
void *operator new (std::size_t count)
{
if (count >= 0x1000)
++allocations;
return malloc(count);
}
void operator delete (void *ptr)
{
return free(ptr);
}
struct mystruct
{
char buf[0x1000];
};
int main()