MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++类友元类的作用范围界定

2021-06-186.6k 阅读

1. C++类友元类概述

在C++编程中,类的封装性是一项重要特性,它将数据和操作数据的方法紧密结合在一起,并通过访问修饰符(如public、private和protected)来控制对类成员的访问。然而,在某些情况下,我们可能需要打破这种封装,让一个类能够访问另一个类的私有或保护成员,这时友元类(friend class)就派上用场了。

友元类是C++提供的一种机制,它允许一个类(友元类)访问另一个类的私有和保护成员。这打破了常规的访问控制规则,为特定的类之间提供了一种特殊的信任关系。

1.1 友元类声明

友元类的声明是在目标类(即被访问的类)内部进行的,使用friend关键字来标识。例如:

class ClassB;  // 前向声明

class ClassA {
private:
    int privateData;
public:
    ClassA(int data) : privateData(data) {}
    friend class ClassB;  // 声明ClassB为ClassA的友元类
};

class ClassB {
public:
    void accessPrivateData(ClassA& a) {
        std::cout << "Accessed private data of ClassA: " << a.privateData << std::endl;
    }
};

在上述代码中,ClassA声明ClassB为其友元类,这样ClassB的成员函数accessPrivateData就可以访问ClassA的私有成员privateData

2. 友元类的作用范围界定基础

2.1 友元关系是单向的

友元关系是单向的,即如果ClassA声明ClassB为友元类,并不意味着ClassA可以访问ClassB的私有和保护成员。只有ClassB可以访问ClassA的相关成员。

class ClassX;
class ClassY {
private:
    int privateValue;
public:
    ClassY(int value) : privateValue(value) {}
    friend class ClassX;
};

class ClassX {
public:
    void printClassYPrivate(ClassY& y) {
        std::cout << "ClassY's private value: " << y.privateValue << std::endl;
    }
    // ClassX不能自动访问ClassY的私有成员
    // 例如,以下操作是不允许的
    // int getMyPrivate() {
    //     return privateValue;
    // }
};

2.2 友元关系不具有传递性

假设ClassA声明ClassB为友元类,ClassB声明ClassC为友元类,这并不意味着ClassC能访问ClassA的私有和保护成员。

class ClassA;
class ClassB;

class ClassA {
private:
    int privateDataA;
public:
    ClassA(int data) : privateDataA(data) {}
    friend class ClassB;
};

class ClassB {
private:
    int privateDataB;
public:
    ClassB(int data) : privateDataB(data) {}
    friend class ClassC;
    void accessClassAPrivate(ClassA& a) {
        std::cout << "ClassA's private data: " << a.privateDataA << std::endl;
    }
};

class ClassC {
public:
    // 以下操作是不允许的,即使ClassB是ClassA的友元且ClassC是ClassB的友元
    // void accessClassAPrivate(ClassA& a) {
    //     std::cout << "ClassA's private data: " << a.privateDataA << std::endl;
    // }
};

2.3 友元关系不具有继承性

如果ClassA声明ClassB为友元类,ClassD继承自ClassBClassD并不能自动成为ClassA的友元类,也就不能访问ClassA的私有和保护成员。

class ClassA;

class ClassB {
public:
    friend class ClassA;
};

class ClassD : public ClassB {
    // ClassD不能自动访问ClassA的私有和保护成员
    // 例如,假设ClassA有私有成员privateDataA
    // void accessClassAPrivate(ClassA& a) {
    //     std::cout << "ClassA's private data: " << a.privateDataA << std::endl;
    // }
};

3. 友元类在不同作用域中的表现

3.1 全局类作为友元

前面的例子中,我们看到的都是全局类之间的友元关系。当一个全局类被声明为另一个全局类的友元时,其作用范围明确,即友元类可以在其成员函数中访问目标类的私有和保护成员。

class GlobalFriend;

class GlobalTarget {
private:
    int privateData;
public:
    GlobalTarget(int data) : privateData(data) {}
    friend class GlobalFriend;
};

class GlobalFriend {
public:
    void accessGlobalTarget(GlobalTarget& target) {
        std::cout << "Accessed global target's private data: " << target.privateData << std::endl;
    }
};

3.2 嵌套类作为友元

3.2.1 外层类声明嵌套类为友元

嵌套类是定义在另一个类内部的类。当外层类声明其嵌套类为友元时,嵌套类可以访问外层类的私有和保护成员。

class Outer {
private:
    int privateData;
public:
    Outer(int data) : privateData(data) {}
    class Inner {
    public:
        void accessOuterPrivate(Outer& outer) {
            std::cout << "Accessed outer's private data: " << outer.privateData << std::endl;
        }
    };
    friend class Inner;
};

3.2.2 嵌套类声明外层类为友元

反过来,如果嵌套类声明外层类为其友元,这在语法上是允许的,但实际意义不大,因为外层类本身就可以访问其所有成员,无需通过友元关系。

class Outer2 {
public:
    class Inner2 {
    private:
        int innerPrivateData;
    public:
        Inner2(int data) : innerPrivateData(data) {}
        friend class Outer2;
    };
};

在这种情况下,Outer2并没有从成为Inner2的友元中获得额外的访问权限,因为它本来就可以访问Inner2的所有成员(包括私有成员)。

3.3 局部类作为友元

局部类是在函数内部定义的类。将局部类声明为友元相对较少见,因为局部类的生命周期和作用域有限。但如果这样做了,局部类可以访问目标类的私有和保护成员。

class TargetForLocalFriend {
private:
    int privateData;
public:
    TargetForLocalFriend(int data) : privateData(data) {}
    friend class LocalFriend;
};

void someFunction() {
    class LocalFriend {
    public:
        void accessTargetPrivate(TargetForLocalFriend& target) {
            std::cout << "Accessed target's private data: " << target.privateData << std::endl;
        }
    };
    TargetForLocalFriend target(10);
    LocalFriend friendObj;
    friendObj.accessTargetPrivate(target);
}

在上述代码中,LocalFriend是定义在someFunction函数内部的局部类,并且被声明为TargetForLocalFriend的友元,因此可以访问TargetForLocalFriend的私有成员。

4. 友元类与命名空间

4.1 命名空间内的友元类

当类定义在命名空间内时,友元关系同样适用。例如:

namespace MyNamespace {
    class ClassInNamespace;

    class FriendInNamespace {
    public:
        void accessClassInNamespace(ClassInNamespace& obj);
    };

    class ClassInNamespace {
    private:
        int privateData;
    public:
        ClassInNamespace(int data) : privateData(data) {}
        friend class FriendInNamespace;
    };

    void FriendInNamespace::accessClassInNamespace(ClassInNamespace& obj) {
        std::cout << "Accessed private data in namespace: " << obj.privateData << std::endl;
    }
}

在这个例子中,FriendInNamespace被声明为ClassInNamespace的友元,并且它们都在MyNamespace命名空间内,FriendInNamespace的成员函数可以访问ClassInNamespace的私有成员。

4.2 跨命名空间的友元类

4.2.1 一个命名空间内的类声明另一个命名空间内的类为友元

namespace NS1 {
    class ClassInNS1;
}

namespace NS2 {
    class FriendInNS2 {
    public:
        void accessClassInNS1(NS1::ClassInNS1& obj);
    };
}

namespace NS1 {
    class ClassInNS1 {
    private:
        int privateData;
    public:
        ClassInNS1(int data) : privateData(data) {}
        friend class NS2::FriendInNS2;
    };
}

namespace NS2 {
    void FriendInNS2::accessClassInNS1(NS1::ClassInNS1& obj) {
        std::cout << "Accessed NS1 class from NS2: " << obj.privateData << std::endl;
    }
}

在上述代码中,NS1::ClassInNS1声明NS2::FriendInNS2为友元,使得NS2::FriendInNS2可以访问NS1::ClassInNS1的私有成员。

4.2.2 友元类的成员函数定义在不同命名空间

如果友元类的成员函数定义在与友元类本身不同的命名空间,需要注意作用域解析。

namespace NS3 {
    class ClassInNS3;
}

namespace NS4 {
    class FriendInNS4 {
    public:
        void accessClassInNS3(NS3::ClassInNS3& obj);
    };
}

namespace NS3 {
    class ClassInNS3 {
    private:
        int privateData;
    public:
        ClassInNS3(int data) : privateData(data) {}
        friend class NS4::FriendInNS4;
    };
}

// 成员函数定义在不同命名空间
void NS4::FriendInNS4::accessClassInNS3(NS3::ClassInNS3& obj) {
    std::cout << "Accessed NS3 class from NS4 (different def namespace): " << obj.privateData << std::endl;
}

这里NS4::FriendInNS4的成员函数accessClassInNS3定义在全局命名空间(严格来说是在没有命名空间限定的区域,但通过作用域解析运算符::明确了属于NS4命名空间),仍然可以正确访问NS3::ClassInNS3的私有成员。

5. 友元类作用范围界定的实际应用场景

5.1 数据结构实现中的辅助类

在实现复杂的数据结构时,可能会有一些辅助类,这些辅助类需要访问数据结构类的私有成员来完成特定的操作。例如,在实现链表数据结构时,可能有一个迭代器类作为链表类的友元。

class LinkedList {
private:
    struct Node {
        int data;
        Node* next;
        Node(int value) : data(value), next(nullptr) {}
    };
    Node* head;
public:
    LinkedList() : head(nullptr) {}
    void addNode(int value) {
        Node* newNode = new Node(value);
        if (!head) {
            head = newNode;
        } else {
            Node* current = head;
            while (current->next) {
                current = current->next;
            }
            current->next = newNode;
        }
    }
    friend class ListIterator;
};

class ListIterator {
private:
    LinkedList::Node* current;
public:
    ListIterator(LinkedList& list) : current(list.head) {}
    int getCurrentData() {
        return current->data;
    }
    void moveNext() {
        if (current) {
            current = current->next;
        }
    }
    bool hasNext() {
        return current != nullptr;
    }
};

在这个例子中,ListIterator作为LinkedList的友元类,可以访问LinkedList的私有成员Nodehead,从而实现对链表的遍历操作。

5.2 测试框架中的使用

在单元测试框架中,测试类可能需要访问被测试类的私有和保护成员来验证其内部状态和行为。通过将测试类声明为被测试类的友元,可以方便地进行这些操作。

class Calculator {
private:
    int result;
public:
    Calculator() : result(0) {}
    void add(int num) {
        result += num;
    }
    int getResult() {
        return result;
    }
    friend class CalculatorTest;
};

class CalculatorTest {
public:
    void testAddition() {
        Calculator calc;
        calc.add(5);
        if (calc.result == 5) {
            std::cout << "Addition test passed." << std::endl;
        } else {
            std::cout << "Addition test failed." << std::endl;
        }
    }
};

在上述代码中,CalculatorTest作为Calculator的友元类,可以直接访问Calculator的私有成员result,以便更深入地测试Calculator类的功能。

5.3 资源管理中的应用

在资源管理类中,可能会有一些辅助类来协助管理资源。例如,一个文件管理类可能有一个文件锁类作为友元,文件锁类需要访问文件管理类的私有成员来进行锁的操作。

class FileManager {
private:
    FILE* file;
    bool isLocked;
public:
    FileManager(const char* filename) {
        file = fopen(filename, "r+");
        isLocked = false;
    }
    ~FileManager() {
        if (file) {
            fclose(file);
        }
    }
    friend class FileLock;
};

class FileLock {
public:
    void lock(FileManager& manager) {
        if (!manager.isLocked) {
            // 这里可以实现实际的锁操作,例如文件锁系统调用
            manager.isLocked = true;
            std::cout << "File locked." << std::endl;
        } else {
            std::cout << "File is already locked." << std::endl;
        }
    }
    void unlock(FileManager& manager) {
        if (manager.isLocked) {
            // 这里可以实现实际的解锁操作
            manager.isLocked = false;
            std::cout << "File unlocked." << std::endl;
        } else {
            std::cout << "File is not locked." << std::endl;
        }
    }
};

在这个例子中,FileLock作为FileManager的友元类,可以访问FileManager的私有成员isLocked来实现文件的锁定和解锁功能。

6. 友元类作用范围界定的注意事项

6.1 破坏封装性

虽然友元类提供了便利,但它确实破坏了类的封装性。过度使用友元类可能导致代码的可维护性降低,因为类的私有和保护成员不再受到严格的访问控制。因此,在使用友元类时,应该谨慎权衡利弊,确保只有在真正必要的情况下才打破封装。

6.2 代码可读性

友元关系可能会使代码的可读性变差,尤其是当友元关系复杂且涉及多个类时。为了提高可读性,应该在友元声明处添加清晰的注释,说明为什么需要这种友元关系,以及友元类将如何使用目标类的私有和保护成员。

6.3 维护成本

由于友元关系的单向性、非传递性和非继承性,在代码维护过程中,如果对类的结构进行修改,可能需要仔细检查友元关系是否仍然正确。例如,如果修改了目标类的私有成员,可能需要相应地修改友元类的实现,以确保其功能不受影响。

7. 友元类与其他访问机制的对比

7.1 友元类与公共成员函数

公共成员函数是类提供给外部代码访问其内部状态的标准方式。与友元类相比,公共成员函数通过定义明确的接口来控制对类成员的访问,符合封装原则。而友元类则是在特定情况下打破封装,提供更直接的访问。 例如,对于一个Circle类:

class Circle {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double getRadius() const {
        return radius;
    }
    // 如果需要更复杂的外部访问,可以提供其他公共函数
    // 例如,计算面积的函数
    double calculateArea() const {
        return 3.14159 * radius * radius;
    }
    // 与友元类不同,这里的访问是通过公共接口
};

在这个例子中,getRadiuscalculateArea是公共成员函数,外部代码通过调用这些函数来访问Circle类的相关信息,而不是像友元类那样直接访问私有成员。

7.2 友元类与继承和多态

继承和多态是C++中实现代码复用和扩展的重要机制。通过继承,子类可以访问父类的保护成员,并且可以通过重写虚函数实现多态行为。而友元类与继承和多态的目的不同,友元类主要用于在不通过继承关系的情况下,让一个类访问另一个类的私有和保护成员。 例如,假设有一个Shape类和一个Rectangle类继承自Shape

class Shape {
protected:
    std::string color;
public:
    Shape(const std::string& c) : color(c) {}
    virtual void draw() const = 0;
};

class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(const std::string& c, double w, double h) : Shape(c), width(w), height(h) {}
    void draw() const override {
        std::cout << "Drawing a " << color << " rectangle with width " << width << " and height " << height << std::endl;
    }
};

在这个继承体系中,Rectangle类可以访问Shape类的保护成员color,但这是基于继承关系。如果有一个与Rectangle没有继承关系的类RectanglePrinter想要访问Rectangle的私有成员widthheight,就可以考虑使用友元类机制。

8. 友元类在现代C++开发中的趋势

随着现代C++编程理念的发展,对封装性和代码可维护性的重视程度越来越高。因此,友元类的使用相对更加谨慎。在很多情况下,通过合理设计类的接口、使用模板元编程等技术,可以在不破坏封装的前提下实现类似的功能。

然而,在一些特定领域,如底层库开发、性能敏感的代码以及与硬件交互的代码中,友元类仍然具有一定的应用价值。例如,在一些图形库中,为了提高渲染效率,可能需要特定的渲染类作为图形对象类的友元,直接访问图形对象的内部数据结构。

总的来说,虽然友元类的使用在现代C++开发中受到一定限制,但在某些场景下,它仍然是一种有效的编程工具,只要开发者能够正确界定其作用范围,权衡好封装性和便利性之间的关系。