
一. C/C内存分布注意栈中存放局部变量静态区存放全局变量、静态数据包括全局的和局部的常量区存放常量或者代码编译好的指令堆中存放动态申请的变量或对象动态数组。我们先来看下面的一段代码和相关问题#includeiostream 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; int* ptr1 (int*)malloc(sizeof(int) * 4); int* ptr2 (int*)calloc(4, sizeof(int)); int* ptr3 (int*)realloc(ptr2, sizeof(int) * 4); free(ptr1); free(ptr3); const int i10; int j1; }1.选择题选项A.栈 B.堆 C.数据段静态区 D.代码段常量区1globalvar在哪里选C因为其属于全局变量。2staticGlobalvar在哪里选C因为其属于静态的全局数据。3staticVar在哪里选C因为其属于静态的局部数据。4localvar在哪里选A因为其属于局部变量。5num1在哪里选A因为局部的数组也在栈上。6char2在哪里选A因为它是一个数组注意数组名是代表整个数组的。7*char2在哪里选A*char代表数组首元素的地址首元素在栈上故解引用也是在栈上。8pChar3在哪里选Aconst修饰的是指向的内容不是修饰本身就算修饰本身也是在栈上。9*pChar3在哪里选D pChar3在栈上面在栈上面解引用找它指向的内容指向常量字符串常量字符串在常量区。常量区的数据不能修改10ptr1在哪里选Aptr1是int型的指针变量ptr1存了一块堆上的一块空间地址。11*ptr1在哪里选B。12i在哪里选A。注意这里的i不是存放在常量区C中const修饰的变量叫做常变量它只不过是不能修改罢了但是想办法是可以修改到的取到它的地址是const int*再把它强制转换成int*再去解引用是能修改的。所以它在栈上。【说明】a.栈又叫堆栈--非静态局部变量/函数参数/返回值等等栈是向下增长的。b.内存映射段是高效的I/O映射方式用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存做进程间通信。c.堆用于程序运行时动态内存分配堆是可以上增长的。d.数据段--存储全局数据和静态数据。e.代码段--可执行的代码/只读常量。二. C语言中动态内存管理方式malloc/calloc/realloc/free2.1 malloc/calloc/realloc的区别malloc是动态开辟calloc动态开辟会初始化默认会把初始化的空间初始化成0realloc是对已有的一块空间进行扩容realloc的性能是很不确定的它又分为原地扩容和异地扩容。#includeiostream using namespace std; int main() { int* p2 (int*)calloc(4 , sizeof(int)); int* p3 (int*)realloc(p2, sizeof(int) * 10); cout p2 endl; cout p3 endl; //这里需要free(p2)么 free(p3); return 0; }这里完全不需要free(p2)因为这里的realloc有可能是原地扩容也有可能是异地扩容由运行结果可知此处是原地扩容原地扩容的话二者地址一样释放一次就可以了如果再free(p2)就会对同一块空间释放两次而不允许对同一块空间释放两次。但是如果是异地扩容也不需要我们free(p2)因为此时realloc会帮我们释放掉。三. C内存管理方式C语言内存管理方式在C中可以继续使用但是有些地方就无能为力而且使用起来比较麻烦因此C又提出了自己的内存管理方式通过new和delete操作符进行动态内存管理。3.1 new/delete操作内置类型“new类型”代表申请这个类型的对象然后返回“类型*”例如“new int”代表动态申请一个int类型的对象。注意new默认是不会初始化的想初始化的话就像如图函数体中第二行所示。int* ptr7new int[10]{1,2,3,10}; //数组 //前4个数为1,2,3,10后面依次补0 delete ptr4; delete[] ptr7;单个对象是delete多个对象是delete[ ]。其实我们就可以认为new在这块涵盖了malloc和calloc的所有功能。3.2 new和delete操作自定义类型#includeiostream using namespace std; class A { A(int a 0) :_a(a) { cout A(): this endl; } ~A() { cout ~A() this endl; } private: int _a; }; struct ListNode { ListNode* _next; int _val; ListNode(int val) :_next(nullptr) ,_val(val) { } }; int main() { //只开空间不调用构造初始化 A* p1 (A*)malloc(sizeof(A)); //除了开空间还会调用构造函数初始化 A* p2 new A; A* p3 new A(10); //delete会调用析构函数 delete p2; delete p3; ListNode* n1 new ListNode(1); ListNode* n2 new ListNode(2); ListNode* n3 new ListNode(3); return 0; }new会失败吗一般情况下new和malloc都极少失败malloc和calloc失败都会返回空指针。new失败会出现“抛异常”我们要做的就是“捕获异常”用“try...catch”的方式去捕获由于这里的内容有些高深所以我在这里仅仅展示一下用法就行 后面我会讲到。#includeiostream using namespace std; int main() { try { int i 1; int* ptr nullptr; do { ptr new int[1024 * 1024]; cout i : ptr endl; } while (ptr); cout i : ptr endl; } catch (const exception e) { cout e.what() endl;//会打印出错误 } return 0; }四. operator new与operator delete函数new和delete是用户进行动态内存申请和释放的操作符operator new和operator delete是系统提供的全局函数new在底层调用operator new全局函数来申请空间delete在底层通过operator delete全局函数来释放空间。五. new和delete的实现原理5.1 内置类型如果申请的是内置类型的空间new和mallocdelete和free基本类似不同的地方是new/delete申请和释放的是单个元素的空间new[]和delete[]申请的是连续空间而且new在申请空间失败时会抛异常malloc会返回NULL。5.2 自定义类型1new的原理①调用operator new申请函数空间。②在申请的空间上执行构造函数完成对象的构造。2delete的原理①在空间上执行析构函数完成对象中资源清理的工作。②调用operator delete函数释放对象的空间。3new T[N]的原理①调用operator new[]函数在operator new[]中实际调用operator new函数完成N个对象空间的申请。②在申请的空间上执行N次构造函数。4delete[]的原理①在释放的对象空间上执行N次构造函数完成N个对象中资源的清理。②调用operator delete[]释放空间实际在operator delete[]调用operator delete来释放空间。六. 定位new表达式(placement-new)了解定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。6.1 使用格式new(place_address)type或者new(place_address)type(initalizer-list)place_address必须是一个指针initializer-list是类型的初始化列表。6.2 使用场景定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化所以如果是自定义类型的对象需要使用new的定义表达式进行显示调构造函数进行初始化。也就是说觉得系统的malloc内存池不够高效要搞一套自己的这个管理内存的这样一个机制这个机制默认只开了空间出来开了空间出来我要想初始化就得显式调用构造显式调用构造就得用定位new。#includeiostream using namespace std; class A { public: A(int a) :_a(a) { cout A(): this endl; } ~A() { cout ~A(): this endl; } private: int _a; }; int main() { A* ptr (A*)operator new(sizeof(A)); //显式调用构造函数--构造函数只能通过定位new去调用 new(ptr)A(1); ptr-~A();//析构函数可以显式调用 operator delete(ptr); return 0; }七. malloc/free和new/delete的区别malloc/free和new/delete的共同点是都是从堆上申请空间并且需要用户手动释放。不同的地方是1.malloc和free是函数new和delete是操作符。用法2.malloc申请的空间不会初始化new可以初始化。用法3.malloc申请空间时需要手动计算空间大小并传递new只需在其后跟上空间的类型即可如果是多个对象[ ]中指定对象个数即可。4.malloc的返回值为void*在使用时必须强转new不需要因为new后面跟的是空间的类型。5.malloc申请空间失败时返回的是NULL因此使用时必须判空new不需要但是new需要捕获异常。6.申请自定义类型对象时malloc/free只会开辟空间不会调用构造函数与析构函数而new在申请空间后会调用构造函数完成对象的初始化delete在释放空间前会调用析构函数完成空间中资源的清理释放。七. 模板初阶7.1 泛型编程如何实现一个通用的交换函数呢#includeiostream using namespace std; void Swap(int left, int right) { int temp left; left right; right temp; } void Swap(double left, double right) { double temp left; left right; right temp; } void Swap(char left, char right) { char temp left; left right; right temp; } .......使用函数重载虽然可以实现但是有以下几个不好的地方1. 重载的函数仅仅是类型不同代码复用率比较低只要有新类型出现时就需要用户自己增加对应的函数。2. 代码的可维护性比较低一个出错可能所有的重载均出错。那是否告诉编译器一个模子让编译器根据不同的类型利用该模子来生成代码呢泛型编程的概念编写与类型无关的通用代码是代码复用的一种手段。模板是泛型编程的基础。模板分为函数模板和类模板两类。7.2 函数模板7.2.1 函数模板概念函数模板代表了一个函数家族该函数模板与类型无关在使用时被参数化根据实参类型产生函数的特定类型版本。7.2.2 函数模板格式templatetypename T1typename T2........typename Tn返回值类型 函数名参数列表{}templatetypename T void Swap(T left, T right) { T temp left; left right; right temp; } int main() { int i 1, j 2; Swap(i, j); char c1 z, c2 a; Swap(c1, c2); return 0; }注意typename是用来定义模板参数关键字也可以使用class切记不能使用struct代替class。7.2.3 函数模板原理函数模板是一个蓝图它本身并不是函数是编译器使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复事情交给了编译器。在编译器编译阶段对于模板函数的使用编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如当用double类型使用函数模板时编译器通过对实参类型的推演将T确定为double类型然后产生一份专门处理double类型的代码对于字符类型也是如此。7.2.4 函数模板的实例化用不同类型的参数使用函数模板时称为函数模板的实例化。模板参数实例化分为隐式实例化和显式实例化。1.隐式实例化让编译器根据实参推演模板参数的实际类型。2.显式实例化在函数名后的中指定模板参数的实际类型。如果类型不匹配编译器会尝试进行隐式类型转换如果无法转换成功编译器将会报错。templateclass T T Add(const T left, const T right) { return left right; } templateclass T void Func(size_t n) { T* ptr new T[n]; cout ptr endl; } int main() { int a1 10, a2 20; double d1 10.1, d2 20.1; //隐式实例化通过实参类型推到模板参数类型 cout Add(a1, a2) endl; cout Add(d1, d2) endl; cout Add((double)a1, d1) endl; cout Add(a1, (int)d1) endl; //显式实例化 cout Addint(a1, d1) endl; cout Adddouble(a1, d1) endl; //以下必须用显式实例化 Funcint(10); return 0; }7.2.5 模板参数的匹配原则1. 一个非模板函数可以和一个同名的函数模板同时存在而且该函数模板还可以被实例化为这个非模板函数。//专门处理int的加法函数 int Add(int left, int right) { return left right; } //通用的加法函数 templateclass T T Add(T left, T right) { return left right; } int main() { int i 1, j 2; coutAdd(i, j)endl;//有现成的先调用现成的 //强制调用模板 cout Addint(i,j) endl; double c 1.1, d 2.2; Add(c, d);//没现成的用模板实例化一个 return 0; }2. 对于非模板函数和同名函数模板如果其他条件都相同在调用时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数那么将选择模板。//专门处理int的加法函数 int Add(int left, int right) { return left right; } //通用加法函数 templatetypename T1,typename T2 T1 Add(const T1 left, const T2 right) { return left right; } int main() { cout Add(1, 2) endl;//调用上面现成的Add函数 cout Add(1, 2.1) endl;//调用上面的通用加法函数 return 0; }3. 模板函数不允许自动类型转换但普通函数可以进行自动类型转换。7.3 类模板7.3.1类模板的定义格式//类模板 //模板不支持声明和定义分离.h和.cpp templateclass T class Stack { public: Stack(size_t n 4) :_a(new T[n]) ,_top(0) ,_capacity(n) {} void Push(const T x) { //扩容 //... _a[_top] x; } private: T* _a; size_t _top; size_t _capacity; }; int main() { //类模板都必须是显式实例化 Stackint st1; //存int st1.Push(1); Stackdouble st2; //存double st2.Push(2.1); return 0; }7.3.2类模板的实例化templateclass Tint class A { public: T x1; T x2; }; templateclass T1 ,class T2int//半缺省参数必须从右往左缺省 class B { public: T1 x1; T2 x2; }; int main() { Aint aa1; A aa2;//如果不传就用缺省值 Adouble aa1; Bdouble, int bb1; Bdouble bb2; return 0; }以上就是我们今天的内容下节我将正式进入“STL”这个板块觉得有帮助的朋友们可以一键三连喔~互三必回