• C++内存管理和模板


    目录

    内存管理

    new T[N]

     new和delete关键字的总结:

    定位new表达式(placement-new):

    作用:

    使用格式:

     使用场景:

      实例:

    调用析构函数的两个方法:

    池化技术:

    面试题:c语言的动态内存申请和c++的有什么不同

    1:本身存在不同:

    2:申请失败的结果不同:

    3:是否初始化

    4:是否需要计算空间的大小

    5:返回值不同:

    6:对于自定义类型的处理不同:

    内存泄露:

     内存泄露是指针丢了还是内存丢了?

    常见的内存泄露的情况:

    模板:

    泛型编程:

    定义:

     写法结构:

    实例:

    函数模板的实例化:

    显示实例化:

    模板可以搞多个参数

    通用和自定义函数同时出现优先自定义:

     实例化栈:

    静态数组:


    内存管理

    new T[N]

     举一个例子:

    1. #include
    2. using namespace std;
    3. class A
    4. {
    5. public:
    6. A(int a = 0)
    7. :_a(a)
    8. {
    9. cout << "A():" << this << endl;
    10. }
    11. ~A()
    12. {
    13. cout << "~A():" << this << endl;
    14. }
    15. private:
    16. int _a;
    17. };
    18. int main()
    19. {
    20. A*p1 = new A;
    21. A*p2 = new A[10];
    22. return 0;
    23. }

    我们写一个简单的类A,类A中只有一个成员变量_a和一个构造函数以及一个析构函数,当调用构造函数和析构函数时会出现提示。

    接下来,我们进行调试,转到反汇编:

     我们用new申请了一个对象的空间,只调用了两个函数:一个是operator new函数,这个函数内部和malloc函数非常相似,不同点在于operator函数申请空间失败的时候会抛异常。

    第二个函数是构造函数。

    总结:对于自定义类型,new申请空间首先会调用operator new函数,然后调用构造函数。

    对于

     我们来查看对应的反汇编:

     这里同样也是调用了两个函数。

    第一个函数operator new[],这个函数又相当于我们调用了10次operator new[]函数,完成申请十个对象。

    第二个函数相当于迭代调用构造函数,

     这里的vector表示的是顺序表,这里有迭代的意思。

    总结:operator new[]:相当于调用了多次operator new函数,并调用了多次构造函数。

     new和delete关键字的总结:

    定位new表达式(placement-new):

    作用:

     定位new表达式的作用是在已分配的原始内存空间中调用构造函数初始化一个对象。

    使用格式:

     使用场景:

      实例:

    1. class A
    2. {
    3. public:
    4. A(int a = 0)
    5. :_a(a)
    6. {
    7. cout << "A():" << this << endl;
    8. }
    9. ~A()
    10. {
    11. cout << "~A():" << this << endl;
    12. }
    13. private:
    14. int _a;
    15. };
    16. int main()
    17. {
    18. A*p1 = new A;
    19. /*A*p2 = new A[10];*/
    20. A*p3 = (A*)malloc(sizeof(A));
    21. if (p3 == nullptr)
    22. {
    23. perror("malloc fail");
    24. exit(-1);
    25. }
    26. new(p3)A(1);
    27. return 0;
    28. }

    我们知道,类的构造函数一般不能主动调用,一般是对象在创建的时候自动进行调用,但是这里我们是通过malloc申请的空间,malloc申请空间是不会调用构造函数的。

    p3指向我们申请的空间,假如我们想要对动态申请的对象调用构造函数,这里就用到了定位new表达式:

    new(p3)A(1);

    这串代码可以这样翻译:对动态申请的对象p3调用构造函数,初始化的值为1.

    我们进行调试,查看是否调用完成构造函数:

     调用完成了构造函数。

    调用析构函数的两个方法:

    方法1:直接使用delete,例如:

    delete p3;

    我们进行编译,假如打印了~A()表示我们调用了析构函数。

    方法1是可行的。 

    方法2:通过p3来调用析构函数。

    举一个例子:

    1. new(p3)A(1);
    2. p3->~A();

    我们可以这样写,进行编译:

     成功调用了析构函数。

    池化技术:

     假如我们不断使用malloc和new在堆上申请空间(类比每一次都去水井取水),但是我们想要高效率的申请空间,我们可以准备一个内存池(为了取水方便,我们在家里使用一个蓄水池),当我们想使用堆上的空间,池中的内存就足够我们使用(当我们想要使用水,使用家里蓄水池的水即可),但是内存池的内存没有调用构造函数进行初始化(自家的蓄水池中的水不干净),所以我们就是用定位new的方法来初始化内存(用净水器的方法过滤家里蓄水池中的水)。

    总结:定位new表达式的作用就是对已经申请过的没有调用构造函数初始化的动态对象进行初始化。

    对于将来要学习的内存池,我们的定位new也能够对内存池哪些没有初始化的内存进行初始化。

    面试题:c语言的动态内存申请和c++的有什么不同

    1:本身存在不同:

    c++的new和delete是操作符,而c语言的malloc和free是函数不同。

    2:申请失败的结果不同:

    c语言申请失败后返回空指针,c++申请失败后抛异常

    3:是否初始化

    malloc申请的对象不会被初始化,而new申请的对象可以被初始化。

    4:是否需要计算空间的大小

    malloc申请时需要我们自己计算所要申请空间的大小,而new申请时,我们只需要输入申请的对象和对象的个数即可。

    5:返回值不同:

    malloc申请对象的返回值是void*,而new申请对象不需要返回值,因为new后面直接跟的是类的类型。

    6:对于自定义类型的处理不同:

    对于自定义类型,new申请动态对象(delete释放空间)时,会调用对象的构造函数(对象的析构函数),而malloc则只会申请空间。

    内存泄露:

     内存泄露是指针丢了还是内存丢了?

    答:内存泄露的本质上是指针丢了,失去了指针,我们就找不到那部分没有释放的空间,就造成了内存泄露。

    注意:只要程序正常结束了,内存就被回收了,就不存在内存泄露了。

    常见的内存泄露的情况:

    1. void MemoryLeaks()
    2. {
    3. //1:内存申请忘记了释放
    4. int *p1 = (int*)malloc(sizeof(int));
    5. int*p2 = new int;
    6. //2:异常安全问题
    7. int *p3 = new int[10];
    8. Func();//这里Func()函数抛异常导致delete [] p3未执行,p3未被释放。
    9. delete[] p3;
    10. }

    模板:

    先举例:

    1. void Swap(int &left, int&right)
    2. {
    3. int tmp = left;
    4. left = right;
    5. right = tmp;
    6. }
    7. int main()
    8. {
    9. int a = 1, b = 2;
    10. Swap(a, b);
    11. double c = 1.1, d = 2.2;
    12. Swap(c, d);
    13. return 0;
    14. }

    如图所示:我们只定义了一个整型的交换函数,对于c语言来说,假如我们想要调用浮点型类型的数据的交换,我们需要额外写一个不同命的函数,例如:

    1. void Swapd(double &left, double&right)
    2. {
    3. double tmp = left;
    4. left = right;
    5. right = tmp;
    6. }
    7. void Swapi(int &left, int&right)
    8. {
    9. int tmp = left;
    10. left = right;
    11. right = tmp;
    12. }
    13. int main()
    14. {
    15. int a = 1, b = 2;
    16. Swapi(a, b);
    17. double c = 1.1, d = 2.2;
    18. Swapd(c, d);
    19. return 0;
    20. }

    对于c++来说,我们方便了一些,我们可以写函数重载:

    1. void Swap(double &left, double&right)
    2. {
    3. double tmp = left;
    4. left = right;
    5. right = tmp;
    6. }
    7. void Swap(int &left, int&right)
    8. {
    9. int tmp = left;
    10. left = right;
    11. right = tmp;
    12. }
    13. int main()
    14. {
    15. int a = 1, b = 2;
    16. Swap(a, b);
    17. double c = 1.1, d = 2.2;
    18. Swap(c, d);
    19. return 0;
    20. }

    但是无论是c语言还是c++,我们要实现几种类型数据的交换,我们就需要写几个函数,无论是否重载。

    现在我们可以使用模板来让编译器替我们定义函数。

    泛型编程:

    定义:

     写法结构:

     这几种写法都是正确的,相同的。

    实例:

    1. template<typename T>
    2. void Swap(T &left, T&right)
    3. {
    4. T tmp = left;
    5. left = right;
    6. right = tmp;
    7. }
    8. int main()
    9. {
    10. int a = 1, b = 2;
    11. Swap(a, b);
    12. double c = 1.1, d = 2.2;
    13. Swap(c, d);
    14. return 0;
    15. }

    我们进行调试:

     我们可以发现,我们分别完成了整型数据的交换和浮点型数据的交换。

    问题1:我们这里写的Swap调用的是下面这个函数吗?

    答:并不是,首先,我们的函数开辟的栈帧带线大小就不同,因为对于字符类型的交换函数,一个字符对象只占一个字节,对于整型类型的交换函数来说,一个整型对象占4个字节,所以整型的交换函数天然的就比字符类型的交换函数调用时所开辟的栈帧空间大。

    函数调用时开辟的栈帧空间不同,所对应的函数就不同,所以我们肯定不会调用这个函数。

    具体的过程是这样的:

    上面是我们写的一个模具,假如我们在后面调用了int double类型的交换函数,那编译器就会自定义int double类型的函数来供我们使用。

    我们来查看反汇编:

     并且我们调用的函数的地址也不相同,所以我们调用的一定不是同一个函数。

    函数模板的实例化:

    如何实例化对象?

    答:根据我们的需求,假如我们调用了int类型的交换函数,我们就实例化int的交换函数。

    我们进行证明:

    1. template<typename T>
    2. void Swap(T &left, T&right)
    3. {
    4. T tmp = left;
    5. left = right;
    6. right = tmp;
    7. }
    8. int main()
    9. {
    10. int a = 1, b = 2;
    11. Swap(a, b);
    12. int c=2, d = 1;
    13. Swap(a, b);
    14. return 0;
    15. }

     我们转到反汇编:

     我们调用的函数是相同的。

    能不能实现不同类型的数据的交换:

    答:我们进行尝试:

    1. template<typename T>
    2. void Swap(T &left, T&right)
    3. {
    4. T tmp = left;
    5. left = right;
    6. right = tmp;
    7. }
    8. int main()
    9. {
    10. int x = 1, y = 2;
    11. double c = 1.1, d = 2.2;
    12. Swap(x, c);
    13. }

    如图所示:

    我们的x和c是不同类型的。

    我们进行编译:

     报错。

    原因是什么?

    答:因为假如我们传参传了一个整型和浮点型,我们的T类型具体对应的是哪个类型不清楚,所以就会报错。

    这里可能发生类型转换吗?

    答:不会,只有在传参和赋值的时候才会产生类型转换,我们这里只有类型推断,所以不能发生类型转换。

     我们再退出一个问题,以下代码可以调用吗?

    1. void Swap(double &left, double&right)
    2. {
    3. double tmp = left;
    4. left = right;
    5. right = tmp;
    6. }
    7. int main()
    8. {
    9. int a = 1;
    10. double c = 2.0;
    11. Swap(a, c);
    12. }

    不可以调用,原因如下:

    答:这里是传参没错,传参或赋值的时候会产生类型转换,会产生临时变量,这个临时变量具有常性,所以我们无法用引用接收常数,即使我们用const修饰参数,因为我们的函数是实现交换,所以两个参数值一定发生变化,用const修饰就不能改变了,产生矛盾。

    第一种写法:

    1. template<class T>
    2. T Add(const T&left, const T&right)
    3. {
    4. return left + right;
    5. }
    6. int main()
    7. {
    8. int a = 1;
    9. double c = 2.0;
    10. cout << Add((double)a, c) << endl;
    11. }

    我们可以采用强制类型转换的方法:

    我们进行调用:

     实现了相加。

    这里去掉了const就会报错,原因如下

    答:a强制类型转换出来的对象具有常性,引用是无法接收常数的。

     上面的这些都是自动推演实例化。

    显示实例化:

    例如:

    1. template<class T>
    2. T Add(const T&left,const T&right)
    3. {
    4. return left + right;
    5. }
    6. int main()
    7. {
    8. int a = 1;
    9. double c = 2.0;
    10. cout << Add<double>(a,c) << endl;
    11. }

    我们在Add后面加上<类型>,这里就相当于隐式类型转化了,相当于把不是double类型的参数隐式雷西那个转换为double。

    注意:显示实例化不会经过自动推演,因为我们已经规定了要调用的函数。

    模板可以搞多个参数

    例如:

    1. template<class T1,class T2>
    2. T1 Add(const T1& left, const T2& right)
    3. {
    4. return left + right;
    5. }
    6. int main()
    7. {
    8. int a = 10;
    9. double b = 20.1;
    10. cout << Add(a, b) << endl;
    11. return 0;
    12. }

    因为我们的模板有两个参数,所以我们可以接收两个不同类型的参数的函数的调用:

    通用和自定义函数同时出现优先自定义:

    例如:

    1. int Add(int left, int right)
    2. {
    3. return left + right;
    4. }
    5. template<class T>
    6. T Add(T left, T right)
    7. {
    8. return left + right;
    9. }
    10. int main()
    11. {
    12. int a = 1, b = 2;
    13. cout << Add(a, b) << endl;
    14. }

    我们进行调试观察:

     我们发现当通用和自定义同时存在时,调用函数会使用自定义的。

    假如我们不想调用自定义的,我们想调用实例化的:

    我们可以这样写:

     我们这样写之后就会调用实例化了:

     实例化栈:

    我们先写一个简写的栈:

    1. class Stack
    2. {
    3. public:
    4. Stack(int capacity = 4)
    5. {
    6. cout << "Stack(int capacity = )" <
    7. _a = (int*)malloc(sizeof(int)*capacity);
    8. if (_a == nullptr)
    9. {
    10. perror("malloc fail");
    11. exit(-1);
    12. }
    13. _top = 0;
    14. _capacity = capacity;
    15. }
    16. // st2(st1)
    17. // st1 = st2;
    18. // st1 = st1;
    19. void Push(int x)
    20. {
    21. // ....
    22. // 扩容
    23. _a[_top++] = x;
    24. }
    25. private:
    26. int* _a;
    27. int _top;
    28. int _capacity;
    29. };

    我们可以发现,我们输入到栈中的元素只会是整型,我们有没有办法输入其他数据类型?

    答:我们进行尝试:

    typedef int SLDateType;

    我们先把int替换成为SLDataType,假如我们想要修改数据类型时,我们只需要修改这里的int即可。

    假如我们想要输入浮点型数据:

     我们进行修改:

     我们进行调试:

     我们成功输入了浮点型数据。

    但是typedef还有一些解决不了的问题:

    例如:

    假如我们创建两个栈对象,一个输入浮点型数据,一个输入整形数据,那我们就不能够完成需求。

    所以,我们发现,我们这里使用typedef的作用是降低维护成本,而不是泛型。

    想解决这个问题,我们有两种思路:

    思路1:再创建一个不同名字的栈,对栈的内容进行修改,实现创建两个栈对象。

    但是这种方法不够泛型,我们可以写一个更好的。

    思路2:模具的方法:

    1. template<typename T>
    2. class Stack
    3. {
    4. public:
    5. Stack(int capacity = 4)
    6. {
    7. cout << "Stack(int capacity = )" <
    8. _a = (T*)malloc(sizeof(T)*capacity);
    9. if (_a == nullptr)
    10. {
    11. perror("malloc fail");
    12. exit(-1);
    13. }
    14. _top = 0;
    15. _capacity = capacity;
    16. }
    17. // st2(st1)
    18. // st1 = st2;
    19. // st1 = st1;
    20. void Push(T x)
    21. {
    22. // ....
    23. // 扩容
    24. _a[_top++] = x;
    25. }
    26. private:
    27. T* _a;
    28. int _top;
    29. int _capacity;
    30. };

    但是这里需要注意一个问题:

    我们入栈的函数的参数最好用引用:因为对于内置类型,传值传参和传引用没有什么区别,但是对于自定义类型,传值传参就会调用拷贝构造,所以我们这里使用传引用传参。

    又因为我们不会对x的值进行修改,所以我们可以在参数类型的上面加上const

    我们如何进行初始化呢?

     我们知道,函数模板可以推演实例化,因为调用函数就会传递参数,编译器会根据参数的类型来推演对应的实例化,但是对于类模板,我们没有推演的时机。

    所以我们可以用显示实例化的方法:

     那么,st1和st2对应的类是相同的类吗?

    答:不是,这两个类不相同,原因是在这里:

     

     假如我们这里不是指针,而是T _a,实例化的不同就会导致T的不同,T的不同类的大小就不同,大小不同的类是不会相同的。

    他们是一个模板实例化出来的

    但是模板参数不同,他们就是不同类型。

    那允许我们把st2赋给st1吗?

    答:不允许,如图:

     总结:不仅有函数模板,同样也有类模板,不过类模板不能进行推演实例化,而只能显示实例化。

    静态数组:

    1. #define N 10
    2. template<class T>
    3. class Array
    4. {
    5. public:
    6. T& operator[](size_t i)
    7. {
    8. return _a[i];
    9. }
    10. private:
    11. T _a[N];
    12. };

    在类Array中,有一个成员变量,该成员变量是一个数组,该成员变量的类型由T来控制,有一个成员函数,该成员函数是符号重载函数,表示把[]进行重载。

    这里就表示显示实例化:我们的类型是int类型。

     

     表示对数组进行初始化,a1是Array类型的成员,所以a1[i]就会返回成员变量_a[i]的改变,从而完成初始化。

    ​​​​​​​总结:我们写类的目的是为了通过成员函数或者其他函数完成对类的成员变量进行操作,来获取我们想要的结果

  • 相关阅读:
    CMake学习(二):使用CMake构建包含其他库的C++项目
    helm简介
    实现多层感知机
    Paper Survey——3DGS-SLAM
    maven 3.8.3 安装并配置的步骤
    文件内容的操作
    JVM调优必备理论知识-GC Collector-三色标记
    打造一站式采购结算平台,纸业B2B电子商务交易平台促进企业降本增效
    redis安全防护
    eBPF会成为服务网格的未来吗?
  • 原文地址:https://blog.csdn.net/qq_66581313/article/details/127533339