• 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
  • 相关阅读:
    Jquery学习系列-制作Menu
    关于javascript里的parseInt() 与 parseFloaat() 文本转换为数字
    javascript简述
    【代码片段】HTML5基本结构及常用默认模版
    赋值运算符
    【代码片段】jQuery实现页面滚动时层智能浮动定位Fixed Floating Elements
    【代码片段】formLogin
    算数运算符
    关于javascript里的toFixed()方法格式化数字
    关于javascript里的setTimerout()设定时间
  • 原文地址:https://www.cnblogs.com/zhangzph/p/4542767.html
Copyright © 2020-2023  润新知