c++内存管理与模板初阶


虚拟进程地址空间

c/c++的一个程序在内存区域中的划分:

由低地址到高地址依次是:代码区->已初始化全局数据区->未初始化全局数据区->堆区->栈区。其中堆区和栈区是相对而生的,堆区和栈区之间有很大的镂空,堆区是向高地址增长,栈区是向低地址增长,栈区是先使用高地址,在使用低地址,在函数栈帧的内部也一样,先使用高地址,在使用低地址。而堆区是先使用地址,在使用高地址。
在这里插入图片描述

这些区域不是指的内存,指的是虚拟进程地址空间。

区域

  1. 全局变量存放在全局区,全局变量在main函数调用之前,就已经创建好了。
  2. 常量字符串放在常量区,对于相同的常量字符串,在常量区只需要有一份即可,常量区数据由硬件保护,不能通过任何方式更改。
  3. 堆区的内存是动态开辟的,需要手动释放,否则会出现内存泄漏

new和delete

new和delete是c++中的操作符,不是函数。

new对于内置类型的数据,只开空间,不初始化。

new int;
new int[4];//new数组在类型后面加[]

new默认对于内置类型的数据,只开空间,不初始化,但是可以自己手动初始化。

new int(2);
new int[5]{1,2,3};//c++98不支持对new的数组初始化,c++11支持。

c++11中,new int[5]{1,2,3}后面没给的默认是0。c++11中new []可以使用{}进行初始化。如果new int的话,释放空间使用delete,如果new一个数组,释放空间使用delete[]。对于内置类型,new/delete与malloc/free没有区别。注意在c++中不支持扩容,没有类似于c语言的realloc函数,c++中如果发现new的空间小了需要扩容,必须自己在开一块空间,将旧空间的内容拷贝到新空间(memcpy),然后释放旧空间。

new/delete和malloc/free的区别在于new对于自定义类型会调用它的构造函数,但是malloc对于自定义类型不会。delete和free都检查了空指针的情况,如果传入的指针是空指针,delete和free是不会处理的

Date* p1=new Date;//会调用默认构造
Date* p2=new Date(2022,1,1);//调用相应的构造函数
Date* p3=(Date*)malloc(sizeof(Date));//不会调用构造函数,p3指向的对象的成员变量是随机值

对于自定义类型malloc只会在堆区开空间,不会调用构造函数。new对于自定义类型:开空间+初始化。free对于自定义类型不会调用它的析构函数,而delete对于自定义类型会调用它的析构函数。new和delete就是专门为类类型准备的。

Date* p=new Date[5];//创建5个对象,并且调用它们的默认构造函数
delete[] p;//将创建的5个对象销毁,并且调用它们的析构函数

new在堆区开辟数组的时候,地址是连续的。new出来的对象,先构造的后析构,后构造的先析构,堆区的内存是从低地址到高地址使用的。

如果new的那个类没有默认构造函数,需要显示调用它的别的构造函数。

class A
{
public:
	A(int a,int b):_a(a),_b(b){}
private:
	int _a;
	int _b;
};
int main()
{
	A* p1 = new A[5]{ {1,1},{2,2},{3,3},{4,4},{5,5} };
    A* p2=new A(1,9);
	delete[] p1;
    delete p2;
	return 0;
}

上面的A类型在new的时候也可以使用匿名对象进行初始化,使用匿名对象进行初始化是属于构造+拷贝构造->直接构造。

new A[2]{A(1,1),A(2,2)};//使用匿名对象初始化

在linux下指定g++采用c++11的标准

g++ test.cpp -o mybin -std=c++11

new的失败机制

malloc如果失败的话返回NULL,因此malloc函数需要检查返回值,而new失败的话是抛异常,即new不需要检查返回值,异常的机制是面向对象出错处理的一种方式。

try
{
    while(1)
    {
        char* p=new char[1000000000];
        printf("%p",p);//注意,cout无法以指针打印char*
    }
}
catch(const exception& e)
{
    cout<<e.what()<<endl;
}

抛异常的机制是如果try里面的语句执行失败直接跳到catch捕获异常并输出。

new和malloc向堆区申请空间时申请的是虚拟内存,不是真实的物理内存。

重新理解构造函数:类的构造函数一般情况下只有2种情况会调用,1是在对象定义的时候自动调用,2是new的时候调用构造函数。还有一种特殊的方法是通过定位new显示调用构造函数。

new/delete原理

new操作符对于自定义类型在汇编层面做了2件事:

  1. call operator new,即调用operator new这个函数,这个函数是专门向堆区申请空间的。operator new是一个函数的函数名,不是运算符重载。
  2. call 构造函数,即调用类类型的构造函数。

即new类类型分为2个过程,一是调用operator new函数在堆区开空间,二是调用构造函数。而operator new这个函数内部还是使用c语言的malloc函数向堆区申请空间的,不过operator new函数在调用malloc函数的时候,加入了抛异常的机制,如果malloc申请空间失败,operator new直接抛异常。

void* operator new(size_t size)
{
    调用malloc函数在堆区申请空间;
    成功返回堆区开辟的空间的起始地址;
    如果检测到malloc函数返回NULL,调用异常的处理机制;
}

实际上,也可以直接使用operator new来在堆区开辟空间,失败抛异常,但是一般不这样做。

int main()
{
    char* p = (char*)operator new(1000);
    operator delete(p);
    return 0;
}

对于delete操作符,它的作用是释放在堆区申请的空间,delete操作符底层是在调用operator delete和调用类类型对象的析构函数。而operator delete又是c语言free函数的封装。

一般不直接用operator new和operator delete,它们是std下的全局函数,直接使用new/delete就好。

重载operator new和operator delete

  1. operator new和operator delete可以被重载,自己可以重载operator new和operator delete检测内存泄漏。
  2. operator new和operator delete可以配合内存池使用,这样就能避免频繁向操作系统申请内存。
  3. 可以为每一个类写一个专属的operator new和operator delete,并且配合内存池的池化技术。当为一个类写它的operator new和operator delete时,在使用new去new这种类类型的对象时,就会调用这个类里面的operator new和调用这个类的构造函数,在使用delete时,就会调用这个类里面的operator delete和这个类的析构函数。
  4. 如果想要提高效率,可以在类里面写一个内存池,并且在类里面自己写operator new和operator delete,在new这个类类型的对象时,调用类里面的operator new直接在内存池申请空间,不用调用全局的operator new了,提高效率。全局的operator new是直接去找操作系统要堆区的空间,而自己可以写一个operator new不找操作系统要,直接找内存池要,效率更高。

new []/delete[]

new []的底层是调用operator new[]加上调用n次构造函数。delete[]的底层是调用operator delete[]加上调用n次析构函数。operator new[]又是调用n次operator new,operator delete[]又是调用n次operator delete.

定位new

对已经存在的空间显示调用构造函数初始化使用定位new.

A* p1=(A*)malloc(sizeof(A));//malloc不会调用构造函数
new(p1)A(10);//显示调用构造函数的写法

定位new经常使用的场景是内存池,因为内存池是向操作系统申请的已经有的空间,而对已经有的空间显示调用构造函数初始化使用定位new.

new多维数组

int main()
{
	auto dp = new int[5][3]{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}};
	cout << typeid(dp).name() << endl;//int (* __ptr64)[3]
	return 0;
}

所有数组都是一维数组,第一个[]里面的数字表示数组的元素个数,除去第一个[],剩下的就是这个一维数组中每一个元素的元素类型。int [5][3]表示数组中有5个元素,每一个元素是int [3]

int(*dp)[3][6]=new int[5][3][6];

new数组的话,[]里面的值必须是常量。

const int m=1,n=2;
int(*dp)[n]=new int[m][n];

模板

c++提供了模板的技术,产生了泛型编程的概念。模板分为函数模板和类模板,使用模板的关键字为typename或者class,注意struct不行。

函数模板:在调用一个用函数模板时,需要经过模板参数推演和实例化2个过程,模板参数推演要求能够推出来模板T具体是哪一种类型,实例化就是根据推出来的T的类型,实例化出一个函数。调用函数模板,模板参数推演出来不同的类型,编译器看到的就是不同的函数,在进行调用的时候call的地址也不同。

在这里插入图片描述

模板不支持隐式类型转化,模板参数推演要求对于一个T只能是同一种类型。注意强转是影响模板的参数推演的。

template<typename T>
T Add(T a,T b)
{
    return a+b;
}
Add(1,2);//可以,推出来T是int
Add(1,3.14);//不可以,推出来T是不同类型
Add(1,(int)3.14);//可以,产生临时拷贝,T推出来是int

这种加了const的报错的原因就是模板不支持隐式类型转化

template<class T>
T Add(const T& a, const T& c)
{
	return a + c;
}
int main()
{
	Add(3, 3.13);//会报错,如果加了强转的话就不会,强转的话就能推出T的具体类型
	return 0;
}

一般不干扰模板参数推演,让编译器去推这个模板,这种模板实例化的过程叫做隐式实例化。可以使用显示实例化的方式指定模板T的类型,不要让编译器去推模板的类型。

Add<int>(1,3.14);
Add<double>(2,3.14);

类模板必须进行显示实例化,模板函数或者模板类,必须能够知道模板时什么类型才能使用,不管是编译器推导出来的还是自己显示实例化的,一定要在使用时能确定模板类型。这也是为什么类模板必须显示实例化的原因。

template<class T>
T* Func()
{
    return new T[5];
}
//这种就必须显示实例化

模板函数可以和同名的非模板函数同时存在,再调用时,优先考虑非模板函数

int Add(int a,int b)
{
	return a+b;
}
template<class T>
T Add(const T& a,const T& b)//这里使用引用是考虑自定义类型
{
    return a+b;
}
Add(1,2);//调用非模板函数
Add(3.14,3.14);//调用模板函数

不建议写下面这种方式的模板,虽然这种用法不会报错

template<class T>
T Add(T a,T b)
{
    return a+b;
}
template<class T1,class T2>
T1 Add(T1 a,T2 b)
{
    return a+b;
}

类模板都使用显示实例化的方式,而且类模板只能显示实例化。

vector<int> vi;
vector<char> vc;
//虽然是用的同一个类模板,但是实际上使用的是不同的类类型。vector<int>和vector<char>是2个类型

在类模板和函数模板中,如果需要向堆区申请空间尽量使用new,不要使用malloc,因为模板T可能是自定义类型,malloc对于自定义类型不会调用它的构造函数。对于类模板不支持声明和定义分离,如果要声明和定义分离,必须在一个文件中。类模板和函数模板都不支持在2个文件中声明和定义分离,要声明和定义分离只能在一个文件中。

在一个文件中声明和定义分离应该这样写:

stack.hpp

template<class T>
class stack
{
public:
    void push(const T& x);
private:
    int _size;
    int _top;
    T* a;
};
//模板在一个文件中声明和定义分离的写法
template<class T>
void stack<T>::push(const T& x)
{
    //.........
}

模板可以给缺省的参数类型:

template<class T=int>
class stack{};
stack<> s;//在实例化对象的时候<>也要加上,默认T是int,也可以显示给 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值