C++ 友元类与友元成员函数:深度解析与实践

友元类

友元类是指能够访问另一个类的 privateprotected 成员的类。

示例:

#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

补充说明:

  1. 即使 DisplayStorage 的友元,也无法访问 Storage 对象的 *this 指针(因为 *this 是函数形参的一部分)。
  2. 友元关系非对称DisplayStorage 的友元,并不意味着 StorageDisplay 的友元;若需双向友元,双方必须各自声明。
  3. 友元关系非传递:若 A 是 B 的友元,B 是 C 的友元,并不表示 A 是 C 的友元。
  4. 友元关系不继承:若 A 声明 B 为友元,B 的派生类不会自动成为 A 的友元。
  5. 友元类声明同时起到前向声明作用,无需额外前向声明即可使用。

友元成员函数

除了将整个类设为友元,也可仅授予某个成员函数访问权。语法与友元非成员函数类似,只需使用成员函数完整限定名。

然而,实践中有一定复杂度:

#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 的完整定义,造成循环依赖。

解决方案

  1. 先完整定义 Display
  2. 前向声明 Storage
  3. Storage 定义中将 Display::displayStorage 设为友元;
  4. 最后给出 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.hStorage.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.hPoint3d.cppVector3d.hVector3d.cppmain.cpp
(答案略)

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"