詳解c++中類的六個預設的成員函式
類的6個預設的成員函式包括:
建構函式、解構函式、拷貝建構函式、賦值運算子過載函式、取地址操作符過載、const
修飾的取地址操作符過載。
這篇文章重點解釋前四個。
(一)建構函式
建構函式,顧名思義,為物件分配空間,進行初始化。它是一種特殊的成員函式,具有
以下特點:
1.函式名與類名相同。
2.無返回值。
3.構造物件的時候系統會自動呼叫建構函式。
4.可以過載。
5.可以在類中定義,也可以在類外定義。
6.如果類中沒有給出建構函式,編譯器會自動產生一個預設的建構函式,如果類中有構
造函式,編譯器就不會產生預設建構函式。
7.全預設的建構函式和無參的建構函式只能有一個,否則呼叫的時候就會產生衝突。
8.沒有this指標。因為建構函式才是建立物件的,沒有建立物件就不會有物件的首地址。
建構函式,說來就是給成員變數進行初始化。而初始化卻有兩種方法:
初始化列表、建構函式函式體內賦值。
舉例:依然使用日期類來說明:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; class Date { public: Date()//無參建構函式 { m_year = 2016; m_month = 7; m_day = 6; } Date(int year = 1900, int month = 1, int day = 1)//全預設的建構函式 { m_year = year; m_month = month; m_day = day; } Date(int year, int month, int day) :m_year(year), m_month(month), m_day(day) //初始化列表初始化成員變數 { } void print() { cout << m_year << "-" << m_month << "-" << m_day << endl; } private: int m_year; int m_month; int m_day; }; int main() { Date date(2016,7,4); date.print(); system("pause"); return 0; }
上邊這段程式碼只是為了解釋初始化列表初始化成員變數和在建構函式體內初始化,也解
釋了無參建構函式和全預設的建構函式。宣告:由於上邊的程式碼同時給出無參和全預設
的建構函式,產生呼叫衝突,編譯不通過。
既然有兩種初始化的方法,我們究竟該怎樣選擇呢??
儘量使用初始化列表,因為它更高效。下邊用程式碼說明它是怎麼個高效法。
<pre name="code" class="cpp">class Time { public: Time(int hour= 1, int minute=1, int second=1) { m_hour = hour; m_minute = minute; m_second = second; cout << "構造時間類物件中..." << endl; } private: int m_hour; int m_minute; int m_second; }; class Date { public: Date(int year, int month, int day,Time t) /*:m_year(year), m_month(month), m_day(day) */ { m_year = year; m_month = month; m_day = day; m_t = t; } void print() { cout << m_year << "-" << m_month << "-" << m_day << endl; } private: int m_year; int m_month; int m_day; Time m_t; }; int main() { Time t(10,36,20); Date d(2016,7,6,t); system("pause"); return 0; }
上邊給出不使用初始化列表初始化日期類中的時間類物件的 辦法,會導致時間類構造兩
次,一次在主函式中定義時間類物件時,一次在引數列表中呼叫。而如果我們將所有
的成員變數都用初始化列表初始化,時間類建構函式只會被呼叫一次,這就是提高效率
所在。
有些成員變數必須再初始化列表中初始化,比如:
1. 常量成員變數。(常量建立時必須初始化,因為對於一個常量,我們給它賦值,是不
對的)2. 引用型別成員變數。(引用建立時必須初始化)
3. 沒有預設建構函式的類成員變數。(如果建構函式的引數列表中有一個類的物件,並
且該物件的類裡沒有預設引數的建構函式時,要是不使用初始化列表,引數中會呼叫無
參或者全預設的建構函式,而那個類中又沒有。)
注意:在上邊的main函式中要是有這樣一句:Date d2();這不是定義一個物件,而是聲
明瞭一個函式名為d2,無參,返回值為Date的函式。
(二)解構函式
解構函式是一種特殊的成員函式,具有以下特點:
1. 解構函式函式名是在類名加上字元~。2. 無引數無返回值(但有this指標)。3. 一個類有且只有一個解構函式,所以肯定不能過載。若未顯示定義,系統會自動生成
預設的解構函式。
4. 物件生命週期結束時,C++編譯系統系統自動呼叫解構函式。
5. 注意解構函式體內並不是刪除物件,而是做一些清理工作。(比如我們在建構函式
中動態開闢過一段空間,函式結束後需要釋放,而系統自動生成的解構函式才不管內
存釋放呢,所以需要人為地寫出解構函式)
注意:物件生命週期結束後,後構造的物件先釋放。
(三)拷貝建構函式:用已有的物件建立一個新的物件。仍然使用上邊的日期類舉例:
int main()
{
Date d1(2016,7,6);
Date d2(d1);
system("pause");
return 0;
}
上邊是用d1建立一個d2,系統會給出預設的拷貝建構函式,並且該函式的引數是一個常
引用,我們想象為什麼必須是引用呢,如果不是又會發生什麼。
如果不是引用,形參是實參的一份臨時拷貝,由於兩者都是物件,此時就會呼叫自己的
拷貝建構函式,陷入無限遞迴中.......
上邊的程式碼,我們用預設的拷貝建構函式可以得到正確的結果,有時就不會。例項:
class Person
{
public:
Person(char *name,int age)
{
m_name = (char *)malloc(sizeof(char)*10);
if (NULL == m_name)
{
cout << "out of memory" << endl;
exit(1);
}
strcpy(m_name,name);
m_age = age;
}
~Person()
{
free(m_name);
m_name = 0;
}
private:
char *m_name;
int m_age;
};
int main()
{
Person p1("yang",20);
Person p2= p1;
system("pause");
return 0;
}
上邊的程式碼會出錯,原因見圖片。
在析構時,同一塊空間釋放兩次就會有問題。
這種僅僅只是值的拷貝的拷貝方式就是淺拷貝。
深拷貝就是為物件重新分配空間之後,然後將值拷貝的拷貝方式。
下邊自己給出拷貝建構函式。
Person(const Person &p)
{
m_name = new char[strlen(p.m_name)+1];
if (m_name != 0)
{
strcpy(m_name,p.m_name);
m_age = p.m_age;
}
}
下邊用圖給出實現機理。
呼叫拷貝建構函式的3種情況:
1.當用類的一個物件去初始化該類的另一個物件時。
2.當函式的形參是類的物件,呼叫函式時進行形參和實參的結合時。
3.當函式的返回值是物件,函式執行完返回呼叫者時。(函式執行結束後,返回的物件
會複製到一個無名物件中,然後返回的物件會消失,當呼叫語句執行完之後,無名對
象就消失了)
呼叫拷貝建構函式的兩種方法:
1.代入法:
Person p2(p1);
2.賦值法:
Person p2 = p1;
(四)賦值運算子過載函式
它是兩個已有物件一個給另一個賦值的過程。它不同於拷貝建構函式,拷貝建構函式是
用已有物件給新生成的物件賦初值的過程。
預設的賦值運算子過載函式實現的資料成員的逐一賦值的方法是一種淺層拷貝。
淺層拷貝會導致的指標懸掛的問題:
class Person
{
public:
Person(char *name,int age)
{
m_name = (char *)malloc(sizeof(char)*10);
if (NULL == m_name)
{
cout << "out of memory" << endl;
exit(1);
}
strcpy(m_name,name);
m_age = age;
}
~Person()
{
free(m_name);
m_name = 0;
}
private:
char *m_name;
int m_age;
};
int main()
{
Person p1("yang",20);
Person p2("yao",20);
p2 = p1;
system("pause");
return 0;
}
看圖:
使用深層拷貝來解決指標懸掛的問題:
<pre name="code" class="cpp">Person & operator=(const Person &p)
{
if (this == &p)
return *this;
delete[]m_name;
m_name = new char[strlen(p.m_name) + 1];
strcpy(m_name,p.m_name);
m_age= p.m_age;
return *this;
}
這樣先將p2物件裡指標指向的舊區域,然後再分配新的空間,再拷貝內容。當然,對於
那些成員變數裡沒有指標變數就不會涉及到指標懸掛問題。
(五)取地址操作符過載
Person * operator&()
{
return this;
}
(六)const修飾的取地址操作符的過載
const Person * operator&() const
{
return this;
}
函式後邊的const表明在函式體中不能改變物件的成員,當然可以改變mutable變數。函
數的返回值是指向常物件的指標。