目錄
- 前言
- 構造函數
- 構造函數形式:
- 構造函數的特性:
- explicit關鍵字
- 析構函數
- 析構函數的概念
- 析構函數的特性
- 含有類類型的成員變量的類析構函數的調用
- 拷貝構造函數
- 拷貝構造函數的概念
- 拷貝構造函數的特性
- 淺拷貝和深拷貝:
- 拷貝構造函數典型調用場景:
- 賦值運算符重載
- 運算符重載
- 全局的operator==
- 賦值運算符重載
- 前置++和后置++重載
- const成員函數
- 取地址及const取地址操作符重載#
前言
如果一個類是沒什么也沒有,就叫做空類。
空類并非什么也沒有,它占一個字節。編譯器會自動生成6個默認成員函數。
默認成員函數:指沒有顯式實現(自己寫),編譯器生成的成員函數。
構造函數
Data類:
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;d1.Init(2022, 7, 5);d1.Print();Date d2;d2.Init(2022, 7, 6);d2.Print();return 0;
}
創建類對象時,每次都要把數據通過Init成員函數設置(麻煩)。構造函數就是在創建類對象時就能將數據設置(解決麻煩)。
構造函數形式:
class Date{public:// 1.無參構造函數Date(){}// 2.帶參構造函數Date(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};void TestDate(){Date d1; // 調用無參構造函數Date d2(2015, 1, 1); // 調用帶參的構造函數// 注意:如果通過無參構造函數創建對象時,對象后面不用跟括號,否則就成了函數聲明// 以下代碼的函數:聲明了d3函數,該函數無參,返回一個日期類型的對象// warning C4930: “Date d3(void)”: 未調用原型函數(是否是有意用變量定義的?)Date d3();}
構造函數的特性:
- 函數名與類名相同。
- 無返回值。
- 對象實例化時編譯器自動調用對應的構造函數。
- 構造函數可以重載。
優先級:顯式實現構造函數>>默認構造函數。
使用默認函數,它對成員變量(基本類型)的賦值時隨機值:
C++把類型分成內置類型(基本類型)和自定義類型。內置類型就是語言提供的數據類型,如:int/char…,自定義類型就是我們使用class/struct/union等自己定義的類型。
對于類類型成員, 編譯器生成默認的構造函數會對自定類型成員_t調用的它的默認成員函數:
(C++11 中針對內置類型成員不初始化的缺陷,又打了補丁,即:內置類型成員變量在 類中聲明時可以給默認值。):解決使用默認函數,看上去沒有什么用,它對成員變量(基本類型)的賦值時隨機值
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本類型(內置類型)int _year=1970;int _month=12;int _day=29;// 自定義類型Time _t;
};
運行結果:
無參的構造函數和全缺省的構造函數都稱為默認構造函數,并且默認構造函數只能有一個。
class Date
{
public:Date(){_year = 1900;_month = 1;_day = 1;}Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1;
}
運行結果:
構造函數是為了初始化,然而初始化只有一次,用構造函數卻能多次,這違背了初始化的初衷。
因此在構造函數里初始化列表,便解決了這一問題。
之前的構造函數都是構造函數體賦值。構造函數體中的語句只能將其稱為賦初值,而不能稱作初始化。
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
初始化列表:一個冒號,再寫成員變量,成員變量后面加(),()里是初始值或表達式。
class Date
{
public:Date(int year, int month, int day): _year(year), _month(month), _day(day){}private:int _year;int _month;int _day;
};
注意:
- 每個成員變量在初始化列表中只能出現一次(初始化只能初始化一次)。
- 成員變量在類中聲明次序就是其在初始化列表中的初始化順序,與其在初始化列表中的先后次序無關。
- 類中包含以下成員,必須放在初始化列表位置進行初始化:
引用成員變量
const成員變量
自定義類型成員(且該類沒有默認構造函數時)
class A
{
public:A(int a):_a(a){}
private:int _a;
};
class B
{
public:B(int a, int ref):_aobj(a),_ref(ref),_n(10){}
private:A _aobj; // 沒有默認構造函數int& _ref; // 引用const int _n; // const
};
否則:
explicit關鍵字
構造函數不僅可以構造與初始化對象,對于接收單個參數的構造函數,還具有類型轉換的作用。 而explicit關鍵字就是限制隱式類型轉換。
接收單個參數的構造函數具體表現:
- 構造函數只有一個參數。
- 構造函數有多個參數,除第一個參數沒有默認值外,其余參數都有默認值。
- 全缺省構造函數。
class Date
{
public:// 1. 單參構造函數,沒有使用explicit修飾,具有類型轉換作用// explicit修飾構造函數,禁止類型轉換---explicit去掉之后,代碼可以通過編譯explicit Date(int year):_year(year){}// 2. 雖然有多個參數,但是創建對象時后兩個參數可以不傳遞,沒有使用explicit修飾,具有類型轉換作用// explicit修飾構造函數,禁止類型轉換explicit Date(int year, int month = 1, int day = 1): _year(year), _month(month), _day(day){}private:int _year;int _month;int _day;
};
void Test()
{Date d1(2022); // 用一個整形變量給日期類型對象賦值d1 = 2023; // 實際編譯器背后會用2023構造一個無名對象,最后用無名對象給d1對象進行賦值。}
C++ 里,單個參數的構造函數(或參數帶默認值、能簡化成單個參數的構造函數 ),會讓編譯器允許 用單個值直接給對象賦值,這叫隱式類型轉換。如果用了explicit,那么就不能用單個值直接給對象賦值。
析構函數
析構函數的概念
析構函數與構造函數功能相反。析構函數不是完成對對象本身的銷毀,局部對象銷毀工作是由編譯器完成的。而對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作。簡單來說就是把對象里占的額外資源(文件、動態內存)清理掉,避免殘留、浪費。
析構函數的特性
- 析構函數名是在類名前加上字符 ~。
- 無參數無返回值類型。
- 一個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。注意:析構
函數不能重載。 - 對象生命周期結束時,C++編譯系統系統自動調用析構函數。
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申請空間失敗!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
void TestStack()
{Stack s;s.Push(1);s.Push(2);
}
含有類類型的成員變量的類析構函數的調用
class Time
{
public:~Time(){cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本類型(內置類型)int _year = 1970;int _month = 1;int _day = 1;// 自定義類型Time _t;
};
int main()
{Date d;return 0;
}
// 程序運行結束后輸出:~Time()
// 在main方法中根本沒有直接創建Time類的對象,為什么最后會調用Time類的析構函數?
// 因為:main方法中創建了Date對象d,而d中包含4個成員變量,其中_year, _month, _day三個是
// 內置類型成員,銷毀時不需要資源清理,最后系統直接將其內存回收即可;而_t是Time類對象,所以在
// d銷毀時,要將其內部包含的Time類的_t對象銷毀,所以要調用Time類的析構函數。但是:main函數
// 中不能直接調用Time類的析構函數,實際要釋放的是Date類對象,所以編譯器會調用Date類的析構函
// 數,而Date沒有顯式提供,則編譯器會給Date類生成一個默認的析構函數,目的是在其內部調用Time
// 類的析構函數,即當Date對象銷毀時,要保證其內部每個自定義對象都可以正確銷毀
// main函數中并沒有直接調用Time類析構函數,而是顯式調用編譯器為Date類生成的默認析構函數
// 注意:創建哪個類的對象則調用該類的析構函數,銷毀那個類的對象則調用該類的析構函數
運行結果:
簡單來說:mian函數中無法調用Time里面的析構函數,但想將d銷毀就要把_t給銷毀,這是編譯器就會自動生成析構函數來去調用Time里面的析構函數將_t銷毀(內置類型系統會直接將其內存回收)
注意:有資源申請時,一定要寫析構函數,否則會造成資源泄漏。
拷貝構造函數
拷貝構造函數的概念
拷貝構造函數,就是以一個類對象來創建一個一摸一樣的類對象。
拷貝構造函數:只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存
在的類類型對象創建新對象時由編譯器自動調用。
拷貝構造函數的特性
拷貝構造函數是構造函數的一個重載形式
拷貝構造函數的參數只有一個且必須是類類型對象的引用,使用傳值方式編譯器直接報錯,
因為會引發無窮遞歸調用
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// Date(const Date d) // 錯誤寫法:編譯報錯,會引發無窮遞歸Date(const 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;
}
若未顯式定義,編譯器會生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲 按
字節序完成拷貝,這種拷貝叫做淺拷貝,或者值拷貝。
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本類型(內置類型)int _year = 1970;int _month = 1;int _day = 1;// 自定義類型Time _t;
};
int main()
{Date d1;// 用已經存在的d1拷貝構造d2,此處會調用Date類的拷貝構造函數// 但Date類并沒有顯式定義拷貝構造函數,則編譯器會給Date類生成一個默認的拷貝構造函數Date d2(d1);return 0;
}
淺拷貝和深拷貝:
淺拷貝是默認的拷貝方式:編譯器自動生成的拷貝構造函數會按 字節序 逐成員復制,對于指針成員,只復制指針的地址值,而不復制指針指向的底層數據。
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申請空間失敗");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType *_array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}
類里面沒有拷貝構造函數,那么編譯器會生成默認構造構造函數,而它是以按 字節序 逐成員復制,但類成員變量里面有指針,拷貝構造函數只會賦值地址值。而當拷貝一個新的類對象時,s1與s2里的array會指向同一塊空間,這時析構會重復釋放兩次通空間。因此有動態資源,就需要深拷貝。
深拷貝是手動實現的拷貝方式:不僅復制對象的成員變量,還會為指針等動態資源重新分配內存,并復制底層數據,讓新對象和原對象擁有完全獨立的資源。
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申請空間失敗");return;}_size = 0;_capacity = capacity;}Stack(const DataType& other){_array = (DataType*)malloc(other._capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申請空間失敗");return;}// 2. 復制原對象的元素(深拷貝核心:不僅復制指針,還要復制數據)memcpy(_array, other._array, other._size * sizeof(DataType));// 3. 復制大小和容量_size = other._size;_capacity = other._capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType *_array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}
簡單來說就是自己手寫完成不是地址值,而是數據的傳遞。
只要類中包含需要手動管理的動態資源,就必須實現深拷貝:
指針指向堆內存(new/delete 分配的資源)。
包含文件句柄、網絡連接等“獨占性資源”。
成員變量是其他帶動態資源的類類型(需確保成員類自身也實現了深拷貝)
拷貝構造函數典型調用場景:
使用已存在對象創建新對象
函數參數類型為類類型對象
函數返回值類型為類類型對象
class Date
{
public:Date(int year, int minute, int day){cout << "Date(int,int,int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}
private:int _year;int _month;int _day;
};
Date Test(Date d)
{Date temp(d);return temp;
}
int main()
{Date d1(2022,1,13);Test(d1);return 0;
}
為了提高程序效率,一般對象傳參時,盡量使用引用類型,返回時根據實際場景,能用引用
盡量使用引用。
賦值運算符重載
運算符重載
C++為了增強代碼的可讀性引入了運算符重載,運算符重載是具有特殊函數名的函數。
函數原型:返回值類型 operator操作符(參數列表)
注意事項:
不能通過連接其他符號來創建新的操作符(不可行):比如operator@。 重載操作符必須有一個類類型參數
用于內置類型的運算符,其含義不能改變,例如:內置的整型+,不 能改變其含義。
作為類成員函數重載時,其形參看起來比操作數數目少1,因為成員函數的第一個參數為隱
藏的this
. :: sizeof ?: . 注意以上5個運算符不能重載。(牢記牢記牢記)*
全局的operator==
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;
}
要使operator==成為全局的,就要將成員變量成公有。因為實在類外實現的,所以訪問類里面的東要公有。
但這就會出限一個問題:封裝性降低。如何解決呢?:友元和將 operator= =放入類里(這個好像與要求背道而馳了。)
operator= =放入類里(參數有一個最左邊的this指針):
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d2)// 這里需要注意的是,左操作數是this,指向調用函數的對象bool operator==(const Date& d2){return _year == d2._year;&& _month == d2._month&& _day == d2._day;}
private:int _year;int _month;int _day;
};
賦值運算符重載
賦值運算符重載格式:
參數類型:const T&,傳遞引用可以提高傳參效率( T指類)
返回值類型:T&,返回引用可以提高返回的效率,有返回值目的是為了支持連續賦值
檢測是否自己給自己賦值
返回*this :要復合連續賦值的含義
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;}
private:int _year ;int _month ;int _day ;
};
賦值運算符只能重載成類的成員函數不能重載成全局函數(不能出類外):
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int _year;int _month;int _day;
};
// 賦值運算符重載成全局函數,注意重載成全局函數時沒有this指針了,需要給兩個參數
Date& operator=(Date& left, const Date& right)
{if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;
}
運行結果:
原因:賦值運算符如果不顯式實現,編譯器會生成一個默認的。此時用戶再在類外自己實現
一個全局的賦值運算符重載,就和編譯器在類中生成的默認賦值運算符重載沖突了。
如果沒有顯式實現時,編譯器會生成一個默認賦值運算符重載,以值的方式逐字節拷貝。那么也會有淺拷貝的類似問題。
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申請空間失敗");return;}_size = 0;_capacity = capacity;}Stack(const DataType& other){_array = (DataType*)malloc(other._capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申請空間失敗");return;}// 2. 復制原對象的元素(深拷貝核心:不僅復制指針,還要復制數據)memcpy(_array, other._array, other._size * sizeof(DataType));// 3. 復制大小和容量_size = other._size;_capacity = other._capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}Stack& operator=(const Stack& other){// 1. 檢查自賦值,避免自身賦值時的錯誤if (this != &other){// 2. 釋放當前對象的舊資源free(_array);// 3. 分配新內存_array = (DataType*)malloc(other._capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申請空間失敗");return *this;}// 4. 深拷貝數據memcpy(_array, other._array, other._size * sizeof(DataType));// 5. 復制其他成員變量_size = other._size;_capacity = other._capacity;}// 6. 返回當前對象引用,支持鏈式賦值return *this;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType *_array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2;s2 = s1; // 現在可以安全地使用賦值運算符了Stack s3 = s1; // 這調用的是拷貝構造函數,不是賦值運算符return 0;
}
賦值運算符重載(處理 “給已有對象賦值”)可以處理這個(s2 = s1;)。
前置++和后置++重載
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的結果// 注意:this指向的對象函數結束后不會銷毀,故以引用方式返回提高效率Date& operator++(){_day += 1;return *this;}// 后置++:// 前置++和后置++都是一元運算符,為了讓前置++與后置++形成能正確重載// C++規定:后置++重載時多增加一個int類型的參數,但調用函數時該參數不用傳遞,編譯器自動傳遞// 注意:后置++是先使用后+1,因此需要返回+1之前的舊值,故需在實現時需要先將this保存一份,然后給this+1// 而temp是臨時對象,因此只能以值的方式返回,不能返回引用Date operator++(int){Date temp(*this);_day += 1;return temp;}
private:int _year;int _month;int _day;
};
int main()
{Date d;Date d1(2022, 1, 13);d = d1++; // d: 2022,1,13 d1:2022,1,14d = ++d1; // d: 2022,1,15 d1:2022,1,15return 0;
}
const成員函數
將const修飾的“成員函數”稱之為const成員函數,const修飾類成員函數,實際修飾該成員函數
隱含的this指針,表明在該成員函數中不能對類的任何成員進行修改。
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << "Print()" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}void Print() const{cout << "Print()const" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}
private:int _year; // 年int _month; // 月int _day; // 日
};
void Test()
{Date d1(2022,1,13);d1.Print();const Date d2(2022,1,13);d2.Print();
}
1.const 對象可以調用非 const 成員函數嗎?
不能。const對象調用成員函數時,編譯器會檢查函數是否為const,非const成員函數可能修改對象狀態,為保證const對象只讀”,禁止調用。
2.非 const 對象可以調用 const 成員函數嗎?
可以。const成員函數承諾 “不修改對象”,非const對象調用時,不會破壞對象狀態,因此允許。
3.const 成員函數內可以調用其它的非 const 成員函數嗎?
不能。const成員函數中,this是const 類名*類型,調用非const成員函數會嘗試去掉const,可能破壞對象const性,編譯器禁止。
4.非 const 成員函數內可以調用其它的 const 成員函數嗎?
可以。非const成員函數中,this是類名*類型,調用const成員函數時,const成員函數的 “只讀”特性與非const對象兼容,允許調用。
取地址及const取地址操作符重載#
這兩個默認成員函數一般不用重新定義 ,編譯器默認會生成。
class Date
{
public :Date* operator&(){return this ;}const Date* operator&()const{return this ;}
private :int _year ; // 年int _month ; // 月int _day ; // 日
};
可以在想讓別人獲取到指定的內容特殊情況時,才需要重載。