1. 第一讲 primitives(基础知识)

目录

1 内存分配的每一层面

1.1 C++ 应用程序使用内存的方式

1.2 C++ memory primitives(C++内存分配基础工具)

1.3 四个层面的基本用法

2 基本构件之一newdelete expression

2.1 new expression(new表达式)

2.2 delete expression(delete表达式)

2.3 Ctor & Dtor 直接调用

3 Array new

3.1 array new, array delete

3.2 array size,in memory block(数组的内存布局)

4 placement new

5 C++分配内存的途径

5.1 C++应用程序,分配内存的途径

5.2 C++容器,分配内存的途径

6 重载

6.1 重载::operator new/::operator delete

6.2 重载operator new/operator delete

6.3 重载operator new[]/operator delete[]

6.4 重载示例

6.5 重载 new()/delete()

6.6 basic_string 使用 new(extra) 扩充申请量

7 allocator

7.1 per-class allocator

7.1.1 v1版本

7.1.2 v2版本:加上了embedded pointer

7.2 static allocator

7.2.1 static allocator的原理:

7.2.2 static allocator 示例与结果

7.2.3 marco for static allocator

7.3 global allocator(with multiple free-lists)

7.4 小结

8 new handler

9 =default,=delete






本章讲解基础的用于分配内存和销毁内存的”工具“

天宝当年,DOS 640K内存

锱铢必较

俱往矣,且看今朝

1 内存分配的每一层面

1.1 C++ 应用程序使用内存的方式

image-20211123164909447

  • C++应用程序可以通过多种方式调用内存分配的”工具“(即接口)
  • CRT指的是C语言运行时库

1.2 C++ memory primitives(C++内存分配基础工具)

image-20211123165207092

1.3 四个层面的基本用法

对上述的 4 个 primitives 的使用示例:

#include <iostream>
#include <complex>
#include <memory>				 //std::allocator  
#include <ext\pool_allocator.h>	 //欲使用 std::allocator 以外的 allocator, 就得自行 #include <ext/...> 
namespace jj01
{
void test_primitives()
{
	cout << "\ntest_primitives().......... \n";
	
    void* p1 = malloc(512);	//512 bytes
    free(p1);

    complex<int>* p2 = new complex<int>; //one object
    delete p2;             

    void* p3 = ::operator new(512); //512 bytes
    ::operator delete(p3);

//以下使用 C++ 標準庫提供的 allocators。
//其接口雖有標準規格,但實現廠商並未完全遵守;下面三者形式略異。
#ifdef _MSC_VER
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 3 個 ints.
    int* p4 = allocator<int>().allocate(3, (int*)0); 
    allocator<int>().deallocate(p4,3);           
#endif
#ifdef __BORLANDC__
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 5 個 ints.
    int* p4 = allocator<int>().allocate(5);  
    allocator<int>().deallocate(p4,5);       
#endif
#ifdef __GNUC__
    //以下兩函數都是 static,可通過全名調用之。以下分配 512 bytes.
    //void* p4 = alloc::allocate(512); 
    //alloc::deallocate(p4,512);   
    
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 7 個 ints.    
	void* p4 = allocator<int>().allocate(7); 
    allocator<int>().deallocate((int*)p4,7);     
	
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 9 個 ints.	
	void* p5 = __gnu_cxx::__pool_alloc<int>().allocate(9); 
    __gnu_cxx::__pool_alloc<int>().deallocate((int*)p5,9);	
#endif
}	
} //namespace

image-20211124123105329


【说明】:

  • ::operator new()底层就是调用的malloc::operator delete()底层就是调用的free
void* p3 = ::operator new(512); //底层就是调用的malloc
::operator delete(p3); //底层就是调用的free
  • allocator之所以用了不同的宏来区分,是因为虽然是标准接口,但在不同的环境下不同的使用方式效率不同。
#ifdef __BORLANDC__
    //此处的allocator<int>()是临时对象,该行执行结束后,对象的生命就结束了
    //分配5个整数,而不是5个字节,因为指定了放的单元类型
    int* p4 = allocator<int>().allocate(5); 
    allocator<int>().deallocate(p4,5);
#endif
  • 较新的GNU版本alloc换成了_pool_alloc

image-20211124135550983

2 基本构件之一newdelete expression

2.1 new expression(new表达式)

image-20211124141129889

  • new 的操作是:分配内存调用构造函数
  • ::operator new全局的函数,而该函数可以重载,如果它被重载了,那么调用的就是重载的函数;而此处的Complex 类并没有重载 operator new,所以调用的就是全局的::operator new;
  • 在函数opeartor new中可以看到,调用了malloc
  • 只有编译器才可以直接调用构造函数;如果程序想直接调用构造函数,可以使用 palcement new 的方式new(p) Complex(1,2)

2.2 delete expression(delete表达式)

 image-20211124142748990

  • delete 的操作是:先调用析构函数,释放内存
  • 注意:程序可以直接调用析构函数
  • operator delete 函数底层就是调用的free

2.3 Ctor & Dtor 直接调用

编写程序测试是否能直接调用构造函数和析构函数

image-20211124143155408

  • 通过指针调用构造函数:pstr->string::string("jjhou"); 编译会失败
  • 第二段和第三段代码是自定义的类,测试通过程序直接调用构造函数
    1. pA->A::A(3); 在VC6中执行会成功,而在GCC中则会执行失败,GCC更加严谨
    2. A::A(5); 在 VC6 中也执行成功,在GCC中执行失败
  • 结论:程序不能直接调用构造函数,可以直接调用析构函数

3 Array new

3.1 array new, array delete

image-20211124144052828

  • pca 如果使用的是delete pca,那么编译器会认为 pca 是一个对象,只会调用一次析构函数
  • cookie记录个数用malloc就会有cookie;
  • Complex类中没有指针,所以其实析构函数是没有什么用的,但是好的编程观念或技巧,就是要统一,使用array new 就要用 array delete;
  • string 类中有指针如果使用了array new,就一定要使用array delete否则类中的指针所指向的空间不会被销毁,就会造成内存泄漏

image-20211124145149853

  • A类一定要写默认构造函数因为使用array new 的时候是没有办法设置初值的,调用的就是默认构造函数
  • 使用placement new来设置初值new (tmp++) A(i); 【注:在 tmp 这个地址放置一个对象】

小结:

  • array new 和 array delete 如果不配套写的话可能会造成内存泄漏泄露的不是数组本身,而是类中的指针所指向的内存
  • 编译器在处理array new 的时候是从上往下,而array delete 则是从下往上
  • 注意 placement new 的用法

3.2 array size,in memory block(数组的内存布局)

下图是VC6的malloc的内存布局:

image-20211124150144477

  • 61h 是cookie代表记录内存的大小60h1 表示这块内存使用了
  • 此处的int 的析构函数是无意义的,不重要的,所以是否使用array delete 都可以;

image-20211124150636765

对于Demo类:

  • 使用array new 的时候内存中会有一个表示分配的对象个数的数,此处的3
  • 使用delete[] p; 的时候因为有[],底层执行free 的时候发现对象个数是 3,于是就调用 3 次析构函数

4 placement new

image-20211124151127682

  • placement new 允许我们将object 建构于已经分配的内存中,所以首先需要有一个指针指向已经分配的内存
  • 没有所谓的placement delete,因为placement new根本没分配内存

5 C++分配内存的途径

5.1 C++应用程序,分配内存的途径

image-20211124152832598

  • 应用程序使用new和delete表达式不可改变,不可重载
  • 成员函数operator new和operator delete可以重载
  • 全局函数operator new和operator delete可以重载但少见,重载影响极大,因此一般很少重载

5.2 C++容器,分配内存的途径

image-20211124160201128

  • 将一个元素放到容器中的时候,容器也要new一块空间来构造出来;

6 重载

6.1 重载::operator new/::operator delete

重载全局的operator new/opeartor delete,即类外重载:

image-20211124160814340

6.2 重载operator new/operator delete

在类中重载,重载成员函数operator new/opeartor delete:

image-20211124161342993

6.3 重载operator new[]/operator delete[]

image-20211124161704922


6.4 重载示例

(1)示例,接口:在Foo类中重载,重载成员函数operator new/opeartor delete

image-20211124163014208

 image-20211124163154368


(2)如果这样写:这样调用全局的重载函数,绕过了重载的函数强制使用全局版本。

image-20211124163838898

6.5 重载 new()/delete()

我们可以重载class member operator new(),实现多个版本:

image-20211124164107910


具体例子:

image-20211124164341507

 image-20211124165129141

  • 第⑤个Foo* p5 = new(100) Foo(1)调用的是void* operator new(size_t size, long extra) 这个函数;其调用的是带有参数的构造函数Foo(int),在该构造函数中抛出了异常,只有在这种情况下(构造函数内抛出异常)对应的operator delete才会被调用;

6.6 basic_string 使用 new(extra) 扩充申请量

image-20211124165847707

  • 每次创建字符串的时候都多带了一包东西,所以需要extra

7 allocator

接下来的几个版本是我们自行开发的小型的内存分配器。

7.1 per-class allocator

7.1.1 v1版本

第一版本的opeartor new 和 operator delete:

image-20211124170754560

  • 这就是个小型的分配器(小型内存池,但是只针对于这个类;
  • 针对的是VC6编译器中的内存块,测试:

image-20211124170814849


7.1.2 v2版本:加上了embedded pointer

第二版本的operator new:

image-20211124170829734

说明:

union {
    AirplaneRep rep;
    Airplane* next; 
};

其中Airplane* next; 借用同一个东西的前4个字节当成指针来使用,这种方法叫做“embedded pointer”,所有内存管理都用了这种技巧

struct AirplaneRep {
    unsigned long miles; //4字节
    char type;//1字节
};

为了内存对齐,struct AirplaneRep 的大小为 8 字节。

for (int i = 1; i < BLOCK_SIZE - 1; i++) 
    newBlock[i].next = &newBlock[i+1]; //next每次移动8个字节


第二版本的opeartor delete:

image-20211124170916005

  • 将收回来的指针放入单向链表的头;但是没有还给操作系统;
  • 写了member operator new/delete的间隔是8,从间隔可以看出,对象都是紧紧相连的,没有耗用掉cookie;
  • 而使用global opeartor new/delete的,每个对象的前后都有cookie,所以间隔是 16;
  • 第二版本相比于第一版的优点:使用了union ,用前4个字节当成指针来使用,即“embedded pointer”方法。
  • 但是还是有个小缺点:收回来的指针全部累计起来了,如果能还给操作系统就更好了。
/*************************************************************************
    > File Name: 01.per-class_allocator.cpp
    > Author: Maureen 
    > Mail: Maureen@qq.com 
    > Created Time: Thu Nov 25 09:52:51 2021
 ************************************************************************/

#include <iostream>
using namespace std;

class Airplane {
private:
    struct AirplaneRep {
        unsigned long miles;
        char type;
    };
private:
    union {
        AirplaneRep rep;
        Airplane* next;
    };
public:
    unsigned long getMiles() { return rep.miles; }
    char getType() { return rep.type; }
    void set(unsigned long m, char t) {
        rep.miles = m;
        rep.type = t;
    }
public:
    static void* operator new(size_t size);
    static void operator delete(void* deadObject, size_t size);
private:
    static const int BLOCK_SIZE;
    static Airplane* headOfFreeList;
};

Airplane* Airplane::headOfFreeList;
const int Airplane::BLOCK_SIZE = 512;

void* Airplane::operator new(size_t size) {
    //如果大小有误,转交给::opeartor new(). 在继承的时候可能发现大小有误
    if (size != sizeof(Airplane)) 
        return ::operator new(size);

    Airplane* p = headOfFreeList;
    if (p) //若p有效,将list头部下移一个元素
        headOfFreeList = p->next;
    else {
        //free list已空,申请(分配)一大块
        Airplane* newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE * sizeof(Airplane)));
        
        //将小块串成一个free list,但跳过 #0,因它将被传回作为本次成果
        for (int i = 1; i < BLOCK_SIZE - 1; i++) 
            newBlock[i].next = &newBlock[i + 1];
        newBlock[BLOCK_SIZE - 1].next = 0; //结束list
        p = newBlock;
        headOfFreeList = &newBlock[1];
    }
    return p;
}

//opeartor delete接获一个内存块
//如果大小正确,就把它加到free list前端
void Airplane::operator delete(void* deadObject, size_t size) {
    if (deadObject == 0) return ;
    if (size != sizeof(Airplane)) {
        ::operator delete(deadObject);
        return ;
    }

    Airplane* carcass = static_cast<Airplane*>(deadObject);

    carcass->next = headOfFreeList;
    headOfFreeList = carcass;
}

int main() 
{
    cout << sizeof(Airplane) << endl;
    size_t const N = 100;
    Airplane* p[N];

    for (int i = 0; i < N; ++i) 
        p[i] = new Airplane;
    
    //随机测试object是否正常
    p[1]->set(1000, 'A');
    p[5]->set(2000, 'B');
    p[9]->set(500000, 'C');

    //输出前10个pointers,用于比较其间隔
    for (int i = 0; i < 10; ++i) 
        cout << p[i] << endl;

    for (int i = 0; i < N; ++i) 
        delete p[i];

    return 0;
}
//测试环境非侯捷的测试环境,所以结果有很大区别



7.2 static allocator

7.2.1 static allocator的原理:

特点:将内存的动作抽取到单一的class——allocator 中;

image-20211124171246154

7.2.2 static allocator 示例与结果

每次开辟的 5 个元素在内存中都是相邻的,但是这一组元素与另外开辟的 5 个元素不一定是相邻的;

image-20211124171305604

image-20211124171339945

/*************************************************************************
    > File Name: 02.static_allocator.cpp
    > Author: Maureen 
    > Mail: Maureen@qq.com 
    > Created Time: Thu Nov 25 10:28:15 2021
 ************************************************************************/

#include <iostream>
using namespace std;

class allocator {
private:
    //单向链表的节点
    struct obj {
        struct obj* next; //embedded pointer
    };

public:
    void* allocate(size_t);
    void deallocate(void*, size_t);
private:
    obj* freeStore = nullptr;
    const int CHUNK = 5;
};

void* allocator::allocate(size_t size) {
    obj* p;

    if (!freeStore) {
        //linked list为空,于是申请一大块
        size_t chunk = CHUNK * size;
        freeStore = p = (obj*)malloc(chunk);

        //将分配得来的一大块当做linked list般,小块小块串接起来
        for (int i = 0; i < (CHUNK - 1); ++i) {
            p->next = (obj*)((char*)p + size);
            p = p->next;
        }
        p->next = nullptr; //last
    }
    p = freeStore;
    freeStore = freeStore->next;
    return p;
}

void allocator::deallocate(void* p, size_t) {
    //将 *p 收回插入 free list前端
    ((obj*)p)->next = freeStore;
    freeStore = (obj*)p;
}

class Foo {
public:
    long L;
    string str;
    static allocator myAlloc;
public:
    Foo(long l): L(l) {  }
    static void* operator new(size_t size) { 
        return myAlloc.allocate(size); 
    }
    static void operator delete(void* phead, size_t size) { 
        return myAlloc.deallocate(phead, size); 
    }
};
allocator Foo::myAlloc;

class Goo {
public:
    complex<double> c;
    string str;
    static allocator myAlloc;
public:
    Goo(const complex<double>& x): c(x) {  }
    static void* operator new(size_t size) {
        return myAlloc.allocate(size);
    }
    static void operator delete(void* phead, size_t size) {
        return myAlloc.deallocate(phead, size);
    }
};
allocator Goo::myAlloc;

int main() {
    Foo* p[100];
    cout << "sizeof(Foo) = " << sizeof(Foo) << endl;
    for (int i = 0; i < 23; ++i) {
        p[i] = new Foo(i);
        cout << p[i] << " " << p[i]->L << endl;
    }

    for (int i = 0; i < 23; ++i) {
        delete p[i];
    }
    
    //============
    
    Goo* p[100];
    cout << "sizeof(Goo) = " << sizeof(Goo) << endl;
    for (int i = 0; i < 17; ++i) {
        p[i] = new Goo(complext<double>(i, i));
        cout << p[i] << " " << p[i]->c << endl;
    }

    for (int i = 0; i < 17; ++i) {
        delete p[i];
    }

    return 0;
}


7.2.3 marco for static allocator

因为每个使用allocator的类的几处写法是固定的,于是将它们写成宏:

image-20211124174203054

使用示例与结果:

image-20211124174815564

代码:宏

//DECLARE_POOL_ALLOC -- used in class definition
#define DECLARE_POOL_ALLOC()\
public:\
    void* operator new(size_t size) { return myAlloc.allocate(size); }\
    void operator delete(void* p) { myAlloc.deallocate(p, 0); }\
protected:\
    static allocator myAlloc;

//IMPLEMENT_POOL_ALLOC -- used in class implementation
#define IMPLEMENT_POOL_ALLOC(class_name)\
allocator class_name::myAlloc



Foo

class Foo {
    DECLARE_POOL_ALLOC()
public:
    long L;
    string str;
public:
    Foo(long l): L(l) {  }
};
IMPLEMENT_POOL_ALLOC(Foo)


Goo

class Goo {
    DECLARE_POOL_ALLOC()
public:
    complex<double> c;
    string str;
public:
    Goo(const complex<double>& x): c(x) {  }
};
IMPLEMENT_POOL_ALLOC(Goo)



7.3 global allocator(with multiple free-lists)

image-20211124174853491

是static allocator的进阶版,针对所有的class,而非针对单一的class。不是用static变量的方式使用allocator,而是全局的。

7.4 小结

  • per-class allocator v1:一般
  • per-class allocator v2:加上了embedded pointer
  • static allocator:将内存的动作抽取到了单一的class——allocator中
  • marco for static allocator:设计一个 marco 宏,简化书写
  • global allocator:是static allocator的进阶版,是一个全局的allocator


8 new handler

image-20211124175114746

  • _callnewh就会调用到由用户指定的handler;
  • 如果调用到了用户指定的handler,说明没有内存可用了,就要在这个handler里让更多的内存可用:查看哪些内存可以释放;

例子:

image-20211124175138803

9 =default,=delete

image-20211124175255474
有默认版本的函数才可以被设置为default,C++中有默认版本的函数: 

  • 拷贝构造函数
  • 拷贝赋值函数
  • 析构函数

验证operator new 和 operator delete 是否也能 default 和 delete:

image-20211124180035282

​ 说明:operator new 和 operator delte 都不能设置为default

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值