天天看點

【C++ Primer】重載運算與類型轉換一、基本概念二、輸入輸出運算符三、算數和關系運算符四、指派運算符五、下标運算符六、遞增遞減運算符七 、成員通路運算符八、函數調用運算符九、重載,類型轉換與運算符十、重載二進制運算符十一、String類的實作:

文章目錄

  • 一、基本概念
  • 二、輸入輸出運算符
    • 1、重載輸出運算符<<
    • 2、重載輸入運算符>>
  • 三、算數和關系運算符
    • 1、相等運算符
    • 2、關系運算符
  • 四、指派運算符
  • 五、下标運算符
  • 六、遞增遞減運算符
  • 七 、成員通路運算符
  • 八、函數調用運算符
    • 1、lambda表達式的原理
    • 2、标準庫定義的函數對象
    • 3、可調用對象與function
    • 4、标準庫function類型:
  • 九、重載,類型轉換與運算符
    • 1、類型轉換運算符
      • (1)類型轉換函數可能産生意外結果
      • (2)c++11 顯示類型轉換運算符
    • 2、避免有二義性的類型轉換
  • 十、重載二進制運算符
  • 十一、String類的實作:

  内置類型運算都有基本的運算符來支援,而我們想要實作類類型的一些運算,就需要自己重載運算符。

一、基本概念

  重載的運算符是具有特殊名字的函數,他們的名字由關鍵字operator和後面要定義的運算符号共同組成。和其他函數一樣,也有傳回類型,參數清單和函數體。

【Note】:

1)當一個重載的運算符是成員函數時,this綁定到左側的對象,成員運算符函數的參數比運算對象的參數少一個。比如+重載成成員函數,那麼它的參數隻能有一個,預設類對象this本身是一個參數。

2)不能重載的運算符 : :: .* . ?:

3)不應該被重載的運算符: 邏輯運算符,逗号運算符,取位址運算符。 重載邏輯運算符的時候不支援短路求值和求值順序的屬性,逗号和取位址本身有内置含義。盡量明智地使用運算符重載。

4)我們重載運算符時最好和内置的形成映射最好,也就是類似程度。比如IO運算符,運算符(若定義也該定義!=)等等。

  選擇作為成員函數還是非成員函數:

(1)成員函數:

=,[ ],( ), ->,

符合指派+=,-=… ,

++,–,(解引用) 改變對象的狀态或與給定類型密切相關的運算符。

(2)非成員函數:

算數+,-,,/…

相等性 ,!=

關系 >,<,>=,<=…

位運算符 ^, |, &

  可以看一個運算符左右兩側的對象是否可以互換位置,不能互換位置則一般為成員函數,可以互換位置則一般為非成員函數。

二、輸入輸出運算符

1、重載輸出運算符<<

  輸出運算符應定義為非成員函數,因為要讀寫類的非共有資料,是以要定義為友元。且傳回值為std::ostream&, 參數為std::ostream&和類對象的引用。

【Note】:

1)通常,輸出運算符應該主要負責列印對象的内容而非控制格式,輸出運算符不應該列印換行符我們應減少對輸出格式的控制。

2、重載輸入運算符>>

  輸入運算符和輸出運算符格式上類似,也是非成員函數,傳回輸入流引用(流不能拷貝)。當流含有錯誤類型的資料時讀取操作可能失敗,讀取資料到檔案末尾或遇到其他流錯誤也會失敗。

【Note】:

1)參數是輸入流引用和類對象和輸出運算符不同的是,輸入運算符必須處理輸入可能失敗的情況,而輸出運算符不需要。

2)不需要逐個檢查,隻在末尾檢查即可,當讀取操作發生錯誤時,輸入運算符應該負責從錯誤中恢複。

  一些輸入運算符可能需要更多的資料驗證工作。

friend ostream &operator<<(ostream &out, MyString &s);
friend istream &operator>>(istream &in, MyString &s);

ostream &operator<<(ostream &out, MyString &s)
{
	out << s.m_p;
	return out; 
}

istream &operator>>(istream &in, MyString &s)
{
	in >> s.m_p;
	return in; 
}
           

三、算數和關系運算符

  我們把算數和關系運算符定義成非成員函數以允許對左側或右側的運算對象進行轉換,一般不需要改變參數狀态,是以都是常量引用。

  如果定義了算數運算符,則他一般也會定義一個對應的複合指派運算符,最有效的方式是使用複合指派來定義算數運算符。

1、相等運算符

  依次比較每個成員是否相等。

【Note】:

1)如果類定義了operator==,那麼類也應該定義operator!=。

2)相等運算符和不相等運算符中的一個應該把工作委托給另外一個。

3)如果某個類邏輯上有相等性的含義,則該類應該定義operator==,這樣做可以使得使用者更加容易的使用标準庫算法(部分标準庫算法必須要有==支援)。

4)shared_ptr 和 unique_ptr 都用get( )傳回的指針來比較是否指向同一個對象

5)weak_ptr 要先使用lock( )擷取shared_ptr 然後在用get( )來比較位址進而判定是否指向同一個對象。

2、關系運算符

  定義了相等運算符的類通常也應該定義關系運算符,因為關聯容器和一些算法要用到小于運算符。是以operator<會比較有用。

【Note】:

1)如果存在唯一一種邏輯可靠的<定義,則應該為這個類定義<運算符,如果類同時還包含==,則當且僅當<的定義和産生的結果一緻時才定義<運算符。

  不要輕易定義<運算符,如果<和比較的邏輯相同(也就是比較的成員相同)才定義<運算符。一些情況我們必須定義<運算符,比如類對象需要存在map或set中的時候等等。

bool operator==(const char *p) const;
bool operator==(const MyString &s) const;
int operator<(const char *p);
int operator>(const MyString &s);

bool MyString::operator==(const char *p) const
{
	if(p == NULL)
	{
		return m_len ==0 ? true : false;
	}
	else
	{
		return m_len == strlen(p) ? !strcmp(m_p,p) : false;
	}
}

bool MyString::operator==(const MyString &s) const
{
	return *this == s ? true : false;
}

//重載大于、小于。
int MyString::operator<(const char *p)
{
	return strcmp(m_p,p);
}

int MyString::operator>(const MyString &s)
{
	return strcmp(m_p,s.m_p);
}
           

四、指派運算符

  類還可以定義其他指派運算符以使用别的類型作為右側運算對象。和拷貝指派運算符及移動指派運算符一樣,其他重載的指派運算符也必須先釋放目前記憶體空間,不同之處是無需檢查自指派。銷毀原資源,更新資源。

  複合指派運算符不非得是類的成員,不過我們還是傾向于把包括複合指派在内的所有指派運算符都定義在類的内部。

  為了與内置類型的複合指派保持一直,類中的複合指派運算符也要傳回其左側運算對象的引用。

【Note】:

1)我們可以重載指派運算符。不論形參的類型是什麼,指派運算符都必須定義為成員函數。

2)指派運算符必須定義成類的成員,複合指派運算符通常也應該這樣做,這兩類運算符都應該傳回對象的引用。

MyString &operator=(const char *p);
MyString &operator=(const MyString &s);
MyString &operator+=(const char *p);
MyString &operator+=(const MyString &s);

//重載指派操作符。
MyString &MyString::operator=(const char *p)
{
	if(m_p != NULL)
	{
		delete [] m_p;
		m_len = 0;
	}
	if(p == NULL)
	{
		m_len = 0;
		m_p = new char[1];
		strcpy(m_p,"");
	}
	else
	{
		m_len = strlen(p);
		m_p = new char[m_len+1];
		strcpy(m_p,p);
	}
	return *this;//傳回引用。
}

//重載指派操作符。
MyString &MyString::operator=(const MyString &s)
{
	if(m_p != NULL)
	{
		delete [] m_p;
		m_len = 0;
	}
	else
	{
		m_len = s.m_len;
		m_p = new char[m_len+1];
		strcpy(m_p,s.m_p);
	}
}

//重載加号運算符。
MyString &MyString::operator+=(const char *p)
{
	strcat(m_p,p);
	return *this;
}

MyString &MyString::operator+=(const MyString &s)
{
	strcat(m_p,s.m_p);
	return *this;
}
           

五、下标運算符

  表示容器的類通常可以通過元素在容器中的位置通路元素,這些類一般會定義下标運算符operator[ ]。

【Note】:

1)下标運算符必須是成員函數。

2)如果一個類包含下标運算符,則它通常會有兩個版本:一個傳回普通引用,另一個是類的常量成員并傳回常量引用。

  下标運算符通常以所通路元素的引用作為傳回值。可以作為左值或右值。

  我們最好同時定義下标運算符的常量版本和非常量版本,當作用于一個常量對象時,下标運算符傳回常量引用以確定我們不會修改傳回值。

char &operator[](int index) const;

//重載下标操作符。
char &MyString::operator[](int index) const
{
	return m_p[index];
}
           

六、遞增遞減運算符

【Note】:

1)定義遞增和遞減運算符的類應該同時定義前置版本和後置版本。這些運算符應該被定義為類的成員。

2)為了與内置版本保持一緻,前置運算符應該傳回遞增或遞減後對象的引用。

3)為了與内置版本保持一緻,後置運算符應該傳回對象的原值(遞增或遞減之前的值),傳回的形式是一個值而非一個引用。

  區分前置運算符和後置運算符:後置版本提供一個額外的不被使用的int類型的參數,使用後置運算符時,編譯器為這個形參提供一個值為0的實參。這個形參的唯一作用就是區分前置版本和後置版本。對于後置版本來說,在遞增或遞減之前首先需要記錄對象的狀态。

  後置版本裡可以調用前置版本。前置版本在遞增之前要判斷是否到達末尾,前置版本遞減要在遞減之後判斷是否出界。如果我們要通過函數調用的方式調用後置版本,則必須為他整型參數傳遞一個值,盡管我們不使用這個值。

七 、成員通路運算符

  解引用運算符檢查是否在範圍内,然後傳回所指元素的一個引用,箭頭運算符不執行任何自己的操作,而是調用解引用運算符并傳回解引用結果的位址。

【Note】:

1)箭頭運算符必須是類的成員,解引用運算符通常也是類的成員,盡管并非必須如此。

2)重載的箭頭運算符必須傳回類的指針或者自定義了箭頭運算符的某個類的對象。

八、函數調用運算符

  如果類重載了函數調用運算符,則我們可以像使用函數一樣使用該類的對象,因為這個類同時也能存儲狀态,是以與普通函數相比更加具有靈活性。

#include <iostream>

struct absInt
{
    int operator()(int val)const
    {
        return val < 0 ? -val : val;
    }
};

int main()
{
    int val = -42;
    absInt t;
    std::cout << t(val) << std::endl;   //t是一個對象而非函數
}
           

【Note】:

1)函數調用運算符必須是成函數。一個類可以員定義多個不同版本的調用運算符,互相之間應該在參數數量或類型上有所差別。

  如果類定義了調用運算符,則該類被稱為函數對象,因為可以調用這種對象,是以我們說這些對象的行為像函數一樣。

  函數對象類除了operator()之外也可以包含其他成員,通常函數對象類包含一些資料成員,這些成員用來定制調用運算符中的操作。

#include <iostream>
#include <string>

class PrintString
{
    public:
        PrintString(std::ostream &o = std::cout, char t = ' '):
            os(o), c(t) { }
        void operator()(const std::string &s)const   //借用輔助工具來完成函數的操作
        {
            os << s << c;
        }

    private:                     //private成員可以用來儲存“輔助”工具
        std::ostream &os;
        char c;
};

int main()
{
    PrintString ps;
    std::string s = "abc";
    ps(s);
}
           

  函數對象比一般函數靈活就是它可以讓另完成函數所需要的輔助成員成為自己的類成員。和lambda類似,函數對象常常作為泛型算法的實參。

1、lambda表達式的原理

  當定義一個lambda時,編譯器生成一個與lambda對應的新的類類型。當向一個函數傳遞一個lambda時,同時定義了一個新類型和該類型的一個對象。

  其實當我們編寫一個lambda後,編譯器将該表達式翻譯成一個匿名的類,并且重載了函數調用運算符即括号運算符。

[](const string &lhs, const string &rhs)
{ return lhs.size() < rhs.size(); }
//上述等價于:
class shrotstring
{
        public:
             bool operator()(const string &lhs, const string &rhs)const
             {    return lhs.size() < rhs.size(); }
};
           

  對于捕獲變量的lambda表達式來說,編譯器在建立類的時候,通過成員函數的形式儲存了需要捕獲的變量。

  lambda産生的類不含預設構造函數,指派運算符及預設析構函數:它是否含有預設的拷貝/移動構造函數則通常視捕獲的資料成員類型而定。

2、标準庫定義的函數對象

  頭檔案

#include <functional>

,标準庫定義了一組表示算術運算符、關系運算符和邏輯運算符的類,每個類分别定義了一個執行指令操作的調用運算符。

//算術:
plus<Type>、minus<Type>、multiplies<Type>、divides<Type>
modulus<Type>、negate<Type>
//關系:
equal_to<Type>、not_equal_to<Type>、greater<Type>
greater_equal<Type>、less<Type>、less_equal<Type>
//邏輯運算符:
logical_and<<#class _Tp#>>、logical_not<<#class _Tp#>>、logical_or<<#class _Tp#>>
           
//我們也可以在算法中使用标準庫函數對象。
sort(svec.begin(), svec.end(), std::greater<std::string>());  
// 而且标準庫規定其函數對象對于指針同樣适用。排序的同樣是string。
sort(svec.begin(), svec.end(), std::less<std::string *>());  
           

3、可調用對象與function

  c++中有幾種可調用的對象:函數,函數指針,lambda表達式,bind建立的對象,以及函數對象(重載了函數調用運算符的類)。

  和其他對象一樣,可調用的對象也有類型。然而兩個不同的可調用對象确有可能享用同一種調用形式。

int add(int a, int b) { return a+b; }
auto t = [](int a, int b) { return a+b; }
class A
{
        int operator()(int a, int b)
        {
                 return a+b;
        }
}
....
類型都是 int(int, int)
           

  但實際操作比如我們想

vector<int(*)(int, int)>

來儲存他們是不行的,隻能儲存第一個,因為他們畢竟是不同的對象!

4、标準庫function類型:

  我們可以使用一個名為function的新的标準庫類型來解決上面的問題。頭檔案

#include <functional>

#include <bits/stdc++.h>
using namespace std;
//普通函數。
int add(int i, int j){ return i + j; }

int main(int argc, char const *argv[])
{
	/*********************
	*标準庫定義的函數對象。
	**********************/
	vector<string> svec{"abc","bcd"};
	sort(begin(svec),end(svec),greater<string>());
	for (const auto s : svec)
	{
		cout << s << endl;
	}

	/***************
	*标準庫function。
	****************/
	//lambda對象類。
	auto mod = [] (int i, int j){ return i % j; };
	//函數對象類。
	struct divide
	{
		int operator()(int i, int j){ return i / j; }
	};
	map<string, function<int(int ,int)>> binops = 
	{
		{"+",add},
		{"-",minus<int>()},
		{"*",[] (int i, int j){ return i*j; }},
		{"/",divide()},
		{"%",mod}
	};
	vector<int> ivec
	{
		binops["+"](4,2),
		binops["-"](4,2),
		binops["*"](4,2),
		binops["/"](4,2),
		binops["%"](4,2)
	};
	for (const auto i : ivec)
	{
		cout << i << endl;
	}
	system("pause");
	return 0;
}
           

【Note】:

1)當我們有函數重載對象時,不能直接将相同的函數名字放入function,必須通過函數指針或者lambda來消除二義性。

九、重載,類型轉換與運算符

  我們能定義類類型之間的轉換,轉換構造函數和類型轉換運算符共同定義了類類型轉換。

1、類型轉換運算符

  類的一種特殊的成員函數,負責将一個類類型的值轉換成其他類型。

class SmallInt
{
	friend ostream &operator<<(ostream &out, SmallInt &s);
public:
	SmallInt(int i = 0):val(i)
	{
		if(i < 0 || i > 255)
			throw out_of_range("bad value");
	}
	SmallInt &operator=(size_t p);
	explicit operator int() const { return val; }
	~SmallInt(){}
private:
	size_t val;
};
           

  type表示某種類型。但是該類型必須能作為傳回類型。類型轉換運算符既沒有顯示的傳回類型,也沒有形參,而且必須定義成類的成員函數,類型轉換通常不應該改變待轉換的對象。

【Note】:

1)一個類型轉換函數必須是類的成員函數,它不能聲明傳回類型,也沒有形參,類型轉換函數通常為const。

2)向bool的類型轉換通常用在條件部分,是以operator bool 一般定義為explicit 的。

3)應該避免過度的使用類型轉換函數。

(1)類型轉換函數可能産生意外結果

  類通常很少定義類型轉換運算符,但是定義像bool類型轉換還是比較常見。

(2)c++11 顯示類型轉換運算符

explicit operator type( ) const { }; 

static_cast<type>(name);
           

  當類型轉換運算符是顯式的我們才能隻能類型轉換。不過必須通過顯式的強制類型轉換才行。但是存在一個例外:既當如果表達式被用作 條件,則編譯器會将顯示的類型轉換自動應用于它。包括while, if, do, for語句頭條件表達式,(!, ||, &&)的運算對象, (? :)條件表達式。 流對象轉換bool也是因為标準庫定義了流向bool顯式類型轉化。

2、避免有二義性的類型轉換

  如果一個類中包含一個或多個類型轉換,則必須確定在類類型和目标類型轉換之間隻存在唯一一種轉換方式。否則我們的代碼可能會存在二義性。

  通常情況下,不要為類定義相同的類型轉換,也不要在類中定義兩個及兩個以上轉換源或轉換目标是算數類型的轉換。我們無法用強制類型轉換來解決二義性,因為強制類型轉換也面臨着二義性。最好不要建立兩個轉換源都是算數類型的轉換。

operator int( )const
operator double( )const
           

  當我們使用兩個使用者定義的類型轉換時,如果轉換函數之前或之後存在标準類型轉換,則标準類型轉換将決定最佳比對到底是哪個。

【Note】:

1)不要讓兩個類執行相同的類型轉換,比如A轉換B的同時B也轉換為A。

2)避免轉換目标是内置算數類型的類型轉換。

3)如果我們對一個類既提供了轉換目标是算數類型的類型轉換,也提供了重載的運算符,則會遇到重載運算符與内置運算符二義性的問題。

#include <bits/stdc++.h>
using namespace std;
class SmallInt
{
	friend ostream &operator<<(ostream &out, SmallInt &s);
public:
	SmallInt(int i = 0):val(i)
	{
		if(i < 0 || i > 255)
			throw out_of_range("bad value");
	}
	SmallInt &operator=(size_t p);
	explicit operator int() const { return val; }
	~SmallInt(){}
private:
	size_t val;
};

SmallInt &SmallInt::operator=(size_t p)
{
	val = p;
	return *this;
}

ostream &operator<<(ostream &out, SmallInt &s)
{
	out << s.val;
	return out; 
}

int main(int argc, char const *argv[])
{
	SmallInt si = 3;
	cout << si << endl;
	int k = static_cast<int>(si) + 3;
	cout << k;
	system("pause");
	return 0;
}
           

十、重載二進制運算符

  為了滿足某些運算符的可交換性,重載二進制操作符時應該将其聲明為友元函數。

#include <bits/stdc++.h>
using namespace std;
class Integer 
{
	friend Integer operator+(int value, Integer integer); 
public:
    Integer();
    Integer(int value);
    Integer operator+(int value);
    void operator=(int value);
    operator int() const;
private:
    int m_value;  
};
Integer::Integer() {
    m_value = 0;
}
Integer::Integer(int value) {
    m_value = value;
}

Integer Integer::operator+(int value) {
    int tmpValue = m_value + value;
    return Integer(tmpValue);
}

void Integer::operator=(int value) {
     m_value = value;
}
Integer::operator int() const {
    return m_value;
}

Integer operator+(int value, Integer integer) {
    int tmpValue = integer.m_value + value;
    return Integer(tmpValue);
}

int main(int argc, char const *argv[])
{
	Integer integer = Integer(10);  
	Integer tmpInteger = 100;   //重載=運算符。
	tmpInteger = integer + 1;   //重載Integer成員函數+運算符。
	tmpInteger = 1 + tmpInteger;//重載友元函數+運算符。
	return 0;
}
           

十一、String類的實作:

#include <bits/stdc++.h>
using namespace std;
class MyString
{
	friend ostream &operator<<(ostream &out, MyString &s);
	friend istream &operator>>(istream &in, MyString &s);
public:
	MyString() = default;
	MyString(const char *p);
	MyString(const MyString &s);
	~MyString();
	MyString &operator=(const char *p);
	MyString &operator=(const MyString &s);
	bool operator==(const char *p) const;
	bool operator==(const MyString &s) const;
	int operator<(const char *p);
	int operator>(const MyString &s);
	MyString &operator+=(const char *p);
	MyString &operator+=(const MyString &s);
	// MyString &operator*();
	// MyString *operator->();
	char &operator[](int index) const;
	inline int Size() const{ return m_len; }
private:
	int m_len;
	char *m_p;
};

class CheckString
{
public:
	CheckString() = default;
	~CheckString() = default;
	bool operator()(const MyString &s1, const MyString &s2) const
	{
		return s1.Size() < s2.Size();
	}
};


//預設構造函數。
MyString::MyString()
{
	m_len = 0;
	m_p = new char[1];
	strcpy(m_p,"");
}

//構造函數。
MyString::MyString(const char *p)
{
	if(p == NULL)
	{
		m_len = 0;
		m_p = new char[1];
		strcpy(m_p,"");
	}
	else
	{
		m_len = strlen(p);
		m_p = new char[m_len+1];
		strcpy(m_p,p);	
	}
}

//拷貝構造函數。
MyString::MyString(const MyString &s)
{
	m_len = s.m_len;
	m_p = new char[m_len+1];
	strcpy(m_p,s.m_p);	
}

//析構函數。
MyString::~MyString()
{
	if(m_p != NULL)
	{
		delete [] m_p;
		m_p = NULL;//防止出現野指針。
		m_len = 0;
	}
}

//重載指派操作符。
MyString &MyString::operator=(const char *p)
{
	if(m_p != NULL)
	{
		delete [] m_p;
		m_len = 0;
	}
	if(p == NULL)
	{
		m_len = 0;
		m_p = new char[1];
		strcpy(m_p,"");
	}
	else
	{
		m_len = strlen(p);
		m_p = new char[m_len+1];
		strcpy(m_p,p);
	}
	return *this;//傳回引用。
}

//重載指派操作符。
MyString &MyString::operator=(const MyString &s)
{
	if(m_p != NULL)
	{
		delete [] m_p;
		m_len = 0;
	}
	else
	{
		m_len = s.m_len;
		m_p = new char[m_len+1];
		strcpy(m_p,s.m_p);
	}
}

//重載下标操作符。
char &MyString::operator[](int index) const
{
	return m_p[index];
}

//重載輸出操作符。
ostream &operator<<(ostream &out, MyString &s)
{
	out << s.m_p;
	return out; 
}

istream &operator>>(istream &in, MyString &s)
{
	in >> s.m_p;
	return in; 
}

//重載等号操作符。
bool MyString::operator==(const char *p) const
{
	if(p == NULL)
	{
		return m_len ==0 ? true : false;
	}
	else
	{
		return m_len == strlen(p) ? !strcmp(m_p,p) : false;
	}
}

bool MyString::operator==(const MyString &s) const
{
	return *this == s ? true : false;
}

//重載大于小于。
int MyString::operator<(const char *p)
{
	return strcmp(m_p,p);
}

int MyString::operator>(const MyString &s)
{
	return strcmp(m_p,s.m_p);
}

//重載加号運算符。
MyString &MyString::operator+=(const char *p)
{
	strcat(m_p,p);
	return *this;
}

MyString &MyString::operator+=(const MyString &s)
{
	strcat(m_p,s.m_p);
	return *this;
}

// MyString &operator*()
// {
// 	return *this;
// }

// MyString *operator->()
// {
// 	return &this->operator*();
// }

int main(int argc, char const *argv[])
{
	MyString s1 = "s11";
	MyString s2("s22");
	MyString s3 = s2;
	s3 = "s33";
	cout << s2[0] << endl;
	cout << s3 << endl;
	if(s3 == "s33")
	{
		cout << "equal" <<endl;
	}

	if(s2 > s3)
	{
		cout << "s2>s3" << endl;
	}
	else
	{
		cout << "s2<s3" << endl;
	}
	s3+=s2;
	cout << s3 << endl;
	vector<MyString> vec{s1,s2,s3};
	stable_sort(begin(vec),end(vec),CheckString());
	cout << vec[1];
	// for (const auto s : vec)
	// {
	// 	cout << s << endl;
	// }
	system("pause");
	return 0;
}
           

繼續閱讀