C++ 輸入輸出運算符重載 感想

在C++中,經常會對輸入輸出運算符進行重載,而在重載的時候用到了友元(Friends)和引用返回(Returning References),這裏對爲什麼會這麼用發表一些思考。
比如,下面的類是一個簡單的Student類,其中重載了<<>>

//
// Created by lgl on 17-3-14.
//
#include <iostream>
#include <string>
#include <iostream>
using namespace std;

class Student{
    string name;
    int age;
    string country;

public:
    Student(){}
    Student(string name, int age, string country){
        this->name = name;
        this->age = age;
        this->country = country;
    }

    friend ostream &operator << (ostream &os, const Student &stu){
        os << "Name: " << stu.name <<
                "; Age: " << stu.age << "; Country: " << stu.country << ".";
        return os;
    }

    friend istream &operator >> (istream &is, Student &stu){
        is >> stu.name >> stu.age >> stu.country;
        return is;
    }
};

int main()
{
    Student s("xiaoMing", 15, "China");
    Student t;
    cin >> t;
    cout << s << endl;
    cout << t << endl;
    return 0;
}

運行:

輸入:
xiaoQiang 78 Earth
輸出:
Name: xiaoMing; Age: 15; Country: China.
Name: xiaoQiang; Age: 78; Country: Earth.

返回引用

先說一下爲什麼會返回引用。
當函數返回引用類型時,沒有複製返回值,返回的是對象本身,比如:

const string &shorterString(const string &s1,const string &s2)
{
    return s1.size() < s2.size() ? s1 : s2;
}

這裏參數s1和s2均沒有發生拷貝,直接傳遞了引用,而在返回的時候,也直接返回了較短的字符串的引用,整個過程中都沒有發生字符串的拷貝,節省了資源,比較高效。
所以對於返回引用,要求必須在函數的參數中,包含有以引用方式或指針方式存在的,需要被返回的參數。如果兩個參數都不是引用,則兩個參數實際上都是放在函數棧裏的局部變量,返回他們的引用類型會造成不可預知的錯誤(因爲函數結束運行時,棧地址被回收了,此時這些地址上的數據可以被其他量覆蓋,再次使用這些地址上的數據,已經不是原來的那個值了。)
比如:

int &smallInt(int a, int b)
{
    return a < b ? a : b;
}

編譯時會發出警告警告:函數可能返回局部變量的地址 [-Wreturn-local-addr] return a < b ? a : b;。(但是如果形參是string則不會發出警告,原因應該是string屬於字符串常量,儲存在只讀數據段,return str只是返回了該字符串在只讀數據段所在的首地址,當函數退出後,該字符串所在的內存不會被回收,所以是正常的。)
關於這些解釋的驗證,詳情見附1。
關於返回值的問題,具體可以參看函數中局部變量的返回這篇文章,通過幾個例子會對返回值和返回引用有更深的理解。
最後,我們再看ostream &operator <<其實就是在返回ostream的引用,所以形參用的也是ostream的引用ostream & os,而之所以用引用是因爲如果不使用引用返回,我們就只能使用值返回,值返回需要複製返回的對象,但是我們無法直接複製一個ostream對象(C++ Primer, 5th Edition, p494),所以不能使用值返回,只能使用引用返回。當然,也可以認爲<<操作的結果是一個可以遞進操作的左值,比如(cout << "haha") << "hehe"如果是臨時對象,就不會有遞進操作的能力

友元

那麼爲什麼要使用友元呢。因爲>><<的前置對象是cincout,不是Student類,所以不能把輸入輸出操作符設計成Student的類成員,而只能設計成普通函數。但是我們又想讓IO運算符讀寫Student類的非公有數據成員,因此自然用到了友元。

通過上面的介紹,我們也就可以記住:重載輸入輸出運算符需要返回引用,因此參數也只能是引用。同時需要將函數定義爲友元。理解了這些特徵,就很容易寫出重載函數了。

附1

#include <iostream>

using namespace std;

string &smallString(string a, string b)
{
    return a.size() < b.size() ? a : b;
}

int &smallInt(int a, int b)
{
    return a < b ? a : b;
}

int main()
{
    int &d = smallInt(3, 5);
    string &s = smallString("xixi", "hahaha");
    cout << d << endl;
    cout << s << endl;
    return 0;
}

函數運行結果爲:

32744
xixi

再次運行爲:

32598
xixi

很明顯int的值在不停變動。如果我們再增添一個函數doNothing,並在調用smallInt之後進行調用:

#include <iostream>

using namespace std;

string &smallString(string a, string b)
{
    return a.size() < b.size() ? a : b;
}

int &smallInt(int a, int b)
{
    return a < b ? a : b;
}

void doNothing(int a, int b)
{
    a < b ? a : b;
}

int main()
{
    int &d = smallInt(3, 5);
    string &s = smallString("xixi", "hahaha");
    doNothing(44, 55);
    cout << d << endl;
    cout << s << endl;
    return 0;
}

則程序穩定輸出:

44
xixi

即:原來屬於3的地址,現在被填上了44。smallInt函數只返回了3所在的地址,後來在調用doNothing的時候,這個地址的內容被44覆蓋了,那麼輸出d的時候,自然就是44。
這很符合之前關於返回引用部分的解釋。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章