下⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后⾯的delete没有得到执⾏,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很⿇烦。智能指针放到这样的场景⾥⾯就让问题简单多了。
double Divide ( int a, int b) { // 当 b == 0 时抛出异常 if (b == 0 ) { throw "Divide by zero condition!" ; } else { return ( double )a / ( double )b; } } void Func () { // 这⾥可以看到如果发⽣除 0 错误抛出异常,另外下⾯的 array 和 array2 没有得到释放。 // 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。 // 但是如果 array2new 的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案 // 是智能指针,否则代码太戳了 int * array1 = new int [ 10 ]; int * array2 = new int [ 10 ]; // 抛异常呢 try { int len, time; cin >> len >> time; cout << Divide (len, time) << endl; } catch (...) { cout << "delete []" << array1 << endl; cout << "delete []" << array2 << endl; delete [] array1; delete [] array2; throw ; // 异常重新抛出,捕获到什么抛出什么 } // ... cout << "delete []" << array1 << endl; delete [] array1; cout << "delete []" << array2 << endl; delete [] array2; } int main () { try { Func (); } catch ( const char * errmsg) { cout << errmsg << endl; } catch ( const exception& e) { cout << e. what () << endl; } catch (...) { cout << " 未知异常 " << endl; } return 0 ; }
RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是
⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指
针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,
资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常
释放,避免资源泄漏问题。
智能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀
样,重载 operator*/operator->/operator[] 等运算符,⽅便访问资源。
template < class T > class SmartPtr { public : // RAII SmartPtr (T* ptr) :_ptr(ptr) {} ~ SmartPtr () { cout << "delete[] " << _ptr << endl; delete [] _ptr; } // 重载运算符,模拟指针的⾏为,⽅便访问资源 T& operator *() { return *_ptr; } T* operator ->() { return _ptr; } T& operator []( size_t i) { return _ptr[i]; } private : T* _ptr; }; double Divide ( int a, int b) { // 当 b == 0 时抛出异常 if (b == 0 ) { throw "Divide by zero condition!" ; } else { return ( double )a / ( double )b; } } void Func () { // 这⾥使⽤ RAII 的智能指针类管理 new 出来的数组以后,程序简单多了 SmartPtr< int > sp1 = new int [ 10 ]; SmartPtr< int > sp2 = new int [ 10 ]; for ( size_t i = 0 ; i < 10 ; i++) { sp1[i] = sp2[i] = i; } int len, time; cin >> len >> time; cout << Divide (len, time) << endl; } int main () { try { Func (); } catch ( const char * errmsg) { cout << errmsg << endl; } catch ( const exception& e) { cout << e. what () << endl; } catch (...) { cout << " 未知异常 " << endl; } return 0 ; }
C++标准库中的智能指针都在<memory>这个头⽂件下⾯,我们包含<memory>就可以是使⽤了,
智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解
决智能指针拷⻉时的思路不同。
auto_ptr 是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给
拷⻉对象,这是⼀个⾮常糟糕的设计,因为他会到被拷⻉对象悬空,访问报错的问题,C++11设计
出新的智能指针后,强烈建议不要使⽤auto_ptr。其他C++11出来之前很多公司也是明令禁⽌使⽤
这个智能指针的。
unique_ptr 是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不⽀持拷
⻉,只⽀持移动。如果不需要拷⻉的场景就⾮常建议使⽤他。
shared_ptr 是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉,
也⽀持移动。如果需要拷⻉的场景就需要使⽤他了。底层是⽤引⽤计数的⽅式实现的。
weak_ptr 是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指
针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr
的⼀个循环引⽤导致内存泄漏的问题。具体细节下⾯我们再细讲。
智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指
针管理,析构时就会崩溃。智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤
对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,
在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使⽤,所以为了简洁⼀点,
unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr<Date[]> up1(new
Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。
template <class T, class... Args> shared_ptr<T> make_shared
(Args&&... args);
shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值
直接构造。
shared_ptr 和 unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个
空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断
是否为空。
shared_ptr 和 unique_ptr 都得构造函数都使⽤explicit 修饰,防⽌普通指针隐式类型转换
成智能指针对象。
1struct Date 2{ 3int _year; 4int _month; 5int _day; 6Date ( int year = 1 , int month = 1 , int day = 1 ) 7:_year(year) 9 ,_month(month) 10 ,_day(day) 11 {} 12 13 ~ Date () 14 { 15 cout << "~Date()" << endl; 16 } 17 }; 18 19 int main () 20 { 21 auto_ptr<Date> ap1 ( new Date); 22 // 拷⻉时,管理权限转移,被拷⻉对象 ap1 悬空 23 auto_ptr<Date> ap2 (ap1); 24 25 // 空指针访问, ap1 对象已经悬空 26 //ap1->_year++; 27 28 unique_ptr<Date> up1 ( new Date); 29 // 不⽀持拷⻉ 30 //unique_ptr<Date> up2(up1); 31 // ⽀持移动,但是移动后 up1 也悬空,所以使⽤移动要谨慎 32 unique_ptr<Date> up3 (move(up1)); 33 34 shared_ptr<Date> sp1 ( new Date); 35 // ⽀持拷⻉ 36 shared_ptr<Date> sp2 (sp1); 37 shared_ptr<Date> sp3 (sp2); 38 cout << sp1. use_count () << endl; 39 sp1->_year++; 40 cout << sp1->_year << endl; 41 cout << sp2->_year << endl; 42 cout << sp3->_year << endl; 43 44 // ⽀持移动,但是移动后 sp1 也悬空,所以使⽤移动要谨慎 45 shared_ptr<Date> sp4 (move(sp1)); 46 47 return 0 ; 48 }
1 template < class T> 2 void DeleteArrayFunc (T* ptr) 3 { 4 delete [] ptr; 5 } 6 7 template < class T > 8 class DeleteArray 9 { 10 public : 11 void operator ()(T* ptr) 12 { 13 delete [] ptr; 14 } 15 }; 16 17 class Fclose 18 { 19 public : 20 void operator ()(FILE* ptr) 21 { 22 cout << "fclose:" << ptr << endl; 23 fclose (ptr); 24 } 25 }; 26 27 int main () 28 { 29 // 这样实现程序会崩溃 30 // unique_ptr<Date> up1(new Date[10]); 31 // shared_ptr<Date> sp1(new Date[10]); 32 33 // 解决⽅案 1 34 // 因为 new[] 经常使⽤,所以 unique_ptr 和 shared_ptr 35 // 实现了⼀个特化版本,这个特化版本析构时⽤的 delete[] 36 unique_ptr<Date[]> up1 ( new Date[ 5 ]); 37 shared_ptr<Date[]> sp1 ( new Date[ 5 ]); 38 39 // 解决⽅案 2 40 41 // 仿函数对象做删除器 42 //unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date> ()); 43 // unique_ptr 和 shared_ptr ⽀持删除器的⽅式有所不同 44 // unique_ptr 是在类模板参数⽀持的, shared_ptr 是构造函数参数⽀持的 45 // 这⾥没有使⽤相同的⽅式还是挺坑的 46 // 使⽤仿函数 unique_ptr 可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调⽤ 47 // 但是下⾯的函数指针和 lambda 的类型不可以 48 unique_ptr<Date, DeleteArray<Date>> up2 ( new Date[ 5 ]); 49 shared_ptr<Date> sp2 ( new Date[ 5 ], DeleteArray<Date>()); 50 51 // 函数指针做删除器 52 unique_ptr<Date, void (*)(Date*)> up3 ( new Date[ 5 ], DeleteArrayFunc<Date>); 53 shared_ptr<Date> sp3 ( new Date[ 5 ], DeleteArrayFunc<Date>); 54 55 // lambda 表达式做删除器 56 auto delArrOBJ = [](Date* ptr) { delete [] ptr; }; 57 unique_ptr<Date, decltype (delArrOBJ)> up4 ( new Date[ 5 ], delArrOBJ); 58 shared_ptr<Date> sp4 ( new Date[ 5 ], delArrOBJ); 59 60 // 实现其他资源管理的删除器 61 shared_ptr<FILE> sp5 (fopen( "Test.cpp" , "r" ), Fclose()); 62 shared_ptr<FILE> sp6 (fopen( "Test.cpp" , "r" ), [](FILE* ptr) { 63 cout << "fclose:" << ptr << endl; 64 fclose(ptr); 65 }); 66 67 return 0 ; 68 }
1 int main () 2 { 3 shared_ptr<Date> sp1 ( new Date( 2024 , 9 , 11 )); 4 shared_ptr<Date> sp2 = make_shared <Date>( 2024 , 9 , 11 ); 5 auto sp3 = make_shared <Date>( 2024 , 9 , 11 ); 6 shared_ptr<Date> sp4; 7 8 // if (sp1.operator bool()) 9 if (sp1) 10 cout << "sp1 is not nullptr" << endl; 11 12 if (!sp4) 13 cout << "sp1 is nullptr" << endl; 14 15 // 报错 16 shared_ptr<Date> sp5 = new Date ( 2024 , 9 , 11 ); 17 unique_ptr<Date> sp6 = new Date ( 2024 , 9 , 11 ); 18 19 return 0 ; 20 }
下⾯我们模拟实现了auto_ptr和unique_ptr的核⼼功能,这两个智能指针的实现⽐较简单,⼤家了
解⼀下原理即可。auto_ptr的思路是拷⻉时转移资源管理权给被拷⻉对象,这种思路是不被认可
的,也不建议使⽤。unique_ptr的思路是不⽀持拷⻉。
⼤家重点要看看shared_ptr是如何设计的,尤其是引⽤计数的设计,主要这⾥⼀份资源就需要⼀个
引⽤计数,所以引⽤计数才⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智
能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计
数,shared_ptr对象析构时就--引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀
个管理资源的对象,则析构资源。
1namespace zkf 2{ 3template < class T > 4class auto_ptr 5{ 6public : 7auto_ptr (T* ptr) 8:_ptr(ptr) 9{} 10auto_ptr (auto_ptr<T>& sp) 11:_ptr(sp._ptr) 12{ 13// 管理权转移 14sp._ptr = nullptr ; 15} 161auto_ptr<T>& operator =(auto_ptr<T>& ap) 17{ 20 // 检测是否为⾃⼰给⾃⼰赋值 21 if ( this != &ap) 22 { 23 // 释放当前对象中资源 24 if (_ptr) 25 delete _ptr; 26 27 // 转移 ap 中资源到当前对象中 28 _ptr = ap._ptr; 29 ap._ptr = NULL ; 30 } 31 32 return * this ; 33 } 34 35 ~ auto_ptr () 36 { 37 if (_ptr) 38 { 39 cout << "delete:" << _ptr << endl; 40 delete _ptr; 41 } 42 } 43 44 // 像指针⼀样使⽤ 45 T& operator *() 46 { 47 return *_ptr; 48 } 49 50 T* operator ->() 51 { 52 return _ptr; 53 } 54 private : 55 T* _ptr; 56 }; 57 58 template < class T > 59 class unique_ptr 60 { 61 public : 62 explicit unique_ptr (T* ptr) 63 :_ptr(ptr) 64 {} 65 66 ~ unique_ptr () 67 { 68 if (_ptr) 69 { 70 cout << "delete:" << _ptr << endl; 71 delete _ptr; 72 } 73 } 74 75 // 像指针⼀样使⽤ 76 T& operator *() 77 { 78 return *_ptr; 79 } 80 81 T* operator ->() 82 { 83 return _ptr; 84 } 85 86 unique_ptr ( const unique_ptr<T>& sp) = delete ; 87 unique_ptr<T>& operator =( const unique_ptr<T>& sp) = delete ; 88 unique_ptr (unique_ptr<T>&& sp) 89 :_ptr(sp._ptr) 90 { 91 sp._ptr = nullptr ; 92 } 93 94 unique_ptr<T>& operator =(unique_ptr<T>&& sp) 95 { 96 delete _ptr; 97 _ptr = sp._ptr; 98 sp._ptr = nullptr ; 99 } 100 private : 101 T* _ptr; 102 }; 103 104 template < class T > 105 class shared_ptr 106 { 107 public : 108 explicit shared_ptr (T* ptr = nullptr ) 109 : _ptr(ptr) 110 , _pcount(new int( 1 )) 111 {} 112 113 template < class D> 114 shared_ptr (T* ptr, D del) 115 : _ptr(ptr) 116 , _pcount(new int( 1 )) 117 , _del(del) 118 {} 119 120 shared_ptr ( const shared_ptr<T>& sp) 121 :_ptr(sp._ptr) 122 , _pcount(sp._pcount) 123 ,_del(sp._del) 124 { 125 ++(*_pcount); 126 } 127 128 void release () 129 { 130 if (--(*_pcount) == 0 ) 131 { 132 // 最后⼀个管理的对象,释放资源 133 _del(_ptr); 134 delete _pcount; 135 _ptr = nullptr ; 136 _pcount = nullptr ; 137 } 138 } 139 140 shared_ptr<T>& operator =( const shared_ptr<T>& sp) 141 { 142 if (_ptr != sp._ptr) 143 { 144 release (); 145 146 _ptr = sp._ptr; 147 _pcount = sp._pcount; 148 ++(*_pcount); 149 _del = sp._del; 150 } 151 152 return * this ; 153 } 154 155 ~ shared_ptr () 156 { 157 release (); 158 } 159 160 T* get () const 161 { 162 return _ptr; 163 } 164 165 int use_count () const 166 { 167 return *_pcount; 168 } 169 170 T& operator *() 171 { 172 return *_ptr; 173 } 174 175 T* operator ->() 176 { 177 return _ptr; 178 } 179 private : 180 T* _ptr; 181 int * _pcount; 182 //atomic<int>* _pcount; 183 184 function< void (T*)> _del = [](T* ptr) { delete ptr; }; 185 }; 186 187 // 需要注意的是我们这⾥实现的 shared_ptr 和 weak_ptr 都是以最简洁的⽅式实现的, 188 // 只能满⾜基本的功能,这⾥的 weak_ptr lock 等功能是⽆法实现的,想要实现就要 189 // 把 shared_ptr 和 weak_ptr ⼀起改了,把引⽤计数拿出来放到⼀个单独类型, shared_ptr 190 // 和 weak_ptr 都要存储指向这个类的对象才能实现,有兴趣可以去翻翻源代码 191 template < class T > 192 class weak_ptr 193 { 194 public : 195 weak_ptr () 196 {} 197 198 weak_ptr ( const shared_ptr<T>& sp) 199 :_ptr(sp. get ()) 200 {} 201 202 weak_ptr<T>& operator =( const shared_ptr<T>& sp) 203 { 204 _ptr = sp. get (); 205 206 return * this ; 207 } private : T* _ptr = nullptr ; }; } int main () { zkf::auto_ptr<Date> ap1 ( new Date); // 拷⻉时,管理权限转移,被拷⻉对象 ap1 悬空 zkf::auto_ptr<Date> ap2 (ap1); // 空指针访问, ap1 对象已经悬空 //ap1->_year++; zkf::unique_ptr<Date> up1 ( new Date); // 不⽀持拷⻉ //unique_ptr<Date> up2(up1); // ⽀持移动,但是移动后 up1 也悬空,所以使⽤移动要谨慎 zkf::unique_ptr<Date> up3 (move(up1)); zkf::shared_ptr<Date> sp1 ( new Date); // ⽀持拷⻉ zkf::shared_ptr<Date> sp2 (sp1); zkf::shared_ptr<Date> sp3 (sp2); cout << sp1. use_count () << endl; sp1->_year++; cout << sp1->_year << endl; cout << sp2->_year << endl; cout << sp3->_year << endl; return 0 ; }
shared_ptr⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉。但是在循环引⽤的场景下会
导致资源没得到释放内存泄漏,所以我们要认识循环引⽤的场景和资源没释放的原因,并且学会使
⽤weak_ptr解决这种问题。
如下图所述场景,n1和n2析构后,管理两个节点的引⽤计数减到1
1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
2. _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
4. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
⾄此逻辑上成功形成回旋镖似的循环引⽤,谁都不会释放就形成了循环引⽤,导致内存泄漏
把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的
引⽤计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引⽤,解决了这⾥的问题
1 struct ListNode 2 { 3 int _data; 4 5 std::shared_ptr<ListNode> _next; 6 std::shared_ptr<ListNode> _prev; 7 8 // 这⾥改成 weak_ptr ,当 n1->_next = n2; 绑定 shared_ptr 时 9 // 不增加 n2 的引⽤计数,不参与资源释放的管理,就不会形成循环引⽤了 10 /*std::weak_ptr<ListNode> _next; 11 std::weak_ptr<ListNode> _prev;*/ 12 13 ~ ListNode () 14 { 15 cout << "~ListNode()" << endl; 16 } 17 }; 18 19 int main () 20 { 21 // 循环引⽤ -- 内存泄露 22 std::shared_ptr<ListNode> n1 ( new ListNode); 23 std::shared_ptr<ListNode> n2 ( new ListNode); 24 25 cout << n1. use_count () << endl; 26 cout << n2. use_count () << endl; 27 28 n1->_next = n2; 29 n2->_prev = n1; 30 31 cout << n1. use_count () << endl; cout << n2. use_count () << endl; 32// weak_ptr 不⽀持管理资源,不⽀持 RAII 33// weak_ptr 是专⻔绑定 shared_ptr ,不增加他的引⽤计数,作为⼀些场景的辅助管理 34//std::weak_ptr<ListNode> wp(new ListNode); 35return 0 ; 36}
weak_ptr 不⽀持RAII,也不⽀持访问资源,所以我们看⽂档发现weak_ptr构造时不⽀持绑定到资
源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以
解决上述的循环引⽤问题。
weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的
shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr⽀持expired检查指向的
资源是否过期,use_count也可获取shared_ptr的引⽤计数,weak_ptr想访问资源时,可以调⽤
lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如
果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
int main () { std::shared_ptr<string> sp1 ( new string( "111111" )); std::shared_ptr<string> sp2 (sp1); std::weak_ptr<string> wp = sp1; cout << wp. expired () << endl; cout << wp. use_count () << endl; // sp1 和 sp2 都指向了其他资源,则 weak_ptr 就过期了 sp1 = make_shared <string>( "222222" ); cout << wp. expired () << endl; cout << wp. use_count () << endl; sp2 = make_shared <string>( "333333" ); cout << wp. expired () << endl; cout << wp. use_count () << endl; wp = sp1; //std::shared_ptr<string> sp3 = wp.lock(); auto sp3 = wp. lock (); cout << wp. expired () << endl; cout << wp. use_count () << endl; *sp3 += "###" ; cout << *sp1 << endl; return 0 ; }
shared_ptr的引⽤计数对象在堆上,如果多个shared_ptr对象在多个线程中,进⾏shared_ptr的拷
⻉析构时会访问修改引⽤计数,就会存在线程安全问题,所以shared_ptr引⽤计数是需要加锁或者
原⼦操作保证线程安全的。
shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr
管,它也管不了,应该有外层使⽤shared_ptr的⼈进⾏线程安全的控制。
下⾯的程序会崩溃或者A资源没释放,bit::shared_ptr引⽤计数从int*改成atomic<int>*就可以保证
引⽤计数的线程安全问题,或者使⽤互斥锁加锁也可以。
struct AA { int _a1 = 0 ; int _a2 = 0 ; ~ AA () { cout << "~AA()" << endl; } }; int main () { bit::shared_ptr<AA> p ( new AA); const size_t n = 100000 ; mutex mtx; auto func = [&]() { for ( size_t i = 0 ; i < n; ++i) { // 这⾥智能指针拷⻉会 ++ 计数 bit::shared_ptr<AA> copy (p); { unique_lock<mutex> lk (mtx); copy->_a1++; copy->_a2++; } } }; thread t1 (func); thread t2 (func); t1. join (); t2. join (); cout << p->_a1 << endl; cout << p->_a2 << endl; cout << p. use_count () << endl; return 0 ; }
Boost库是为C++语⾔标准库提供扩展的⼀些C++程序库的总称,Boost社区建⽴的初衷之⼀就是为
C++的标准化⼯作提供可供参考的实现,Boost社区的发起⼈Dawes本⼈就是C++标准委员会的成员
之⼀。在Boost库的开发中,Boost社区也在这个⽅向上取得了丰硕的成果,C++11及之后的新语法
和库有很多都是从Boost中来的。
C++ 98 中产⽣了第⼀个智能指针auto_ptr。
C++ boost给出了更实⽤的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等.
C++ TR1,引⼊了shared_ptr等,不过注意的是TR1并不是标准版。
C++ 11,引⼊了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的
scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
结束语 本篇博客结束,至此,C++所有的基础知识总结完毕,Linux见!