友元类
友元类是指能够访问另一个类的 private 和 protected 成员的类。
示例:
#include <iostream>
class Storage
{
private:
    int m_nValue{};
    double m_dValue{};
public:
    Storage(int nValue, double dValue)
        : m_nValue{ nValue }, m_dValue{ dValue }
    { }
    // 将 Display 声明为 Storage 的友元类
    friend class Display;
};
class Display
{
private:
    bool m_displayIntFirst{};
public:
    explicit Display(bool displayIntFirst)
        : m_displayIntFirst{ displayIntFirst }
    { }
    // 由于 Display 是 Storage 的友元,其成员可直接访问 Storage 的私有成员
    void displayStorage(const Storage& storage)
    {
        if (m_displayIntFirst)
            std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
        else
            std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
    }
    void setDisplayIntFirst(bool b)
    {
        m_displayIntFirst = b;
    }
};
int main()
{
    Storage storage{ 5, 6.7 };
    Display display{ false };
    display.displayStorage(storage); // 6.7 5
    display.setDisplayIntFirst(true);
    display.displayStorage(storage); // 5 6.7
    return 0;
}
输出:
6.7 5
5 6.7
补充说明:
- 即使 Display是Storage的友元,也无法访问Storage对象的*this指针(因为*this是函数形参的一部分)。
- 友元关系非对称:Display是Storage的友元,并不意味着Storage是Display的友元;若需双向友元,双方必须各自声明。
- 友元关系非传递:若 A 是 B 的友元,B 是 C 的友元,并不表示 A 是 C 的友元。
- 友元关系不继承:若 A 声明 B 为友元,B 的派生类不会自动成为 A 的友元。
- 友元类声明同时起到前向声明作用,无需额外前向声明即可使用。
友元成员函数
除了将整个类设为友元,也可仅授予某个成员函数访问权。语法与友元非成员函数类似,只需使用成员函数完整限定名。
然而,实践中有一定复杂度:
#include <iostream>
class Display; // Display 的前向声明
class Storage
{
private:
    int m_nValue{};
    double m_dValue{};
public:
    Storage(int nValue, double dValue)
        : m_nValue{ nValue }, m_dValue{ dValue }
    { }
    // 试图将 Display::displayStorage 设为友元
    friend void Display::displayStorage(const Storage& storage); // 错误:尚未看到 Display 的完整定义
};
class Display
{
private:
    bool m_displayIntFirst{};
public:
    explicit Display(bool displayIntFirst)
        : m_displayIntFirst{ displayIntFirst }
    { }
    void displayStorage(const Storage& storage) // 编译错误:尚未看到 Storage 的完整定义
    {
        if (m_displayIntFirst)
            std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
        else
            std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
    }
};
上述代码无法编译:
- 将成员函数设为友元,编译器必须已看到其所属类的完整定义;
- 同时,成员函数参数若使用 Storage类型,又需Storage的完整定义,造成循环依赖。
解决方案:
- 先完整定义 Display;
- 前向声明 Storage;
- 在 Storage定义中将Display::displayStorage设为友元;
- 最后给出 Display::displayStorage的类外定义。
完整代码:
#include <iostream>
class Storage; // 前向声明
class Display
{
private:
    bool m_displayIntFirst{};
public:
    explicit Display(bool displayIntFirst)
        : m_displayIntFirst{ displayIntFirst }
    { }
    void displayStorage(const Storage& storage); // 声明,引用 Storage 需前向声明
};
class Storage
{
private:
    int m_nValue{};
    double m_dValue{};
public:
    Storage(int nValue, double dValue)
        : m_nValue{ nValue }, m_dValue{ dValue }
    { }
    friend void Display::displayStorage(const Storage& storage); // 友元声明
};
// 类外定义 Display::displayStorage
void Display::displayStorage(const Storage& storage)
{
    if (m_displayIntFirst)
        std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
    else
        std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}
int main()
{
    Storage storage{ 5, 6.7 };
    Display display{ false };
    display.displayStorage(storage);
    return 0;
}
若将每个类分别放入独立头文件(Display.h、Storage.h)及对应源文件,再统一编译,则无需上述调整顺序,代码更清晰。
习题
在几何学中,点(Point) 是空间中的位置,可由 3 个坐标 x, y, z 表示,例如 Point{2.0, 1.0, 0.0}。
在物理学中,向量(Vector) 是具有大小与方向的量,可由 3 个轴向分量 x, y, z 表示,例如 Vector{2.0, 0.0, 0.0} 表示沿 x 轴正方向、长度为 2 的向量。
将向量加至点可得到新位置,如 Point{2.0, 1.0, 0.0} + Vector{2.0, 0.0, 0.0} → Point{4.0, 1.0, 0.0}。
在计算机图形学中,此类点和向量常用于表示顶点及形变。
给定程序框架:
#include <iostream>
class Vector3d
{
private:
    double m_x{}, m_y{}, m_z{};
public:
    Vector3d(double x, double y, double z)
        : m_x{ x }, m_y{ y }, m_z{ z } { }
    void print() const
    {
        std::cout << "Vector(" << m_x << ", " << m_y << ", " << m_z << ")\n";
    }
};
class Point3d
{
private:
    double m_x{}, m_y{}, m_z{};
public:
    Point3d(double x, double y, double z)
        : m_x{ x }, m_y{ y }, m_z{ z } { }
    void print() const
    {
        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;
}
步骤 1
将 Point3d 设为 Vector3d 的友元类,并实现 Point3d::moveByVector()。
(答案略)
步骤 2
不再将 Point3d 整个类设为友元,而是仅将成员函数 Point3d::moveByVector 设为 Vector3d 的友元。
(答案略)
步骤 3
将步骤 2 的解法拆分为 5 个文件:Point3d.h、Point3d.cpp、Vector3d.h、Vector3d.cpp、main.cpp。
(答案略)
