類的6個默認的成員函數包括:
構造函數、析構函數、拷貝構造函數、賦值運算符重載函數、取地址操作符重載、const修飾的取地址操作符重載。
這篇文章重點解釋前四個。
(一)構造函數
構造函數,顧名思義,為對象分配空間,進行初始化。它是一種特殊的成員函數,具有
以下特點:
1.函數名與類名相同。
2.無返回值。
3.構造對象的時候系統會自動調用構造函數。
4.可以重載。
5.可以在類中定義,也可以在類外定義。
6.如果類中沒有給出構造函數,編譯器會自動產生一個缺省的構造函數,如果類中有構造函數,編譯器就不會產生缺省構造函數。
7.全缺省的構造函數和無參的構造函數只能有一個,否則調用的時候就會產生沖突。
8.沒有this指針。因為構造函數才是創建對象的,沒有創建對象就不會有對象的首地址。
構造函數,說來就是給成員變量進行初始化。而初始化卻有兩種方法:
初始化列表、構造函數函數體內賦值。
舉例:依然使用日期類來說明:
#define _CRT_SECURE_NO_WARNINGS 1 #includeusing 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; }
上邊這段代碼只是為了解釋初始化列表初始化成員變量和在構造函數體內初始化,也解釋了無參構造函數和全缺省的構造函數。聲明:由於上邊的代碼同時給出無參和全缺省的構造函數,產生調用沖突,編譯不通過。
既然有兩種初始化的方法,我們究竟該怎樣選擇呢??
盡量使用初始化列表,因為它更高效。下邊用代碼說明它是怎麼個高效法。
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; }
看圖:
使用深層拷貝來解決指針懸掛的問題:
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對象裡指針指向的舊區域,然後再分配新的空間,再拷貝內容。當然,對於那些成員變量裡沒有指針變量就不會涉及到指針懸掛問題。
關於剩下的兩個默認成員函數,之後再補充。