1. 程式人生 > >C++基礎教程面向物件(學習筆記(13))

C++基礎教程面向物件(學習筆記(13))

友元函式和類

在本章的大部分內容中,我們一直在傳播保護資料私密性的優點。但是,您可能偶爾會發現一些情況,您會發現在這些類之外需要緊密協作的類和函式。例如,您可能有一個儲存資料的類,以及一個在螢幕上顯示資料的函式(或另一個類)。雖然儲存類和顯示程式碼已經分開以便於維護,但顯示程式碼與儲存類的細節密切相關。因此,通過從顯示程式碼隱藏儲存類細節沒有太多用處。

在這種情況下,有兩種選擇: 1)讓顯示程式碼使用儲存類的公開功能。但是,這有幾個潛在的缺點。首先,必須定義這些公共成員函式,這需要時間,並且可能使儲存類的介面混亂。其次,儲存類可能必須公開顯示程式碼的功能,而這些功能並不是其他任何人都不想訪問的。沒有辦法說“此功能僅供顯示類使用”。

2)或者,使用友元類和友元函式,您可以讓顯示程式碼訪問儲存類的私有細節。這使得顯示程式碼可以直接訪問儲存類的所有私有成員和功能,同時保持其他的獨立性!在本課中,我們將詳細介紹如何完成此操作。

友元函式

一個友元函式是可以訪問類的私有成員,友元函式就像一個普通的函式。在所有其他方面,友元函式就像一個普通的函式。友元函式可以是普通函式,也可以是其他類的成員函式。要宣告友元函式,只需在您希望成為類的友元函式原型前面使用friend關鍵字。是否在類的私有或公共部分聲明瞭友元函式並不重要。

以下是使用友元函式的示例:

class Accumulator
{
private:
    int m_value;
public:
    Accumulator() { m_value = 0; } 
    void add(int value) { m_value += value; }
 
    // 使reset()函式成為此類的友元
    friend void reset(Accumulator &accumulator);
};
 
// reset()現在是Accumulator類的友元
void reset(Accumulator &accumulator)
{
    // 並且可以訪問Accumulator物件的私有資料
    accumulator.m_value = 0;
}
 
int main()
{
    Accumulator acc;
    acc.add(5); // 將5新增到 accumulator
    reset(acc); // 復位accumulato到0
 
    return 0;
}

在這個例子中,我們聲明瞭一個名為reset()的函式,它接受類Accumulator的物件,並將m_value的值設定為0.因為reset()不是Accumulator類的成員,所以通常reset()不會能夠訪問Accumulator的私人成員。但是,因為Accumulator已經專門宣告這個reset()函式是該類的友元函式,所以reset()函式可以訪問Accumulator的私有成員。

注意,我們必須將Accumulator物件傳遞給reset()。這是因為reset()不是成員函式。除非給定一個,否則它沒有* this指標,也沒有要使用的Accumulator物件。

這是另一個例子:

class Value
{
private:
    int m_value;
public:
    Value(int value) { m_value = value; }
    friend bool isEqual(const Value &value1, const Value &value2);
};
 
bool isEqual(const Value &value1, const Value &value2)
{
    return (value1.m_value == value2.m_value);
}

在這個例子中,我們宣告isEqual()函式是Value類的友元函式。isEqual()將兩個Value物件作為引數。因為isEqual()是Value類的朋友,所以它可以訪問所有Value物件的私有成員。在這種情況下,它使用該訪問權對兩個物件進行比較,如果它們相等則返回true。

雖然上面的兩個例子都是相當人為的,但後一個例子與我們在討論運算子過載時在第9章中遇到的情況非常相似!

多個友元函式

函式可以同時是多個類的友元函式。例如,請考慮以下示例:

class Humidity;
 
class Temperature
{
private:
    int m_temp;
public:
    Temperature(int temp=0) { m_temp = temp; }
 
    friend void printWeather(const Temperature &temperature, const Humidity &humidity);
};
 
class Humidity
{
private:
    int m_humidity;
public:
    Humidity(int humidity=0) { m_humidity = humidity; }
 
    friend void printWeather(const Temperature &temperature, const Humidity &humidity);
};
 
void printWeather(const Temperature &temperature, const Humidity &humidity)
{
    std::cout << "The temperature is " << temperature.m_temp <<
       " and the humidity is " << humidity.m_humidity << '\n';
}
 
int main()
{
    Humidity hum(10);
    Temperature temp(12);
 
    printWeather(temp, hum);
 
    return 0;
}

關於這個例子,有兩點值得注意。首先,因為PrintWeather是兩個類的朋友,所以它可以從兩個類的物件訪問私有資料。其次,請注意示例頂部的以下行:

class Humidity;

這是一個類原型,它告訴編譯器我們將來要定義一個名為Humidity的類。如果沒有這一行,編譯器會告訴我們在解析Temperature類中的PrintWeather()原型時它不知道Humidity是什麼。類原型與函式原型具有相同的作用 - 它們告訴編譯器什麼樣的東西,所以它現在可以使用並在以後定義。但是,與函式不同,類沒有返回型別或引數,因此類原型總是簡單的class ClassName,其中ClassName是類的名稱。

友元類

也可以使整個類成為另一個類的友元。這使得友元類的所有成員都可以訪問另一個類的私有成員。這是一個例子:

class Storage
{
private:
    int m_nValue;
    double m_dValue;
public:
    Storage(int nValue, double dValue)
    {
        m_nValue = nValue;
        m_dValue = dValue;
    }
 
    // Make the Display class a friend of Storage
    friend class Display;
};
 
class Display
{
private:
    bool m_displayIntFirst;
 
public:
    Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
 
    void displayItem(Storage &storage)
    {
        if (m_displayIntFirst)
            std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
        else // display double first
            std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
    }
};
 
int main()
{
    Storage storage(5, 6.7);
    Display display(false);
 
    display.displayItem(storage);
 
    return 0;
}

由於Display類是Storage的友元,因此任何使用Storage類物件的Display成員都可以直接訪問Storage的私有成員。該程式產生以下結果: 6.7 5 關於友元類的一些補充說明。首先,即使Display是Storage的朋友,Display也無法直接訪問儲存物件的* this指標。其次,僅僅因為Display是Storage的友元,這並不意味著Storage也是Display的朋友。如果你想讓兩個班級成為彼此的友元,那麼他們都必須將另一個類稱為友元。最後,如果A類是B的朋友,而B是C的友元,那並不意味著A是C的友元。

注意:使用友元函式和類時要小心,因為它允許友元函式或類,違反封裝。如果類的細節發生變化,友元的詳細資訊也將被迫改變。因此,將您對友元函式和類的使用限制在最低限度。

友元成員函式

您可以將單個成員函式設為友元,而不是將整個類視為友元。這與將普通函式作為友元類似地完成,除了使用包含className ::字首的成員函式的名稱(例如Display :: displayItem)。

然而,實際上,這可能比預期的要複雜一些。讓我們轉換前面的例子,使Display :: displayItem成為朋友成員函式。您可以嘗試這樣的事情:

class Display; //類Display的前向宣告
 
class Storage
{
private:
	int m_nValue;
	double m_dValue;
public:
	Storage(int nValue, double dValue)
	{
		m_nValue = nValue;
		m_dValue = dValue;
	}
 
	// Make the Display::displayItem member function a friend of the Storage class
	friend void Display::displayItem(Storage& storage); // 錯誤:儲存沒有看到類Display的完整定義
};
 
class Display
{
private:
	bool m_displayIntFirst;
 
public:
	Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
 
	void displayItem(Storage &storage)
	{
		if (m_displayIntFirst)
			std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
		else // display double first
			std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
	}
};

然而,事實證明這不起作用。為了使成員函式成為朋友,編譯器必須已經看到了friend成員函式的類的完整定義(而不僅僅是前向宣告)。由於類Storage尚未看到類Display的完整定義,因此編譯器在我們嘗試使成員函式成為朋友時會出錯。

幸運的是,只需在類Storage的定義之前移動類Display的定義,就可以輕鬆解決這個問題。

class Display
{
private:
	bool m_displayIntFirst;
 
public:
	Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
 
	void displayItem(Storage &storage) // 錯誤:編譯器不知道Storage是什麼
	{
		if (m_displayIntFirst)
			std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
		else // display double first
			std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
	}
};
 
class Storage
{
private:
	int m_nValue;
	double m_dValue;
public:
	Storage(int nValue, double dValue)
	{
		m_nValue = nValue;
		m_dValue = dValue;
	}
 
	// Make the Display::displayItem member function a friend of the Storage class
	friend void Display::displayItem(Storage& storage); // ok.
};

但是,我們現在有另一個問題。因為成員函式Display :: displayItem()使用Storage作為引用引數,我們只是將儲存定義移動到Display的定義之下,編譯器會抱怨它不知道儲存是什麼。我們無法通過重新排列定義順序來修復此問題,因為我們將撤消之前的修復。

幸運的是,這也可以通過幾個簡單的步驟來解決。首先,我們可以新增類儲存作為前向宣告。其次,在完整定義Storage類之後,我們可以將Display :: displayItem()的定義移出類。

這是這樣的:

class Storage; // 類儲存的前向宣告
 
class Display
{
private:
	bool m_displayIntFirst;
 
public:
	Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
	
	void displayItem(Storage &storage); // 此宣告行所需的上述宣告
};
 
class Storage // Storage完全定義
{
private:
	int m_nValue;
	double m_dValue;
public:
	Storage(int nValue, double dValue)
	{
		m_nValue = nValue;
		m_dValue = dValue;
	}
 
	//使Display :: displayItem成員函式成為Storage類的友元(需要檢視類Display的完整宣告,如上所述)
	friend void Display::displayItem(Storage& storage);
};
 
//現在我們可以定義Display :: displayItem,它需要看到類Storage的完整宣告
void Display::displayItem(Storage &storage)
{
	if (m_displayIntFirst)
		std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
	else // display double first
		std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
}
 
int main()
{
    Storage storage(5, 6.7);
    Display display(false);
 
    display.displayItem(storage);
 
    return 0;
}

現在一切都會正確編譯:類Storage的前向宣告足以滿足Display類中Display :: displayItem()的宣告,Display的完整定義滿足宣告Display :: displayItem()作為Storage的朋友,並且類Storage的完整定義足以滿足成員函式Display :: displayItem()的定義。如果這有點令人困惑,請參閱上面程式中的註釋。

這看似乎起來很痛苦。幸運的是,這種功能只是必要的,因為我們試圖在一個檔案中做所有事情。更好的解決方案是將每個類定義放在單獨的標頭檔案中,並在相應的.cpp檔案中使用成員函式定義。這樣,所有的類定義都會立即在.cpp檔案中可見,並且不需要重新安排類或函式!

Summary

友元函式或類是可以訪問另一個類的私有成員的函式或類,就好像它是該類的成員一樣。這允許友元函式或類與其他類密切合作,而不會讓其他類公開其私有成員(例如通過訪問功能)。

當兩個或多個類需要以一種親密的方式一起工作時,或者更常見的是,在定義過載運算子時(我們將在第9章中介紹),很少使用Friending。

注意,使特定成員函式成為友元需要首先看到成員函式的類的完整定義。

Quiz time

1)在幾何中,點是空間中的位置。我們可以將3d空間中的點定義為座標x,y和z的集合。例如,Point(2.0,1.0,0.0)將是座標空間x = 2.0,y = 1.0和z = 0.0的點。

在物理學中,向量是具有幅度(長度)和方向(但沒有位置)的量。我們可以將3d空間中的向量定義為x,y和z值,表示向量沿x,y和z軸的方向(長度可以從這些中匯出)。例如,Vector(2.0,0.0,0.0)將是表示沿著正x軸(僅)的方向的向量,長度為2.0。

可以將向量應用於點以將點移動到新位置。這是通過將向量的方向新增到點的位置以產生新位置來完成的。例如,Point(2.0,1.0,0.0)+ Vector(2.0,0.0,0.0)將產生點(4.0,1.0,0.0)。

點和向量通常用在計算機圖形中(表示形狀頂點的點,矢量表示形狀的移動)。

給出這個程式:

#include <iostream>
 
class Vector3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Vector3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
};
 
class Point3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Point3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
 
	void moveByVector(const Vector3d &v)
	{
		// 將此函式實現為Vector3d類的友元
	}
};
 
int main()
{
	Point3d p(1.0, 2.0, 3.0);
	Vector3d v(2.0, 2.0, -3.0);
 
	p.print();
	p.moveByVector(v);
	p.print();
 
	return 0;
}

1a)使Point3d成為Vector3d的友元類,並實現函式Point3d :: moveByVector()

解決方案:

#include <iostream>
 
class Vector3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Vector3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
 
	friend class Point3d; // Point3d現在是Vector3d類的友元
};
 
 
class Point3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Point3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
 
 
	void moveByVector(const Vector3d &v)
	{
		m_x += v.m_x;
		m_y += v.m_y;
		m_z += v.m_z;
	}
};
 
 
int main()
{
	Point3d p(1.0, 2.0, 3.0);
	Vector3d v(2.0, 2.0, -3.0);
 
	p.print();
	p.moveByVector(v);
	p.print();
 
	return 0;
}

2b)不要讓類Point3d成為類Vector3d的友元,而是使成員函式Point3d :: moveByVector成為Vector3d類的友元。

解決方案:

class Vector3d; //首先,我們需要告訴編譯器存在一個名為Vector3d的類
 
class Point3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Point3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
 
	void moveByVector(const Vector3d &v); // 所以我們可以在這裡使用Vector3d
       // 注意:我們不能在這裡定義這個函式,因為還沒有宣告Vector3d(只是向前宣告)
};
 
class Vector3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Vector3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
 
	friend void Point3d::moveByVector(const Vector3d &v); // Point3d :: moveByVector()現在是Vector3d類的友元
};
 
// 現在已經聲明瞭Vector3d,我們可以定義函式Point3d :: moveByVector()
void Point3d::moveByVector(const Vector3d &v)
{
	m_x += v.m_x;
	m_y += v.m_y;
	m_z += v.m_z;
}
 
int main()
{
	Point3d p(1.0, 2.0, 3.0);
	Vector3d v(2.0, 2.0, -3.0);
 
	p.print();
	p.moveByVector(v);
	p.print();
 
	return 0;
}

3b)使用5個單獨的檔案重新實現測驗問題1b的解決方案:Point3d.h,Point3d.cpp,Vector3d.h,Vector3d.cpp和main.cpp。 解決方案: Point3d.h:

// 定義Point3d類的標頭檔案
 
#ifndef POINT3D_H
#define POINT3D_H
 
class Vector3d; //函式moveByVector()的類Vector3d的前向宣告
 
class Point3d
{
    private:
        double m_x;
        double m_y;
        double m_z;
        
    public:
        Point3d(double x = 0.0, double y = 0.0, double z = 0.0) : m_x(x), m_y(y), m_z(z) {}
 
        void print();
        void moveByVector(const Vector3d &v); // 此行所需的前述宣告
};
 
#endif

Point3d.cpp:

// 這裡定義的Point3d類的成員函式
 
#include <iostream> // 引入std::cout
#include "Point3d.h" // Point3d類在此宣告
#include "Vector3d.h" // 用於函式moveByVector()的引數
 
void Point3d::moveByVector(const Vector3d &v)
{
    // 將向量分量新增到相應的點座標
    m_x += v.m_x;
    m_y += v.m_y;
    m_z += v.m_z;
}
 
void Point3d::print()
{
    std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
}

Vector3d.h

// 定義Vector3d類的標頭檔案
 
#ifndef VECTOR3D_H
#define VECTOR3D_H
 
#include "Point3d.h" // 用於將Point3d :: moveByVector()宣告為友元
 
class Vector3d
{
    private:
        double m_x;
        double m_y;
        double m_z;
 
    public:
        Vector3d(double x = 0.0, double y = 0.0, double z = 0.0) : m_x(x), m_y(y), m_z(z) {}
 
        void print();
        friend void Point3d::moveByVector(const Vector3d &v);
};
 
#endif

Vector3d.cpp:

// 此處定義的Vector3d類的成員函式
 
#include <iostream>
#include "Vector3d.h" // 在此檔案中宣告的Vector3d類
 
void Vector3d::print()
{
    std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
}

main.cpp中:

#include "Vector3d.h" // 用於建立Vector3d物件
#include "Point3d.h" // 用於建立Point3d物件
 
int main()
{
    Point3d p(1.0, 2.0, 3.0);
    Vector3d v(2.0, 2.0, -3.0);
 
    p.print();
    p.moveByVector(v);
    p.print();
 
    return 0;
}

我希望最後這個例子大家可以好好做一下,挺不錯的!!!