C++内存管理+模板初尝试:暴风雨前的尝试

1.C/C++的内存分布

  • 栈:非静态局部变量、函数参数、返回值等
  • 内存映射段:一种高效的I/O映射方式,装载一个动态内存库
    • 用户可以通过系统接口创建共享共享内存,做进程间通信
  • 堆:用于程序运行时动态内存分配
  • 数据段(静态区):存储全局数据和静态数据
  • 代码段:可执行的代码/只读常量

2.C语言中动态管理内存的方式

  • malloc/calloc/realloc/free
c 复制代码
void Test()
{
	int* p1 = (int*) malloc(sizeof(int));
	free(p1);
	// 1.malloc/calloc/realloc的区别是什么?
	int* p2 = (int*)calloc(4, sizeof (int));
	int* p3 = (int*)realloc(p2, sizeof(int)*10);
	// 这里需要free(p2)吗?
	free(p3 );
}
  • malloc、calloc、realloc的区别
    • malloc(size):申请一块size字节的连续内存只分配而不初始化,其中的值未定义
    • calloc(numb,size):不仅申请numb个size字节的空间,并且把整块内存初始化未0 ;同时还会检查numb*size是否溢出
      • calloc = malloc + memset(0)
    • realloc(ptr,new_size):把ptr指向的那块动态内存,调整为new_size大小,有三种情况:
      • 原地缩容/扩容
      • 移动到新位置,拷贝旧内容过去
      • 失败返回NULL,原来的ptr仍然有效
  • malloc的实现原理:
    • glibc的malloc本质就是用户态内存分配器,把从系统拿到的堆内存切成chunk,维护元数据,并通过tcache/fastbin/smallbin等机制加速分配,降低锁竞争,减少碎片

    • 大块内存通常还可能走mmap

3.C++内存管理方式

  • 通过new和delete进行动态内存管理

3.1 操作内置类型

cpp 复制代码
void Test()
{
	// 动态申请一个int类型的空间
	int* ptr4 = new int;
	// 动态申请一个int类型的空间并初始化为10
	int* ptr5 = new int(10);
	// 动态申请10个int类型的空间
	int* ptr6 = new int[3];
	delete ptr4;
	delete ptr5;
	delete[] ptr6;
}
  • new和delete,new[]和delete是互相搭配好的,不能随意拆开

3.2 new和delete操作自定义类型

  • new和delete还会额外调用构造函数和析构函数
cpp 复制代码
#include <iostream>
using namespace std;

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}

private:
	int _a;
};


int main()
{
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A;
	free(p1);
	delete p2;

	return 0;
}

4.operator new与operator delete

  • operator new和operator delete都是全局函数
  • operator new通过malloc来申请空间 , 如果申请成功直接返回,如果错误则会抛异常(这个malloc可不行)
  • operator delete通过free来释放空间
cpp 复制代码
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空               
间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// try to allocate size bytes
	void *p;
	while ((p = malloc(size)) == 0)
	
	if (_callnewh(size) == 0)
  	{
		// report no memory
		// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
		static const std::bad_alloc nomem;
		_RAISE(nomem);
 	}
	return (p);
}

void operator delete(void *pUserData)
{
	_CrtMemBlockHeader * pHead;
	RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
	if (pUserData == NULL)
		return;
	_mlock(_HEAP_LOCK);  /* block other threads */
	__TRY
	/* get a pointer to memory block header */
		pHead = pHdr(pUserData);
	/* verify block type */
		_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
		//下面这句就是调用free的代码
		_free_dbg( pUserData, pHead->nBlockUse ); 
	
	__FINALLY
	_munlock(_HEAP_LOCK);  /* release other threads */
	__END_TRY_FINALLY
	return;
}

5. new和delete的实现原理

  • 申请内置类型空间:malloc/free和new/delete没太大区别
    • new/delete申请和释放的是单个元素的空间,而new[]/delete[]申请的是连续的空间,并且申请失败会抛异常
    • malloc则是返回NULL,不会主动抛异常

自定义类型

  • new的底层调用路线:
    • new->operator new -> malloc(底层可能调用) -> 构造
    • 注意:malloc是MSVC运行库中的一种常用实现方式,而不是一定就用malloc
  • delete的底层调用
    • delete->析构->operator delete->free(或者别的)
    • 注意:free是MSVC运行库中的一种常用实现方式
  • new T[N]的底层调用
    • new T[N]-> operator new[] 分配整块原始内存 -> 底层可能复用 operator new -> 再底层可能调用 malloc -> 对 N 个元素逐个构造
    • ②new[] -> operator new[] + n个构造
      • 如果是需要析构的数组对象,运行时通常可能会额外记录元素个数,这样在 delete[] 时就知道需要调用多少次析构函数。
      • 但如果是内置类型或平凡析构类型,通常不需要为了逐个析构而额外记录元素个数。
  • delete []的底层调用
    • ①delete[] -> N次析构 ->operator delete[] -> operator delete -> free
    • ②delete[] -> N次析构+operator delete[]
      • 注:析构的顺序是逆序析构

6. 定位new表达式

  • 在 已经分配的原始内存空间中调用构造函数,初始化一个对象

  • 格式:

    • new(place_address) type
    • new (place_address) type(intializer-list)
  • 一般配合内存池使用,因为内存池中分配的内存一般没有初始化

    • 如果是自定义的对象,一般利用new调用构造函数并初始化
cpp 复制代码
class A
{
public:
	A(int a = 0)
	: _a(a)
	{
	cout << "A():" << this << endl;
	}
	~A()
	{
	}
	cout << "~A():" << this << endl;
private:
	int _a;
};

// 定位new/replacement new
int main()
{
	// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没
	有执行
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
	p1->~A();
	free(p1);
	
	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A(10);
	p2->~A();
	operator delete(p2);
	return 0;
}

内存池

  • 正常情况下,如果频繁的创建和销毁对象,例如:
    • 游戏里的子弹对象
    • 网络服务器里的连接对象
    • 高性能程序里的小对象
  • 每次都走new/delete,就会带来几个问题:
    • 开销大:频繁申请内存所携带的成本
    • 容易产生碎片
    • 性能不稳定
  • 所以,当一次性申请很多内存时,可以自己管理,但如果后面对象要空间时,直接从内存池拿就行

内存池和系统堆

  • 系统堆 = 城市里的公共停车位
  • 内存池 = 自己租下来的停车场
cpp 复制代码
char* pool = (char*)malloc(1024 * 1024);
  • 这里的1MB就是池子
    • 以后创建对象A,就不再new A
    • 而是在这1MB里找一块足够大的位置给A
  • 内存池给的也只是纯内存,但是其中没有真正构造出一个A对象,所以,你就可以用placement new
cpp 复制代码
void* mem = pool_alloc(sizeof(A));
A* p = new(mem) A();
在mem这块已经分配好内存的空间上直接调用构造函数,把对象A放进去
  • 内存池和placement new,二者经常配合使用

7.常见面试题

  • malloc/free和new/delete的区别:

    • malloc和free是函数,new和delete是操作符
    • malloc申请的空间不会初始化,new可以初始化
    • malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,
      如果是多个对象,[]中指定对象个数即可
    • malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
    • malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需
      要捕获异常
    • 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new
      在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成
      空间中资源的清理
  • 内存泄漏

    • 什么是内存泄漏:指因为疏忽或者错误,导致程序未能释放已经不再使用的内存
    • 内存泄漏的危害:长期运行的程序出现内存泄漏,会导致响应越来越慢,最终卡死
    • 内存泄漏的分类:
      • 堆内存泄漏
      • 系统资源泄漏:套接字、文件描述符、管道等这些系统分配的资源没有用对应的函数释放掉
    • 内存泄漏的检测:一般用第三方工具
  • 如何避免内存泄漏:

    • 良好的工程设计规范,以及良好的代码习惯
    • RAII思想或者只能指针管理
    • 部分公司自带内部实现的私有内存管理库,自带内存泄漏检测的功能选项
    • 使用内存泄漏工具(不太靠谱)

8.函数模板

  • 函数模板与类型无关,使用时被参数化,根据实参类型产生函数的特定版本

函数模板格式

cpp 复制代码
template<typename T1,typename T2,......,typename Tn>
返回值类型 函数名{参数列表){}
  • typename是用来定义模板参数关键字,也可以用class
cpp 复制代码
template<typename T>
void Swap( T& left,  T& right)
{
	T temp = left;
	left = right;
	right = temp;
}

函数模板的原理

  • 函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器
  • 编译器阶段,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用
  • 比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然
    后产生一份专门处理double类型的代码

函数模板的实例化

①隐式实例化:让编译器根据实参推演模板参数的实际类型

cpp 复制代码
template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}
 
int main()
{
    int a1 = 10, a2 = 20;
    double d1 = 10.0, d2 = 20.0;
    Add(a1, a2);
    Add(d1, d2);
    return 0;
}

②显式实例化:在函数名后的<>中指定模板参数的实际类型

cpp 复制代码
int main(void)
{
	int a = 10;
	int b = 20.0;
	
	//显式实例化
	Add<int>(a,b);
	return 0;
}

模板参数的匹配原则

①一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

cpp 复制代码
// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}
void Test()
{
	Add(1, 2);
	Add<int>(1,2);     
}

②对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从模板中产生初一个实例(如果模板可以产生一个更匹配的函数,那就选择模板)

cpp 复制代码
// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
	return left + right;
}
void Test()
{
	Add(1, 2);     // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0);   // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}

③模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

9.类模板

cpp 复制代码
template<class T1, class T2, ..., class Tn> 
class 类模板名
{
	// 类内成员定义
};
cpp 复制代码
// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{ 
public :
	Vector(size_t capacity = 10)
        : _pData(new T[capacity])
        , _size(0)
        , _capacity(capacity)
    {}
// 使用析构函数演示:在类中声明,在类外定义。
	~Vector();
	void PushBack(const T& data);
	void PopBack();
	// ...
	size_t Size() {return _size;}
	T& operator[](size_t pos)
    {
        assert(pos < _size);
        return _pData[pos];
    }
    
private:
    T* _pData;
    size_t _size;
    size_t _capacity;
};

//注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
    if(_pData)
        delete[] _pData;
    _size = _capacity = 0;
}
  • 类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类
cpp 复制代码
// Vector类名,Vector<int>才是类型
 Vector<int> s1;
 Vector<double> s2;
相关推荐
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 79. 单词搜索 | C++ 标准方向数组 DFS 与回溯
c++·leetcode·深度优先
沐雪轻挽萤2 小时前
4. C++17新特性-内联变量 (Inline Variables)
开发语言·c++
玖釉-3 小时前
深入解析 meshoptimizer:基于 meshopt_spatialClusterPoints 的空间聚类与 Mesh Shader 前置优化
c++·windows·图形渲染·聚类
biter down3 小时前
STL list
开发语言·c++
wenhaoran113 小时前
CF1800F Dasha and Nightmares
c++·算法·字符串·codeforces·位运算
极客智造3 小时前
深入理解 C++ 友元机制:语法、特性与工程实践
c++
郭涤生3 小时前
C++ 标准库中性能较高的函数总结复习
c++
小此方3 小时前
Re:思考·重建·记录 现代C++ C++11篇 (三) 深度解构:可变参数模板、类功能演进与 STL 的新版图
开发语言·c++·stl·c++11·现代c++
2401_892070983 小时前
八大排序算法
数据结构·c++·排序算法