C++ 再識類和對象
類的6個默認(rèn)成員函數(shù)
一個類中如果什么成員都沒有,那么這個類稱為空類??疹愔惺鞘裁炊紱]有嗎?其實不然,任何一個類,再我們不寫的情況下,都會自動生成下面6個默認(rèn)成員函數(shù):

本篇文章將對這幾個默認(rèn)成員函數(shù)進行簡單介紹。
構(gòu)造函數(shù)
1.概念
我們先來看一下下面這個日期類:
class Date
{
public:
void SetDate(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.SetDate();
d1.Print();
return 0;
}
對于Date類,每次創(chuàng)建對象時可以調(diào)用SetData函數(shù)來設(shè)置對象的日期,但是如果每次創(chuàng)建對象時都需要調(diào)用該函數(shù)來設(shè)置日期信息,未免有些麻煩,那么能否再對象創(chuàng)建的同時就進行初始化呢?
這里就需要用到類的默認(rèn)成員函數(shù)–構(gòu)造函數(shù)了。
構(gòu)造函數(shù)是一個特殊的成員函數(shù),名字與類名相同,創(chuàng)建類類型對象時由編譯器自動調(diào)用,保證每個數(shù)據(jù)成員都有 一個合適的初始值,并且在對象的生命周期內(nèi)只調(diào)用一次。
2.特性
需要注意,構(gòu)造函數(shù)雖然名為構(gòu)造函數(shù),但是其作用并非為成員變量開辟空間,而是初始化對象。其特征如下:
函數(shù)名與類名相同。
沒有返回值。
編譯器會再對象實例化時自動調(diào)用構(gòu)造函數(shù)。
構(gòu)造函數(shù)可以重載。
需要注意的是在類實例化對象的時候,如果變量后面帶上了(),而括號內(nèi)沒有參數(shù),那么這就成了函數(shù)聲明,該函數(shù)無參,且返回值為類名。
class Date
{
public:
Date()//無參的構(gòu)造函數(shù)
{
_year = 0;
_month = 1;
_day = 1;
}
//帶參的構(gòu)造函數(shù)
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//調(diào)用無參的構(gòu)造函數(shù)
Date d2(0, 1, 1);//調(diào)用帶參的構(gòu)造函數(shù)
Date d3();//無參,返回值為Date的函數(shù)聲明
return 0;
}
隱式構(gòu)造函數(shù)
如果類中沒有顯式定義構(gòu)造函數(shù),那么c++編譯器將會自動生成一個無參的默認(rèn)構(gòu)造函數(shù),而如果用戶顯式定義了構(gòu)造函數(shù),那么編譯器將不再生成構(gòu)造函數(shù)。
需要注意的是編譯器自己生成的構(gòu)造函數(shù)在初始化對象時做了一個偏心的處理:即對于內(nèi)置類型,編譯器不會處理;而對于自定義類型,編譯器會自定義類型調(diào)用它自己的默認(rèn)構(gòu)造函數(shù)。內(nèi)置類型指的是語法已經(jīng)定義好的類型,如:int,double,long等等;自定義類型是使用struct/class/union定義的類型。
這是什么意思呢?我們通過下面這個代碼來理解:
class C
{
public:
C()
{
cout << "C()" << endl;
}
private:
int _c;
};
class Date
{
public:
//若用戶顯式定義了構(gòu)造函數(shù),那么編譯器將不再生成
/*Date()
{
_year = 0;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
private:
//內(nèi)置類型
int _year;
int _month;
int _day;
//自定義類型
C c1;
};
int main()
{
Date d1;//調(diào)用無參的構(gòu)造函數(shù)
return 0;
}


通過調(diào)試可以發(fā)現(xiàn),d1自身的內(nèi)置類型變量仍為隨機值,編譯器調(diào)用的構(gòu)造函數(shù)并沒有處理,而對于自定義類型,可以看到編譯器調(diào)用了自定義類型中的默認(rèn)函數(shù),但是實際上如果調(diào)用編譯器自己生成的默認(rèn)構(gòu)造函數(shù),最終的結(jié)果就是所有的內(nèi)置類型變量仍然為隨機值,這么看下來好像編譯器自己生成的構(gòu)造函數(shù)好像沒什么用?
實則不然,比如我們曾做過用棧實現(xiàn)隊列的題,這道題的思路是用兩個棧來回倒保證隊列的先進先出,而這里面的兩個結(jié)構(gòu)棧和用棧實現(xiàn)的隊列的代碼為:
class Stack//棧
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
cout << "malloc fail" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
private:
int* _a;
int _top;
int _capacity;
};
struct MyQueue//用兩個棧實現(xiàn)隊列
{
Stack s1;
Stack s2;
};
可以看到在用MyQueue這個類實例化對象時,編譯器調(diào)用Stack中的構(gòu)造函數(shù)分別對成員變量s1和s2初始化,因此,我們無需再對其進行初始化了,這相對來說方便了許多。
無參和全缺省的函數(shù)均為默認(rèn)構(gòu)造函數(shù)
無參的構(gòu)造函數(shù)和全缺省的構(gòu)造函數(shù)都被稱為默認(rèn)構(gòu)造函數(shù),但是需要注意的是:無參的構(gòu)造函數(shù)和全缺省的構(gòu)造函數(shù)二者只能存在一個,這是因為,如果二者都存在的話,那么在實例化對象不帶參數(shù)時,編譯器無法區(qū)分是調(diào)用哪一個函數(shù)。
class Date
{
public:
Date()
{
_year = 0;
_month = 1;
_day = 1;
}
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//錯誤,編譯器無法識別要調(diào)用哪一個構(gòu)造函數(shù)
return 0;
}
在實際過程中,我們更傾向于使用全缺省的構(gòu)造函數(shù),因為它包含了無參的構(gòu)造函數(shù)的情況。
成員變量的命名風(fēng)格
可以注意到的是在定義類的時候成員變量前都加了一個_,這是為了防止下面這種情況:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
year = year;
month = month;
day = day;
}
void Print()
{
cout << year << "/" << month << "/" << day << endl;
}
private:
int year;
int month;
int day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}

可以看到,d1調(diào)用了構(gòu)造函數(shù)后,其成員變量認(rèn)為隨機值。這是因為在year = year這句代碼中,兩個year變量均為函數(shù)形參,實際上編譯器在處理這種變量時,會遵循局部優(yōu)先原則,即編譯器在函數(shù)形參中找到了year變量,就不會繼續(xù)擴大搜索范圍去尋找成員變量中的year變量,而在Print函數(shù)中,編譯器由于在形參中未找到y(tǒng)ear變量,因此繼續(xù)擴大搜索范圍,在成員變量中找到了year并使用之。
因此,在聲明成員變量的命名時需要遵循一定的規(guī)范,常見的有:(1)在變量名前加_,如_year (2)在變量名后加_,如year_ (3)駝峰法,如mYear,m表示member。
另外,上述情況可以通過使用this指針進行解決,即將代碼改為this->year = year;但在實際使用過程中,最好還是注重成員變量的命名
補充
由于早期c++語法設(shè)計的缺陷,編譯器默認(rèn)生成的構(gòu)造函數(shù)并不會對內(nèi)置類型變量初始化,因此在c++11后,語法委員會在成員變量聲明處打了一個補丁,運行,變量聲明的同時加上缺省值,比如:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
//注意,此處僅為缺省值,仍為變量聲明,而非初始化(定義)
int _year = 0;
int _month = 1;
int _day = 1;
};
析構(gòu)函數(shù)
1.概念
與構(gòu)造函數(shù)相比,析構(gòu)函數(shù)相對簡單一些。析構(gòu)函數(shù)的作用與構(gòu)造函數(shù)的相反,析構(gòu)函數(shù)并不是完成對象的銷毀,因為局部對象的銷毀工作是由編譯器來完成的。一個詞來概括析構(gòu)函數(shù)的作用就是清理,即對象在銷毀的時候會自動調(diào)用析構(gòu)函數(shù),完成類當(dāng)中的一些資源清理工作。
2.特性
析構(gòu)函數(shù)是一種特殊的成員函數(shù),其特征如下:
析構(gòu)函數(shù)名是類名前加上~號
析構(gòu)函數(shù)無參數(shù)無返回值
一個類有且只有一個析構(gòu)函數(shù)
若析構(gòu)函數(shù)為顯式定義,那么系統(tǒng)會自動生成默認(rèn)的析構(gòu)函數(shù)。
與構(gòu)造函數(shù)一樣,系統(tǒng)的默認(rèn)析構(gòu)函數(shù)對于內(nèi)置類型變量不會處理,對于自定義變量會調(diào)用其自身的析構(gòu)函數(shù)。
其次,對于Date類這樣的類,由于其內(nèi)部沒有什么資源需要處理,因此不需要析構(gòu)函數(shù);對于Stack這樣的類,其內(nèi)部由資源需要處理,比如對malloc出來的空間進行釋放,因此需要實現(xiàn)析構(gòu)函數(shù)。
還是之前的代碼,在用兩個棧實現(xiàn)隊列中,在Stack類中實現(xiàn)了構(gòu)造函數(shù)和析構(gòu)函數(shù),那么用MyQueue實例化my變量后無法自己實現(xiàn)初始化和空間的釋放:
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
cout << "malloc fail" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
free(_a);
_a = NULL;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
struct MyQueue
{
Stack s1;
Stack s2;
};
int main()
{
//我們無需自己對mq進行初始化和清理空間
//編譯會自動調(diào)用構(gòu)造函數(shù)和析構(gòu)函數(shù)
MyQueue mq;
return 0;
}
c++編譯器在對象生命周期結(jié)束時自動調(diào)用析構(gòu)函數(shù)
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;//編譯器在執(zhí)行這句代碼的同時會調(diào)用類中的析構(gòu)函數(shù)
}
拷貝構(gòu)造函數(shù)
1.概念
拷貝構(gòu)造函數(shù),顧名思義,其作用就是創(chuàng)建一個和被拷貝對象一模一樣的對象。
拷貝構(gòu)造函數(shù)只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象創(chuàng)建新對象時由編譯器自動調(diào)用。
2.特性
拷貝構(gòu)造函數(shù)也是特殊的成員函數(shù),其特征是:
拷貝構(gòu)造函數(shù)是構(gòu)造函數(shù)的一個重載形式
參數(shù)只有一個且為引用傳參
拷貝構(gòu)造函數(shù)的參數(shù)只有一個且必須為引用傳參,使用傳值方式會引發(fā)無窮遞歸調(diào)用。
class Date
{
public:
Date()
{
_year = 0;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
那么為什么說傳值會導(dǎo)致無窮遞歸調(diào)用呢?首先我們需要理解到調(diào)用函數(shù)傳值給形參也是一種拷貝,比如說:

同樣的,對于拷貝構(gòu)造函數(shù),若形參為傳值調(diào)用,那么在上述代碼中將d2賦值給形參d時也會調(diào)用拷貝構(gòu)造函數(shù),而每一次調(diào)用拷貝構(gòu)造函數(shù)都會經(jīng)過依次賦值操作,從而導(dǎo)致無窮遞歸調(diào)用:

而傳引用就能夠很好的解決這個問題,其次,傳指針也可以達到目的,不過一般傳引用的話可以增強代碼可讀性。
若未顯式定義,系統(tǒng)會生成默認(rèn)的拷貝構(gòu)造函數(shù)
與構(gòu)造函數(shù)一樣,如果我們自己沒有實現(xiàn)拷貝構(gòu)造函數(shù),那么編譯器會生成默認(rèn)的拷貝構(gòu)造函數(shù);但是與構(gòu)造函數(shù)不同的是,默認(rèn)的拷貝構(gòu)造函數(shù)對于內(nèi)置類型和自定義類型變量都會處理:
(1)對于內(nèi)置類型,默認(rèn)的拷貝構(gòu)造函數(shù)會對對象進行淺拷貝,即按照內(nèi)存存儲中的字節(jié)序?qū)ο筮M行拷貝,也叫值拷貝。
(2)對于自定義類型,默認(rèn)的拷貝構(gòu)造函數(shù)會調(diào)用自定義類型中自己的拷貝構(gòu)造函數(shù)。
class A
{
public:
A()
{
_a = 0;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
private:
int _a;
};
class Date
{
public:
Date()
{
_year = 0;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//調(diào)用默認(rèn)的拷貝構(gòu)造函數(shù)
/*Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}*/
private:
int _year;
int _month;
int _day;
A aa;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}

淺拷貝的注意事項
通過上面我們知道了默認(rèn)的拷貝構(gòu)造函數(shù)能夠?qū)崿F(xiàn)淺拷貝,也就是說,對于Date這樣的類,我們無需自己實現(xiàn)拷貝構(gòu)造函數(shù)只用默認(rèn)的拷貝構(gòu)造函數(shù)就能夠?qū)崿F(xiàn)拷貝目的,那么是否用編譯器自己的函數(shù)就夠了呢?
其實不然,比如我們熟知的Stack類,如果直接調(diào)用系統(tǒng)默認(rèn)的拷貝構(gòu)造函數(shù):
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
cout << "malloc fail" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
free(_a);
_a = NULL;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1(8);
Stack s2(s1);
return 0;
}
上述代碼,我們運行后發(fā)現(xiàn),程序崩潰了,這是為什么呢?這是因為系統(tǒng)默認(rèn)的拷貝構(gòu)造函數(shù)拷貝出了一份與s1一模一樣的s2:

而我們知道當(dāng)對象的生命周期結(jié)束時,系統(tǒng)會自動調(diào)用析構(gòu)函數(shù)對類空間進行清理,由于s2是后壓棧的,因此會先清理,這時s2._a所指的空間已經(jīng)free還給操作系統(tǒng)了,但是s1還會再次調(diào)用析構(gòu)函數(shù),將已經(jīng)釋放的s1._a所指向的空間再一次釋放(注意,s2._a釋放完后s1._a仍指向原空間,此時s1._a為野指針),這個操作最終會導(dǎo)致程序崩潰。

可見編譯器默認(rèn)的拷貝構(gòu)造函數(shù)并不能解決所有的問題,淺拷貝會導(dǎo)致一些錯誤,那么要如何解決淺拷貝的帶來的問題呢?這就要我們之后介紹的深拷貝來解決了。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
C/C++自主分配出現(xiàn)double free or corruption問題解決
這篇文章主要為大家介紹了C/C++出現(xiàn)double free or corruption問題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04

