目录
- 1. C/C++中各种资源的内存分布
- 1.1 C/C++程序内存区域划分
- 1.2 各资源的内存分布情况(练习)
- 2. C++中的动态内存管理方式
- 2.1 new/delete开辟内置类型空间
- 2.2 new/delete开辟销毁自定义类型空间
- 3. operator new 与 operator delete函数
- 4. new与delete的实现原理
- 5. 定位new表达式与池化计数
- 6. malloc/free与new/delete的异同
1. C/C++中各种资源的内存分布
1.1 C/C++程序内存区域划分
正在执行的程序是在计算机的内存空间上运行的,C/C++为了程序的高效运行,将内存划分了多个区域来进行对不同特性种类资源的区别管理。
1.2 各资源的内存分布情况(练习)
//全局变量,数据段
int globalVar = 1;
//静态全局变量,数据段
static int staticGlobalVar = 1;
void Test()
{
//静态变量,数据段
static int staticVar = 1;
//局部变量,栈
int localVar = 1;
//局部变量,数组,栈
//sizeof(num1),代表整个数组,40字节
int num1[10] = { 1, 2, 3, 4 };
//局部变量,字符数组,栈
//sizeof(char2),代表整个数组,5字节
//strlen(char2),字符串长度,4
char char2[] = "abcd";
//sizeof(pChar3),指针,4/8字节
//strlen(pChar3),字符串长度,4
//只读字符串,代码段
const char* pChar3 = "abcd";
//动态开辟空间,堆
//ptr1指针,4/8字节
int* ptr1 = (int*)malloc(sizeof(int) * 4);
//calloc会用0进行申请空间的初始化
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
2. C++中的动态内存管理方式
- 在编写程序时,我们时常需要一段可以动态增长我们可以控制其开辟与销毁的空间,C语言中我们学习过在内存中动态开辟空间的方式,我们通过malloc与free来进行空间开辟与销毁。
- 而C++中有新的动态开辟空间的方法,new与delete,它们在使用上更加方便,且可以应用于更广泛的场景,接下来,就让我们来进行对其的学习与使用。
2.1 new/delete开辟内置类型空间
new:
//开辟一个指定类型大小的空间
int* ptr1 = new int;
//为开辟的空间赋于指值
int* ptr2 = new int(10);
//开辟连续n个指定类型大小的空间
int* ptr3 = new int[10];
//连续空间的初始化赋值
int* ptr4 = new int[3]{1,2,3};
delete:
//释放大小为1个指定类型大小的空间
delete ptr1;
//释放大小为多个指定类型空间大小的空间
delete[] ptr3
注: new/delete 与 new[]/delete[] 必须配合使用,不能混用
2.2 new/delete开辟销毁自定义类型空间
- new/delete开辟自定义类型的动态空间,会自动调用自定义类型的构造与析构函数
class A
{
private:
int _a;
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
A(const A& tmp)
{
_a = tmp._a;
cout << "A(const A&)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
A* pa1 = new A;
delete pa1;
A* pa2 = new A[3];
delete[] pa2;
return 0;
}
- 开辟自定义类型空间的初始化方式
A aa1;
A aa2;
A aa3;
//一段空间
//方法1:(用存在的对象)
A* pa1 = new A(aa1);
//方法2:(创建匿名对象)
A* pa2 = new A(A());
//方法3:(隐式类型转换,构造 + 拷贝构造,优化为构造)
A* pa3 = new A(3);
//多段空间
//方法1:
A* pa4 = new A[3]{aa1, aa2, aa3};
//方法2:
A* pa5 = new A[3]{A(), A(), A()};
//方法3:(前三个元素初始化为1,2,3,后面会赋值的部分会全部默认初始化为0,类似数组)
A* pa6 = new A[10]{1, 2, 3};
3. operator new 与 operator delete函数
- new和delete是我们进行动态内存申请和释放的操作符,而其实现空间的开辟与释放的方式为,去调用用名为operator new 和operator delete的两个函数。
- 这两个函数是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间,接下来就让我们来学习这两个函数的相关知识。(虽然名为operator,但与运算符重载无关,两者为全局函数)
- operator new与operator delete的实现,底层为直接调用malloc与free来实现空间的动态开辟,其用法也与malloc/free相同。(operator new与operator delete两者可以直接调用)
int* pa = (int*)operator new(sizeof(int));
*pa = 10;
cout << *pa << endl;
operator delete(pa);
- 既然operator new/delete实现依旧是调用malloc/free实现动态开辟空间,那为什么不去直接使用malloc/free?
- malloc申请空间失败会返回0(NULL),此返回值不符合面向对象的编程特性,所以C++对其进行了一层封装,使得申请空间失败后抛出异常,为了与malloc的封装匹配,于是将free也进行了封装,封装为operator delete。
- 调用抛出异常演示:
//连续申请空间,申请空间不足,开辟失败
void func_test()
{
char* c1 = new char[1024 * 1024 * 1024];
//cout << c1 << endl;
//char类型的变量,流插入操作自动识别时会默认识别为字符串而不是指针
cout << (void*)c1 << endl;
//捕获异常的操作会改变执行流,不再执行下面操作,而会抛出异常
//类似goto
char* c2 = new char[1024 * 1024 * 1024];
cout << (void*)c2 << endl;
}
int main()
{
try
{
func_test();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
4. new与delete的实现原理
- 申请开辟自定义类型空间时,new操作符会先开辟出指定大小的空间,而后调用构造函数初始化开辟出的空间。(申请空间 + 调用构造)
- delelte销毁自定义类型的申请空间时,会先调用自定义类型的析构函数,而后再进行空间的销毁释放。(调用析构,销毁空间)
- new/delete操作符在编译时会直接按照上述调用步骤,生成汇编指令。(汇编调试,call,jump)
- new调用new,申请时,会先开辟空间,再调用构造,构造时再调用new。销毁时,会先调用析构,析构先销毁里层new申请的空间,而后再销毁外层new申请的空间。
class Stack
{
private:
int* _a;
int _capacity;
int _top;
public:
Stack(int n = 4)
{
cout << "Stack()" << endl;
_a = new int[n];
_top = 0;
_capacity = 4;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _a;
_top = 0;
_capacity = 0;
}
};
int main()
{
Stack* p1 = new Stack;
delete[] p1;
return 0;
}
- new/delete与new[]/delete[]不匹配使用可能会产生的风险与delete[]的工作原理:
(汇编调试)
int main()
{
Stack* p1 = new Stack[10];
delete[] p1;
return 0;
}
- 10个Stack类型的变量组成的空间大小应为120字节,可是,经过汇编调试后,大小却为124字节。这是因为,new[]申请连续的自定义类型空间时,会额外在头部申请一个四字节大小的空间用来存放申请的自定义类型空间个数,delete[]在析构时会向前调整四个字节,读取需要调用析构的次数。(delete获取需要调用析构的次数)
- 当使用delete去释放new[] 出的空间时,不会向前调整四个字节,而是调用一次析构函数后,从第一个元素的首地址释放空间,会导致内存泄漏。
- 编译器的优化:因为类A的成员变量只有一个int变量,且构造时没有开辟额外空间,当我们将类A的析构函数屏蔽后(没有使用析构的必要),当再次new[]一段连续空间时,将不再于头部开辟额外空间存储元素个数。delete所要做的就只是释放开辟的空间,这一点,使用free也同样可以做到。
class A
{
private:
int _a;
public:
A()
{
cout << "A()" << endl;
}
/*~A()
{
cout << "~A()" << endl;
}*/
};
int main()
{
A* pa = new A[10];
//用delete去销毁new[]申请的空间
delete pa;
//free(pa);
return 0;
}
5. 定位new表达式与池化计数
- 在前面的学习中我们了解到,类的析构函数可以显示调用,而构造函数却不可以。那么,当我们不使用new的方式开辟出自定义类型的空间后,有没有办法对这段空间使用构造函数进行初始化呢,接下来,我们引入定位new表达式。
//调用默认构造
new(指针:指定空间地址)类型
//赋予初始值
new(指针)类型(初始值)
示例:
class A
{
private:
int _a;
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
A* pa1 = (A*)operator new(sizeof(A));
//调用构造
new(pa1)A;
//析构
pa1->~A();
//释放空间
operator delete(pa1);
A* pa2 = (A*)operator new(sizeof(A));
//调用构造,并指定初始值
new(pa1)A(10);
pa2->~A();
operator delete(pa2);
return 0;
}
- 定位new应用场景:
<1> 因为new申请动态开辟的空间是在堆上,很多时候我们需要频繁的调用申请空间,这样的效率非常低。
<2> 所以,C++中创建了内存池来应对这一类问题,先提前申请出一大块空间备用,这块空间被称为内存池,当我们需要申请空间时,不用再去堆上申请,而是可以直接找内存池申请划分。
<3> 这些申请来的空间(自定义类型)是已经开辟好的,这些空间没有调用构造函数进行初始化,无法使用,而定位new就可以解决这样的问题。
6. malloc/free与new/delete的异同
相同点:
- 都是于堆区上开辟空间,且都需要手动释放
不同点:
- malloc申请的空间不会进行初始化,而new申请出的空间可以进行初始化
- malloc申请空间需要计算空间大小,而new只需要指定对象个数
- malloc的返回必须要强转为对应类型的指针,而new在申请空间时就声明了类型
- malloc申请空间失败返回空,new申请失败抛出异常
- malloc开辟销毁空间时不会调用构造,析构函数,而new/delete开辟销毁空间时会调用构造与析构