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;
}
我希望最後這個例子大家可以好好做一下,挺不錯的!!!