前言:
本章将介绍C
目录
1. C/C++内存管理
1. 1 C语言的内存管理回顾:
在我们之前学C语言的过程中,已经接触过了动态内存管理,我们当时用的是使用C语言的方式。
- malloc
void * malloc (size_t size) - 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针,空间是未经初始化的。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
同时配合着free函数一起使用,申请 — 释放空间
void free (void * ptr);
-
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
-
如果参数 ptr 是NULL指针,则函数什么事都不做。
-
calloc
void * calloc (size_t num, size_t size); -
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
-
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
-
ralloc
void * realloc (void ptr, size_t size); -
ptr 是要调整的内存地址
-
size 调整之后新大小
-
返回值为调整之后的内存起始位置。
-
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间.
情况1:原有空间之后有足够大的空间,就地扩容。
情况2:原有空间之后没有足够大的空间,异地扩容,需要改变ptr指针。
1. 2 C++的内存管理:
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
1.new 和 malloc的区别:
- 对于内置类型而言,用malloc和new,除了用法不同,没有本质区别
- 它们区别在于自定义类型
- malloc只开空间,new开空间 + 调用构造函数初始化
- delete 调用析构函数清理资源+释放空间
图解:
正确使用的代码如下:
struct ListNode
{
ListNode* _next;
int _val;
ListNode(int val = 0)
:_next(nullptr)
,_val(val)
{}
};
ListNode* BuyListNode(int x)
{
struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
assert(node);
node->_next = NULL;
node->_val = x;
return node;
}
int main()
{
int* p1 = new int;//一个对象
int* p2 = new int[10];//多个对象
int* p3 = new int(3);//new一个int对象,初始化成10
//int* p4 = new int[10](10); - 不能这样,是错的
int* p4 = new int[10]{ 10 };//new10个int对象,初始化成{}中的值
//C++11支持的语法 - 初始化的是第一个
delete p1;
delete[] p2;
//释放的时候要匹配
//不匹配不一定会内存泄漏,但是有可能会崩溃
//建议一定要匹配
delete p3;
delete[] p4;
//BuyListNode是开空间加初始化
struct ListNode* n1 = BuyListNode(1);
//new是
ListNode* n2 = new ListNode(2);//会去调用该类的构造函数
return 0;
}
释放的时候要匹配,不然有可能出问题。
2.new 和 delete 的特点:
C++内存管理和C语言中内存管理的区别:不在于内置类型,而是 在于自定义类型。
malloc/free 和 new/delete 的区别在于:
答:malloc/free是函数,new/delete是关键字
在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。new 函数没有原地扩容的功能,如果要扩容,只能重新New来申请。
代码实现:
class Stack
{
public:
Stack(int capacity = 10)
{
cout << "Stack(int capacity = 10)" << endl;
_a = new int[capacity];
_capacity = capacity;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[]_a;
_capacity = 0;
_top = 0;
}
void Push(int x)
{}
private:
int* _a;
int* _top;
int _capacity;
};
int main()
{
//Stack st;
Stack* ps1 = (Stack*)malloc(sizeof(Stack));
%%没办法进行初始化,因为成员变量是私有化的。
assert(ps1);
Stack* ps2 = new Stack;//开空间+调用构造函数初始化
free(ps1);
delete ps2;//调用析构函数清理资源+释放空间
return 0;
}
注意:
- ps1和ps2是两个指针,指向一段动态开辟的空间
- new会开空间进行初始化,调用构造函数初始化
- ps1都不好初始化,因为类中的成员变量是私有的
1.3 C++开空间失败了的情况:
- 在我们之前学的C语言中,我们知道,当malloc开辟空间失败了之后,会返回一个空指针,所以用malloc之后,我们要对返回的指针进行判空。
- 但是C++中的new是不需要判空的,在其开辟失败的时候会抛异常
1.4 operator new 和 operator delete:
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数。
new在底层调用operator new全局函数来申请空间,delete在底层通过,operator delete全局函数来释放空间。
- operator new 是封装了malloc,malloc失败了就抛异常。
- operator delete 也进行了封装抛异常检查等,最终调用了_free_dbg,而C语言的free其实一个宏函数,它也是调用了_free_dbg,所以也能理解成operator delete 封装了free。
注意:可以重载全局的**operator new (operator delete)**的行为来控制new 和delete的行为
int main()
{
//跟malloc功能一样,但是失败以后抛异常
Stack* ps2 = (Stack*)operator new(sizeof(Stack));
operator delete(ps2);
Stack* ps1 = (Stack*)malloc(sizeof(Stack));
assert(ps1);
free(ps1);
Stack* ps3 = new Stack;
//call operator new
//call Stack构造函数
//面向对象编程不再用返回值的方式来处理,它们更喜欢抛异常
return 0;
}
1.5 重载operator new与operator delete
一般情况下不需要对 operator new 和 operator delete进行重载,除非在申请和释放空间时候有某些特殊的需求。
比如:在使用new和delete申请和释放空间时,打印一些日志信息,可以简单帮助用户来检测是否存在内存泄漏。
也可以重载一个类专属的operator new (必须是Static),当使用类new时,会有限匹配类自己专属的Operator new.同时也需要注意,这里的operator new和 operator delete
函数是静态的,要加上static。因为在new一个对象的时候我们手上是没有对象的,也就是说无法通过对象调用一般的函数。加上static就可以不通过对象而直接调用这两个函数了。这是一个细节
-
new的底层原理是调用operator new 和构造函数。
-
new T[N]的原理:
1.调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请。
2.在申请的空间上执行N次构造函数。
- delete[]的原理:
1.在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
2.调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。
1.6 定位new:(了解)(常用于给内存池申请出来的空间初始化)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
- new (place_address) type或者new (place_address) type(initializer-list)
- place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
代码示例:
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没
有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
free(p1);
}
内存泄漏指的是:指针丢了,不是内存丢了。
普通的内存泄露不怕,进程只要正常结束的,申请的内存会还给操作系统。
6 内存空间分布
2. 模板
2.1 模板的引入
我们如何实现一个通用的交换函数?
- 经过我们之前的学习,我们知道可以使用函数重载。
使用函数重载虽然可以实现,但是有一下几个不好的地方:
1.重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
2.代码的可维护性比较低,一个出错可能所有的重载均出错
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
模板的处理是在编译阶段处理的。
- C++提出的编程思想叫泛型编程,不再是针对某种类型,能适应广泛类型,跟具体类型无关的代码
- 而泛型编程所用的东西叫做 — 模板
2.2 函数模板
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
1. 函数模板格式
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){ }
注意:typename是用来定义模板参数关键字,也可以使用class
代码示例:
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);
**底层原理:**它们调用的肯定不是同一个函数。虽然它们看起是来调用了同一个函数(这是编译器优化的结果),但是它们调用的是模板自动生成的相应函数。
2. 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化
1.隐式实例化:让编译器根据实参推演模板参数的实际类型2.显式实例化:在函数名后的<>中指定模板参数的实际类型,如:// 显式实例化
Add< int >(a, b);
3. 模板参数的匹配原则
1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数
2.3 类模板
1.类模板的定义格式
template<class T1, class T2, …, class Tn>
class 类模板名 { // 类内成员定义 };
代码示例:
// 动态顺序表
// 注意: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;
}
2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类
类模板实例化,只能显示实例化。
// Vector是类模板名称,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
注意事项:
模板不支持分离编译,声明和定义需要放在同一个文件中,.hpp中
尾声
看到这里,相信大家对这个C++有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦