嗨喽,大家好呀,今天阿鑫给大家带来类和对象最终章以及内存管理上!经过半个多月的艰苦奋斗我们终于将要翻过类和对象这座山,下面让我们开始今天的博客学习吧!
类和对象最终与内存管理上
- 匿名对象
- c++内存管理方式
- operator new与operator delete函数
- 利用new实现一个简单链表
1.匿名对象
class A
{
public:A(int b = 0):_a(b),_a1(b),_a2(b){cout << "A(int b = 0)" << endl;}//explicit//单参数类型转换/*A(int a):_a(a){cout << "A(int a)" << endl;}*///多参数类型转换A(int m, int n):_a1(m), _a2(n){cout << "A(int m, int n)" << endl;_a = 0;}A(const A& aa)//拷贝构造:_a(aa._a){cout << "A(const A& aa)" << endl;}~A(){cout << "~A()" << endl;_a = -1;_a1 = -1;_a2 = -1;}private:int _a;int _a1;int _a2;};
A(2);
注意:匿名对象,生命周期只存在这一行
我们补充一下析构的顺序以及全局变量和静态变量构造与main函数的先后关系
- 对于后定义的对象先进行析构。
- 全局对象在main函数前就构造了,而静态对象生命周期是全局的但是第一次调用时才会进行构造,只会初始化一次
在开始我们的讲解之前,我们先来回顾一下c和c++的程序内存空间划分,并且用一道题目来回顾一下我们之前学习过的内容
答案:CCCAA
AAADAB
有问题的同学自行阅读这张图,相信就能解决你的问题。
2.C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
2.1 new/delete操作内置类型
下面给出示范
int main
{//对于内置类型int* p1 = new int[10];int* p2 = new int[10];delete p1;delete[] p2;return 0;
}
默认不初始化,但是可以初始化
对于自定义类型
class A
{
public:A(int b = 0):_a(b){cout << "A(int b = 0)" << endl;}//explicit//单参数类型转换/*A(int a):_a(a){cout << "A(int a)" << endl;}*///多参数类型转换A(int m, int n):_a1(m), _a2(n){cout << "A(int m, int n)" << endl;_a = 0;}A(const A& aa)//拷贝构造:_a(aa._a){cout << "A(const A& aa)" << endl;}~A(){cout << "~A()" << endl;}private:int _a;int _a1;int _a2;};int main()
{A* p1 = new A(3);delete p1;A* p2 = new A({4,5});delete p2;A* p3 = new A;//不传参调用默认构造delete p3;A* p4 = new A[10]{ 1,2,3,4,5 };//隐式类型转换delete[] p4;A* p5 = new A[10]{ 1,2,3,4,{5,6} };delete[] p5;return 0;
}
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
3 .operator new与operator delete函数
3 .1operator new与operator delete函数
new和delete是用户进行动态内存电请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
A* p1 = new A;
delete p1;
当我们在执行这两句代码时
第一句会转换成
operator new->(malloc)+构造函数
第二句会转换成
析构 + operator delete
并且是由顺序的,我们可以通过汇编语言来看到这个过程
3 .2operator new和构造函数的区别
当您在C++中使用new关键字来创建一个对象(比如一个栈类实例)时,以下是发生的事情的简要概述:
需要注意的是:内存分配时并没有对成员变量初始化,是编译器通过计算判断出需要创建的成员变量内存需要的大小,这个内存大小通常是根据对象的成员变量所确定,等到构造函数时才会真正对成员变量初始化
1.内存分配:new操作符首先调用operator new来为对象分配足够的内存空间。这个空间的大小通常等于对象的所有成员变量所占用的空间的总和,再加上任何可能由于内存对齐而产生的填充。
2.构造函数调用:一旦内存被成功分配,new操作符会调用对象的构造函数。构造函数负责初始化对象的成员变量。这可能包括为成员变量设置初始值,以及执行任何必要的初始化代码(比如为栈上的资源开辟空间)。
对于栈类来说,构造函数可能会执行以下操作:
初始化成员变量,比如栈顶指针或栈的大小。
为栈分配内部存储空间,这通常涉及到动态内存分配(比如使用new为数组分配空间)。
设置栈的初始状态(比如将栈顶指针设置为指向栈的底部)。
重要的是要理解,operator new只负责分配内存,而对象的实际初始化工作是由构造函数完成的。构造函数可以执行任何必要的初始化步骤,包括为对象的成员变量或内部资源分配空间。
下面是一个简单的栈类的例子,它展示了构造函数如何为栈的内部数组分配空间:
class Stack {
private: int* elements; // 指向存储栈元素的数组的指针 size_t capacity; // 栈的容量 size_t size; // 栈中当前元素的数量 public: // 构造函数 Stack(size_t cap) : capacity(cap), size(0) { // 使用 new 为存储栈元素的数组分配内存 elements = new int[capacity]; // 此时,elements 指向一个包含 capacity 个 int 的数组,用于存储栈元素 } // 析构函数 ~Stack() { // 释放存储栈元素的数组所占用的内存 delete[] elements; } // 其他栈操作函数... void push(int value) { if (size < capacity) { elements[size++] = value; } else { // 处理栈满的情况,可能抛出异常或自动扩容等 } } int pop() { if (size > 0) { return elements[--size]; } else { // 处理栈空的情况,可能抛出异常等 } } // ... 其他成员函数 ...
};
在这个例子中:
Stack 类的成员变量包括一个指向 int 数组的指针 elements、一个表示栈容量的 size_t 类型变量 capacity 和一个表示当前栈大小的 size_t 类型变量 size。
构造函数接受一个 cap 参数,并使用 new 为 elements 分配一个大小为 cap 的 int 数组。这个数组用于存储栈中的元素。
析构函数使用 delete[] 释放 elements 指向的数组所占用的内存。
push 和 pop 方法操作这个动态分配的数组,以实现栈的基本功能。
所以,operator new 只负责分配 Stack 对象本身的内存,而栈中实际存储元素的数组是通过构****造函数内部额外的 new 调用分配的。这允许我们根据需要在运行时动态地调整栈的大小。
4. 利用new实现一个简单链表
struct ListNode
{struct ListNode* _next;int _val;struct ListNode(int val):_next(nullptr),_val(val){}};
void func()
{struct ListNode* node1 = new struct ListNode(1);// //得到的是一个ListNode类的指针,要用指定类型的指针接受// //new失败了会抛异常,不需要再检查返回值struct ListNode* node2 = new struct ListNode(2);struct ListNode* node3 = new struct ListNode(3);node1->_next = node2;node2->_next = node3;delete node1;delete node2;delete node3;
}
int main()
{try{func();}catch (const exception& e){cout << e.what() << endl;}return 0;
}