=========================================================================
相关代码gitee自取:
C语言学习日记: 加油努力 (gitee.com)
=========================================================================
接上期:
【C++初阶】六、类和对象(初始化列表、static成员、友元、内部类)-CSDN博客
=========================================================================
目录
一 . C/C++内存分布
C/C++中程序内存区域划分:
二 . C++内存管理方式
回顾:C语言中动态内存管理方式malloc / calloc / realloc / free
C++的内存管理方式
new / delete -- 操作内置类型:
new / delete -- 操作自定义类型:
常见面试题 -- malloc / free 和 new / delete 的区别
三 . operator new 和 operator delete 函数
operator new / operator delete
operator new 全局函数:
operator delete 全局函数:
图示 -- operator new / delete 全局函数:
new 和 delete 的实现原理
对于内置类型:
(重点)对于自定义类型:
四 . 定位new表达式(placement-new)(了解)
本篇博客相关代码
Test.cpp文件 -- C++文件:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
一 . C/C++内存分布
C/C++中程序内存区域划分:
不同的数据有不同的存储需求,内存中有各种区域满足不同的需求
- 栈(堆栈):
存放非静态局部变量 / 函数参数 / 返回值 ……,栈是向下增长的
- 内存映射段:
内存映射段是最高效的 I/O映射方式 ,用于装载一个共享的动态内存库。
用户可以使用系统接口创建共享内存,做进程间通信
- 堆:
用于程序运行时动态内存分配,堆是向上增长的
(动态使用:数据结构、算法中需要动态开辟一些空间)
- 数据段(静态区):
操作系统角度叫数据段,语言角度叫静态区。
存储全局数据和静态数据
(整个程序运行期间都可能会使用到的数据)
- 代码段(常量区):
操作系统角度叫代码段,语言角度叫常量区。
存储可执行代码(汇编指令)和常量
(只读数据)图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
二 . C++内存管理方式
回顾:
C语言中动态内存管理方式malloc / calloc / realloc / free
之前学习C语言的时候有写过动态内存管理相关内容,
有需要的话可以进行查看:学C的第三十二天【动态内存管理】_高高的胖子的博客-CSDN博客
C++的内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力了,
而且使用起来会比较麻烦,因此C++中又提出了自己的内存管理方式:
通过 new 和 delete 操作符进行动态内存管理
new / delete -- 操作内置类型:
- new -- 申请单个空间:
内置类型指针 指针名 = new 内置类型;
- new -- 申请多个空间:
内置类型指针 指针名 = new 内置类型[申请单位空间个数];
- new -- 申请单个空间并进行初始化:
内置类型指针 指针名 = new 内置类型(初始化值);
- new -- 申请多个空间并进行初始化:
内置类型指针 指针名 = new 内置类型[申请单位空间个数]{第一个初始化值, 第二个初始化值……};
- delete -- 释放new申请的空间:
//释放new申请的单个空间: delete 内置类型指针名; //释放new申请的多个空间: delete[] 内置类型指针名;
- 对于内置类型的对象申请和释放,
C++的 new / delete 和 C语言的 malloc / calloc / realloc / free
除了用法上(“强转”和计算开辟空间大小)外,(底层)几乎没有任何区别图示:
---------------------------------------------------------------------------------------------
new / delete -- 操作自定义类型:
- new -- 申请单个空间:
对于自定义类型,使用C++中的new开辟动态空间的话,
会在开辟空间后顺便调用其构造函数进行自定义类型对象的初始化//开辟单个空间并自动调用 默认构造函数 进行初始化: 自定义类型指针 指针名 = new 自定义类型; //开辟单个空间并调用 有参构造函数 进行初始化: 自定义类型指针 指针名 = new 自定义类型(初始化值);
- new -- 申请多个空间:
对于自定义类型,使用new申请多个空间时,
同样会在开辟空间后顺便调用其构造函数进行自定义类型对象的初始化//方式一:通过有名对象: (先初始化多个自定义类型对象); 自定义类型指针 指针名 = new 自定义类型[申请单位空间个数]{有名对象1, 有名对象2……}; //方式二:通过匿名对象: 自定义类型指针 指针名 = new 自定义类型[申请单位空间个数]{匿名对象1, 匿名对象2……}; //方式三:通过内置类型的隐式类型转换为自定义类型: 自定义类型指针 指针名 = new 自定义类型[申请单位空间个数]{内置类型1, 内置类型2……};
- delete -- 释放new申请的空间:
//释放new申请的单个空间: delete 自定义类型指针名; //释放new申请的多个空间: delete[] 自定义类型指针名;
- 对于自定义类型的对象申请和释放,
C++的 new 除了会开辟动态空间外,还会自动调用其构造函数进行初始化图示:
---------------------------------------------------------------------------------------------
常见面试题 -- malloc / free 和 new / delete 的区别
共同点:
malloc / free 和 new / delete 都是从堆上申请空间的,并且都需要用户手动释放
---------------------------------------------------------------------------------------------
不同点:
- malloc 和 free 是函数 ;new 和 delete 是操作符
- malloc 申请的空间不会被初始化 ;new 申请的空间则会被初始化
- malloc 申请空间时,需要手动计算开辟的空间大小并传递;
new 申请空间时,只需要在其后写出空间的类型即可,
如果是多个对象,[ ]中指定对象个数即可
- malloc 的返回值为 void* ,在使用时必须进行强转;
new 则不需要,因为 new 后跟的是空间的类型
- malloc 申请空间失败时,返回的是空指针NULL,因此使用时必须判空;
new 则不需要,但是 new 需要捕获异常
- 在申请自定义类型对象时:
malloc / free 只会开辟空间,不会调用构造函数和析构函数;
new 在申请空间后会调用构造函数完成对象的初始化,
delete 在释放空间前会调用析构函数完成空间中资源的清理
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
三 . operator new 和 operator delete 函数
operator new / operator delete
new 和 delete 是C++中进行动态内存申请和释放的操作符,
operator new 和 operator delete 是系统提供的全局函数,
new 在底层会调用 operator new 全局函数来申请空间;
delete 在底层会调用 operator delete 全局函数来释放空间。
operator new 全局函数:
- 虽然函数名中有 operator ,但并不是重载函数
- C语言中,malloc 如果申请空间失败的话,会返回空指针,
这不符合C++面向对象编程的要求,所以需要对其进行封装
- operator new 全局函数就是对 malloc 的封装,
所以 operator new 全局函数底层会调用 malloc ,
让 malloc 申请空间失败后会抛出异常,从而能够符合C++面向对象编程的要求,
operator new 全局函数和 malloc 一样只会申请空间不会调用构造函数初始化
---------------------------------------------------------------------------------------------
operator delete 全局函数:
- operator delete 全局函数同样也不是重载函数,而是一个全局函数
- operator delete 全局函数是对 free 的封装,
所以 operator delete 全局函数底层会调用 free ,
相较 free ,operator delete 全局函数多了一些检查,
operator delete 全局函数和 free 一样只会释放空间不会调用析构函数
---------------------------------------------------------------------------------------------
图示 -- operator new / delete 全局函数:
new 和 delete 的实现原理
对于内置类型:
如果申请的是内置类型对象的空间,new 和 malloc,delete 和 free 基本类似,
不同的地方是:new / delete 申请和释放的是单个元素的空间;new[ ] / delete[ ] 操作的则是连续的空间,
而且 new 在申请空间失败时会抛出异常,而C语言中malloc则会返回空指针
---------------------------------------------------------------------------------------------
(重点)对于自定义类型:
- new 的原理(申请单个动态空间):
第一步 -- 为自定义类型对象开辟动态空间 -- 调用 operator new 全局函数
(new => operator new => malloc)
第二步 -- 初始化申请的空间 -- 调用 构造函数 完成对象的初始化
- delete 的原理(释放单个动态空间):
第一步 -- 先清理自定义类型对象申请的资源 -- 调用对应的 析构函数
第二步 -- 再释放自定义类型对象的动态空间 -- 调用 operator delete 全局函数
(delete => operator delete => free)
- new T[N] 的原理(申请多个动态空间):
第一步 -- 调用 operator new[ ] 函数开辟动态空间,
在 operator new[ ] 中实际也是调用了 operator new 全局函数,
一次性完成了N个对象空间的申请
第二步 -- 在申请的空间上执行N次构造函数,完成N个对象的初始化
- delete[ ] 的原理(释放多个动态空间):
第一步 -- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
第二步 -- 调用 operator delete[ ] 释放空间,
在 operator delete[ ] 中实际也是调用了 operator delete 全局函数图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
四 . 定位new表达式(placement-new)(了解)
- 定位new表达式是在已分配的原始内存空间中调用构造函数来初始化一个对象
(通过对象指针能够显式调用构造函数进行初始化)
- 使用格式:
调用默认构造函数 -- new (place_address) type
调用有参构造函数 -- new (place_address) type (initializer-list)
place_address:必须是一个指针 ;initializer-list:类型的初始化列表
- 使用场景:
定位new表达式在实际中一般是配合内存池进行使用。
因为内存池分配出的内存没有被初始化,所以如果是自定义类型的对象,
则需要使用new的定位表达式进行显式调用构造函数来进行初始化图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
本篇博客相关代码
Test.cpp文件 -- C++文件:
#define _CRT_SECURE_NO_WARNINGS 1 #include <iostream> #include <assert.h> using namespace std; 全局变量(链接属性:其它文件也可用): //int globalVar = 1; // 静态全局变量(链接属性:当前文件可用): //static int staticGlobalVar = 1; // //void Test() //{ // //静态变量(链接属性:函数中可用): // static int staticVar = 1; // // //普通变量: // int localVar = 1; // // //普通数组: // int num1[10] = { 1,2,3,4 }; // // //字符数组: // char char2[] = "abcd"; // /* // * 数组符号:[],本质就是将内容从 // * 常量区(代码段)复制到栈中 // */ // // //字符指针: // const char* pChar3 = "abcd"; // /* // * 这里没有使用数组符号[]进行拷贝, // * 所以指针是直接指向常量区中“abcd”的位置的 // */ // // //malloc开辟动态空间: // int* ptr1 = (int*)malloc(sizeof(int) * 4); // //calloc开辟动态空间: // int* ptr2 = (int*)calloc(4, sizeof(int)); // //realloc开辟动态空间: // int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); //} A类: //class A //{ //public: //公有成员函数: // // //构造函数(全缺省): // A(int a = 0) // : _a(a) // { // //调用则打印: // cout << "A():" << this << endl; // } // // //析构函数: // ~A() // { // //调用则打印: // cout << "~A():" << this << endl; // } // //private: //私有成员变量: // // int _a; //}; 链表结点类: //struct ListNode //{ // int _val; //结点值 // ListNode* _next; //next指针 // // //构造函数: // ListNode(int val) // : _val(val) // , _next(nullptr) // {} // //}; 主函数: //int main() //{ // //C++动态内存管理(对于内置类型): // int* p1 = new int; //申请单个空间 // // //申请多个空间: // int* p2 = new int[10]; //申请10个单位空间 // //申请40个字节的int数组 -- 后面加:[] // // /* // * 使用new申请动态空间(p1、p2): // * 对于内置类型 -- 申请后不会对空间进行初始化,会是随机值 // *(对于 p1 和 p2,单纯就是开空间) // */ // // //申请动态空间并初始化单个空间: // int* p3 = new int(1); //后面加:(初始化值) // //申请一个int类型的空间并初始化为1 -- 后面加:() // //注意 new int[10] 和 new int(1) 的区别(易混淆) // // //申请动态空间并初始化多个空间: // int* p4 = new int[10] {1,2,3}; // /* // * 后面加:[]{} // * [初始化空间个数]{第一个初始化值,第二个初始化值,第三个初始化值……} // * // * 这里是:[10]{1,2,3} ,即申请了10个单位空间,但只初始化了前3个, // * 之后剩余的7个空间会被默认初始化为0, // * 即开辟的空间为:{1, 2, 3, 0, 0, 0, 0, 0, 0, 0} // */ // // //C++释放new申请的空间 -- delete: // delete p1; // delete[] p2; // delete p3; // delete[] p4; // // /* // * 总结: // * 对于内置类型的对象申请和释放, // * 这里 C++的new(delete) 和 // * C语言的malloc/calloc/realloc(free) // * 除了用法上(“强转”和计算开辟空间大小)外, // * (底层)几乎没有任何区别 // */ // // // // // //对于自定义类型: // // //C语言动态内存管理: // A* p5 = (A*)malloc(sizeof(A)); //C语言malloc // /* // * 这里使用了C语言malloc对自定义类型进行动态空间开辟, // * 这里虽然可以开辟,但是无法对其(自定义类型空间)进行初始化, // * 因为这里A类中的成员变量是私有的,无法直接调用 // * // * 所以malloc不方便解决动态申请的自定义类型对象的初始化问题 // */ // // //C++动态内存管理: // A* p6 = new A; //调用默认构造函数(开辟空间后顺便初始化) // //new申请动态空间并初始化 -- 自定义类型 // // A* p7 = new A(1); //调用有参构造函数(开辟空间后顺便初始化) // //new申请动态空间并初始化 -- 自定义类型 // // /* // * C++中,使用new为自定义类型对象申请空间时, // * 除了会开辟动态空间,还会自动调用其构造函数进行初始化, // * // * new的本质:开辟动态空间 + 调用构造函数初始化 // * // * 解决 C语言中开辟空间后无法进行初始化 的问题 // */ // // //使用new申请一个链表结点: // ListNode* n1 = new ListNode(1); //结点1 // ListNode* n2 = new ListNode(2); //结点2 // ListNode* n3 = new ListNode(3); //结点3 // /* // * C++的new带来的便利: // * 使用new开辟链表结点,会在开辟后顺便调用其构造函数 // * 进行结点的初始化,不用像C语言中还需要为了开辟结点空间 // * 而单独设置一个函数 // */ // // // //使用new单词申请多个链表结点: // // //方式一:通过有名对象 // A aa1(1); // A aa2(1); // A aa3(1); // A* p8 = new A[3]{ aa1, aa2, aa3 }; // // //方式二:通过匿名对象 // A* p9 = new A[3]{ A(2), A(2), A(2) }; // // //方式三:将内置类型隐式类型转化为自定义类型 // A* p10 = new A[3] {3, 3, 3}; // // /* // * 想要对自定义类型初始化,就需要调用其对应类的构造函数, // * 这里要初始化A类型对象,{}大括号中就需要传A类型对象 // * // * 方式一:A类的有名对象,能找到A类中的构造函数,能初始化 // * // * 方式二:A类的匿名对象,也能找到A类中的构造函数,能初始化 // * // * 方式三: // * 1、内置类型 -> 构造函数 -> 产生临时对象 // * 2、临时对象 -> 拷贝构造函数 -> (A类)匿名对象 // -> 找到A类中的构造函数 // */ // // //释放自定义类型对象空间: // delete p6; // delete[] p10; // /* // * delete对于自定义类型: // * 先调用析构函数销毁对象清理资源, // * 再调用释放动态空间, // */ // // return 0; //} //int main() //{ // try // { // char* p1 = new char[0x7fffffff]; // /* // * 十六进制:0x7fffffff -- 接近2G // * // * 当new开辟的空间过大时可能会开辟失败, // * 开辟失败则会抛出异常 // *(C语言开辟失败会返回空指针) // */ // // cout << (void*)p1 << endl; // /* // * char* 在被cout识别时后先被识别为char, // * 而不是我们想要打印的指针(地址), // * 所以要强转为void*类型 // */ // } // catch (const exception& e) // { // //try……catch……捕获异常: // cout << e.what() << endl; // } // // // return 0; //} 栈类: //class Stack //{ //public: //公有成员函数: // // //构造函数: // Stack(int capacity = 4) // { // //调用了构造函数则打印: // cout << "Stack(int capacity = 4)" << endl; // // //使用new开辟栈容量大小的空间: // _a = new int[capacity]; // // _top = 0; //栈顶值默认为0 // _capacity = capacity; //设置栈容量 // } // // //析构函数: // ~Stack() // { // //调用了析构函数则打印: // cout << "~Stack()" << endl; // // //使用delete释放new开辟的空间: // delete[] _a; // // _a = nullptr; //置为空指针 // _top = 0; //栈顶值置为0 // _capacity = 0; //栈容量置为0 // } // //private: //私有成员变量: // // int* _a; //栈指针 // int _top; //栈顶值 // int _capacity; //栈容量 // //}; // 主函数: //int main() //{ // Stack s1; // // //使用new申请单个栈对象: // Stack* p1 = new Stack; // //new:开辟空间 + 调用构造函数 // /* // * 这里涉及到两层空间: // * // * 栈对象开辟空间: // * 先开辟空间,空间大小会自动计算, // * 这里栈的三个私有成员变量大小为12个字节, // * 此时这12字节大小的空间就是对象, // * 此时指针p1就指向这个12个字节的空间 // * // * 栈底层数组开辟空间: // * 开辟空间后,调用构造函数进行初始化: // * _a = new int[capacity]; // * 构造函数中栈底层数组又需要再new一次, // */ // // // //使用delete释放这个空间: // delete p1; // //delete:调用析构函数 + 释放空间 // /* // * 这里释放的空间也有两层: // * // * 先“销毁”栈底层数组: // * delete这里需要先调用栈对象的析构函数, // * 来“销毁”栈底层数组(_a指针指向的数组) // * // * 再释放整个栈对象: // * 再释放整个栈对象。如果先释放栈对象的话, // * 栈底层数据指针_a,就会变成野指针了 // */ // // //operator new 和 operator delete是在库里面的全局函数, // //封装了malloc和free: // Stack* p2 = (Stack*)operator new(sizeof(Stack)); // operator delete(p2); // /* // * operator new / operator delete 和 // * new / delete 是不一样的, // * 但和 malloc / free 是一样的(用法也一样), // * // * new / delete 是操作符, // * 而 operator new / operator delete 是函数调用, // * new 除了开辟空间还会调用构造函数初始化空间, // * operator new 和malloc一样,只会开辟空间不会初始化; // * delete 会先调用析构函数清理空间,再释放new开辟的空间, // * operator delete 和free一样,只会释放空间不会调用析构函数 // * 所以 operator new / operator delete 只是 malloc / free 的封装 // * // * new 实现的两步: 1、开辟对象空间 2、调用构造函数初始化 // * 其中第一步中,要实现空间开辟可以使用C语言的malloc, // * 但是malloc失败只会返回空指针,这不符合面向对象编程的要求, // * 所以需要先对malloc进行封装,即 operator new , // * operator new 失败后就可以抛出异常,符合面向对象编程要求, // * 所以new关键字的第一步使用的就是malloc封装后operator new, // * 如果开辟失败捕获异常,就不会指向第二步的初始化了 // *(operator new/delete -> 封装malloc/free -> 处理失败抛异常问题) // */ // // //上面都是new开辟单个空间,那如果开辟多个空间呢: // Stack* p3 = new Stack[10]; //开辟多个空间 // /* // * new 实现的两步: 1、开辟对象空间 2、调用构造函数初始化 // * // * 1、开辟对象空间 // * new 开辟单个空间和开辟多个空间同样都会调用operator new, // * 开辟多个空间实际只会调用一次operator new, // * 一次性就开辟多个连续的空间(这里是10个连续的空间) // *(operator new[] -> operator new -> malloc) // * // * 2、调用构造函数初始化 // * 调用10次Stack构造函数进行初始化 // */ // // delete[] p3; //释放new开辟的连续空间 // /* // * 1、先调用10次析构函数; // * 2、释放空间: // * operator delete[] -> operator delete -> free // * // * 补充: // * 前面我们用new开辟了10个连续的空间, // * 按理来说应该是120个字节(这里一个栈对象12个字节), // * 但实际开辟了124个字节,多的4个字节存储着开辟的空间个数, // * 这里存储就是10,这样第一步中就可以知道要调多少次析构函数, // * 第二步中也可以知道释放时要释放多少个连续的空间, // * 所以我们使用delete释放连续空间时“delete[]"中的[], // * 我们不需要显式写出要释放多少个连续空间, // * 因为在用new开辟连续空间的时候就已经存储好了该值 // *(对于构造函数中申请了资源的自定义类型来说) // * // * 所以 new 要和 delete 配对使用, // * new[] 要和 delete[] 配对使用, // * malloc 要和 free 配对使用 // */ // // return 0; //} //A类: class A { public: //公有成员函数: //构造函数(全缺省): A(int a = 0) : _a(a) { //调用则打印: cout << "A():" << this << endl; } //析构函数: ~A() { //调用则打印: cout << "~A():" << this << endl; } private: //私有成员变量: int _a; }; int main() { //构造函数只能自动调用: A aa1; //初始化时自动调用 //不能显式调用构造函数: A* p1 = (A*)operator new(sizeof(A)); //开辟动态空间 //operator new 不会顺便调用构造函数进行初始化 //但又不能显式调用构造函数进行初始化: p1->A(1); //不能像437行那样显式调用构造函数, //但可以通过 定位new 显式调用构造函数: new(p1)A(1); /* * 定位new是在已分配的原始内存空间中调用构造函数 * 来初始化一个对象 * * 格式: * 默认构造:new(对象指针)对象类名 * 有参构造:new(对象指针)对象类名(初始化值) */ //虽然构造函数不能显式调用,但析构函数是可以的: p1->~A(); //析构函数可以显式调用也可以自动调用 //释放空间: operator delete(p1); /* * 某种程度上来说: * * A* p1 = (A*)operator new(sizeof(A)); * + * new(p1)A(1); * * operator new开辟空间配合定位new,可以实现new的功能 *(operator new开辟空间,定位new再显式调用构造函数初始化) */ /* * 某种程度上来说: * * p1->~A(); * + * operator delete(p1); * * p1->~A();显式调用析构函数配合operator delete释放空间, * 可以实现delete的功能 */ /* * 虽然可以模拟实现new和delete, * 但一般也不会这么操作 * * 因为new有两步操作, * 1、operator new -> malloc(去堆中申请空间) * 2、调用 构造函数 * 所以如果频繁使用new申请小对象的话,一直去找堆的话, * 效率可能会比较低。 * * 这时就需要使用到 内存池, * 把堆的内存较大地申请到内存池中, * 这时当需要申请内存时就不到堆中申请了, * 而是到内存池中申请,不够了再到堆中申请, * 这时就不用一直到堆中申请,提高效率 * 内存池只开了空间,没有初始化,也不能初始化, * 因为数据可能会是私有的(池化技术) * * 假设我们有一个内存池,在内存池中申请对象空间, * 这时的初始化工作就可以交给 定位new , * 通过 定位new 显式调用构造函数来初始化对象, * 这时要释放空间还给内存池的话, * 就需要显式调用析构函数来释放空间 */ return 0; } //C++常见面试题:指针和引用的区别、malloc和new的区别