類與物件中篇
- 前言
- 類的六個默認成員函式
- 建構式和解構式
- 1、構造
- 2、析構
- 3、拷貝建構式
- 賦值運算子多載
- const成員
前言
這篇偏難,萬字總結,需要讀者細心觀看,需要有上篇的基礎,
類的六個默認成員函式
如果一個類中什么都沒有,簡稱為空類,但空類里面會自動生成6個默認成員函式,因為這是由編譯器生成的,我們看不到,
空類:class Date {};

建構式和解構式
1、構造
建構式是特殊的成員函式,需要注意的是,建構式的雖然名稱叫構造,但是需要注意的是建構式的主要任務并不是開空間創建物件,而是初始化物件,
- 函式名與類名相同,
- 無回傳值,
- 物件實體化時編譯器自動呼叫對應的建構式,
- 建構式可以多載,
知道了建構式的特點,那么我們可以來看看建構式到底有什么用?
class Date
{
public:
//建構式支持多載,那么也支持預設(全預設,半預設,預設)
Date(int year = 1, int month = 2, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.SetDate(2018, 5, 1);
d1.Display();
Date d2(2021, 10, 21);
d2.Display();
//注意:如果通過無參建構式創建物件時,物件后面不用跟括號,否則就成了函式宣告
Date d3();
return 0;
}
通過這一段代碼,我們可以得知,在不使用建構式時,我們只能創建物件,在呼叫方法來設定日期,使用建構式,可以在創建物件時就將日期資訊帶進去了,避免了呼叫函式的繁瑣,
- 關于建構式全預設的問題
- 通過代碼我們發現,當建構式寫成全預設的時候,我們創建物件不傳參也不會報錯,編譯器會把它當成默認建構式(即無參),此時我們不需要在自己定義一個無參建構式,
- 我們也可以在創建物件時傳參,也不會報錯,所以這是建構式寫成全預設的好處
- 傳參即有參構造,不傳參即無參建構式
- 重點 1
- 這里需要注意的是,建構式支持多載,我們通過有參建構式去去初始化物件的時候,也要顯示的寫上無參建構式,或者語法會出錯,
- 如果類中沒有顯式定義建構式,則C++編譯器會自動生成一個無參的默認建構式,一旦用戶顯式定義編譯器將不再生成
- 建構式既然可以多載,那么也支持預設值的用法
- 無參的建構式和全預設的建構式都稱為默認建構式,并且默認建構式只能有一個,注意:無參建構式、全預設建構式、我們沒寫編譯器默認生成的建構式,都可以認為是默認成員函式
- 重點 2
建構式是一個特殊的成員函式,名字與類名相同,創建型別別物件時由編譯器自動呼叫,保證每個資料成員都有一個合適的初始值,并且在物件的生命周期內只呼叫一次
- 重點3
關于編譯器生成的默認成員函式,有很多小伙伴會疑惑:我們不顯示寫建構式的情況下,編譯器會生成默認的建構式,但是看起來默認建構式又啥都沒干,d物件呼叫了編譯器生成的默認建構式,但是d物件year/month/_day,依舊是隨機值,那編譯器默認生成的建構式有什么用?
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
private:
// 內置型別
int _year;
int _month;
int _day;
Time _tt;//自定義型別 struct、class、union
};
int main()
{
Date d;
return 0;
}
運行這個程式我們會發現Time 類的建構式被呼叫了,

編譯器默認生成的建構式的作用:編譯器生成默認的建構式會對自定義型別成員 _tt 呼叫的它的默認成員函式,
C++把型別分成內置型別(基本型別)和自定義型別,內置型別就是語法已經定義好的型別:如int/char…,自定義型別就是我們使用class/struct/union自己定義的型別
- 重點4
對于成員變數,我們要注意成員變數的風格,設定其名稱時要注意不要與形參重名,避免起沖突,
看程式編譯的結果,成員變數未檢測到,Set函式中相當于形參賦給形參了,

雖然從上一篇,我們知道可以用this指標解決命名沖突的問題,

但是,我們自己寫成員變數的時候就要注意這種沖突,避免程式崩潰,
因此我們可以用駝峰法命名或者加一個下劃線來避免命名沖突(常用的方法是加下劃線)

其他方式也可以的,主要看公司要求,一般都是加個前綴或者后綴標識區分就行,
2、析構
解構式:解構式不是完成物件的銷毀,區域物件銷毀作業是由編譯器完成的,而物件在銷毀時會自動呼叫解構式,完成類的一些資源清理作業
- 與建構式一樣解構式是特殊的成員函式,
- 解構式名是在類名前加上字符 ~,
- 無引數無回傳值,
- 一個類有且只有一個解構式,若未顯式定義,系統會自動生成默認的解構式,
- 物件生命周期結束時,C++編譯系統系統自動呼叫解構式
#include<iostream>
using namespace std;
#include<assert.h>
typedef int DataType;
class SeqList
{
public:
// 使用了預設,當創建物件時不傳參,也不會報錯
SeqList(int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList()
{
if (_pData)
{
free(_pData); // 釋放堆上的空間
_pData = NULL; // 將指標置為空
_capacity = 0;
_size = 0;
}
cout << "~SeqList()" << endl;
}
private:
int* _pData;
size_t _size;
size_t _capacity;
};
int main()
{
SeqList q(20);
//物件宣告周期結束就會呼叫解構式,清理指標_pData,_size,_capacity
return 0;
}

與建構式一樣,如果一個類中包含自定義型別,也會呼叫它的解構式
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person
{
private:
String _name;
int _age;
};
int main()
{
Person p;
return 0;
}

3、拷貝建構式
在大多數情況下,我們可能會復制一個物件,也就是拷貝的意思,那么我們如何在創建物件時,創建一個與這個物件一某一樣的新物件呢?
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷貝建構式也是建構式,相當于多載
//const保證不修改,因為是拷貝,所以修改的話沒有任何意義
//但這里必須得用參考,為什么?繼續看下面
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();
return 0;
}
將物件當做引數傳過去即可

建構式:只有單個形參,該形參是對本型別別物件的參考(一般常用const修飾),在用已存在的類型別物件創建新物件時由編譯器自動呼叫
上面的代碼中留了一個問題,為什么拷貝構造的函式要用參考的形式,如果用傳值的方式會出現什么后果?

這里會出現無窮遞回,傳參也是一個拷貝構造,注意拷貝構造方法傳參考,不要傳值
我們再來看看若未顯示定義拷貝建構式,編譯器默認生成的拷貝建構式,
class A
{
public:
A()
{
}
A(const A& a)
{
cout << "const A& a" << endl;
}
};
class Date
{
public:
Date(int year = 1900, 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;
A _a;
};
int main()
{
Date d1;
// 這里d2呼叫的默認拷貝構造完成拷貝,d2和d1的值也是一樣的
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}

通過編譯結果我們可以看到,使用編譯器的默認建構式效果是一樣的,編譯器生成的默認拷貝建構式已經可以完成位元組序的值拷貝了,
我們不寫,編譯器默認生成拷貝構造,跟構造和析構又不太一樣的
不會去區分內置型別和自定義型別成員,都會處理
1、內置型別,位元組序的淺拷貝(按位元組序的拷貝)
2、自定義型別,會去呼叫他的拷貝構造完成拷貝
但這只是對于日期類,如果對于其他類也可以使用編譯器默認的拷貝建構式嘛?
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
}

這也就是位元組序的值拷貝(淺拷貝)帶來的危險,至于怎么解決,我們后期在來學習,
所以我們可以總結為以下幾點
- 拷貝建構式是建構式的一個多載形式,
- 拷貝建構式的引數只有一個且必須使用參考傳參,使用傳值方式會引發無窮遞回呼叫
- 要注意淺拷貝帶來的危險
賦值運算子多載
引入:C++為了增強代碼的可讀性引入了運算子多載,運算子多載是具有特殊函式名的函式,也具有其回傳值型別,函式名字以及引數串列,其回傳值型別與引數串列與普通的函式類似
函式名:關鍵字operator加多載的運算子符號
函式原型:回傳值型別 operator運算子(引數串列)
注意:
- 不能通過連接其他符號來創建新的運算子:比如operator@
- 多載運算子必須有一個型別別或者列舉型別的運算元
- 用于內置型別的運算子,其含義不能改變,例如:內置的整型+,不 能改變其含義
- 作為類成員的多載函式時,其形參看起來比操作數數目少 1 成員函式的運算子有一個默認的形參this,限定為第一個形參
- .* 、:: 、sizeof 、?: 、. 注意以上5個運算子不能多載,這個經常在筆試選擇題中出現
我們先來看看 == 運算子的多載
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
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(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
}
int main()
{
Test();
return 0;
}
我們通過多載 == 運算子,然后就可以判斷兩個物件是否相等了,但是這里沒有封裝成成員函式,安全性不高,接下來我們多載成成員函式
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
//private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
d1 == d2;
// d1==d2 -> d1.operator(d2) -> d1.operator(&d1,d2)
d1.operator==(d2);// 與d1==d2效果一樣
return 0;
}
這里我們多載成了成員函式之后,發現不能直接去呼叫這個函式了,只能用物件去呼叫這個介面函式,體現了封裝的好處,
我們這里也發現封裝之后有兩種呼叫方式,他們的效果是一樣的,匯編代碼是一樣的,

我們再來看看經常用到的賦值運算子多載
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
//注意回傳參考,為什么就不用我說了,前面講到的臨時變數
}
void Traverse()
{
cout << _year << _month << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 10, 22);
Date d2;
d2 = d1;
d2.Traverse();
return 0;
}

通過多載,我們就可以將 d1 賦值給 d2,但是這里同學們發現了為什么運算子多載要回傳參考嘛?

回傳參考,減少了一次拷貝構造
賦值運算子主要有四點:
- 引數型別
- 回傳值
- 檢測是否自己給自己賦值
- 回傳*this
- 一個類如果沒有顯式定義賦值運算子多載,編譯器也會生成一個,完成物件按位元組序的值拷貝
這里我們會發現編譯器生成的默認賦值多載函式已經可以完成位元組序的值拷貝,但是我們也還是需要自己寫賦值多載函式的,上面講到的對同一塊空間釋放的問題,忘記的可以翻翻上面,也是淺拷貝的問題,后面再說,
總結:
- 運算子默認都是給內置型別變數使用的
- 自定義型別的變數想用這些運算子,得自己進行運算子多載
- 自己進行運算子多載,也就是寫一個函式去定義運算子的行為
const成員
const修飾類的成員函式:
將const修飾的類成員函式稱之為const成員函式,const修飾類成員函式,實際修飾該成員函式隱含的this指標,表明在該成員函式中不能對類的任何成員進行修改,

class Date
{
public:
//加了const,表示成員函式為一個常函式,不能對任何成員做修改
void Display() const
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year = 0; // 年
int _month = 0; // 月
int _day = 0; // 日
};
void Test()
{
Date d1;
d1.Display();
}
int main()
{
Test();
return 0;
}
如果修改了,會出現下面的語法錯誤

const總結
- 成員函式加const,是有好處的,這樣const 物件 和 非const物件都可以呼叫
- 不是所有成員函式都要加const
- 如果一個成員函式內部需要修改成員變數,那就不用加const,比如初始化的函式等
- 如果對成員不做任何修改的函式,在寫的時候最好加上const
const 常考
- const物件可以呼叫非const成員函式嗎? 不能
- 非const物件可以呼叫const成員函式嗎? 能
- const成員函式內可以呼叫其它的非const成員函式嗎? 不能
- 非const成員函式內可以呼叫其它的const成員函式嗎? 能
const權限可以縮小,不能方法
不明白的可以去看看const的用法

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/332129.html
標籤:其他
下一篇:基于比較的七種常見排序演算法
