【c++面向对象编程】第9篇:友元(friend):破坏封装的“特权”——真的有害吗?

目录

一、一个让人纠结的场景

二、友元的两种形式

[1. 友元函数](#1. 友元函数)

[2. 友元类](#2. 友元类)

三、友元的典型应用场景

场景1:运算符重载(最常见)

场景2:两个紧密耦合的类

场景3:单元测试中访问私有状态

四、友元的缺点:为什么被"警告"

[1. 破坏封装](#1. 破坏封装)

[2. 减少可读性](#2. 减少可读性)

[3. 容易被滥用](#3. 容易被滥用)

[五、友元 vs public:怎么选择?](#五、友元 vs public:怎么选择?)

六、完整例子:有理数类

七、四个常见误区

[1. 以为友元可以继承](#1. 以为友元可以继承)

[2. 以为友元类可以访问派生类的私有成员](#2. 以为友元类可以访问派生类的私有成员)

[3. 把友元写在public/protected/private里](#3. 把友元写在public/protected/private里)

[4. 过度使用友元导致设计腐化](#4. 过度使用友元导致设计腐化)

八、这一篇的收获


一、一个让人纠结的场景

你写了一个Point类,表示二维坐标点:

cpp

复制代码
class Point {
private:
    double x, y;
public:
    Point(double x, double y) : x(x), y(y) {}
    double getX() const { return x; }
    double getY() const { return y; }
};

然后你想写一个函数,计算两个点之间的距离:

cpp

复制代码
double distance(const Point& p1, const Point& p2) {
    // 需要访问p1.x, p1.y, p2.x, p2.y
    // 但它们是private!
    return sqrt(pow(p1.getX() - p2.getX(), 2) + 
                pow(p1.getY() - p2.getY(), 2));
}

目前只能通过getX()/getY()来访问。这没问题,但有两个小烦恼:

  1. 性能:函数调用有开销(虽然很小)

  2. 语义distancePoint关系密切,把它写成成员函数不合适(距离不属于单个点),用public接口总觉得隔了一层

有没有办法让distance函数直接访问xy

有------把distance声明为Point友元函数

cpp

复制代码
class Point {
private:
    double x, y;
public:
    Point(double x, double y) : x(x), y(y) {}
    
    // 声明友元函数:这个函数可以访问我的私有成员
    friend double distance(const Point& p1, const Point& p2);
};

// 实现友元函数
double distance(const Point& p1, const Point& p2) {
    return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
}

现在distance可以像成员函数一样直接访问xy,但它仍然是一个普通函数(不是成员函数)。


二、友元的两种形式

1. 友元函数

全局函数或另一个类的成员函数可以成为友元。

cpp

复制代码
class Box {
private:
    int width;
    
public:
    Box(int w) : width(w) {}
    
    // 全局函数作为友元
    friend void printWidth(const Box& b);
    
    // 另一个类的成员函数作为友元(声明时需注明所属类)
    friend void OtherClass::setWidth(Box& b, int w);
};

void printWidth(const Box& b) {
    cout << b.width;   // 可以访问私有成员
}

2. 友元类

整个类成为另一个类的友元,该类的所有成员函数都能访问对方的私有成员。

cpp

复制代码
class Engine {
private:
    int horsepower;
    void ignite() { cout << "引擎点火" << endl; }
    
    // 声明Car为友元类:Car可以访问Engine的所有私有成员
    friend class Car;
};

class Car {
public:
    void start(Engine& e) {
        e.horsepower = 300;   // ✅ 可以访问
        e.ignite();            // ✅ 可以访问
    }
};

注意 :友元关系是单向的CarEngine的友元,不代表EngineCar的友元。也不具有传递性:如果AB的友元,BC的友元,不代表AC的友元。


三、友元的典型应用场景

场景1:运算符重载(最常见)

重载<<运算符让自定义类支持cout << obj时,通常需要友元。

cpp

复制代码
class Complex {
private:
    double real, imag;
public:
    Complex(double r, double i) : real(r), imag(i) {}
    
    // 友元函数重载<<
    friend ostream& operator<<(ostream& os, const Complex& c);
};

ostream& operator<<(ostream& os, const Complex& c) {
    os << c.real << "+" << c.imag << "i";
    return os;
}

为什么必须是友元?因为operator<<的第一个参数是ostream&,不是Complex对象,所以不能写成成员函数。如果不想用友元,就只能写public getter,但这样会暴露太多接口。

场景2:两个紧密耦合的类

比如链表(List)和节点(Node),节点通常只让链表操作,其他类不应该直接碰节点内部。

cpp

复制代码
class List;  // 前置声明

class Node {
private:
    int data;
    Node* next;
    
    friend class List;  // 只有List能操作Node的内部指针
};

class List {
private:
    Node* head;
public:
    void insert(int val) {
        Node* newNode = new Node{val, head};  // 可以访问Node的私有成员
        head = newNode;
    }
};

场景3:单元测试中访问私有状态

有时需要测试类的内部状态,可以把测试类或测试函数声明为友元。

cpp

复制代码
class MyClass {
private:
    int internalState;
    
#ifdef UNIT_TEST
    friend class MyClassTest;  // 只在测试编译时启用
#endif
};

四、友元的缺点:为什么被"警告"

1. 破坏封装

封装的意义在于:类的内部实现可以随时修改,只要保持public接口不变,外部代码不受影响。

有了友元之后,情况变了:

cpp

复制代码
class Database {
private:
    string connectionString;  // 友元函数可以访问
    friend void debugPrint(const Database& db);
};

void debugPrint(const Database& db) {
    cout << db.connectionString;  // 直接依赖内部细节
}

如果某天你把connectionString改名成connStrdebugPrint会编译失败。友元代码和类内部产生了强耦合

2. 减少可读性

看一个类的声明时,public接口已经够多了,再加上一堆friend声明,会让人困惑:"这个类到底暴露给多少外部实体?"

3. 容易被滥用

新手容易把友元当成"更方便的getter/setter":

cpp

复制代码
// ❌ 不好的做法:把无关的函数都声明为友元
class Data {
private:
    int value;
public:
    friend void func1(Data&);
    friend void func2(Data&);
    friend void func3(Data&);
    // 为什么不直接把value改成public???
};

五、友元 vs public:怎么选择?

情况 推荐方案
外部功能可以通过public接口实现 用public,不用友元
功能与类紧密相关,但不是类的核心职责 考虑友元(如运算符重载)
功能需要高性能,避免getter/setter开销 用友元(但先确认性能确实是瓶颈)
一个辅助类专门服务于主类 把辅助类声明为友元
只是不想写getter ❌ 不要用友元,要么写getter,要么把变量改成public

一个判断标准:如果删除友元声明后,必须把多个成员变成public才能让外部正常工作,那友元可能是合理的。如果只需要开放一两个getter,那用getter更干净。


六、完整例子:有理数类

用友元重载算术运算符,展示友元在数学类中的典型用法:

cpp

复制代码
#include <iostream>
#include <numeric>  // for gcd
using namespace std;

class Rational {
private:
    int numerator;      // 分子
    int denominator;    // 分母
    
    void reduce() {
        int g = gcd(numerator, denominator);
        numerator /= g;
        denominator /= g;
        if (denominator < 0) {  // 分母保持正数
            numerator = -numerator;
            denominator = -denominator;
        }
    }
    
public:
    Rational(int num = 0, int den = 1) : numerator(num), denominator(den) {
        if (denominator == 0) {
            throw invalid_argument("分母不能为0");
        }
        reduce();
    }
    
    // 友元函数声明
    friend Rational operator+(const Rational& a, const Rational& b);
    friend Rational operator*(const Rational& a, const Rational& b);
    friend ostream& operator<<(ostream& os, const Rational& r);
};

// 实现友元函数
Rational operator+(const Rational& a, const Rational& b) {
    int num = a.numerator * b.denominator + b.numerator * a.denominator;
    int den = a.denominator * b.denominator;
    return Rational(num, den);
}

Rational operator*(const Rational& a, const Rational& b) {
    return Rational(a.numerator * b.numerator, a.denominator * b.denominator);
}

ostream& operator<<(ostream& os, const Rational& r) {
    if (r.denominator == 1) {
        os << r.numerator;
    } else {
        os << r.numerator << "/" << r.denominator;
    }
    return os;
}

int main() {
    Rational a(1, 2);   // 1/2
    Rational b(2, 3);   // 2/3
    
    cout << a << " + " << b << " = " << a + b << endl;   // 7/6
    cout << a << " * " << b << " = " << a * b << endl;   // 1/3
    
    Rational c(4, 6);   // 会自动约分为2/3
    cout << "4/6 约分后: " << c << endl;
    
    return 0;
}

输出:

text

复制代码
1/2 + 2/3 = 7/6
1/2 * 2/3 = 1/3
4/6 约分后: 2/3

这个例子中,友元函数直接访问numeratordenominator,让运算符重载的实现简洁自然。如果用getter,代码会变丑,而且没有本质的封装收益------因为这两个函数本身就是类接口的一部分。


七、四个常见误区

1. 以为友元可以继承

cpp

复制代码
class Base {
    friend void func(Base& b);
};

class Derived : public Base {
    // func(Derived&) 不能访问Derived的私有成员
    // 友元不继承
};

2. 以为友元类可以访问派生类的私有成员

友元关系只针对声明的那个类,不自动延伸到派生类。

3. 把友元写在public/protected/private里

友元声明不受访问修饰符影响,写在哪里都一样。但按惯例,通常写在类的最开始(public之前)或最后。

cpp

复制代码
class Demo {
    friend class A;     // ✅ 习惯放这里
public:
    // ...
private:
    // ...
    friend class B;     // ✅ 也可以,但没必要
};

4. 过度使用友元导致设计腐化

如果发现你的类有5个以上的友元声明,或者友元类和主类的关系很松散,说明你的封装出了问题,该重新审视设计了。


八、这一篇的收获

你现在应该明白:

  • 友元函数/类可以访问类的私有成员,是封装的一个"受控漏洞"

  • 最合理的应用场景是运算符重载 (特别是<<>>)和紧密耦合的辅助类

  • 友元破坏封装、增加耦合,应该谨慎使用,不要当成"方便版public"

  • 友元不可继承、不传递、单向

💡 小作业:实现一个Matrix类(矩阵),用友元重载*运算符实现矩阵乘法。两个矩阵相乘需要访问对方的私有数据(二维数组),用友元实现比用getter更自然。


下一篇预告:第10篇《类的组合与嵌套:一个类中包含另一个类的对象》------一个类作为另一个类的成员变量时,如何正确初始化?构造和析构的顺序是怎样的?下篇讲清楚组合关系(has-a)的实现细节。

相关推荐
敖正炀1 小时前
JDBC 批处理内核:addBatch、executeBatch 与驱动 SQL 重写
java
LJianK11 小时前
乐观锁算线程同步吗?
java·开发语言·jvm
用户298698530141 小时前
Java 后端处理 Word 修订:批量接受与拒绝的自动化方案
java·后端
WL_Aurora1 小时前
IDEA + Maven 环境配置超详细教程(图文详解)
java·maven·intellij-idea
小雅痞1 小时前
[Java][Leetcode middle] 73. 矩阵置零
java·leetcode·矩阵
William_wL_1 小时前
【C++】priority_queue(优先级队列)的使用和实现
c++
代码中介商1 小时前
C++ STL入门:vector与字符串流详解
开发语言·c++
funnycoffee1231 小时前
cisco Firepower 4110 9300 FXOS set chassis hostname
java·服务器·数据库
fqbqrr1 小时前
2605C++,C++类的继承1
c++