天天看點

C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員

本文架構:

C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員

類和對象(2)

  • 1 this指針
  • 2 預設成員函數
    • 2.1 構造函數
    • 2.2 析構函數
    • 2.3 拷貝構造函數
    • 2.4 指派運算符重載
      • 2.4.1 運算符重載
      • 2.4.2 指派運算符重載
    • 2.5-2.6 取位址及const取位址操作符重載
  • 3 const 成員
    • 3.1 const修飾類的成員函數
    • 3.2關于函數調用的分析

1 this指針

C++編譯器給每個“非靜态成員函數”增加了一個隐藏的指針參數,讓該指針指向目前對象(函數運作時調用該函數的對象),在函數體中對所有成員變量的通路都是通過該指針通路的。但這個this指針對使用者來說是透明的,即使用者不需要來傳遞,編譯器自動完成。

簡言之,請看下面程式圖解:

下面是一個日期類内的成員函數,通過次函數可以發現this指針在成員函數中扮演的角色。下圖展示的兩段程式表明this指針是成員函數中預設隐藏的,通過該指針去通路類内的成員變量。

C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員

關于this指針有幾個需要注意的點:

  1. this指針的類型:類類型*const
  2. 隻能在成員函數的内部使用
  3. this指針本質其實是一個成員函數的形參,this指針一般存儲在棧上,對象調用成員函數時,将對象位址作為實參傳遞給this形參。對象中不存儲this指針。
  4. this指針是成員函數第一個隐含的指針形參,一般情況由編譯器通過ecx寄存器自動傳遞,不需要使用者傳遞。
  5. this是C++的一個關鍵字,不能用來定義參數變量
  • 思考:this指針可以為空嗎?
    C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員
    如上圖,兩段代碼,代碼一可以調用左邊類執行結果為Show(),而代碼二會出錯,問題在下圖:
    C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員

    分析:

    代碼一:調用類内的show函數,show函數内沒有使用到成員變量,是以沒有使用到this指針,程式可以正常運作。(說明:p->show()這個函數并沒有對p指針解引用,show函數沒有使用到this指針,show函數位址也沒有存到對象裡面,是以不會引發空指針通路的問題,是以就不會發生程式奔潰。)

    代碼二:調用的類内的PrintA函數,而PrintA函數中使用了類内的成員變量,是以此函數就使用了this指針,void PrintA()轉化成 void PrintA(A* this),this指針接收到對象p傳來的空指針,引發了通路問題。

2 預設成員函數

下面這個類既沒有成員函數,也沒有成員變量,我們稱之為空類。那麼空類裡面有什麼?很容易想到預設成員函數。下面介紹6個預設成員函數,記住預設成員函數就是我們可以不寫,會自動生成的。

2.1 構造函數

一個特殊的成員函數,主要任務是初始化對象。

特性:

  1. 函數名與類名相同
  2. 無傳回值
  3. 對象執行個體化時,編譯器自動調用(保證對象一定會被初始化,不會被忘記)對應的構造函數
  4. 構造函數可以重載(表示可以有多種初始化方式)

代碼展示:

class Date
{
public:
	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//無參數構造函數
	Date(){};

	//帶參數構造函數
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	//調用預設參數
	Date d2;
	d2.Display();
	
	//調用帶構造參數
	Date d3(2020,2,1);
	d3.Display();

	return 0;
}
           
C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員
  1. 如果類中沒有顯示定義構造函數,則C++編譯器會自動生成一個無參數的預設構造函數,一旦使用者顯示定義,編譯器将不再生成。
C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員

筆記:

對于内置類型(基本類型,語言原生定義的類型,如int,char,指針等),不初始化。

對于自定義類型(用class,struct等定義的類型),如下圖定義的類A的“A _aa;”等,編譯器會調用預設構造函數初始化。

C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員

6. 無參數的構造函數和全預設的構造函數都稱之為預設構造函數,并且預設構造函數隻能有一個。三種構造函數

  • 自己不寫,編譯器預設生成的構造函數
  • 自己寫的無參數的構造函數
Date()
	{
		_year = 2020;
		_month = 1;
		_day = 2;
	}
           
  • 自己寫的全預設的構造函數,(推薦)
Date(int year = 0, int month = 1, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
           

程式設計個人風格:

成員變量的命名方式:

int year_;
int mYear;
int m_year;//m 代表 member,成員變量
           

2.2 析構函數

析構函數是特殊的成員函數,其功能與構造函數相反,析構函數不是完成對象的銷毀,局部對象銷毀工作是有編譯器完成的。而對象在銷毀時會自動調用析構函數,完成類的一些資源清理工作。

析構函數有如下特征:

  1. 析構函數名是在類名前加字元 ~
  2. 無參數、無傳回值
  3. 一個類有且隻有一個析構函數(析構函數不能重載)。若未顯示定義,系統會調用預設的析構函數。
  4. 對象聲明周期結束時,C++編譯系統會自動調用析構函數。
class Date
{
public:
	//全預設構造函數
	Date(int year = 2020, int month = 1, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//析構函數不需要寫,沒有什麼可清理
	//預設生成的析構函數,不做什麼
private:
	int _year;
	int _month;
	int _day;
};

class Stack
{
public:
	//全預設構造函數
	Stack(int capacity = 4)
	{
		if (capacity <= 0)
		{
			_a = nullptr;
			_size = _capacity = 0;
		}
		else
		{
			_a = (int*)malloc(sizeof(int)*capacity);
			_capacity = capacity;
			_size = 0;
		}
	}
	//析構函數
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}
private:
	int *_a;
	int _size;
	int _capacity;
};

int main()
{
	Date d;
	Stack st;
	return 0;
}
           

析構和構造的順序:

若上述棧類定義了兩個棧對象

Stack st1; 
Stack st2;
           

則他們的構造析構順序為:st1先構造,st2後構造,st2先析構,st1後析構。

解釋:對象是定義在函數中的,函數調用會建立棧幀,棧幀中的對象構造和析構也要符合後進先出規則。

  1. 對于編譯器自動生成的預設析構函數,對自定義類型成員調用它的析構函數。(簡言之:自定義類型,調用對應的析構函數;内置類型,不處理)
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;//内置類型,不處理
};
           

2.3 拷貝構造函數

隻有單個形參,該形參是對本類類型對象的引用(一般用const修飾),在用已存在的類類型對象建立新對象時由編譯器自動調用。

  • 拷貝構造函數是構造函數的一個重載形式。
  • 拷貝構造函數的參數隻有一個,且必須使用引用傳參, 用傳值方式會引發無窮遞歸調用。
#include <iostream>
using namespace std;

// 日期類
class Date
{
public:
	//構造函數
	Date(int year = 0, int month = 1, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	//拷貝構造函數
	//Date(Date &d)
	Date(const Date &d)//使用const修飾Date表示不希望修改對象
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		//d._day = _day;//左值不可改變,因為使用了const
	}
	/*Date(const Date *d)//這樣寫可通過,不好,調用方式 Date d2(&d1);
	{
	_year = d->_year;
	_month = d->_month;
	_day = d->_day;
	}*/

	/*Date(Date d) //傳值拷貝,無窮遞歸,隻要傳值,就會調用拷貝構造函數
	{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	}*/

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2021, 5, 30);
	d1.Print();

	return 0;
}
           

分析傳值調用:

C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員
結論:每次傳值都會引發對象的拷貝,最終會形成無窮遞歸。
  • 若拷貝構造函數未顯示定義,系統生成預設的拷貝構造函數。 預設的拷貝構造函數的對象按記憶體存儲位元組序完成拷貝,稱這種拷貝為淺拷貝或者值拷貝。
#include <iostream>
using namespace std;

// 日期類
class Date
{
public:
	//構造函數
	Date(int year = 0, int month = 1, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//預設拷貝構造函數
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2021, 5, 30);
	d1.Print();
	//d2調用的是預設拷貝構造完成拷貝,d2和d1的值也是一樣的
	Date d2(d1);
	d2.Print();

	return 0;
}
           

總結:

  • 有時候淺拷貝不能達到使用效果,是以有了深拷貝。

像如Stack、連結清單等這樣的類,編譯器預設生成的拷貝構造完成的是淺拷貝,淺拷貝會導緻析構兩次,程式會奔潰,不滿足我們的需求,需要自己實作深拷貝。

淺拷貝适合日期類這樣的類。

  1. 調用析構函數時,這塊空間被free了兩次。
  2. 其中一個對象插入删除資料,都會導緻另一個對象也插入删除資料。

    拷貝構造函數對内置類型,完成淺拷貝或者值拷貝。對于自定義類型,成員會調用它的拷貝構造完成拷貝。

2.4 指派運算符重載

2.4.1 運算符重載

C++為了增強代碼可讀性引入運算符重載,運算符重載是具有特殊函數名的函數,也有傳回值類型,函數名字以及參數清單,其傳回值類型與參數清單與普通的函數類似。

函數名字為:關鍵字ope

注意點;

  • 不能通過連接配接其他符号來建立新的操作符:如[email protected]
  • 重載操作符必須有一個類類型或者枚舉類型的操作數
  • 用于内置類型的操作符,其含義不能改變,如内置類型 - ,不能改變為+
  • 作為類成員的重載函數時,其形參看起來比操作數數目少一個成員函數(this指針提供)
  • 像 .* 、:: 、sizeof、?: 、 . 這五種運算符不能重載。

一、全局operator==

#include <iostream>
using namespace std;

// 日期類
class Date
{
public:
	//構造函數
	Date(int year = 0, int month = 1, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	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;
}

int main()
{
	Date d1(2021, 5, 30);
	Date d2(2021, 5, 30);
	d1.Print();
	d2.Print();

	cout << (d1 == d2) << endl;
	return 0;
}
           

二、類内成員函數operator==

C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員
#include <iostream>
using namespace std;

// 日期類
class Date
{
public:
	//構造函數
	Date(int year = 0, int month = 1, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//類内運算符重載
	//bool operator(Date* this,const Date& d)
	bool operator==(const Date &d)
	{
		return _year == d._year //this._year == d._year
			&& _month == d._month //this._month == d._month
			&& _day == d._day; //this._day == d._day;

	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2021, 5, 30);
	Date d2(2021, 5, 30);
	d1.Print();
	d2.Print();

	//調用方式等價于 d1.operator == (d2); 
	//即 d1.operator == (&d1, d2); &1傳遞給this指針 d2傳遞給形參d
	cout << (d1 == d2) << endl;

	return 0;
}
           

2.4.2 指派運算符重載

指派運算符重載的幾個特點:

  • 參數類型
  • 傳回值
  • 檢測是否自己給自己指派
  • 傳回*this
  • 一個類如果沒有顯示定義指派運算符重載,編譯器也會自動生成一個,完成對象按位元組序的值拷貝。

    一、隻能 實作 d1 = d2 功能

    C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員
#include <iostream>
using namespace std;

// 日期類
class Date
{
public:
	//構造函數
	Date(int year = 0, int month = 1, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//拷貝構造函數
	Date(const Date &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	//指派運算符重載
	//d1 = d2
	//void operator=(Date* this, const Date &d)
	void operator=(const Date &d)
	{
		if (this != &d)//檢查不是自己給自己指派,才需要拷貝。
		{
			_year = d._year;//this._year = d._year;
			_month = d._month;//同上
			_day = d._day;//同上
		}
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	Date d2(2021, 5, 20);
	Date d3;

	//調用指派運算法 d1 = d2
	//調用方式 對于 d1 == d2 有 d1.operator=(&d1,d2);    
	d1 = d2;
	d1.Print();
	d2.Print();
	d3.Print();

	return 0;
}
           

二、連續指派運算符重載

C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員
#include <iostream>
using namespace std;

// 日期類
class Date
{
public:
	//構造函數
	Date(int year = 0, int month = 1, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//拷貝構造函數
	Date(const Date &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	//連續指派運算符重載
	//d1 = d2 = d3 
	//Date& operator=(Date* this, const Date &d)
	Date& operator=(const Date &d)
	{
		if (this != &d)//檢查不是自己給自己指派,才需要拷貝。
		{
			_year = d._year;//this._year = d._year;
			_month = d._month;//同上
			_day = d._day;//同上
		}

		return *this;
	}

private:
	int _year;
	int _month;
	int _day;
};



int main()
{
	Date d1;
	Date d2(2021, 5, 20);
	Date d3;

	//調用指派運算法 連續指派
	//調用方式 對于 d1 == d2 有 d1.operator=(&d1,d2);    
	d3 = d1 = d2;
	d1.Print();
	d2.Print();
	d3.Print();

	return 0;
}
           

對比:拷貝構造函數和指派運算符重載

調用方式:

  • 拷貝構造函數:

    已知d1,用同類對象d1初始化一個d2。

Date d1(2021, 5, 30);
	
	Date d2(d1);
           
  • 指派運算符重載

    雖然也是拷貝,但是實作已知d1和d2。相當于把d2的值拷貝給d1。

Date d1;
	Date d2(2021, 5, 20);
	
	d1 = d2;
           

對比:函數重載和運算符重載

都使用了重載這個名詞

  • 函數重載時,支援定義同名函數
  • 運算符重載是為了讓自定義類型可以像内置類型一樣去使用運算符

有時候編譯器生成的預設指派重載函數可以實作按位元組序的值拷貝,但有時候并不能達到我們要求,是以對于下面程式出現的奔潰問題,提出了深拷貝。

class String
{
public:
	String(const char* str = "")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	//析構函數
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
	}

private:
	char* _str;
};
int main()
{
	String s1("blogs");
	String s2("yumoz");
	s1 = s2;
}
           

2.5-2.6 取位址及const取位址操作符重載

C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員

附上代碼:

#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date* operator&()
	{
		cout << "Date* operator&()" << endl;
	    return this;
	}

	const Date* operator&() const
	{
		cout << "const Date* operator&()const" << endl;
		return this;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2021, 5, 27);
	const Date d2(2021, 4, 27);

	Date* ptr1 = &d1;
	const Date* ptr2 = &d2;

	cout << &d1 << endl;
	cout << &d2 << endl;

	return 0;
}

           

3 const 成員

3.1 const修飾類的成員函數

将const修飾的類成員函數稱之為const成員函數,const修飾類成員函數,實際修飾該成員函數隐含的this指針,表明在該成員函數中不能對類的任何成員進行修改。

C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員

3.2關于函數調用的分析

C++類和對象(2)(this指針、6個預設成員函數、const成員)1 this指針2 預設成員函數3 const 成員

對于:

1)可以調用

2)可以調用

3)可以調用 const Date* 到const Date*(權限縮小)

4)不可調用,出錯,錯誤為下面展示。const Date* 到 Date* (權限放大)

錯誤 1 error C2662: “void Date::Print(void)”: 不能将“this”指針從“const Date”轉換為“Date &”

幾個思考題:

  1. const對象可以調用非const成員函數嗎?

    答:不可以,仔細閱讀上圖,這種情況輸入權限放大,将隻讀類型調用可讀可寫類型,故會出錯。

  2. 非const對象可以調用const成員函數嗎?

    答:可以,可讀可寫調用隻讀,權限縮小,是以可以。

  3. const成員函數内可以調用其他的非const成員函數嗎?

    答:不可以,const Date* 調用Date* ,權限放大,不可以。

  4. 非const成員函數可以調用其他的const成員函數嗎?

    答:可以,Date* 調用const Date* 權限縮小,故可以調用。

繼續閱讀