類的六個默認成員函數
寫在前面:
如果我們定義一個空類如下:
class name
{
};
這時候是不是類裏面什麼都沒有呢?
不是!
如果一個類中什麼成員都沒有,簡稱爲空類。空類中什麼都沒有嗎?並不是的,任何一個類在我們不寫的情 況下,都會自動生成下面6個默認的具有一定功能的成員函數。
初始化和清理
構造函數完成初始化工作,析構函數完成一些資源的清理工作
拷貝複製
拷貝構造是完成同類對象初始化創建對象,賦值重載主要把賦值對象給另一個對象
取地址重載
主要是取地址和const對象取地址,這兩個很少自己實現。
我們具體再來說一下吧!
1、構造函數(主要完成初始化)
先來看一下我們一般是如何初始化的。
#include<iostream>
#include<stdlib.h>
using namespace std;
class Date
{
public:
void Init(int _year,int _month,int _day)
{
year=_year;
month=_month;
day=_day;
}
void Print()
{
cout<<year<<" "<<month<<" "<<day<<endl;
}
private:
int year;
int month;
int day;
};
int main()
{
Date d1,d2;
d1.Init(2019,10,27);
d1.Print();
d2.Init(2021,6,8);
d2.Print();
system("pause");
return 0;
}
可以看到對於Date類,可以通過 Init 公有的方法給對象設置內容,但是如果每次創建對象都調用該方法設置信息,未免有點麻煩,那能否在對象創建時,就將信息設置進去呢?
因此引入構造函數的概念
1.定義:
構造函數是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,保證每個數據成員 都有一個合適的初始值,並且在對象的生命週期內只調用一次。
2.特性:
構造函數是特殊的成員函數,需要注意的是,構造函數的雖然名稱叫構造,但是需要注意的是構造函數的主要任務並不是開空間創建對象,而是初始化對象。
特徵:
- 函數名與類名相同。
- 無返回值。
- 對象實例化時編譯器自動調用對應的構造函數。
- 構造函數可以重載
class Date
{
public:
Date()//無參構造函數,初始化時給的是隨機值
{}
Date(int _year,int _month,int _day)//帶參構造函數
{
year=_year;
month=_month;
day=_day;
}
void Print()
{
cout<<year<<" "<<month<<" "<<day<<endl;
}
private:
int year;
int month;
int day;
};
int main()
{
Date d1;//注意是分號結尾,不然就成了聲明一個返回值爲Date類的無參函數
d1.Print();
Date d2(2019,2,3);//調用帶參構造函數
d2.Print();
system("pause");
return 0;
}
這是我們實現的構造函數,那當我們沒有定義構造函數時會怎麼樣呢?
class Date
{
public:
//Date(int _year,int _month,int _day)//帶餐構造函數
//{
// 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();
system("pause");
return 0;
}
這裏我們可以看到,並沒有構造函數,而我們還是可以成功定義 d1 並完成隨機值的初始化,這是爲什麼呢?
沒有定義構造函數,對象也可以創建成功,因此此處調用的是編譯器生成的默認構造函數
注意:
無參的構造函數和全缺省的構造函數都稱爲默認構造函數,並且默認構造函數只能有一個。注意:無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,都可以認爲是默認成員函數。
class Date
{
public:
Date()//定義兩個默認構造函數
{
year=2018;
month=3;
day=6;
}
Date(int _year=2109,int _month=5,int _day=5)//帶參構造函數
{
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();
//Date d2(2018,5,6);
//d2.Print();
system("pause");
return 0;
}
可以看到這裏報了一個錯和一個警告,第一個是兩個函數構成重載,但函數調用不明確,不能達到初始化的效果,且報了一個警告,顯示指定多個默認構造函數,因此建議大家,儘量定義一個。但是大家還是覺得構造函數沒什麼用處,彆着急,以後深入學習之後,你就會對它有更深刻的理解。
2、析構函數(完成清理工作)
1.定義:
析構函數:與構造函數功能相反,析構函數不是完成對象的銷燬,局部對象銷燬工作是由編譯器完成的。而對象在銷燬時會自動調用析構函數,完成類的一些資源清理工作。
2.特徵:
析構函數是特殊的成員函數。
其特徵如下:
- 析構函數名是在類名前加上字符" ~ " 。
- 無參數無返回值。
- 一個類有且只有一個析構函數,若未顯式定義,系統會自動生成默認的析構函數。
- 對象生命週期結束時,C++編譯系統系統自動調用析構函數。
來看一段代碼,看他是否會調用析構函數。
class String
{
public:
String(const char* _str="xiaoming")
{
str=(char*)malloc(strlen(_str)+1);
strcpy(str,_str);
cout<<"String"<<endl;
}
~String()
{
cout<<"~String()"<<endl;
free(str);
}
private:
char *str;
};
class Student
{
public:
//String b;
};
int main()
{
String c;
//a.b.~String();
system("pause");
return 0;
}
這裏的結果應該是:
可以看到,調用了我們定義的構造和析構函數。
(在這裏發生了一個小插曲,在我的vs2012無論如何都不調用我定義的析構函數,後來在朋友vs2013上顯示調用了,建議大家還是儘量不要用vs2012的吧,版本問題很難受)
3、拷貝構造函數
1.定義:
構造函數:只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象 創建新對象時由編譯器自動調用
拷貝構造函數也是特殊的成員函數,其特徵如下:
- 拷貝構造函數是構造函數的一個重載形式。
- 拷貝構造函數的參數只有一個且必須使用引用傳參,使用傳值方式會引發無窮遞歸調用
#include<iostream>
#include<stdlib.h>
using namespace std;
class Date
{
public:
Date(int _year=1900,int _month=6,int _day=8)
{
year=_year;
month=_month;
day=_day;
}
Date(const Date&d)
{
year=d.year;
month=d.month;
day=d.day;
}
void Print()
{
cout<<year<<" "<<month<<" "<<day<<endl;
}
private:
int year;
int month;
int day;
};
int main()
{
Date d1;
Date d2(d1);
d1.Print();
d2.Print();
system("pause");
return 0;
}
看一下結果:
可以看到,我們就好像給d1造了一雙胞胎一樣,是因爲本質上,拷貝構造與構造函數構成重載。
若未顯示定義,系統生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按字節序完成拷貝,這種拷貝我們叫做淺拷貝,或者值拷貝。
比如:
class Date
{
public:
Date(int _year=1900,int _month=6,int _day=8)
{
year=_year;
month=_month;
day=_day;
}
//這裏我們不定義拷貝構造函數,看編譯器是否會生成默認的構造函數
void Print()
{
cout<<year<<" "<<month<<" "<<day<<endl;
}
private:
int year;
int month;
int day;
};
int main()
{
Date d1;
Date d2(d1);
Date d3(d2);
d1.Print();
d2.Print();
d3.Print();
system("pause");
return 0;
}
運行結果:
可以看到編譯器是會自動生成一個默認拷貝構造函數的。
現在只是簡單的值拷貝,以後會講到深拷貝。
4.賦值運算符重載
C++爲了增強代碼的可讀性引入了運算符重載,運算符重載是具有特殊函數名的函數,也具有其返回值類型,函數名字以及參數列表,其返回值類型與參數列表與普通的函數類似。
函數名字爲:關鍵字operator後面接需要重載的運算符符號。
函數原型:返回值類型 operator操作符(參數列表)
注意:
不能通過連接其他符號來創建新的操作符:比如operator@
重載操作符必須有一個類類型或者枚舉類型的操作數 用於內置類型的操作符,
其含義不能改變,例如:內置的整型+,不能改變其含義。
作爲類成員的重載函數時,其形參看起來比操作數數目少1成員函數的 操作符有一個默認的形參this,限定爲第一個形參
.* 、 :: 、 sizeof 、 ?: 、 . 注意以上5個運算符不能重載。
具體來看一個重載**" == "** 來判斷兩個時間是否相同的功能實現:
#include<iostream>
#include<stdlib.h>
using namespace std;
class Date
{
public:
Date(int _year,int _month,int _day)
{
year=_year;
month=_month;
day=_day;
}
~Date()
{
year=0;
month=0;
day=0;
}
//private://這裏定義成public是因爲需要訪問,成員變量,那就失去了封裝性,這個在以後的友元函數再做介紹
int year;
int month;
int day;
};
bool operator==(const Date& d1,const Date& d2)
{
return d1.year==d2.year&&
d1.month==d2.month&&
d1.day==d2.day;
}
void Test()
{
Date d1(2019,2,3);
Date d2(2019,2,3);
Date d3(2019,4,5);
Date d4(2018,5,4);
cout<<(d1==d2)<<endl;
cout<<(d3==d4)<<endl;
}
int main()
{
Test();
system("pause");
return 0;
}
看一下結果:
可以看到,當時間相同時返回1,不同時返回 0。
通過上面的代碼,我們就簡單的實現了,操作符 " == " 的重載。這是我們定義的全局的operator 也可以定義在類裏面作爲成員函數。
class Date
{
public:
Date(int _year=1900,int _month=1,int _day=1)
{
year=_year;
month=_month;
day=_day;
}
bool operator==(const Date& d2)//這裏有一個默認的形參Date *this
{ //因此不用寫const Date& d1,否則會顯示調用參數過多
return year==d2.year
&&month==d2.month
&&day==d2.day;
}
//private:
int year;
int month;
int day;
};
void Test()
{
Date d1(2019,3,4);
Date d2(2019,3,4);
cout<<(d1==d2)<<endl;
}
int main()
{
Test();
system("pause");
return 0;
}
5、6取地址及const取地址操作符重載
這兩個默認成員函數一般不用重新定義 ,編譯器默認會生成。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int year;
int month;
int day;
};
int main()
{
Date d;
cout<<&d<<endl;
cout<<&d<<endl;
system("pause");
return 0;
}
這兩個運算符一般不需要重載,使用編譯器生成的默認取地址的重載即可,只有特殊情況,才需要重載,比 如想讓別人獲取到指定的內容!
另外再講一下const
1 const修飾類的成員函數
將const修飾的類成員函數稱之爲const成員函數,const修飾類成員函數,實際修飾該成員函數隱含的this 指針,表明在該成員函數中不能對類的任何成員進行修改。
看一個例子:
class Date
{
public:
void Init(int _year,int _month,int _day) //const
{
year=_year;
month=_month;
day=_day;
}
void Print() const
{
cout<<year<<" "<<month<<" "<<day<<endl;
}
private:
int year;
int month;
int day;
};
int main()
{
//const Date d;
//d.Init(2019,2,3);
//d.Print();
Date d1;
d1.Init(2019,2,3);
d1.Print();
system("pause");
return 0;
}
可以看到當我們要修改某個值時,不能用const來修飾函數,而只讀的話,是允許的,而當const 訪問const時也是可以的。
總結爲下:
const對象調用const對象沒問題
普通對象調用普通對象也沒問題
但const不能調用普通對象(非const對象),而普通對象可以調用const對象。
簡單理解起來就是:權限不能被放大。(例如,我是隻讀,你可以讀,但是你不能修改我)