全面理解内存管理

《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()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值