zoukankan      html  css  js  c++  java
  • C++ 学习笔记之---类和动态内存分配

    参考自《C++ Primer Plus 6th Edition》

     

    程序对内存的使用:

    链接:http://zhidao.baidu.com/link?url=An7QXTHSZF7zN9rAuY05mvaHHar0xIpgK6Yqp9oAkm2GmZYoTAz9UpN4JuhWJvSLsbu0-lOcO47PzXcNWda6gK


    1.  栈区 (stack) - 程序运行时由编译器自动分配,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。程序结束时由编译器自动释放。

    2.  堆区 (heap) - 在内存开辟另一块存储区域。一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

    3.  全局区 (静态区) (static) - 编译器编译时即分配内存。全局变量和静态变量的存储是放在一块的。初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。- 程序结束后由系统释放。

    4.  文字常量区 - 常量字符串就是放在这里的。程序结束后由系统释放

    5. 程序代码区-存放函数体的二进制代码。

    定义静态成员变量:

      可以在类声明中定义静态成员变量,使用 static 修饰。不过,虽说是成员变量,但是不属于这个类的任何一个对象。它们是是分开存储的。

      因为对所有对象,这个变量的值都是一样的,存储上也只用存一份就好。访问的时候,使用 "className::varName" 即可。绝大多数语言中可以定义静态变量,只是法上稍有不同。Java中的静态变量,既可以通过对象来访问,也可以通过类来访问。C++中就只能通过类名来访问。不过,Java通过对象来访问静态变量,实质上是通过类名来访问的。好吧,这个问题无关痛痒。

      其次,C++不允许在类声明中初始化静态成员变量。而且初始化的时候要使用作用域运算符,"className::varName"。一种"内部"的感觉。

    在类中定义常量:

      1. 编译时确定的常量

        存储: 对所有对象而言,这个常量都是一样的。因此和对象分开存储,仅保留一份副本。

        实现: 1. 枚举: 如 enum {SIZE = 100 }; 这就定义了一个枚举常量 SIZE = 100。

            当然,你可以定义多个,并给定类型名。

           2. 静态成员变量: 如 const static int a = 5;

             3. 用const限定并初始化, 如声明成员 const int id = 5 (C++ 11 拓展)
      

      2. 运行时确定的常量
        存储: 不同对象,可以有不同常量,属于对象的普通成员

        实现: 声明用const修饰的成员,然后用构造函数的成员初始化列表.

    #include <iostream>
    
    class Student
    {
    public:
        const int id;
        Student(int ID) : id(ID) {
        }
    };
    
    int main() {
        Student a(10);    // a的id常量为10
        Student b(20);    // b的id常量为20
        std::cout << a.id << " " << b.id << std::endl;
    }
    View Code

        

      成员初始化列表的初始化工作,是在对象创建后,构造函数函数体的代码执行前做的。对于内置类型成员的初始化,不管是放在初始化列表中初始化,还是放在函数体中初始化,效率是一样的。不过,对于对象成员来说,使用初始化列表来初始化,效率更高。暂且不提。要注意的一点是: 成员初始化列表只能用于构造函数。

    复制构造函数 与 赋值运算符:

    函数原型:

    copy constructor:    className (const className &)

    assignment operator:  className& operator=(const className &)

    当定义的类,有指针成员,且使用new初始化的时候,需要定义"深拷贝"的复制构造函数和赋值运算

    。(暂不考虑定位new,因为常规new申请的内存位于堆中,需要程序员手动delete。而定位new申请的内存地址是自行指定的,如果定在堆,则情况相同。如果定义在静态内存中,那就没我们的事儿了。交给OS 吧)

    基本概念:

    深拷贝:

      将一个对象拷贝给另一个对象的时候,被赋值的对象存储赋值对象的一个额外副本。若类成员中含有指针成员,且用new初始化的时候,被赋值的成员,会申请一块内存,将赋值对象的指针成员所指的内存的内容复制到这块内存中。两个指针各自指向自己申请的内存

    浅拷贝:

      和深拷贝相似,浅拷贝对于非指针成员都是直接赋值。但是当类成员中含指针成员,且用new初始化的时候,被赋值的成员指针并不会额外申请一块内存,而仅仅是将自己指向赋值对象的指针成员所指的那块内存。两个指针指向同一块内存

     当我们没有定义类的复制构造函数和赋值运算符时,编译器会生成默认的版本,它们使用浅拷贝

    回到上面所说的,为什么我们需要定义"深拷贝"的复制构造函数和赋值运算法捏 ? 难道,是因为默认的浅拷贝会导致错误 ?

    没错!  我们知道,如果定义的类中含指针成员,如果它将会使用new申请新内存。在析构函数中,我们会用delete释放相应的内存占用。

    考虑两种情况:

    1. 一个对象使用另外一个已有对象初始化,这样将调用默认复制构造函数(有可能还会调用赋值操作符,视编译器而定)。由于使用浅拷贝,就会存在这两个对象的指针成员指向同一块内存的情况,当这两个对象弃用时,会调用它们的析构函数。这样会出现同一块内存被释放两次的情况,出现未知的错误。

    类似地,如果你定义了一个返回对象的函数,也会造成同一块内存释放两次的情况,为啥 ? 因为这还将调用复制构造函数,按值传递意味着创建原始变量的一个副本。caller和这个函数(callee)中的对象的指针指向同一块内存。当函数返回的时候,函数中的这个对象要被kill掉,调用析构函数了,释放掉占用的内存... 放心,这些都不会告诉你的。嗯,当caller中的那个对象析构时,那块内存又被释放了一次... 仍然是不可预知的错误。类似地,创建临时对象的时候,也会调用复制构造函数,这将发生同样的趣事--同样的奇怪的错误。

    2. 两个已有对象之间的赋值,这将调用默认赋值运算符函数。后面的情况和1相同,都是浅拷贝闹的--两个对象的指针指向同一块内存,然后被释放两次。

    啰嗦一句,“当定义的类中含有指针成员,且使用常规new(或定位new,定位在申请的堆内存中)初始化的时候,需定义深拷贝的复制构造函数和赋值操作符”,不然会被外星人抓走。

    其他的的内存分配、回收问题

     

    将涉及定位new的使用。(不考虑内存不够用的情况)

    . 如果使用定位new运算符,定位在静态内存中,就不必释放了 (交给OS吧)

    . 如果先用常规new运算符,申请了一块堆内存。然后,再使用定位new运算符在这块堆内存中为我们的对象申请内存捏 ?

      这种情况下,你却不能delete这些对象。因为,对对象指针执行delete操作,不仅会调用析构函数,而后还会回收成员所占用的内存。你如果delete了这个对象,然后又delete那块堆内存,就会造成某些内存被释放两次的情况 (正是原来存放对象成员的内存)。

      但是! 也因为你没有delete这些对象,这些对象是不会调用析构函数的。万一调用析构函数是必须的 (比如: 对象中有一个指针成员,该指针成员指向了一块用常规new申请的另外一块堆内存,不调用析构函数,这一块内存不就无法回收了吗 ?  飘渺孤鸿影~  寂寞开无主~  又恨又爱的孤岛内存~ )

      但是! 解决方法还是有的,我们可以显式调用析构函数啊 ! 像这样: p->~className(); 这样,对象就会调用它的析构函数,且不会回收成员所占的内存了。

    一个简单的例子:

    #include <iostream>
    #include <string>
    #include <new>
    using namespace std;
    
    class Student
    {
    private:
        string name;
    public:
        Student(const string& s): name(s) {
        }
        ~Student() {
            cout << name << " destroyed
    ";
        }
    };
    
    int main() {
        double * buffer = new double[512];
    
        Student *s1 = new (buffer) Student("Peter");
        Student *s2 = new (buffer + sizeof(Student)) Student("Tom");
    
        /* 下面两条语句将引发错误,后面delete[] buffer,
         * 导致同一块内存被释放两次*/
        //delete s1;
        //delete s2;
    
        /*显式调用析构函数, 这里按栈的顺序了,其实都行,不走寻常路 o_O */
        s2->~Student();
        s1->~Student();
        delete[] buffer;
    
        return 0;
    }
    View Code
  • 相关阅读:
    题解 CF171G 【Mysterious numbers
    题解 P1157 【组合的输出】
    题解 P3955 【图书管理员】
    题解 P2036 【Perket】
    题解 CF837A 【Text Volume】
    题解 CF791A 【Bear and Big Brother】
    题解 CF747A 【Display Size】
    题解 P1332 【血色先锋队】
    题解 P2660 【zzc 种田】
    题解 P4470 【[BJWC2018]售票】
  • 原文地址:https://www.cnblogs.com/zhangzph/p/4542767.html
Copyright © 2011-2022 走看看