• C++ 类 析构函数


    一、析构函数的定义


    析构函数为成员函数的一种,名字与类名相同,在前面加‘~’没有参数和返回值在C++中“~”是位取反运算符。
    一个类最多只能有一个析构函数。析构函数不返回任何值,没有函数类型,也没有函数参数,因此它不能被重载。

    构造函数可能有多个,但析构函数只能有一个,就像人来到人世间,可能出生的环境家庭不同(重载构造函数),但最终都会死亡(析构函数)。

    class C
    {
    public:
        ~C ( ) { }
        ~C (int i) { }    // error C2524: “C”: 析构函数 必须有“void”参数列表
        // warning C4523: “C”: 指定了多个析构函数
    };

    析构函数对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等。
    如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做。如果定义了析构函数,则编译器不生成缺省析构函数。
    析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作。

    例:

    class A
    {
    private :
        char * p;
    public:
        A ( )
        {
            p = new char[10];
        }
        ~ A ( )
        {
            delete [] p;
        }
    };

    若A类没写析构函数,则在生成A对象后,new出来的内存空间未清除,可能造成内存泄露。

    在创建一类的对象数组时,对于每一个数组元素,都会执行缺省的构造函数。同样,对象数组生命期结束时,对象数组的每个元素的析构函数都会被调用。

    #include<iostream>
    using namespace std;
    unsigned count = 0;
    class A
    {
    public:
        A ( )
        {
            i = ++count;
            cout << "Creating A " << i <<endl;
        }
        ~A ( )
        {
            cout << "A Destructor called " << i <<endl;
        }
    private :
        int i;
    };
    int main( )
    {
        A ar[3];  // 对象数组
        return 0;
    }

    程序执行结果为:
    Creating A 1
    Creating A 2
    Creating A 3
    A Destructor called 3
    A Destructor called 2
    A Destructor called 1

    类似于栈的后进先出

    二、析构函数的调用


    如果出现以下几种情况,程序就会执行析构函数:
    (1)如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。
    (2)static局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。
    (3)如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数) 时,调用该全局对象的析构函数。
    (4)如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。
    (5)调用复制构造函数后。 

    例:

    例4.35
    #include <iostream>
    using namespace std;
    class CMyclass
    {
    public:
        ~CMyclass( )
        {
            cout << "destructor" << endl;
        }
    };
    CMyclass obj;
    CMyclass fun(CMyclass sobj )
    {
        return sobj;   //函数调用返回时生成临时对象返回
    }
    void main( )
    {
        obj = fun(obj);  //函数调用的返回值(临时对象)被用过后,该临时对象析构函数被调用
    }

    程序执行结果为:
    destructor   // 形参和实参结合,会调用复制构造函数,临时对象析构
    destructor   // return sobj函数调用返回,会调用复制构造函数,临时对象析构
    destructor   // obj对象析构


    总之,在临时对象生成的时候会有构造函数被调用,临时对象消亡导致析构函数调用。

    三、构造函数和析构函数的调用情况


    构造函数用于对对象中的变量赋初值,析构函数用于释放所定义的对象的所有内存空间。构造函数和析构函数都不需要用户调用的,构造函数在定义对象时自动调用,析构函数当对象的生存期结束的时候会自动的调用。一般来说,析构函数的调用顺序与构造函数相反。但对象存储类型可以改变析构函数的调用顺序。
    全局范围中定义的对象的构造函数在文件中的任何其他函数(包括main)执行之前调用(但不同文件之间全局对象构造函数的执行顺序是不确定的)。全局变量是需要在进入main()函数前即初始化的,所以在一个程序中一个全局变量的构造函数应该是第一个被调用的,比main()函数都要早同时全局对象又必须在main()函数返回后才被销毁,当main终止或调用exit函数时调用相应的析构函数,所以它的析构函数是最后才被调用。
    当程序执行到对象定义时,调用自动局部对象的构造函数。该对象的析构函数在对象离开范围时调用(即离开定义对象的块时)。自动对象的构造函数与析构函数在每次对象进人和离开范围时调用。
    static局部对象的构造函数只在程序执行首次到达对象定义时调用一次,对应的析构函数在main终止或调用exit函数时调用。

    #include <iostream>
    using namespace std;
    class A
    {
    public:
        A( int value )
        {
            i = value;
            cout << "Object "<<i<<" constructor";
        }
        ~A()    // destructor
        {
            cout <<"Object "<<i<<" destructor"<< endl;
        }
    private:
        int i;
    };
    A first(1);  // global object全局变量
    void func()
    {
        A fifth(5);
        cout <<" (local automatic in create)"<< endl;
        static A sixth( 6 );
        cout <<" (local static in create)"<< endl;
        A seventh(7);
        cout <<" (local automatic in create)"<< endl;
    }
    int main()
    {
        cout <<" (global created before main)" <<endl;
        A second(2);
        cout <<" (local automatic in main)"<< endl;
        static A third(3);  // local object
        cout <<" (local static in main)"<< endl;
        func();     // call function to create objects
        A fourth(4);      // local object
        cout <<" (local automatic in main)"<< endl;
        return 0;
    }

    程序执行结果为:
    Object 1 constructor (global creasted before main)
    Object 2 constructor (local automatic in main)
    Object 3 constructor (local static in main)
    Object 5 constructor (local automatic in create)
    Object 6 constructor (local static in create)
    Object 7 constructor (local automatic in create)
    Object 7 destructor
    Object 5 destructor
    Object 4 constructor (local automatic in main}
    Object 4 destructor
    Object 2 destructor
    Object 6 destructor
    Object 3 destructor
    Object 1 destructor

    上例中,main函数中声明3个对象,对象second和fourth是局部自动对象,对象third是static局部对象。这些对象的构造函数在程序执行到对象定义时调用。对象fourth和second的析构函数在到达main函数结尾时候依次调用。由于对象third是static局部对象,因此到程序结束时才退出,在程序终止时候删除所有其他对象之后和调用first的析构函数之前调用对象third的析构函数。函数func声明3个对象。对象fifth和seventh是局部自动对象,对象sixth是static局部对象。对象seventh和fifth的析构函数在到func结束时候依次调用。由于对象sixth是static局部对象,因此到程序结束时才退出。sixth的析构函数在程序终止时删除所有其他对象之后和调用third和first的析构函数之前调用。

    若函数参数是类类型,调用函数时要调用复制构造函数,用实际参数初始化形式参数。当函数返回类类型时,也要通过复制构造函数建立临时对象。

    例:

    #include <iostream>
    using namespace std;
    class A
    {
    public:
        A()
        {
            cout<<"A constructor"<<endl;
        }
        ~A()
        {
            cout<<"A destructor"<<endl;
        }
        A(A &)
        {
            cout <<"A copy constructor"<<endl;
        }
    };
    class B
    {
    public:
        B()
        {
            cout<<"B constructor"<<endl;
        }
        ~B()
        {
            cout<<"B destructor"<<endl;
        }
        B(B &)
        {
            cout<<"B copy constructor"<<endl;
        }
    };
    A a;
    B b;
    void func1(A obj) {}
    void func2(B &obj) {}
    int main()
    {
        func1(a);
        func2(b);
        return 0;
    }

    程序执行结果为:
    A constructor       // a构造
    B constructor       // b构造
    A copy constructor   // func1函数参数调用A的复制构造函数
    A destructor   // func1函数参数析构
    B destructor   // b析构
    A destructor   // a析构

    上例中函数func1的参数是A的值传递方式,实参初始化形参时需要调用复制构造函数,函数调用结束后,栈上的形参消亡时要调用A的析构函数。函数func2的参数是B的引用传递方式,形参只是实参的一个别名,并没有创建新的对象,因此不会调用复制构造函数和析构函数。

    这里再附上一道面试题:

    #include <iostream>
    using namespace std;
    void hello( )
    {
        cout << "  Hello, world!
    ";
    }
    int main( )
    {
        hello( );
        return 0;
    }

    试修改上面的程序,使其输出变成:
     Begin
       Hello, world!
     End
    限制:(1)不能对main()进行任何修改;(2)不能修改hello()函数。
    这里用到了构造函数和析构函数的调用了。

    #include <iostream>
    using namespace std;
    class A {
    public:
      A ( ) { cout << "Begin
    "; }
      ~A ( ) { cout << "End
    "; }
    };
    
    void hello( ) {cout << "  Hello, world!
    "; }
    
    A a;      // a是一个全局对象
    int main( ) {
      hello( ); 
      return 0;
    }

    下面总结下不同存储类型构造函数和析构函数的调用。


    构造函数的调用:
    (1)全局变量:程序运行前;
    (2)函数中静态变量:函数开始前;
    (3)函数参数:函数开始前;
    (4)函数返回值:函数返回前;
    析构函数的调用:
    (1)全局变量:程序结束前;
    (2)main中变量:main结束前;
    (3)函数中静态变量:程序结束前;
    (4)函数参数:函数结束前;
    (5)函数中变量:函数结束前;
    (6)函数返回值:函数返回值被使用后;

     对于相同作用域和存储类别的对象,调用析构函数的次序正好与调用构造函数的次序相反

    四、私有析构函数


    有时你想这样管理某些对象,要让某种类型的对象能够自我销毁,也就是能够“delete this”。很明显这种管理方式需要此类型对象被分配在堆中。若我们必须在堆中创建对象,为了执行这种限制,你必须找到一种方法禁止以调用“new”以外的其它手段建立对象。这很容易做到,非堆对象(non-heap object),即栈对象在定义它的地方被自动构造,在生存时间结束时自动被释放,所以只要禁止使用隐式的析构函数,就可以实现这种限制。
    把这些调用变得不合法的一种最直接的方法是把析构函数声明为private,把构造函数声明为public。你可以增加一个专用的伪析构函数,用来访问真正的析构函数,客户端调用伪析构函数释放他们建立的对象。

    #include <iostream>
    using namespace std;
    class A
    {
    public:
        A()
        {
            cout<<"A"<<endl;
        }
        void destroy() const
        {
            cout<<"delete A"<<endl;
            delete this;
        }
    private:
        ~A() {}
    };
    int main( )
    {
        A *pa = new A;
        pa->destroy();
        return 0;
    }

    程序执行结果为:
            A
            delete A
    若A类对象是在栈上创建:
        A a;  // error C2248: “A::~A”: 无法访问 private 成员(在“A”类中声明)
    编译时会提示不能访问私有成员。因为在栈上生成对象时,类对象在离开作用域时会调用析构函数释放空间,此时无法调用私有的析构函数。C++在编译过程中,所有的非虚函数调用都必须分析完成。即使是虚函数,也需检查可访问性。因此,当在栈上生成对象时,对象会自动析构,也就说析构函数必须可以访问。而堆上生成对象,由于析构时机由程序员控制,所以不一定需要析构函数。

    被声明为私有析构函数的类对象只能在堆上创建,并且该类不能被继承。

    class A
    {
    private:
        ~A() {}
    };
    class B : public A
    {
        // error C2248: “A::~A”: 无法访问 private 成员(在“A”类中声明)
    }  // warning C4624: “B”: 未能生成析构函数,因为基类析构函数不可访问
  • 相关阅读:
    vue 中的键盘事件
    红米k40刷类原生系统
    (历史) 1960s,大家争先在共享内存上实现原子性 (互斥) 但几乎所有的实现都是错的,直到 Dekker's Algorithm,还只能保证两个线程的互斥
    Go Memory Model 内存模型 同步 goroutine
    理解并发程序执行 (Peterson算法、模型检验与软件自动化工具
    源码 连接池 设计
    Thread Exception Captured Application Crash Report
    Check if the context is expired.
    A Quick Guide to Go's Assembler
    敏感问题调查 干扰变量 抛硬币
  • 原文地址:https://www.cnblogs.com/wkfvawl/p/10620639.html
Copyright © 2020-2023  润新知