下面代码只是作为C语言基础复习,您粗浅看一下即可:
#include
using namespace std;
int globalVar = 1;//全局变量,存储在数据段
static int staticGlobalVar = 1;//静态全局变量,存储在数据段
void Test(void)
{
static int staticVar = 1;//静态局部变量,存储在数据段
int localVar = 1;//局部变量,存储在栈
int num1[10] = { 1,2,3,4 };//多个局部变量一次性创建,整体存储在栈
char char2[] = "abcd";//(这里实际上是把常量区得“abcd\0”拷贝给char数组)常量表达式“abcd”,存储在代码段,但是char2数组存储在栈
const char* pChar3 = "abcd";//常量表达式,“abcd”存储在代码段,pChar3存储在栈上
int* ptr1 = (int*)malloc(sizeof(int) * 4);//ptr1存储在栈上,*ptr1存储在堆上
int* ptr2 = (int*)calloc(4, sizeof(int));//ptr2存储在栈上,*ptr2存储在堆上
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);//ptr3存储在栈上,*ptr3存储在堆上
free(ptr1);
free(ptr3);
}
int main()
{
Test();
return 0;
}
#define SIZE 10
int main()
{
int* arr = (int*)malloc(sizeof(int) * SIZE);
if (arr == NULL)
{
exit(-1);
}
for (int i = 0; i < SIZE; i++)
{
arr[i] = i * i;
printf("%d ", arr[i]);
}
printf("\n");
for (int j = 0; j < SIZE; j++)
{
free(arr + j);
}
return 0;
}
C
语言在这方面使用malloc
、calloc
、realloc
、free
来管理堆空间,这一套函数在C++
中依旧可以使用,但是会有很大的问题。
class Data
{
public:
Data(int x = 10)
{
_x = x;
}
private:
int _x;
};
int main()
{
Data* px = (Data*)malloc(sizeof(Data));
//空间是申请出来了,该怎么调用构造函数呢?
//px->_a = 1;//不可以,成员变量是私有的
//px->Data(1);//不可以,构造函数只能由编译器调用(自动),不可以我们手动调用
return 0;
}
于是C++依旧建立了一套新的方案new
和delete
。
首先我们来学会使用new
和delet
,注意:new
和delete
是操作符,而不是函数。
#include
using namespace std;
void Test()
{
//1.动态申请一个int类型的空间
int* ptr1 = new int;
//2.动态申请一个int类型的空间并初始化为10
int* ptr2 = new int(10);
// 动态申请10个int类型的空间
int* ptr3 = new int[10];
for (int i = 0; i < 10; i++)
{
ptr3[i] = i * i;
cout << ptr3[i] << " ";
}
delete ptr1;
delete ptr2;
delete[] ptr3;//注意方括号必须带上
}
int main()
{
Test();
return 0;
}
单个元素使用new
和delete
操作符,多个元素使用new[]
和delete[]
操作符。
另外C++98
不支持进行初始化,但是C++11
支持new
使用()
以及{}
初始化,也就是C++11
后可以这么写:
#include
class Data
{
friend std::ostream& operator<<(std::ostream& out, Data& d);
public:
//1.构造函数
Data(int data = 0)
:_data(data)
{
//std::cout << "Data()" << std::endl;
}
//2.析构函数
~Data()
{
//std::cout << "~Data()" << std::endl;
}
private:
int _data;
};
//2.友元函数:流重载函数
std::ostream& operator<<(std::ostream& out, Data& d)
{
out << d._data;
return out;
}
int main()
{
#define NUMEBER 10
//1.new出单个内置类型
//1.1.初始化方法1
int* p1 = new int(1);
std::cout << "p1 = " << p1 << " " << "*p1 = " << *p1 << std::endl;
//1.2.初始化方法2
int* p2 = new int{ 2 };
std::cout << "p2 = " << p2 << " " << "*p2 = " << *p2 << std::endl;
//1.3.初始化方法3
int* p3 = new int{ (3) };
std::cout << "p3 = " << p3 << " " << "*p3 = " << *p3 << std::endl;
//2.new出内置类型数组
//2.1.初始化方法1
int* parr1 = new int[NUMEBER] { (1), (2), (3), (4), (5) };
std::cout << "parr1 = " << parr1 << std::endl;
for (int i = 0; i < NUMEBER; i++)
{
std::cout << parr1[i] << " ";
}
std::cout << std::endl;
//2.2.初始化方法2
int* parr2 = new int[NUMEBER] { { 1 }, { 2 }, { 3 }, { 4 }, { 5 } };
std::cout << "parr2 = " << parr2 << std::endl;
for (int i = 0; i < NUMEBER; i++)
{
std::cout << parr2[i] << " ";
}
std::cout << std::endl;
//2.3.初始化方法3
int* parr3 = new int[NUMEBER] { {(1)}, { (2) }, { (3) }, { (4) }, { (5) } };
std::cout << "parr3 = " << parr3 << std::endl;
for (int i = 0; i < NUMEBER; i++)
{
std::cout << parr3[i] << " ";
}
std::cout << std::endl;
//2.4.初始化方法4
int* parr4 = new int[NUMEBER] { 1, 2, 3, 4, 5 };
std::cout << "parr4 = " << parr4 << std::endl;
for (int i = 0; i < NUMEBER; i++)
{
std::cout << parr4[i] << " ";
}
std::cout << std::endl;
std::cout << "------------" << std::endl;//内置类型和自定义类型的分隔符
//3.new出单个自定义类型
//3.1.初始化方法1
Data* d1 = new Data(1);
std::cout << "d1 = " << d1 << " " << "*d1 = " << *d1 << std::endl;
//3.2.初始化方法2
Data* d2 = new Data{ 2 };
std::cout << "d2 = " << d2 << " " << "*d2 = " << *d2 << std::endl;
//3.3.初始化方法3
Data* d3 = new Data{ (3) };
std::cout << "d3 = " << d3 << " " << "*d3 = " << *d3 << std::endl;
//4.new出自定义类型数组
//4.1.初始化方法1
Data* darr1 = new Data[NUMEBER] { (1), (2), (3), (4), (5) };
std::cout << "darr1 = " << darr1 << std::endl;
for (int i = 0; i < NUMEBER; i++)
{
std::cout << darr1[i] << " ";
}
std::cout << std::endl;
//4.2.初始化方法2
Data* darr2 = new Data[NUMEBER] { { 1 }, { 2 }, { 3 }, { 4 }, { 5 } };
std::cout << "darr2 = " << darr2 << std::endl;
for (int i = 0; i < NUMEBER; i++)
{
std::cout << darr2[i] << " ";
}
std::cout << std::endl;
//4.3.初始化方法3
Data* darr3 = new Data[NUMEBER] { { (1) }, { (2) } , { (3) }, { (4) }, { (5) } };
std::cout << "darr3 = " << darr3 << std::endl;
for (int i = 0; i < NUMEBER; i++)
{
std::cout << darr3[i] << " ";
}
std::cout << std::endl;
//4.4.初始化方法4
Data* darr4 = new Data[NUMEBER] { 1, 2 , 3 , 4 , 5 };
std::cout << "darr4 = " << darr4 << std::endl;
for (int i = 0; i < NUMEBER; i++)
{
std::cout << darr4[i] << " ";
}
std::cout << std::endl;
//5.释放资源
delete p1;
delete p2;
delete p3;
delete[] parr1;
delete[] parr2;
delete[] parr3;
delete[] parr4;
delete d1;
delete d2;
delete d3;
delete[] darr1;
delete[] darr2;
delete[] darr3;
delete[] darr4;
return 0;
}
C++
不支持扩容 ,没有一个类似realloc
的操作符。
使用new
和delete
更加适应自定义类型:
#include
using namespace std;
void Test()
{
//1.动态申请一个int类型的空间
int* ptr1 = new int;
//2.动态申请一个int类型的空间并初始化为10
int* ptr2 = new int(10);
//3.动态申请10个int类型的空间
int* ptr3 = new int[10] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};//如果没有写满会默认给0,哪怕是只有{}
for (int i = 0; i < 10; i++)
{
cout << ptr3[i] << " ";
}
//4.释放空间
delete ptr1;
delete ptr2;
delete[] ptr3;//注意方括号必须带上
}
int main()
{
Test();
return 0;
}
class Data
{
public:
Data(int x = 10)
{
_x = x;
}
private:
int _x;
};
int main()
{
//Data* px = (Data*)malloc(sizeof(Data));
//空间是申请出来了,该怎么调用构造函数呢?
//px->_a = 1;//不可以,成员变量是私有的
//px->Data(1);//不可以,构造函数只能由编译器调用(自动),不可以我们手动调用
Data* px1 = new Data(1);//申请空间+自动调用构造函数初始化
delete px1;//销毁空间+自动调用析构函数
Data* px2 = new Data[10]{1, 2};//申请空间+自动调用构造函数初始化
delete px2;//销毁空间+自动调用析构函数
Data* px3 = new Data[10]{Data(1), Data(2)};//申请空间+自动调用构造函数初始化
delete px3;//销毁空间+自动调用析构函数
Data d1(1);
Data d2(2);
Data* px4 = new Data[10]{d1, d2};//申请空间+自动调用构造函数初始化
delete px4;//销毁空间+自动调用析构函数
return 0;
}
利用C++
的new
来写一个用来测试的链表简直不要太快:
#include
using namespace std;
class ListNode
{
public:
ListNode(int val = 0)
:_val(val)
,_next(nullptr)
{}
int _val;
ListNode* _next;
};
int main()
{
ListNode* l1 = new ListNode(1);
ListNode* l2 = new ListNode(2);
ListNode* l3 = new ListNode(3);
ListNode* l4 = new ListNode(4);
ListNode* l5 = new ListNode(5);
l1->_next = l2;
l2->_next = l3;
l3->_next = l4;
l4->_next = l5;
delete l1;
delete l2;
delete l3;
delete l4;
delete l5;
return 0;
}
对于内置类型使用这两个操作符来说,和malloc
与free
没有什么本质区别。但是在申请自定义类型大小的空间时,就有一些很大的变化了。
#include
using namespace std;
class Data
{
public:
Data(int data = 0)
:_data(data)
{
cout << "Data()" << endl;
}
~Data()
{
cout << "~Data()" << endl;
}
private:
int _data;
};
int main()
{
Data* dd1 = new Data;//不仅仅是开辟了空间还会调用类的成员函数(这里就调用了构造函数进行初始化)
delete dd1;//释放空间(这里就调用了析构函数清理了对象中的资源)
Data* dd2 = new Data[10];//不仅仅是开辟了空间还会调用类的成员函数(这里就调用了构造函数进行初始化)
delete[] dd2;//释放空间(这里就调用了析构函数清理了对象中的资源)
return 0;
}
调用构造函数和析构函数就是与C语言最大的区别,因此实际上new
和delete
是为了自定义类型而准备的(这也就是为什么在C++98
中一开始没有初始化,这是暗示在使用在自定义类型中需要通过写构造函数来进行初始化,C++98
很有可能觉得没有必要添加初始化这一套)。一定要注意析构函数释放的是对象成员所使用的堆资源,而不是释放对象的空间。
new
和delet
匹配,new[]
和delete[]
匹配。
而new
出来的空间不可以使用free()
释放,malloc()
出来的空间不可以使用delete
释放。
另外对于申请失败的情况,C
与C++
也不一样。malloc()
失败返回空指针,new
失败抛异常。(抛出异常是面向对象语言出错的一种处理方式,相比与C
语言的返回错误码更好一些)
而C++
处理堆错误的方式如下,有点类似go to
的使用:
#include
using namespace std;
int main()
{
try
{
char* p1 = new char[0xffffffff];
char* p2 = new char[0xffffffff];
char* p3 = new char[0xffffffff];
//如果您的电脑足够强大,可以尝试多加几句上述的代码,直到出现调用错误
cout << "hello word" << endl;
}
catch(const exception& e)//如果new失败就会跳转到这里
{
cout << e.what() << endl;//并且这里是输出错误的理由
}
return 0;
}
new
和delete
是用户动态申请内存的释放内存的操作符,而operator new()
与operator delete()
是系统提供的全局函数。
new
在底层调用operator new()
来申请空间,然后在申请的空间上执行构造函数,完成对象的构造。
delete
在底层先执行析构函数,完成对象中资源的清理,调用operator delete()
来释放空间。
注意:这里的
operator
只是函数名字的一部分,不是重载关键字,不要误会了。
您可以看一下new
和delete
调用的汇编代码,内部就调用了这两个全局函数。
可以观察到底层也是使用了malloc()
函数开空间,并且处理了申请异常部分:
/*
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);
}
另外我们也可以像使用malloc()
一样使用operator new()
,但是失败还是抛出异常。
可以观察到底层最终也是通过free()
来释放空间的:
/*
operator delete: 该函数最终是通过free来释放空间的
*/
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;
}
/*free的实现*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
同样这个全局函数也是可以直接使用的。
吐槽:不过也有不少人吐槽这两个全局函数的名字,和运算符重载实在是很像……
实际上这两个函数就是malloc()
和free()
的封装(为了new
和delete
准备的)。因此我们直接使用也是可以的。
#include
using namespace std;
int main()
{
try
{
char* p = (char*)operator new(sizeof(char));
operator delete(p);
}
catch (const exception& e)
{
cout << e.what() << endl;//并且这里是输出错误的理由
}
return 0;
}
在实际使用C++
中,我们一般不直接使用这两个全局函数,包括malloc()
和free()
都很少使用了。
但是有的时候有重载这两个全局函数的需求。
#include
using namespace std;
class Data
{
public:
Data(int data)
:_data(data)
{
cout << "Data()" << endl;
}
~Data()
{
cout << "~Data()" << endl;
}
private:
int _data;
};
void* operator new(size_t size, const char* fileName, const char* funcName, size_t lineNo)
{
void* p = ::operator new(size);
cout << fileName << "-" << funcName << "-" << lineNo << "-" << p << "-" << size << endl;
return p;
}void operator delete(void* p, const char* fileName, const char* funcName, size_t lineNo)
{
cout << fileName << "-" << funcName << "-" << lineNo << "-" << p << endl;
::operator delete(p);
}
int main()
{
int* p1 = new(__FILE__, __FUNCTION__, __LINE__) int;
operator delete(p1, __FILE__, __FUNCTION__, __LINE__);
#ifdef _DEBUG
#define new new(__FILE__, __FUNCTION__, __LINE__)
#define delete(p) operator delete(p, __FILE__, __FUNCTION__, __LINE__)
#endif
int* p2 = new int;
delete(p2);
return 0;
}
这样就可以方便以后定位内存泄露了(不过也挺少这么干的)。
有时候频繁申请内存就会造成内存碎片,下面的代码等以后再回来看看就能明白了:
#include
using namespace std;
class ListNode
{
public:
ListNode(int val)
:_val(val),_next(nullptr)
{}
void* operator new(size_t n)
{
cout << "void* operator new(size_t n)->STL内存池allocator申请" << endl;
//使用内存池和模板
void* obj = alloc.allocate(1);
return obj;
}
void operator delete(void* ptr)
{
cout << "void operator delete(void* ptr)->内存池deallocate释放" << endl;
alloc.deallocate((ListNode*)ptr, 1);
}
private:
int _val;
ListNode* _next;
static allocator<ListNode> alloc;
};
allocator<ListNode> ListNode::alloc;
int main()
{
ListNode* node1 = new ListNode(1);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(3);
delete node1;
delete node2;
delete node3;
}
这样每个类都可以使用自己专属的new,这里了解一下就可以,以后再来认真学习。
另外还有一个operator new[]()
和operator delete[]()
,这里就不再赘述了。
new T[N]
调用operator new[]()
,在operator new[]()
中实际调用operator new()
完成N
个对象的申请,然后在申请的空间上执行N
次构造函数
delete[]
在释放的对象空间上执行N
次析构函数,完成资源清理,然后调用operator delete[]
释放空间,实际在operator delete[]
中调用operator delete()
来释放空间。
因此这里有一个小点需要补充:
#include
using namespace std;
int main()
{
try
{
int* p = new int[10];
//delete p;//方式1
//free(p);//方式2
}
catch (const exception& e)
{
cout << e.what() << endl;//并且这里是输出错误的理由
}
return 0;
}
上述方式1
和方式2
是否会造成内存泄露呢?实际上是不会的,因为delete[]
内部实际上调用的是operator delete[]
,这个函数内部调用的又是operator delete()
因此对于内置类型来说,实际上是没有影响的。
真正会出现问题的是内置类型,因为内置类型会调用构造函数和析构函数,导致出现问题。
//以下是VS2022下实验结果
#include
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
private:
int _a;
};
class B
{
public:
B(int b = 0)
:_b(b)
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
private:
int _b;
};
int main()
{
B* p2 = new B[5];
free(p2);//方式1
delete p2;//方式2
//这里p2在申请空间的时候,会多申请一部分空间(有可能是4字节)存储“5”来表示开辟的空间个数。因此申请的空间有部分是需要指针偏移来销毁的。
//然后再调用delete[]的时候,就可以根据这个“5”来调用5次析构函数。
//但是由于直接使用“free()”和“delete”就无法释放这部分用来表示使用多少次析构函数的空间。
//因此真正的问题是指针错位的问题。
A* p1 = new A[5];
free(p1);//方式1
delete p1;//方式2
//这里p2在申请空间的时候,会多申请一部分空间(有可能是4字节)存储“5”来表示开辟的空间个数。因此申请的空间有部分是需要指针偏移来销毁的。
//然后再调用delete[]的时候,就可以根据这个“5”来调用5次析构函数。
//由于这个类没有析构函数,所以有的编译器会很聪明,知道调用的是默认析构函数,就会直接返回指向标识空间的指针
//因此直接返回的指针没有发生错位,不会出现很大的问题。
return 0;
}
//但是换一个编译器极有可能都会奔溃,所以只要匹配使用即可……
还有的时候如果没有匹配,少调用了析构函数会直接造成内存泄漏。
在我们定义出来的类中,我们可以发现,构造函数不可以被显式调用,而析构函数却可以(因为可以提前释放资源,而不必等系统自动调用)。
那么如果我们一定要显式使用构造函数呢?这就可以使用定位new
来试试了。
定位new
表达式是在已分配的原始内存空间中调用构造函数初始化一个对象,下面是定位new
的使用格式。
new (指向某空间的指针) type;
new (指向某空间的指针) type(初始化列表);
在定位new中支持显式调用构造函数,使得开发者更加得自由:
#include
using namespace std;
class Data
{
public:
Data(int a = 0, int b = 0, int* c = nullptr)
:_a(a)
,_b(b)
{
cout << "Data(int a = 0, int b = 0, int c = 指针)" << " " << this << endl;
c = new int;
_c = c;
*_c = 0;
}
~Data()
{
cout << "~Data()" << this << endl;
delete _c;
}
private:
int _a;
int _b;
int* _c;
};
int main()
{
//1.使用new申请一块指针,指向一块对象空间
Data* pdata1 = new Data;
//2.使用malloc申请一块指针,指向一块对象空间
Data* pdata2 = (Data*)malloc(sizeof(Data));
if (!pdata2) exit(-1);
//但是此时出现了一个问题没有办法代码初始化内部的成员,
//在上述代码中,我们使用new运算符创建了Data类型的对象指针。
//创建对象时,将自动调用Data的构造函数来完成对象的初始化。
//我们不需要显式调用构造函数,编译器会为我们处理。
//pdata1->Data(1, 2, 3);//非法
//pdata2->Data(1, 2, 3);//非法
//pdata1->~Data();//合法
//pdata2->~Data();//合法
//这个时候就可以使用定位new
new(pdata1)Data(1, 2);//对已经分配的原始内存空间中调用构造函数初始化一个对象
new(pdata2)Data(10, 20);//对已经分配的原始内存空间中调用构造函数初始化一个对象
delete pdata1;//释放空间的同时自动调用析构函数
pdata2->~Data();
free(pdata2);//释放空间,但是内部资源需要我们手动调用析构函数
return 0;
}
或者这么使用:
#include
using namespace std;
class Data
{
public:
Data(int a = 0, int b = 0, int* c = nullptr)
:_a(a)
,_b(b)
{
cout << "Data(int a = 0, int b = 0, int c = 指针)" << " " << this << endl;
c = new int;
_c = c;
*_c = 0;
}
~Data()
{
cout << "~Data()" << " " << this << endl;
delete _c;
}
void Print()
{
cout << _a << " " << _b << " " << (void*)_c << endl;
}
private:
int _a;
int _b;
int* _c;
};
int main()
{
Data* d = (Data*)operator new(sizeof(Data));//不会自动调用析构函数
d->Print();
cout << endl;
cout << "手动构造" << endl;
new(d)Data(1, 2);
d->Print();
cout << endl;
cout << "手动析构" << endl;
d->~Data();
cout << endl;
return 0;
}
甚至这里的内存地址指向的空间不一定是堆空间的,也可能是栈空间的。
相当于手动使用了new
的过程,这一操作使得内存申请更加自由,也更容易产生内存泄露。
有时也认为堆空间的效率不高(在频繁使用),因此可以提前在堆空间申请一个很大的空间,作为内存池管理起来,这个时候定位new
就有很大的用处了。
因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new
的定位表达式进行显示调构造函数进行初始化。不过由于我们没有学过内存池,所以我们以后再说……
具体来说,有几个作用:
自定义内存管理:使用定位
new
可以让我们在某个特定的内存位置上构造对象,这对于实现自定义的内存管理策略很有用。比如,如果我们想将对象放置在特定的内存池或缓冲区中,就可以使用定位new
来构造对象。特殊的内存需求:有时候我们可能需要在已经分配的特定内存块上构造对象,例如与硬件设备进行交互时需要使用特定的内存地址。
对象重用:定位
new
可以重新使用已经存在的对象内存,而无需重新分配新的内存。这在一些特殊场景下有效,例如对象池或内存池的实现。
在一些C++
工程师的面试题目中,经常考察的几个区别就是:进程与线程的区别、malloc/free
与new/deleta
的区别、指针和引用的区别、TCP
与UDP
的区别……
其中malloc/free
和new/delete
的区别可以从两个点入手:语法使用区别、本质功能区别、判空与异常
语法定义上:malloc()
和free()
是函数,而new
和delet
是关键字。
语法使用上:malloc()
申请空间时需要计算空间的大小并且传递,new
只需要根据空间的类型,如果是多个对象则在[]
中指定即可。
语法使用上:malooc()
的返回值为void*
,需要强制转化,而new
不需要。
语法使用上:malloc()
申请的空间无法初始化,而new
申请的空间可以初始化。
本质功能:malloc()
只会申请空间,不会进行初始化,new
可以借助对象的构造函数初始化。free()
只会释放空间,delet
在释放对象空间的同时还会调用对象的析构函数,完成资源清理。
判空与异常:malloc()
申请空间失败是返回NULL
,因此需要判空。而new
是抛出异常来被捕获,因此不需要判空。
共同的地方是:都是从堆上申请,并且都需要经过释放。