目錄
- 析構函數
- 概念
- 特性
- 對象的銷毀順序
感謝各位大佬對我的支持,如果我的文章對你有用,歡迎點擊以下鏈接
🐒🐒🐒 個人主頁
🥸🥸🥸 C語言
🐿?🐿?🐿? C語言例題
🐣🐣🐣 python
🐓🐓🐓 數據結構C語言
🐔🐔🐔 C++
🐿?🐿?🐿? 文章鏈接目錄
析構函數
概念
通過上一篇文章我們知道一個對象是怎么來的,那一個對象又是怎么沒呢的?
析構函數:與構造函數功能相反,析構函數不是完成對對象本身的銷毀,局部對象銷毀工作是由編譯器完成的。
而對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作(有點像數據結構中的Destroy函數,作用就是清理鏈表 樹 或堆上的空間清理,如果不清理會出現內存泄漏的情況)
注意對象空間的開辟和銷毀不需要我們去解決,這些都是由系統去完成的(全局 對象 靜態都是系統自己去解決),而像堆上的空間就需要我們去完成了,比如malloc開辟空間的時候需要我們去完成,在最后釋放的時候也是需要我們自己去free掉空間
特性
析構函數是特殊的成員函數,其特征如下:
- 析構函數名是在類名前加上字符 ~
- 無參數無返回值類型。
具體結構如下:
class Date
{~Date(){;}
};
類名前的~在C語言中表示按位與取反,這里的取反有完全相反的意思,所以 ~放在析構函數這里就是想說明析構函數的作用和構造函數是完全不同的
特別注意析構函數是沒有參數的,而構造函數是有參數的,因為構造函數要構造,傳參可以初始化,而析構函數完全沒必要傳參,所以就沒有參數
- 一個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。注意:析構函數不能重載
class Date
{
public:Date(){_year = 1;}~Date(){cout << "~Date()" << endl;}
private:int _year;
};
int main()
{Date d1;return 0;
}
-
對象生命周期結束時,C++編譯系統系統自動調用析構函數。
-
關于編譯器自動生成的析構函數,是否會完成一些事情呢?下面的程序我們會看到,編譯器生成的默認析構函數,對自定類型成員調用它的析構函數。
-
如果類中沒有申請資源時,析構函數可以不寫,直接使用編譯器生成的默認析構函數
比如Date類;有資源申請時,一定要寫,否則會造成資源泄漏,比如Stack類。
由于析構函數和構造函數都是特殊的類,所以都是有this指針的
class Date
{
public:Date(){_year = 1;}~Date(){cout << this << endl;cout << "~Date()" << endl;}void Print(){cout << this << endl;cout << "Print()" << endl;}
private:int _year;
};
void func()
{Date d2;
}
int main()
{func();Date d1;d1.Print();return 0;
}
通過調試我們可以看到,d1和d2的地址以this指針的方式傳給函數,d1和d2在生命周期結束時會調用析構函數,而析構函數里面是打印this指針
我們來看看下面的代碼來具體理解析構函數
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申請空間失敗");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();擴容_array[_size] = data;_size++;}//~Stack()//{// if (_array)// {// free(_array);// _array = NULL;// _capacity = 0;// _size = 0;// }//}
private:DataType* _array;int _capacity;int _size;
};
int main()
{Stack s;s.Push(1);s.Push(2);
}
在C語言中當沒有調用Destroy函數會發生內存泄漏,具體過程如下
main函數會在棧上開辟一塊空間,這塊空間中也包含Stack s的指針DataType* _array(只是這個指針在mian函數開辟的空間里)
DataType* _array中_arrray的作用是保存Stack s開辟空間的地址
在main函數執行完后,會將main函數在棧上開辟的空間都銷毀,其中就包括了指針_array
由于_array是保存著Stack s開辟空間的地址,最終會因為指針_array被銷毀,導致找不到Stack s開辟出的空間
所以沒調用Destroy函數發生的后果是很嚴重的,并且我們經常會忘記調用Destroy函數,為了解決這個問題才有了析構函數,因為析構函數自動調用,并且編譯器可以自動生成析構函數,這對我們來說是非常方便的
但是需要注意的是默認生成的析構函數和默認生成的構造函數類似,對內置類型不做處理,自定義類型的成員會去調用他的析構函數
對象的銷毀順序
生命周期對于現在學到的來說有兩種,一種是局部(存在一些函數中,因為調用函數會開辟棧幀,所以函數結束后棧幀也會被銷毀,函數中的局部變量也就銷毀了),另一種是靜態或者全局的(存在靜態區里,在mian函數結束后就會銷毀)
而對象生命周期結束時,C++編譯系統系統自動調用析構函數,那如果有多個對象生命周期同時結束,系統會優先給誰調用析構函數
class Date
{
public:Date(int year){_year = year;
}~Date(){cout << "~Date()" << _year<<endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(1);Date d2(2);
}
這段代碼中我們只定義了一個成員變量_year,其他的_month以及_day都只是聲明,不占用內存空間,mian函數中Date d1(1),Date d2(2)是對_year進行初始化,在函數結束后兩個對象的生命周期都會結束,而銷毀的順序如圖
這個調用的順序像棧中的后進先出,Date d1先入棧,所以最后調用析構函數,事實上對象確實存儲在棧上的,因為類其實是一個函數,在函數調用時會建立棧幀,所以空間存儲在棧上
class Date
{
public:Date(int year){_year = year;
}~Date(){cout << "~Date()" << _year<<endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(1);Date d2(2);static Date d3(3);
}
那如果讓Date d3加上一個static去修飾結果會怎么樣
加上static修飾后Date d3的存儲區域就發生變化了,d3存儲在一個單獨的靜態區中,雖然d3是一個局部變量,但是他的生命周期在經過static修飾后變成全局,所以d3會在main函數結束后銷毀,而在main函數結束前會將里面的d1和d2等局部變量先銷毀掉,所以d3排在最后
我們再來看看下面的代碼,這段代碼中定義了一個函數func,將類的對象定義在函數中,其中d4是被static修飾的,而d3沒有被修飾,然后在main函數中調用func
class Date
{
public:Date(int year){_year = year;
}~Date(){cout << "~Date()" << _year<<endl;}
private:int _year;int _month;int _day;
};
void func()
{Date d3(3);static Date d4(4);
}
int main()
{Date d1(1);Date d2(2);func();
}
銷毀順序是3 2 1 4,具體原因還是因為func也是一個函數,空間開辟在棧上的,滿足后進先出原則,所以先銷毀對象d3(對于d3為什么是最先銷毀,可能是因為他在函數func中,算是一個局部中的局部吧),然后又是d2 d1,d4因為被static修飾,所以最后銷毀
我們再在main函數外定義一個對象d5又會怎么樣
class Date
{
public:Date(int year){_year = year;
}~Date(){cout << "~Date()" << _year<<endl;}
private:int _year;int _month;int _day;
};
void func()
{Date d3(3);static Date d4(4);
}
Date d5(5);
int main()
{Date d1(1);Date d2(2);func();
}
結果是3 2 1 4 5
d5雖然沒有被static修飾,但是他定義在main函數外的,所以他自己就是一個全局變量,但是這里的全局變量有兩個,一個是d4,一個是d5,他們的銷毀順序是否也和自己的位置有關呢?
我們將d5的定義移到func函數上邊,發現沒有變化,所以推測可能是因為d4是在func函數中,所以相對于d5來講,d4的聲明周期是局部的
class Date
{
public:Date(int year){_year = year;
}~Date(){cout << "~Date()" << _year<<endl;}
private:int _year;int _month;int _day;
};
static Date d6(6);
Date d5(5);
void func()
{Date d3(3);static Date d4(4);
}
int main()
{func();Date d1(1);Date d2(2);
我們再在main函數外定義一個d6,用static修飾他的順序又會怎么樣
當d6和d5交換順序后,發現銷毀的順序變化了,所以我們得出結論,全局的銷毀順序和局部的銷毀順序也是一樣的,當d5后入棧時,d5就先銷毀,而static修飾全局變量d6,并不會改變d6的銷毀順序
如果在多個函數func中定義類的順序會怎么樣
class Date
{
public:Date(int year){_year = year;
}~Date(){cout << "~Date()" << _year<<endl;}
private:int _year;int _month;int _day;
};
static Date d6(6);
Date d5(5);
void func2()
{Date d7(7);static Date d8(8);
}
void func1()
{Date d3(3);static Date d4(4);
}int main()
{Date d1(1);Date d2(2);func1();func2();
}
這里說下我的想法,func1和func2因為都是在棧上開的空間,所以他們的銷毀的順序滿足后進先出,具體判斷誰先銷毀的方法就是看誰最先被調用,也就是在main函數中去看func1是否比func2先調用,如果先比func2調用,那就說明func1先開辟空間,所以func1要比func2后銷毀
最終順序總結如下
局部對象(后定義先析構)->局部靜態->全局對象(后定義先析構)
類中沒有顯示定義析構函數,系統則會自動生成默認的析構函數,那這個析構函數是否和構造函數一樣基本上什么事都不做呢?
由于自定義類型的盡頭是內置類型,對應類而言如果類中沒有申請資源時,析構函數可以不寫(因為不寫不會有影響),有資源申請時,一定要寫,否則會造成資源泄漏
為什么析構不可以自己去處理內置類型呢?
因為內置類型中有指針等許多不能隨便處理的類型,假如指針指向了一塊空間,如果析構函數可以處理內置類型的話,有可能會直接把指針指向的空間給銷毀了,這樣指針就變成了野指針