注意的是:
在之前C语言中,我们一般用malloc函数进行动态内存开辟。
一般都有的步骤是 调用函数动态开辟、判断返回值、释放空间。
int* a = (int*)malloc(sizeof(int));
if(a == NULL)
{
perror("malloc fail");
exit(-1);
}
free(a);
C中的内存管理在C++中也能继续使用。
C++规定通过一个new操作符来动态开辟空间,通过delete操作符来释放空间。
如
//动态申请一块int大小的空间
int* p1 = new int;
//动态申请一块int大小的空间并初始化
int* p2 = new int(10);
//动态申请多块int大小的空间并初始化
int* p3 = new int[10]{0};
//释放空间
delete p1;
delete p2;
//对多块空间释放,需要加[],[]内可以不写具体数字
delete[] p3;
注意new和delete操作符匹配,new[]和delete[] 操作符匹配。
那C++的动态开辟和C语言的动态开辟有什么区别呢?
下面我们继续探讨
先弄一个类A
#include
using std::cout;
using std::endl;
class A
{
public:
A(int a = 1)
:_a(a)
{
cout << "A(int a = 1)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A* a1 = (A*)malloc(sizeof(A));
free(a1);
cout << "-----" << endl;
A* p1 = new A(1);
delete p1;
return 0;
}
首先可以明显的看到,new/delete和malloc/free的一个很大区别就是,对于类类型new/delete会调用构造函数和析构函数。
实际上:
new的底层实现
下面是官方实现的operator new函数,从中大概来看,其实new的底层就是malloc,只是多个当申请失败会抛异常(这里先知道会抛异常,不用知道是什么)。
这里值得注意的是,operator new只是一个库里面实现的全局函数,并不是new的操作符重载,因为参数里面没有自定义类型。(这是一个对新手的误区,以为是操作符重载)
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
void *p;
while ((p = malloc(size)) == 0) //malloc返回null就进入循环
if (_callnewh(size) == 0)
{
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
从底层代码我们可以了解new开辟失败返回异常,我们再来看看new开辟失败p1还会像malloc一样返回null指针吗。
#include
using namespace std;
void Test1()
{
while (1)
{
//循环每次开辟空间 看看当开辟失败的时候p1返回是否为空
char* p1 = new char[1024*1024*1024];
if (p1 == nullptr)
{
perror("malloc fail");
exit(-1);
}
}
}
int main()
{
try
{
Test1();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
从输出结果来看,并不会返回空指针,并且只抛异常输出异常结果。(这里我们先不讨论异常以后会谈滴)
delete底层实现
这里我们只需要知道,delete的底层代码调用了free()函数来释放空间。(其实发现在delete的时候还有互斥锁)
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
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_dbg( pUserData, pHead->nBlockUse );
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
总结:
对于内置类型
new/delete和malloc/free基本一致,不同的是new开辟空间失败会抛异常,而malloc开辟失败返回NULL指针。
对于自定义类型
我们现在已经知道了
new的实现原理
1.调用operator new 函数申请空间
2.在申请的空间上执行构造函数
delete的实现原理
1.在空间上执行析构函数,完成对象中资源的清理工作
2.调用operator delete函数释放空间。
那么new[] 和 delete[] 的原理是什么样的?
new[] 的实现原理
1.调用operator new[] 函数,实际调用operator new函数完成N个对象空间的申请,而operator new 实际也是用malloc完成空间申请。
2.在申请的空间上执行多次构造函数
delete[] 的实现原理
1.调用operator delete[] 函数,实际调用operator delete函数完成空间的释放,而operator delete 实际也是通过free完成释放。
2.先会多次调用析构函数,完成多个对象空间的资源清理
new是先开空间,再调用构造函数初始化。
定位new是在已分配原始内存空间中调用构造函数初始化一个对象。
使用方式:
new(place_address)type 或者 new(place_address)type(initializer-list)
place_address是一个指针,initializer-list是类型初始化列表。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p1 = (A*)malloc(sizeof(A));
if (p1 == nullptr)
{
perror("malloc fail");
exit(-1);
}
new(p1)A(1);
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(1);
p2->~A();
operator delete(p2);
return 0;
}
定位new在实际中一般是配合内存池进行使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
接下来我们就可以总结一下malloc和new的区别了
当我们面对同一个函数需要处理多个类型的情况时,函数模板可以给我们提供很大的便利。
函数模板的格式
template
返回值类型 函数名(参数列表){}
typename是用来定义模板参数关键字,也可以使用class。
用法:
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1, b = 2;
Swap(a, b);
double c = 1.1, d = 2.2;
Swap(c, d);
cout << a << " " << b << endl;
cout << c << " " << d << endl;
return 0;
}
值得注意的是,上面Swap函数是一个模板(一个蓝图),本身并没有实例化,只有当我们调用的时候编译器会根据参数类型自动推演实例化一个函数。
我们知道编译器会根据参数类型进行推演,那么当参数类型不同会发生什么呢?
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
//自动推演
//cout << Add(a1, d1) << endl; //编译报错
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
//强转可以运行,不过强转生成临时变量具有常性,函数需要加const
cout << Add((double)a1, d2) << endl;
cout << Add(a1, (int)d2) << endl;
//显示实例化
//这种写法可以指定类型
//a1 到double类型 有隐式类型转换会产生临时变量具有常性 需要加const
cout << Add<double>(a1, d2) << endl;
cout << Add<int>(a1, d2) << endl;
return 0;
}
参数不一致第一种处理方法就是强转或者指定类型
//这样写两个不同类型就能直接相加
template<class T1, typename T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
cout << Add(a1, d2) << endl;
cout << Add(d1, a2) << endl;
return 0;
}
参数不一致第二种处理方法就是添加两个类型
当我们写了的函数能用的话,编译器也不会再生成,只有指定调通用的才会推演生成。
int Add(int left, int right)
{
return left + right;
}
template<class T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
int a = 1, b = 2;
Add(a, b);//调专门的不调通用的
Add<int>(a, b); //要调通用的编译器自己推演
return 0;
}
类模板的定义格式:
template
class 类模板名
{
// 类内成员定义
};
例子:
template<typename T>
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = )" <<capacity<<endl;
_a = (T*)malloc(sizeof(T)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(const T& x)
{
// ....
// 扩容
_a[_top++] = x;
}
private:
T* _a;
int _top;
int _capacity;
};
int main()
{
//类型是Stack st1使得类实例化
Stack<double> st1;
st1.Push(1.1);
//类型是Stack st2使得类实例化
Stack<int> st2;
st2.Push(1);
return 0;
}
类模板不是真正的类,只有当实例化后才是类。
由于类的实例化一开始不会传参,所以类模板没有推演时机。
虽然是同一个类模板实例化的,但是参数不同,类型不同,大小不一样。
并且类模板中成员函数的定义与声明最好写在同一文件中
如果定义与声明分离,由于类模板未实例化问题,测试页对头文件(函数声明)进行了实例化,但是没有对函数定义进行实例化,导致无法放入符号表里。
最后在链接会发生链接错误。
如果一定要声明与定义分离,需要在每个包含类模板的文件实例化(这就很麻烦,所以尽量避免声明与定义分离)。
头文件中实例化写法
//stack类实例化
template
class stack<int>;
本章完~