• C++11


    C++11简介

    在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1),使得C03这个名字已经取代了C98称为C11之前的最新C标准名称。不过由于C03(TC1)主要是对C98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C98/03标准。从C0x到C11,C标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C98/03,C11则带来了数量可观的变化,其中包含了约140个新特性,以及对C03标准中约600个缺陷的修正,这使得C11更像是从C98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。

    小故事:
    1998年是C标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C标准。所以最终定名为C11。

    列表初始化{}

    在C++98中,标准允许使用花括号对数组或者结构体元素进行统一的列表初始值设定。

    1. struct A
    2. {
    3. int a;
    4. }
    5. int main()
    6. {
    7. int arr[] = {1, 2};
    8. A a = {1};
    9. return 0;
    10. }

    C++中定义了几种不同的初始化方式,现在我们要将一个int类型的变量a初始化为0.

    1. int a = 0;
    2. int a = {0};
    3. int a{0};
    4. int a(0);
    5. int main()
    6. {
    7. int a = 0;
    8. int a1 = { 0 };
    9. int a2{ 0 };
    10. int a3(0);
    11. int arr[]{ 1,2,3 };
    12. int arr1[5]{ 0 };
    13. cout << a << endl;
    14. cout << a1 << endl;
    15. cout << a2 << endl;
    16. cout << a3 << endl;
    17. return 0;
    18. }

    输出结果 0 0 0 0

    C++11扩大了花括号括起的列表的适用范围,现在可以用于所有的内置类型和用户自定义的类型,使用初始化列表时,可以添加等号,也可以不添加。

    这种初始化方式称为列表初始化。


    创建对象的时候也可以使用列表初始化的方式来调用构造函数进行初始化。

    1. class Date
    2. {
    3. public:
    4. Date(int y, int m, int d)
    5. :_y(y)
    6. ,_m(m)
    7. ,_d(d)
    8. {
    9. cout << "Date(int y, int m, int d)" << endl;
    10. }
    11. private:
    12. int _y;
    13. int _m;
    14. int _d;
    15. };
    16. int main()
    17. {
    18. Date date(2023, 10, 8);
    19. Date time{2024, 10, 8};
    20. return 0;
    21. }

    std::initializer_list

    有时候,我们不知道应该提前向某个函数传递几个参数,为了能够编写出能处理不同类型数量实参的参数,C++11新标准提供了两种主要的方式:如果所有的参数类型相同,可以传递一个名为 initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板(暂时先不说)。

    initializer_list用于表示某种特定类型值的数组,它定义在同名的头文件当中。并且用法和vector类似。有size,begin,end等。。在定义 initializer_list的时候,必须指定模板类型。

    1. #include <initializer_list>
    2. int main()
    3. {
    4. initializer_list<int> il = { 1, 2, 3 };
    5. initializer_list<int> il1 = il;
    6. initializer_list<int>il2(il1);
    7. il.size();
    8. auto it = il.begin();
    9. auto ed = il.end();
    10. return 0;
    11. }

    和vector不一样的是 initializer_list对象中的元素永远是常量值,并且无法改变它对象中元素的值。


    使用场景

    initializer_list一般是作为构造函数的参数,C++!1对STL中的不少容器增加 initializer_list作为参数的构造函数,这样初始化容器对象就方便了,也可以作为 operator=的参数,这样就可以用大括号赋值。

    1. #include <list>
    2. #include <vector>
    3. #include <map>
    4. int main()
    5. {
    6. vector<int> v = { 1,2, 3, 4 };
    7. list<int> lt = { 1, 2 };
    8. map<string, string> dict = { {"sort","排序"},{"insert","插入"} };
    9. return 0;
    10. }

    auto

    在C98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C11中废弃了auto原来的用法,可以让编译器自动分析表达式的类型。auto可以自动推导,这就很显然,auto定义的变量,一定要有初始值

    1. int i = 10;
    2. auto a = &i;
    3. cout << typeid(a).name() << endl;

    输出:int *

    decltype

    从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值进行初始化,C++11中引入了 decltype,它的作用是选择并返回操作数的数据类型,在这个过程中,编译器分析并得到它的类型,但是并不会实际计算表达式的值。

    1. template<class T1, class T2>
    2. void F(T1 a, T2 b)
    3. {
    4. decltype(a * b) ret;
    5. cout << typeid(ret).name() << endl;
    6. }
    7. int main()
    8. {
    9. int x = 1;
    10. double y = 2.2;
    11. decltype(x * y) ret;
    12. decltype(&x) p;
    13. cout << typeid(ret).name() << endl;
    14. cout << typeid(p).name() << endl;
    15. F(1, 'a');
    16. return 0;
    17. }

    typeid(var).name()可以输出var的类型。如果var是引用类型则输出结果不会带  &


    1. int i = 0;
    2. int& a = i;
    3. // decltype((i)) e;
    4. cout << typeid(decltype((i))).name()<< endl;
    5. cout << typeid(decltype(a)).name() << endl;

    这个e会报错,因为 如果变量名上加上了一对括号,则得到的类型与不加括号时会有不同。如果使用的是不加括号的变量,则得到的结果就是表达式的类型,相反,加上括号得到的结果会是引用类型


    如果 decltype中的内存是指针解引用,那么这个变量就是引用类型,必须初始化。

    decltype((var))像这样得到的结果永远是引用。为单括号的时候,只有当var本身是一个引用的时候才是引用。

    nullptr

    在之前我们会用到名为NULL的预处理变量来给指针赋值,它的值是0,这样就可能会带来一些问题,因为0既能表示整形常量,也能表示指针常量。C++11中新增了 nullptr,用于表示空指针。

    1. #ifndef NULL
    2. #ifdef __cplusplus
    3. #define NULL 0
    4. #else
    5. #define NULL ((void *)0)
    6. #endif
    7. #endif

    范围for循环

    C++11中新增了一个范围for,这个东西非常的好用,如果你想对string中的每个字符做点什么事,范围for挺合适的。这种遍历语句会遍历给定序列中的每个元素。

    1. for (declaration : expression)
    2. {
    3. statement
    4. }
    5. expression部分是一个对象,用于表示一个序列。
    6. declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化为下一个变量。
    7. 其实就是用迭代器实现的。

    1. int main()
    2. {
    3. string str = "hao hao xue xue xi";
    4. for (auto t : str)
    5. {
    6. cout << t << " " << endl;
    7. }
    8. return 0;
    9. }

    左值引用和右值引用

    左值是一个表示数据的表达式(如变量名或解引用的指针),可以获取它的地址,可以对他赋值,左值可以出现在赋值符号的左边,右值不能出现在赋值符号的左边。定义时const修饰符后的左值,不能给他赋值,但是可以对他取地址。左值引用就是给左值的引用,给左值取别名。

    1. int main()
    2. {
    3. // 左值
    4. int* p = new int(0);
    5. int b = 1;
    6. const int c = 2;
    7. // 对上面左值的引用
    8. int*& rp = p;
    9. int& rb = b;
    10. const int& rc = c;
    11. int& pvalue = *p;
    12. return 0;
    13. }

    右值也是一个表示数据的表达式,如字面常量,表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

    右值引用是通过 &&来完成的。

    右值引用有一个重要的性质,只能绑定到一个将要销毁的对象,因此,我们可以自由的将一个右值引用的资源移动到另一个对象身上(后面会说什么意思)

    1. int main()
    2. {
    3. double x = 1.1, y = 2.2;
    4. // 右值
    5. 10;
    6. x + y;
    7. fmin(x, y);
    8. // 对右值的右值引用
    9. int&& rr1 = 10;
    10. double&& rr2 = x + y;
    11. double&& rr3 = fmin(x, y);
    12. }

    需要注意的是右值不能取地址的,但是给右值取别名之后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说,不可以对10取地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1.如果不能修改rr1,可以用const引用。

    1. int main()
    2. {
    3. int&& rr1 = 10;
    4. rr1 = 20;
    5. cout << rr1 << endl;
    6. } // 输出 20

    无论左值引用还是右值引用,都是给对象取别名。


    左值

    左值引用只能引用左值,不能引用右值,但是const左值既可以引用左值,也可以引用右值。

    1. int a = 10;
    2. int& ra = a;
    3. int&& rra = 10;
    4. const int& ra1 = rra;

    右值

    右值引用只能引用右值,不能引用左值,但是右值引用可以引用move以后的左值。

    虽然不能将一个右值引用直接绑定到一个左值上,但是我们可以显示的将一个左值转换为对应的右值引用类型。通过调用一个move的新标准库函数来获得绑定到做之上的右值引用,此函数定义在头文件utility中。

    move函数调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。应该使用std::move,避免潜在的名字冲突

    1. int main()
    2. {
    3. int a = 10;
    4. int&& r = std::move(a); // 右值引用可以引用move以后的值
    5. cout << r << endl;
    6. } // 输出 10

    右值引用使用场景和意义

    上面说了左值引用和右值引用,那么为什么C++11还要提出右值引用呢?

    如果我们用前面自己实现的string做题,在很多情况下,会发生对象拷贝(接收函数返回值,拷贝等)在某些情况下,这些对象拷贝后就会立刻销毁。如下面代码的情况,虽然编译器会进行优化,但是如果b非常的大,在拷贝的时候,也会减少性能。

    1. string func()
    2. {
    3. string b;
    4. cin >> b;
    5. return b;
    6. }
    7. int main()
    8. {
    9. string a = func();
    10. return 0;
    11. }

    提出右值引用后,我们可以 移动对象

    移动构造函数

    与拷贝构造函数类似,但移动构造函数是从对象中交换资源而不是拷贝资源,且第一个参数是该类类型的一个右值引用。但是也不能随便的交换资源,要保证所交换的对象处于 即将被销毁的,销毁该对象对整个程序没有影响的,在完成交换之后,源对象指向的就是即将被销毁的对象。在此过程中,并不会分配任何空间。而是会接管上面代码中 b的空间。这样就完成了对象的移动操作,此对象继续存在,将要销毁的对象继续销毁。

    自己先造一个轮子,用库里的看不出来。

    1. using namespace std;
    2. namespace haifan
    3. {
    4. class string
    5. {
    6. public:
    7. typedef char* iterator;
    8. iterator begin()
    9. {
    10. return _str;
    11. }
    12. iterator end()
    13. {
    14. return _str + _size;
    15. }
    16. string(const char* str = "")
    17. :_size(strlen(str))
    18. , _capacity(_size)
    19. {
    20. //cout << "string(char* str)" << endl;
    21. _str = new char[_capacity + 1];
    22. strcpy(_str, str);
    23. }
    24. // s1.swap(s2)
    25. void swap(string& s)
    26. {
    27. ::swap(_str, s._str);
    28. ::swap(_size, s._size);
    29. ::swap(_capacity, s._capacity);
    30. }
    31. // 拷贝构造
    32. string(const string& s)
    33. :_str(nullptr)
    34. {
    35. cout << "string(const string& s) -- 深拷贝" << endl;
    36. string tmp(s._str);
    37. swap(tmp);
    38. }
    39. // 赋值重载
    40. string& operator=(const string& s)
    41. {
    42. cout << "string& operator=(string s) -- 深拷贝" << endl;
    43. string tmp(s);
    44. swap(tmp);
    45. return *this;
    46. }
    47. // 移动构造
    48. string(string&& s)
    49. :_str(nullptr)
    50. , _size(0)
    51. , _capacity(0)
    52. {
    53. cout << "string(string&& s) -- 移动语义" << endl;
    54. swap(s);
    55. }
    56. // 移动赋值
    57. string& operator=(string&& s)
    58. {
    59. cout << "string& operator=(string&& s) -- 移动语义" << endl;
    60. swap(s);
    61. return *this;
    62. }
    63. ~string()
    64. {
    65. delete[] _str;
    66. _str = nullptr;
    67. }
    68. char& operator[](size_t pos)
    69. {
    70. assert(pos < _size);
    71. return _str[pos];
    72. }
    73. void reserve(size_t n)
    74. {
    75. if (n > _capacity)
    76. {
    77. char* tmp = new char[n + 1];
    78. strcpy(tmp, _str);
    79. delete[] _str;
    80. _str = tmp;
    81. _capacity = n;
    82. }
    83. }
    84. void push_back(char ch)
    85. {
    86. if (_size >= _capacity)
    87. {
    88. size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
    89. reserve(newcapacity);
    90. }
    91. _str[_size] = ch;
    92. ++_size;
    93. _str[_size] = '\0';
    94. }
    95. //string operator+=(char ch)
    96. string& operator+=(char ch)
    97. {
    98. push_back(ch);
    99. return *this;
    100. }
    101. const char* c_str() const
    102. {
    103. return _str;
    104. }
    105. private:
    106. char* _str;
    107. size_t _size;
    108. size_t _capacity; // 不包含最后做标识的\0
    109. };
    110. }

    1. HaiFan::string func()
    2. {
    3. HaiFan::string a = "aaaa";
    4. return a;
    5. }
    6. int main()
    7. {
    8. HaiFan::string str = func();
    9. return 0;
    10. }

    比如上面的代码,如果没有移动构造,程序运行的期间,执行的都是深拷贝,有了移动构造函数,就会大大的提高性能。

    移动赋值运算符

    如果说,我们要将一个非常大的对象(即将被销毁的)赋值给源对象,这会拉低性能,如果我们用右值引用,则可以直接将这两个对象的资源进行交换,这样就大大的提高了性能。

    1. int main()
    2. {
    3. HaiFan::string str;
    4. str = "aa";
    5. return 0;
    6. }

    可变参数模板

    一个可变参数模板就是一个接收可变参数的模板函数或模板类。刻板数目的参数被称为参数包。存在两种参数包:模板参数包,表示0个或多个模板参数。函数参数包,表示0个或多个函数参数。

    1. // Args是一个模板参数包,args是一个函数形参参数包
    2. // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
    3. template <class ...Args>
    4. void ShowList(Args... args)
    5. {}
    1. template<class T, class ...Args>
    2. void ShowList(T value, Args... args)
    3. {
    4. cout << sizeof...(args) << endl; // 插件args中的参数个数
    5. }
    6. int main()
    7. {
    8. ShowList(1); // 0
    9. ShowList(1,2); // 1
    10. ShowList(1,2,3); // 2
    11. ShowList(1,2,3,4); // 3
    12. return 0;
    13. }

    传递的参数和包里面的个数不一样是因为,value匹配的第一个参数。

    如果args是一个可变参数,那我们可以对其直接进行一些操作吗?

    1. template<class T, class ...Args>
    2. void ShowList(T value, Args... args)
    3. {
    4. cout << sizeof...(args) << endl; // 插件args中的参数个数
    5. for (int i = 0; i < sizeof...(args); i++)
    6. {
    7. cout << args[i] << endl;
    8. } // 报错信息 必须在上下文中扩展参数包
    9. }

    展开参数包的两种方式

    1. template <class T>
    2. void ShowList(T value)
    3. {
    4. cout << value << " ";
    5. cout << endl;
    6. }
    7. template<class T, class ...Args>
    8. void ShowList(T value, Args... args)
    9. {
    10. cout << value << " ";
    11. ShowList(args...);
    12. }
    13. int main()
    14. {
    15. ShowList(1);
    16. ShowList(1,2);
    17. ShowList(1,2,3);
    18. ShowList(1,2,3,4);
    19. return 0;
    20. }

    要对参数包进行操作,可以通过递归展开参数包。

    1. template< class T>
    2. void PrintArg(T t)
    3. {
    4. cout << t << " ";
    5. }
    6. template<class ...Args>
    7. void CppPrint(Args... args)
    8. {
    9. int a[] = { (PrintArg(args), 0)... };
    10. cout << endl;
    11. }
    12. int main()
    13. {
    14. CppPrint(1, 2, 3);
    15. return 0;
    16. }

    这样也可以展开参数包,通过数组,编译器会把后面的自动推出来,逗号表达式取得是最右面的值,利用0进行初始化。

    完美转发

    模板中的 && 万能引用

    模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

    1. void Fun(int& x) { cout << "左值引用" << endl; }
    2. void Fun(const int& x) { cout << "const 左值引用" << endl; }
    3. void Fun(int&& x) { cout << "右值引用" << endl; }
    4. void Fun(const int&& x) { cout << "const 右值引用" << endl; }
    5. // 万能引用 既可以接收左值,也可以接收右值
    6. // 实参左值 他就是左值引用(引用折叠)
    7. // 实参右值 他就是右值引用
    8. template<typename T>
    9. void PerfectForward(T&& t)
    10. {
    11. Fun(t);
    12. }
    13. int main()
    14. {
    15. PerfectForward(10); // 右值
    16. int a;
    17. PerfectForward(a); // 左值
    18. PerfectForward(std::move(a)); // 右值
    19. const int b = 8;
    20. PerfectForward(b); // const 左值
    21. PerfectForward(std::move(b)); // const 右值
    22. return 0;
    23. }

    在运行代码之后,全是左值引用。

    我们可以用库里面的一个函数 std::forward,当用于一个指向模板参数类型的右值引用函数参数T&&时,forward会保持实参类型的所有细节

    1. template<typename T>
    2. void PerfectForward(T&& t)
    3. {
    4. Fun(std::forward<T>(t));
    5. }

    完美转发,t是左值引用,保持左值属性,t是右值引用,保持右值属性。

    STL中的一些变化

    在C++11中新增了 , , , 这几个新容器,但是实际最有用的是 unordered_map/set这两个容器,都是用哈希来实现的,查找等效率都是O(1)。

    如果我们在仔细的看,会发现基本每个容器中都增加了一些C++11的方法,但其实很多用的都比较少。比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义并不大,因为begin和end也是可以返回const迭代器的,这些都属于锦上添花的操作。

    实际上C++11更新后,容器中增加的新方法最后用的接口插入接口函数的右值引用版本。

    平常我们用的 尾插push_back,在 vector> v的情况下,我们尾插需要先用make_pair构造,然后再插入,emplace_back利用参数包,直接完成构造。

    有了右值引用版本,在某些情况下,可以大大的提升性能。

    lambda表达式

    如果我们要对某种自定义类型进行排序,直接用sort函数是不可以的,我们可以用仿函数解决,也可以用lambda表达式解决。

    1. #include <iostream>
    2. #include <vector>
    3. #include <algorithm>
    4. using namespace std;
    5. struct Goods
    6. {
    7. string _name; // 名字
    8. double _price; // 价格
    9. int _evaluate; // 评价
    10. Goods(const char* str, double price, int evaluate)
    11. :_name(str)
    12. , _price(price)
    13. , _evaluate(evaluate)
    14. {}
    15. };
    16. struct ComparePriceLess
    17. {
    18. bool operator()(const Goods& gl, const Goods& gr)
    19. {
    20. return gl._price < gr._price;
    21. }
    22. };
    23. struct ComparePriceGreater
    24. {
    25. bool operator()(const Goods& gl, const Goods& gr)
    26. {
    27. return gl._price > gr._price;
    28. }
    29. };
    30. int main()
    31. {
    32. vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
    33. 3 }, { "菠萝", 1.5, 4 } };
    34. sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    35. return g1._price < g2._price; });
    36. sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    37. return g1._price > g2._price; });
    38. sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    39. return g1._evaluate < g2._evaluate; });
    40. sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    41. return g1._evaluate > g2._evaluate; });
    42. }

    像这种自定义类型,如果要排序,每次比较的逻辑都不一样,这给程序猿带来了一些不便。

    lambda表达式语法

    书写格式 [capture-list](paramenters)mutable->return-type{statement}。

    1. lambda表达式各部分说明

    [capture-list] 捕捉列表, 该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代 码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

    (parameters) 参数列表, 与普通函数的参数列表一致,如果不需要传递参数,则可以连同()一起省略

    mutable 默认情况下,与普通函数总是一个const函数,mutable可以取消其常量性。使用该修饰 符时,参数列表不可以省略(即参数为空)。

    ->returntype 返回值类型。 用追踪返回类型形式声明函数的返回值类型, 没有返回值时此部分可省 略,返回值类型明确情况下,也可以省略,由编译器对返回类型进行推导。

    {statement} 函数体。 在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

    注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{},该lambda函数不能做任何事情。

    lambda函数中可以调用其他函数吗?

    1. void func()
    2. {
    3. cout << "123" << endl;
    4. }
    5. int main()
    6. {
    7. int a = 0;
    8. int b = 2;
    9. // 全局的函数可以使用
    10. auto add = [](int x, int y)->int {func(); return x + y; };
    11. auto swap1 = [](int& x, int& y) {
    12. int c = x;
    13. x = y;
    14. y = c;
    15. add(x, y);
    16. }; // 局部的不可以使用
    17. return 0;
    18. }
    1. 捕捉列表说明

    捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式传值还是传引用。

    [var] 表示值传递方式捕捉变量var

    [=] 表示值传递方式捕获所有父作用域中的变量(包括this)

    [&var] 表示引用传递捕捉变量var

    [&] 表示引用传递捕捉所有父作用域的变量(包括this)

    [this] 表示值传递方式捕捉当前的this指针

    auto add = [](int x, int y)->int {func(); return x + y; };

    注意:

    父作用域指包含lambda函数的语句块

    语法上捕捉列表可由多个捕捉项组成,并以逗号分割

    如 [=,&a] 以引用传递的方式捕捉变量a和b,值传递的方式捕捉其他所有变量

    捕捉列表不允许变量重复传递,否则就会导致编译错误

    [=, a] = 已经以值传递的方式捕捉了所有变量,捕捉a重复

    在块作用域以外的lambda函数捕获列表必须为空

    在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译错误

    lambda表达式之间不能相互赋值,即使看起来类型相同

    1. int main()
    2. {
    3. int a = 1;
    4. int b = 2;
    5. //auto swap1 = [a, b]() {
    6. // int c = 0;
    7. // a = b;
    8. // b = c;
    9. //}; // 这里会导致编译错误,这是因为捕获的变量是const属性
    10. auto swap1 = [a, b]() mutable {
    11. int c = 0;
    12. a = b;
    13. b = c;
    14. };
    15. cout << a << " " << b << endl;
    16. return 0;
    17. } // 输出结果 12

    为什么在调用玩swap1函数之后结果却没有改变,这是因为捕获变量之后,是临时变量。不会影响外面的a和b。


    想要真正的改变a和b,我们可以用引用捕捉。

    1. auto swap1 = [&a, &b]() mutable {
    2. int c = 0;
    3. a = b;
    4. b = c;
    5. };

    这里的&可不是把地址取出来,而是引用的意思。


    1. auto swap1 = [&]() mutable {
    2. cout << a << " " << b;
    3. }; // 输出结果 12
    4. auto swap2 = [=]() mutable {
    5. cout << a << " " << b;
    6. }; // 输出结果 12

    cosnt对象能不能被捕捉。

    1. int a = 1;
    2. int b = 2;
    3. const int c = 3;
    4. auto swap1 = [&]() mutable {
    5. cout << a << " " << b;
    6. c++; // 报错,能被捕捉,但是不能修改
    7. }; // 输出结果 12

    继上面说,要想在让函数调用局部变量中的函数,可以用捕捉列表捕捉局部函数,从而完成调用

    1. auto add = [](int a, int b) { return a + b; };
    2. auto swap2 = [add, a, b]() mutable {
    3. cout << add(a, b) << endl;
    4. };

    新的类功能

    原来C++类中,有6个默认成员函数:

    1. 构造函数
    2. 析构函数
    3. 拷贝构造函数
    4. 拷贝赋值重载
    5. 取地址重载
    6. const 取地址重载
      最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
      C++11 新增了两个:移动构造函数和移动赋值运算符重载。
      针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
      如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
      意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类
      型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
      如果实现了就调用移动构造,没有实现就调用拷贝构造。
      如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
      的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
      置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
      值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
      完全类似)
      如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
    1. class Person
    2. {
    3. public:
    4. Person(const char* name = "", int age = 0)
    5. :_name(name)
    6. , _age(age)
    7. {}
    8. ~Person()
    9. {}
    10. private:
    11. haifan::string _name;
    12. int _age;
    13. };
    14. int main()
    15. {
    16. Person s1;
    17. Person s2 = s1;
    18. Person s3 = std::move(s1);
    19. //Person s4;
    20. //s4 = std::move(s2);
    21. return 0;
    22. } // 用上面造的轮子,输出结果是 深拷贝,如果把Person析构函数注释掉,输出的是移动构造
    23. // Person(Person&& p) = default; 定义了default关键字之后,编译器就不会在生成=default的那个
    24. // 默认成员函数
    25. // Person(const Person& p) = default; 再定义这个之后,可以让编译器强制的执行string中的移动构造
    26. // 和移动赋值

    包装器

    function包装器也叫做适配器。C++中的function本质是一个类模板,也是一个包装器。

    定义在头文件functional中。

    1. ret = func(x)
    2. // 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
    3. // 是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
    4. // 为什么呢?我们继续往下看
    1. template<class F, class T>
    2. T useF(F f, T x)
    3. {
    4. static int count = 0;
    5. cout << "count:" << ++count << endl;
    6. cout << "count:" << &count << endl;
    7. return f(x);
    8. }
    9. double f(double i)
    10. {
    11. return i / 2;
    12. }
    13. struct Functor
    14. {
    15. double operator()(double d)
    16. {
    17. return d / 3;
    18. }
    19. };
    20. int main()
    21. {
    22. // 函数名
    23. cout << useF(f, 11.11) << endl;
    24. // 函数对象
    25. cout << useF(Functor(), 11.11) << endl;
    26. // lamber表达式
    27. cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
    28. return 0;
    29. }

    这个useF会被实例化成三份对象,导致模板效率低。


    包装器可以很好的解决这个问题。

    function是一个模板,当创建一个具体的function类型时我们要提供该类型能够表示的对象的调用形式。如:

    1. function<int(int,int)>
    2. 声明了一个funcion类,它可以接收两个int,返回一个int的可调用对象。
    3. 如:
    4. function<double(double)> f1 = f; // 函数指针
    5. function<double(double)> f2 = Functor(); // 函数对象类的对象
    6. function<double(double)> f3 = [](double d) -> double {return d / 4; }; // lambda

    通过f1,f2,f3可以完成调用。


    1. int add(int a, int b)
    2. {
    3. return a + b;
    4. }
    5. int sub(int a, int b)
    6. {
    7. return a - b;
    8. }
    9. int div1(int a, int b)
    10. {
    11. return a / b;
    12. }
    13. int mul(int a, int b)
    14. {
    15. return a * b;
    16. }
    17. #include <unordered_map>
    18. int main()
    19. {
    20. map<string, function<int(int, int)>> m = {
    21. {"+", add},
    22. {"-", sub},
    23. {"/", div1},
    24. {"*", mul},
    25. };
    26. for (auto t : m)
    27. {
    28. cout << t.second(1, 2) << endl;
    29. }
    30. for (auto i = m.begin(); i != m.end(); i++)
    31. {
    32. cout << i->second(1, 2) << endl;
    33. }
    34. cout << m["+"](1, 2) << endl;
    35. cout << m["-"](1, 2) << endl;
    36. cout << m["*"](1, 2) << endl;
    37. cout << m["/"](10, 5) << endl;
    38. return 0;
    39. }

    在这个代码中,我们依次调用了m中存储的每个操作。在第一个调用中,我们获得的元素存放着一个指向add函数的指针,因此调用m["+"](10,5)实际上是使用该指针调用add,并传入10和5,接下来调用的m["-"]返回一个存放着std::minus类型对象的funciton,我们将执行该对象的调用运算符。


    函数重载和function

    再有函数重载的时候,我们不能直接将重载函数的名字存入funciton类型的对象中。

    1. int add(int a, int b)
    2. {
    3. return a + b;
    4. }
    5. double add(double a, double b)
    6. {
    7. return a + b;
    8. }

    这样就会报错,解决二义性的问题这一条途径是存储函数指针,而非函数的名字。

    1. int (*fp)(int, int) = add;
    2. map<string, function<int(int, int)>> m = {
    3. {"+", fp},
    4. {"-", sub},
    5. {"/", div1},
    6. {"*", mul},
    7. };
    8. cout << m["+"](1, 2) << endl;

    同样我们也能用lambda来消除二义性问题。

    bind

    对于偶尔用的函数,我们可以用lambda写,如果我们需要多次调用一个函数,通常应该定义一个函数,而不是靠lambda。

    bind函数定义再头文件functional中,是一个函数模板,可以将bind看作一个通用的函数适配器。它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。

    1. 调用bind的一般形式为
    2. auto newCallable = bind(callable, arg_list);
    3. 其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给
    4. 顶的callable的参数。当我们调用newCallable的时候,newCallable会调用callable,并传递
    5. 给它arg_list中的参数。


    function<int(int, int)> rAdd = bind(add, placeholders::_1, placeholders::_2);

    像arg_list中会出现 placeholders::_1 这样的名字,意思是 add的第一个参数。_2就是第二个参数。

    如果将他们调换位置。

    function<int(int, int)> rAdd = bind(add,placeholders::_2, placeholders::_1);

    _1还是第一个参数,_2还是第二个参数。

    1. function<int(int, int)> rAdd = bind(add, placeholders::_1, placeholders::_2);
    2. cout << rAdd(1, 1) << endl;

    placeholders是命名空间,这个命名空间又定义在std中。


    现在有一个函数,有abc三个参数,给c传递固定值。

    1. int func(int a, int b, int c)
    2. {
    3. return a * (b - c);
    4. }
    5. int main()
    6. {
    7. function<int(int, int)> f = bind(func, placeholders::_1, placeholders::_2, 3);
    8. cout << f(1, 10) << endl;
    9. return 0;
    10. }

    可以直接给c固定值。

    不管这个要传固定值的参数位于func形参列表中的哪一个位置,bind中都是要从_1开始写。

    1. int func(int c, int a, int b)
    2. {
    3. return a * (b - c);
    4. }
    5. int main()
    6. {
    7. function<int(int, int)> f = bind(func, 3 ,placeholders::_1, placeholders::_2);
    8. cout << f(1, 10) << endl;
    9. return 0;
    10. }

    上面绑定的都是普通的函数,如果要绑定类的成员函数呢?

    1. class A
    2. {
    3. public:
    4. static int sub1(int a, int b)
    5. {
    6. return a - b;
    7. } // 用bind绑定静态的,只需要指定域即可。
    8. int sub2(int a, int b)
    9. {
    10. return a - b;
    11. } // 如果还是按照静态绑定一样,指定作用域,会报错。
    12. };
    13. int main()
    14. {
    15. function<int(int, int)> f1 = bind(A::sub1, placeholders::_1, placeholders::_2);
    16. A a;
    17. function<int(int, int)> f2 = bind(&A::sub2, &a,placeholders::_1, placeholders::_2);
    18. //1.非静态的成员函数要加上& 符号。
    19. //2. 非静态的应该是3个参数,还有一个this指针。这个参数也可以换成一个匿名对象
    20. return 0;
    21. }

    智能指针

    概念

    再C++中,动态内存管理是通过一对运算符来完成的,new再动态内存中为对象分配空间并返回一个指向该对象的指针,delete,接受一个动态对象的指针,销毁该对象,释放该对象与之关联的内存。

    动态内存再管理的时候很容易出问题,有时候再写了大量的代码之后,可能会忘了释放内存,这就导致了内存泄漏,有时再尚有指针引用内存的情况下,我们就delete,在这种情况下就会产生引用非法内存的指针。在有了异常之后,动态内存管理变得更加不容易,有了catch,会直接不执行下面的代码,若下面的代码涉及到了释放内存等,这也会导致内存泄漏。

    为了更容易的管理内存,C++11中给出了智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的智能指针有两种,区别在于管理底层指针的方式: shared_ptr 允许多个指针指向同一个对象; unique_ptr 则独占所指向的对象。标准库还定义了一个名为 weak_ptr的伴随类,他是一种弱引用,指向 shared_ptr 所管理的对象。这三种类型都定义在memory头文件中。


    在C++98中,其实就已经引入了智能指针 auto_ptr

    1. #include <iostream>
    2. using namespace std;
    3. class A
    4. {
    5. public:
    6. A(int b)
    7. :a(b)
    8. {}
    9. ~A()
    10. {
    11. cout << "~A " << this << endl;
    12. }
    13. private:
    14. int a;
    15. };
    16. int main()
    17. {
    18. auto_ptr<A> ap1(new A(1));
    19. auto_ptr<A> ap2(new A(2));
    20. auto_ptr<A> ap3(ap1);
    21. return 0;
    22. }

    这段代码在把ap1拷贝给ap3的时候,会做一件事情。

    如图所示,ap1变为empty,这是管理权转移,在拷贝的时候,会把被拷贝对象的资源管理权转移给拷贝对象,导致被拷贝对象悬空,访问就会出问题。

    如果在拷贝代码的下一行添加上

    1. ap1->a++;
    2. ap3->a++;

    程序就会崩溃。

    不太建议使用auto_ptr


    下面是一个简单的模拟实现

    1. namespace haifan
    2. {
    3. template <class T>
    4. class auto_ptr
    5. {
    6. public:
    7. auto_ptr(T* ptr)
    8. :_ptr(ptr)
    9. {}
    10. ~auto_ptr()
    11. {
    12. delete _ptr;
    13. }
    14. T& operator* ()
    15. {
    16. return *_ptr;
    17. }
    18. T* operator-> ()
    19. {
    20. return _ptr;
    21. }
    22. // 管理权转移
    23. auto_ptr(auto_ptr<T>& ap)
    24. :_ptr(ap._ptr)
    25. {
    26. ap._ptr = nullptr;
    27. }
    28. private:
    29. T* _ptr;
    30. };
    31. }

    RAII

    RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
    存、文件句柄、网络连接、互斥量等等)的简单技术。
    在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
    对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做
    法有两大好处:
    不需要显式地释放资源。
    采用这种方式,对象所需的资源在其生命期内始终保持有效

    unique_ptr

    一个unqieu_ptr 拥有它所指向的对象,只能有一个unique_ptr指向一个给定对象。当该对象被销毁的时候,它所指向的对象也被销毁。

    unique_ptr不支持普通的拷贝或者赋值操作。

    1. unique_ptr<string> p1(new string("aaa"));
    2. unique_ptr<string> p2(p1); // 错误,不支持拷贝
    3. unique_ptr<string> p3;
    4. p3 = p1; // 错误,不支持赋值

    这个智能指针简单粗暴,拷贝有问题,ok,那你就别拷贝了。只让一个人使用。

    1. namespace haifan
    2. {
    3. template <class T>
    4. class unique_ptr
    5. {
    6. public:
    7. unique_ptr(T* ptr)
    8. :_ptr(ptr)
    9. {}
    10. ~unique_ptr()
    11. {
    12. delete _ptr;
    13. }
    14. T& operator* ()
    15. {
    16. return *_ptr;
    17. }
    18. T* operator-> ()
    19. {
    20. return _ptr;
    21. }
    22. // 防止拷贝和赋值
    23. unique_ptr(unique_ptr<T>& ap) = delete;
    24. unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
    25. private:
    26. T* _ptr;
    27. };
    28. }

    不让进行拷贝和赋值也是不行的,总有一些场景需要完成这两样操作。

    shared_ptr

    1. shared_ptr<string> sp1(new string("aaa"));
    2. shared_ptr<string> sp2(sp1);
    3. shared_ptr<string> sp3;
    4. sp3 = sp1;

    shared_ptr可以和别人同时管理同一块空间。

    简单模拟实现

    1. namespace haifan
    2. {
    3. template <class T>
    4. class shared_ptr
    5. {
    6. shared_ptr(T* ptr)
    7. :_ptr(ptr)
    8. ,_cnt(new int(1))
    9. {}
    10. ~shared_ptr()
    11. {
    12. delete _ptr;
    13. }
    14. T& operator* ()
    15. {
    16. return *_ptr;
    17. }
    18. T* operator-> ()
    19. {
    20. return _ptr;
    21. }
    22. shared_ptr(shared_ptr<T>& sp)
    23. {
    24. *_cnt++;
    25. _ptr = sp._ptr;
    26. _cnt = sp._cnt;
    27. }
    28. void destory()
    29. {
    30. if (--(*cnt) == 0);
    31. {
    32. delete _ptr;
    33. delete _cnt;
    34. }
    35. }
    36. shared_ptr<T>& operator=(shared_ptr<T>& sp)
    37. {
    38. if (_ptr != sp._ptr)
    39. {
    40. destory();
    41. _ptr = sp;
    42. _cnt = sp._cnt;
    43. (*_cnt)++;
    44. }
    45. }
    46. private:
    47. T* _ptr;
    48. int* _cnt;
    49. };
    50. }

    shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

    1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共
      享。
    2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减
      一。
    3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
    4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对
      象就成野指针了

    shared_ptr的循环引用

    1. struct Node
    2. {
    3. int val;
    4. shared_ptr<Node> _next;
    5. shared_ptr<Node> _prev;
    6. Node()
    7. {
    8. cout << "Node()" << endl;
    9. }
    10. ~Node()
    11. {
    12. cout << "delete" << endl;
    13. }
    14. };
    15. int main()
    16. {
    17. shared_ptr<Node> sp1(new Node);
    18. shared_ptr<Node> sp2(new Node);
    19. sp1->_next = sp2;
    20. sp2->_prev = sp1;
    21. }

    这个代码,看上去没任何问题,运行起来也没有任何问题,但是有最大的问题--内存泄漏。

    代码输出结果 Node() Node()

    如果24行25行随便屏蔽一行,输出结果就变成 两个Node()和两个delete。

    这是shared_ptr的循环引用

    _prev管着左边的节点,_next管着右边的节点。

    什么时候_prev析构? 右边节点析构时,_prev析构

    什么时候_next析构? 左边节点析构时,_next析构

    左边节点什么时候析构? _prev析构左边节点就析构

    唉,你看,这成套娃了。

    这就是循环引用。

    既然有了问题,就要解决它

    weak_ptr

    这个智能指针是专门用来解决shared_ptr中的循环引用问题的。

    weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr 的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr 指向对象,对象也还是会被释放,因此,weak_ptr的名字抓住了这种智能指针弱共享对象的特点。

    1. struct Node
    2. {
    3. int val;
    4. weak_ptr<Node> _next;
    5. weak_ptr<Node> _prev;
    6. Node()
    7. {
    8. cout << "Node()" << endl;
    9. }
    10. ~Node()
    11. {
    12. cout << "delete" << endl;
    13. }
    14. };
    15. int main()
    16. {
    17. shared_ptr<Node> sp1(new Node);
    18. shared_ptr<Node> sp2(new Node);
    19. sp1->_next = sp2;
    20. sp2->_prev = sp1;
    21. }

    简单模拟实现

    1. namespace haifan
    2. {
    3. template <class T>
    4. class weak_ptr
    5. {
    6. public:
    7. weak_ptr
    8. (T* ptr)
    9. :_ptr(ptr)
    10. ,_cnt(new int(1))
    11. {}
    12. weak_ptr(const shared_ptr<T>& sp)
    13. :_ptr(sp._ptr)
    14. {}
    15. weak_ptr<T>& operator= (const shared_ptr<T>& sp)
    16. {
    17. _ptr = sp._ptr;
    18. return *this;
    19. }
    20. T& operator* ()
    21. {
    22. return *_ptr;
    23. }
    24. T* operator-> ()
    25. {
    26. return _ptr;
    27. }
    28. private:
    29. T* _ptr;
    30. };
    31. }

    定制删除器

    上面介绍了智能指针,但是在我们写代码的时候,有时候会写出 new A[10]和 new A(1)这样的代码,如果是第二个,delete没问题,如果是第一个,应该是delete []。如果delete的类型不匹配,就会出现错误(shared_ptr的默认释放空间是delete不是delete[])。

    通过定制删除器就可以完成。

    D del 就是定制删除器。

    1. template<class T>
    2. struct DeleteArray
    3. {
    4. void operator() (T* ptr)
    5. {
    6. delete[] ptr;
    7. }
    8. };
    9. int main()
    10. {
    11. shared_ptr<Node> sp1(new Node[10], DeleteArray<Node>());
    12. shared_ptr<Node> sp2((Node*)malloc(sizeof(Node)), [](Node* ptr) {
    13. free(ptr);
    14. });
    15. }

    1. #pragma once
    2. #include <functional>
    3. namespace haifan
    4. {
    5. template <class T>
    6. class shared_ptr
    7. {
    8. shared_ptr(T* ptr = nullptr)
    9. :_ptr(ptr)
    10. , _cnt(new int(1))
    11. {}
    12. template <class D>
    13. shared_ptr(T* ptr, D del)
    14. :_ptr(ptr)
    15. ,_cnt(new int(1))
    16. ,_del(del)
    17. {}
    18. ~shared_ptr()
    19. {
    20. delete _ptr;
    21. }
    22. T& operator* ()
    23. {
    24. return *_ptr;
    25. }
    26. T* operator-> ()
    27. {
    28. return _ptr;
    29. }
    30. shared_ptr(shared_ptr<T>& sp)
    31. {
    32. *_cnt++;
    33. _ptr = sp._ptr;
    34. _cnt = sp._cnt;
    35. }
    36. void destory()
    37. {
    38. if (--(*cnt) == 0);
    39. {
    40. delete _ptr;
    41. delete _cnt;
    42. _del(_ptr);
    43. }
    44. }
    45. shared_ptr<T>& operator=(shared_ptr<T>& sp)
    46. {
    47. if (_ptr != sp._ptr)
    48. {
    49. destory();
    50. _ptr = sp;
    51. _cnt = sp._cnt;
    52. (*_cnt)++;
    53. }
    54. }
    55. private:
    56. T* _ptr;
    57. int* _cnt;
    58. function<void(T*)> _del = [](T* ptr){delete ptr;};
    59. };
    60. }
    61. }

    内存泄漏

    什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
    存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
    该段内存的控制,因而造成了内存的浪费。
    内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
    内存泄漏会导致响应越来越慢,最终卡死。

    C/C++程序中一般我们关心两种方面的内存泄漏:
    堆内存泄漏(Heap leak)
    堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
    块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
    内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
    系统资源泄漏
    指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
    掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

    如何检测内存泄漏(了解)
    在linux下内存泄漏检测:linux下几款内存泄漏检测工具
    在windows下使用第三方工具:VLD工具说明
    其他工具:内存泄漏工具比较
    2.4如何避免内存泄漏

    1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:
      这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智
      能指针来管理才有保证。
    2. 采用RAII思想或者智能指针来管理资源。
    3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
    4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

    总结一下:
    内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄
    漏检测工具。

  • 相关阅读:
    特征工程-主成分分析PCA
    微信小程序开发15 项目实战 基于云开发开发一个在线商城小程序
    Qt 打印调试信息-怎样获取QTableWidget的行数和列数-读取QTableWidget表格中的数据
    Nginx: 413 – Request Entity Too Large Error and Solution
    ECMAScript modules规范示例详解
    2.力扣c++刷题-->移除元素
    内存:linear address,线性地址;维基的重要性
    群晖搭建docker系统和办公服务2
    藏在 Java 数组的背后,你可能忽略的知识点
    C++ —— 类和对象(终)
  • 原文地址:https://blog.csdn.net/weixin_73888239/article/details/133825525