適合具備 C 語言基礎的 C++ 教程之四
前言
在上一則教程中,我們講述了重載運算符中前 ++和后++的重載函數的實現,闡述了在 C++中可以將運算符進行重載的方法,這種方法大大地便利了程序員編寫代碼,在接下來地敘述中,我們將著重講述運算符重載時地一些更為細致地內容,其中就包括當重載地運算符返回值為引用和非引用兩種狀態時,代碼執行效率地高低以及采用在類內實現運算符重載函數的方法。
返回值為引用和非引用的區別
在上述所示的類當中,增加一部分代碼,加入析構函數以及拷貝構造函數,代碼如下所示:
- class Point
- {
- private:
- int x;
- int y;
- public:
- Point()
- {
- cout<<"Point()"<<endl;
- }
- Point(int x, int y) : x(x), y(y)
- {
- cout<<"Point(int x, int y)"<<endl;
- }
- Point(const Point& p)
- {
- cout<<"Point(const Point& p)"<<endl;
- x = p.x;
- y = p.y;
- }
- ~Point()
- {
- cout<<"~Point()"<<endl;
- }
- friend Point operator++(Point &p);
- friend Point operator++(Point &p, int a);
- void printInfo()
- {
- cout<<"("<<x<<", "<<y<<")"<<endl;
- }
- };
在上述的代碼中,我們在構造函數以及拷貝構造函數析構函數都加入了打印信息,其中,運算符重載函數前++和后++函數沿用之前的一樣,返回值不是引用,與此同時,我們在前 ++和后 ++函數中也加入打印信息的代碼,代碼如下所示:
- /* ++p */
- Point operator++(Point &p)
- {
- cout << "++p" << endl;
- p.x += 1;
- p.y += 1;
- return p;
- }
- /* p++ */
- Point operator++(Point &p, int a)
- {
- cout << "p++" << endl;
- Point n;
- n = p;
- p.x += 1;
- p.y += 1;
- return n;
- }
上述便是前 ++和 后 ++的重載函數,緊接著,書寫主函數的代碼,觀察當返回值為非引用的時候,代碼的運行效果,主函數代碼如下所示:
- int main(int argc, char **argv)
- {
- Point p1(1, 2);
- cout<<"begin"<<endl;
- ++p1;
- cout << "******************"<<endl;
- p1++;
- cout<<"end"<<endl;
- return 0;
- }
上述代碼的運行結果如下所示:
lhp7d3H1crAE9u2
依據運行結果我們分析一下,第一條輸出信息 Point(int x, int y)是因為執行了 Point p1(1,2);語句而調用的構造函數,++p這條輸出信息同樣也是因為執行了 ++p;而調用的構造函數,那緊接著的兩條輸出信息是如何產生的呢,我們回過頭去看看++p的函數,可以看到 ++p的函數是一個返回值為 Point類型的函數,而上述中的輸出語句 Point(const Point& p)和 ~Point()就是在創建這個返回值對象時調用的構造函數以及當返回值返回后調用的析構函數;而緊接著的輸出信息是 p++和 Point()以及~Point(),p++這個輸出信息自然是因為調用的后 ++重載運算符函數的構造函數而輸出的打印信息,那緊接著的 Point()和 ~Point()是因為在后 ++重載運算符函數中,創建的局部變量 Point n,進而調用了 Point()函數,以及函數退出之后,局部變量銷毀,調用了析構函數。
上述詳細地分析了各個打印信息輸出的原因,通過上述的打印信息我們可以清楚知道程序在什么地方調用了構造函數,在什么地方調用了析構函數,再次回顧上述的函數調用過程,可以看出來其實調用的Point(const Point& p)和~Point()是多余的,那要如何改進代碼呢,我們只需要將前 ++運算符重載函數的返回值類型改為引用就行,這樣就不會創建臨時的變量,同時也就不會在調用構造函數和析構函數,改動之后的代碼如下所示:
- Point& operator++(Point &p)
- {
- cout<<"++p"<<endl;
- p.x += 1;
- p.y += 1;
- return p;
- }
那么上述代碼的運行結果是什么呢?在主函數不變的情況下,輸出結果如下所示:
M4QzImA1uYxnBK9
可以看到上述結果中,之前在 ++p后輸出的兩條信息現在因為將返回值設置為引用之后就消失了,說明這樣的方法避免了調用構造函數和析構函數,節省了程序運行的空間,那如果將后++重載函數設置為引用可不可行呢,很顯然,如果返回的是 n的引用,那么這在語法中就是錯誤的,因為n是局部變量,局部變量在函數調用結束就銷毀了,是不能作為引用對象的。如果返回的是 p呢,那么函數的運行結果將發生改變,換句話說就是不是實現的后 ++這個功能了。
最后,總結一下,對于一個函數來說,函數的返回結果如果作為值返回,那么代碼的執行效率較低;如果作為引用返回,那么代碼的執行效率較高,但是會存在一個問題,引用返回可能會導致函數運行出錯,所以,在保證函數運行沒有錯誤的前提下,為了提高效率應該使用的是引用返回。
緊接著,我們知道我們在使用 C++進行編碼的時候,基本不會再采用 C語言中的語法 printf這個語句,隨之替代的是 cout這個語句,我們也知道我們使用 cout進行輸出的時候,往往采用的是下面這樣的輸出方式:
- cout << "m=" << m << endl; /* 此時 m 不是一個實例化對象 */
但是如果說此時 m 是一個實例化的對象,那么像上述這樣輸出就是存在問題的,這個時候,就需要對 <<運算符進行重載,重載的代碼如下所示:
- ostream& operator<<(ostream &o, Point p)
- {
- cout<<"("<<p.x<<", "<<p.y<<")";
- return o;
- }
稍微對上述代碼進行一下解釋, 這里為什么返回值是ostream&呢,是因為對于 cout來說,它是ostream類的實例化對象,在使用 cout進行輸出的時候,它所遵循的一個輸出格式是 cout <<,因此,這里的返回值是 ostream。為什么返回值是引用呢,是為了滿足下面所示代碼的運行,同時輸出了 m和 p1,結合上述代碼,我們來編寫主函數,主函數代碼如下所示:
- int main(int argc, char **argv)
- {
- Point p1(1,2);
- Point m;
- m = p1++;
- cout << "m =" << m << "p1 =" << p1 << endl;
- }
上述代碼的運行結果如下所示:
1cGujg7yqZSIfpK
可以看到在重載了運算符 <<之后,輸出實例化的對象也是可行的。
類內實現運算符重載函數
在上述代碼中我們實現的 +運算符重載函數以及前 ++運算符重載函數和后++運算符重載函數,都是在類外實現的,那么如果要在類內實現以上幾個運算符重載函數,應該如何寫呢,我們先回顧一下,在類外面實現的+運算符重載函數的函數聲明如下所示:
- friend Point operator+(Point &p1, Point &p2); /* 因為在類外要能夠訪問類里面的數據成員,因此這里使用的是友元 */
上述是在類外實現運算符重載函數時的函數原型,那么如果函數的定義就是在類里面實現的,函數又該如何編寫呢?首先,如果是在類里面實現,那么當前使用這個類進行實例化的對象本身就可以使用 *this來表征一個對象,這個時候,如果要重載 +運算符函數,那么就只需要一個Point類的形參就行,代碼如下所示:
- class Point
- {
- private:
- int x;
- int y;
- public:
- /* 省略相關構造函數的代碼,可以結合前文補全 */
- Point operator+(Point &p)
- {
- cout<<"operator+"<<endl;
- Point n;
- n.x = this->x + p.x;
- n.y = this->y + p.y;
- return n;
- }
- }
對比上述在類外面實現的代碼,對于重載的運算符 +來說,只有一個形參了,而與其相加的另一個對象使用的是this來替代。依據這樣的一種思路,我們繼續將前 ++和后 ++重載的運算符函數進行改寫,改寫之后的代碼如下所示:
- class Point
- {
- private:
- int x;
- int y;
- public:
- /* Point p(1,2); ++p */
- Point& operator++(void)
- {
- cout<<"operator++(void)"<<endl;
- this->x += 1;
- this->y += 1;
- return *this;
- }
- /* Point p(1,2); p++; */
- Point operator++(int a)
- {
- cout<<"operator++(int a)"<<endl;
- Point n;
- n = *this;
- this->x += 1;
- this->y += 1;
- return n;
- }
- };
結合上述的代碼,我們再來編寫主函數,主函數的代碼如下所示:
- int main(int argc, char ** argv)
- {
- Point p1(1,2);
- Point p2(2,3);
- Point m;
- Point n;
- cout << "begin" << endl;
- m = ++p1; /* m = p1.operator++(); */
- cout << "m =" << m << "p1 =" << p1 << endl;
- cout << "*********************" << endl;
- n = p2++; /* n = p2.operator++(0); */
- cout << "n =" << n << "p2 =" << p2 << endl;
- return 0;
- }
上述代碼中,注釋掉的代碼和沒注釋的代碼前后是等價的,只是說注釋掉的代碼看起來更加直觀,更加容易理解其背后的原理,而注釋前的代碼則更加簡潔。這里額外說一點,<<的重載函數是不能夠放到類內實現的,因為這個重載函數的形參不是 Point類的,所以其只能在類外才能實現。
上述中,敘述了在類內實現的重載運算符函數,接下來敘述一下 =運算符在類內實現的重載函數,我們以之前所說的 Person類來實現這個功能,Person類的代碼實現如下所示:
- class Person
- {
- private:
- char *name;
- int age;
- char *work;
- public:
- Person()
- {
- name = NULL;
- work = NULL;
- }
- Person(char *name, int age, char *work)
- {
- this->age = age;
- this->name = new char[strlen(name) + 1];
- strcpy(this->name,name);
- this->work = new char[strlen(work) + 1];
- strcpy(this->work, work);
- }
- /* 拷貝構造函數 */
- Person(Person &p)
- {
- this->age = p.age;
- this->name = new char[strlen(p.name) + 1];
- strcpy(this->name,p.name);
- this->work = new char[strlen(p.work) + 1];
- strcpy(this->work, p.work);
- }
- ~Person()
- {
- if (this->name)
- delete this->name;
- if (this->work)
- delete this->work;
- }
- void PrintInfo(void)
- {
- cout << "name =" << name << "age =" << age << "work =" << work << endl;
- }
- }
基于上述的代碼,我們可以書寫如下的主函數代碼:
- int main(int argc, char **argv)
- {
- Person p1("zhangsan", 18, "doctor");
- Person p2;
- p2 = p1;
- }
上述中,我們還沒有將 =運算符進行重載,就使用了 =實現了實例化對象的運算,這樣會存在一個什么問題呢,我們從源頭來進行分析,=運算符執行的是值拷貝,那么在執行了上述語句之后,p2和p1之間的關系是這樣的:
ywhv3zYKCaRjrXx
通過上述所示的圖片可以看出,如果不將 =進行重載,那么會讓 p1和 p2的name 和 work指向同一塊內存,這會造成什么問題呢,如果此時已經將 p1的內存釋放掉了,而這個時候又要釋放 p2的內存,這種情形就會出錯,同一塊內存不能夠釋放兩次。
因此,就需要對 =運算符進行重載,重載的代碼如下所示:
- /* 注意此處的代碼是在類里面實現的成員函數,這里省略的一部分代碼 */
- Person& operator=(Person &p)
- {
- if (this == &p)
- return *this;
- this->age = p.age;
- if (this->name)
- delete this->name;
- if (this->work)
- delete this->work;
- this->name = new char[strlen(p.name) + 1];
- strcpy(this->name, p.name);
- this->work = new char[strlen(p.work) + 1];
- strcpy(this->work, p.work);
- }
這樣子就會避免上述情況的出現,我們現在繼續來書寫主函數:
- int main(int argc, char **argv)
- {
- Person p1("zhangsan", 18, "doctor");
- cout<<"Person p2 = p1" <<endl;
- Person p2 = p1;
- Person p3;
- cout<<"p3=p1"<<endl;
- p3 = p1;
- cout<<"end"<<endl;
- p1.PrintInfo();
- p2.PrintInfo();
- p3.PrintInfo();
- return 0;
- }
上述主函數運行的結果如下所示:
2kiKb8NEfYynTdo
通過上述代碼我們看到,實際上代碼 Person p2 = p1的運行并不是調用的 = 的重載函數,而是調用的拷貝構造函數,只有 p3= p1才是調用的 =的重載函數。
在本章節的最后,額外補充一點,剛剛提到了拷貝構造函數,實際上拷貝構造函數的形參大多數都是加了const修飾符的,也就是像如下所示的這樣子:
- Person& operator=(const Person &p)
而這個時候,如果我們定義的 Person p1也是 const的,也就是像這樣:
- const Person p1("zhangsan", 18, "doctor");
那這個時候在使用 p1.PrintInfo()的時候就會出錯,因為此時必須把該成員函數也表明為 const的才行,代碼如下所示:
- /* 類內成員函數,省略部分代碼 */
- void PrintInfo(void) const
- {
- cout << "name =" << name << "age =" << age << "work =" << work << endl;
- }
總結一下也就是說:const對象只能夠調用const成員函數,而const表示的是此函數沒有對當前對象進行修改
小結
上述就是本期教程分享的內容,到本期教程截至,C++相對于 C語言不同的一些語法特性就到此結束了。下期教程將介紹 C++如何實現面向對象的方法。本期教程所涉及到的代碼可以通過百度云鏈接的方式獲取到。
鏈接:https://pan.baidu.com/s/1BC55_QH-iV23-ON0v1OGSA
提取碼:iyf7
本文轉載自微信公眾號「wenzi嵌入式軟件」,可以通過以下二維碼關注。轉載本文請聯系wenzi嵌入式軟件公眾號。