MST

星途 面试题库

面试题:C++友元关系与代码设计原则及优化

在大型项目中,过度使用友元关系可能会破坏封装性。请结合代码示例分析,在哪些情况下合理使用友元关系可以在不严重破坏封装性的前提下提升代码效率和灵活性,同时阐述如何通过设计模式等手段减少友元关系的负面影响,以符合代码设计的开闭原则、依赖倒置原则等。
35.5万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

合理使用友元关系提升效率和灵活性的场景

  1. 运算符重载
    • 例如,实现复数类Complex,对于+运算符,如果使用成员函数重载,Complex对象只能在左侧,不太符合数学使用习惯。使用友元函数重载+运算符,可以更自然地实现复数加法。
    class Complex {
    private:
        double real;
        double imag;
    public:
        Complex(double r = 0, double i = 0) : real(r), imag(i) {}
        // 友元函数声明
        friend Complex operator+(const Complex& a, const Complex& b);
    };
    Complex operator+(const Complex& a, const Complex& b) {
        return Complex(a.real + b.real, a.imag + b.imag);
    }
    
    • 这里友元函数可以直接访问Complex类的私有成员,方便地实现运算符功能,且没有严重破坏封装性,因为+运算符本质上是对复数对象数据的一种操作,与类关系紧密。
  2. 辅助类访问
    • 假设有一个Point类和一个Line类,Line类用于表示由两个Point组成的线段。Line类可能需要直接访问Point类的坐标值(私有成员)来进行一些计算,如计算线段长度。
    class Point {
    private:
        double x;
        double y;
    public:
        Point(double a = 0, double b = 0) : x(a), y(b) {}
        // 声明Line类为友元
        friend class Line;
    };
    class Line {
    private:
        Point start;
        Point end;
    public:
        Line(const Point& s, const Point& e) : start(s), end(e) {}
        double length() {
            double dx = end.x - start.x;
            double dy = end.y - start.y;
            return std::sqrt(dx * dx + dy * dy);
        }
    };
    
    • Line类作为Point类的友元,能直接访问Point类的私有成员,使得Line类的实现更简洁高效,同时也没有过度暴露Point类的实现细节,因为Line类与Point类在业务逻辑上紧密相关。

减少友元关系负面影响的设计模式手段

  1. 桥接模式
    • 例如,有一个图形绘制系统,存在不同的图形类(如圆形、矩形)和不同的绘制平台(如Windows、Linux)。可以使用桥接模式将图形类和绘制平台分离。假设图形类有一些私有数据用于表示图形属性,绘制平台类需要访问这些属性来进行绘制。
    • 传统方式可能会使用友元关系让绘制平台类访问图形类的私有成员。但使用桥接模式,通过定义抽象接口和实现接口的方式,避免了友元关系。
    • 抽象图形类:
    class DrawAPI {
    public:
        virtual void drawCircle(double x, double y, double radius) = 0;
    };
    class Shape {
    protected:
        DrawAPI* drawAPI;
    public:
        Shape(DrawAPI* api) : drawAPI(api) {}
        virtual void draw() = 0;
    };
    
    • 具体图形类(以圆形为例):
    class Circle : public Shape {
    private:
        double x, y, radius;
    public:
        Circle(double a, double b, double r, DrawAPI* api) : Shape(api), x(a), y(b), radius(r) {}
        void draw() override {
            drawAPI->drawCircle(x, y, radius);
        }
    };
    
    • 具体绘制平台类(以Windows绘制平台为例):
    class WindowsDrawAPI : public DrawAPI {
    public:
        void drawCircle(double x, double y, double radius) override {
            // Windows 特定的绘制代码
            std::cout << "Drawing Circle at (" << x << ", " << y << ") with radius " << radius << " on Windows" << std::endl;
        }
    };
    
    • 这里通过桥接模式,DrawAPI实现类不需要成为Shape类的友元,就可以访问绘制所需的参数,符合开闭原则(可以轻松添加新的图形或绘制平台)和依赖倒置原则(依赖抽象而非具体实现)。
  2. 代理模式
    • 假设一个敏感数据类SensitiveData,有一些外部模块需要访问其部分功能,但不能直接访问其私有成员。可以创建一个代理类DataProxy
    • SensitiveData类:
    class SensitiveData {
    private:
        int secretValue;
    public:
        SensitiveData(int value) : secretValue(value) {}
        int getSecretValue() {
            return secretValue;
        }
    };
    
    • DataProxy类:
    class DataProxy {
    private:
        SensitiveData* data;
    public:
        DataProxy(SensitiveData* d) : data(d) {}
        int accessSecretValue() {
            // 可以在这里添加额外的逻辑,如权限检查
            return data->getSecretValue();
        }
    };
    
    • 外部模块通过DataProxy访问SensitiveData,避免了直接让外部模块成为SensitiveData的友元,符合开闭原则(可以在代理类中添加新的功能)和依赖倒置原则(依赖SensitiveData的抽象接口,如getSecretValue函数)。