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!\n";
}
int main( )
{
    hello( );
    return 0;
}

 

試修改上面的程序,使其輸出變成:
 Begin
   Hello, world!
 End
限制:(1)不能對main()進行任何修改;(2)不能修改hello()函數。
這里用到了構造函數和析構函數的調用了。

#include <iostream>
using namespace std;
class A {
public:
  A ( ) { cout << "Begin\n"; }
  ~A ( ) { cout << "End\n"; }
};

void hello( ) {cout << "  Hello, world!\n"; }

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”: 未能生成析構函數,因為基類析構函數不可訪問

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM