运算符重载与友元

运算符重载

基本概念

在 C++ 中,当操作数包含类对象时,运算符操作本质是调用对应的函数(称为“运算符重载函数”)。

核心逻辑

  • 示例:A a, b; a + b; 等价于 a.operator+(b);
  • 特殊规则:赋值运算符(=)是类的默认成员函数,无需手动定义即可使用;其余大多数运算符(+<<++ 等)需手动重载才能用于类对象,否则编译报错。
  • 函数本质:运算符重载函数的函数名为 operator++ 为要重载的运算符),格式与普通函数一致,包含返回值类型、参数列表。

声明格式示例

class A {
public:
    A& operator+(const A& r); // 加法运算符重载声明
    // 返回值类型:A&(对象引用)
    // 函数名:operator+
    // 参数列表:const A& r(右侧操作数,左侧操作数为调用对象本身)
};

运算符重载分类与示例

运算符重载分为「类成员函数重载」和「非类成员函数重载」两类,核心区别在于函数归属与参数传递方式。

类成员运算符重载

重载函数作为类的成员函数,左侧操作数为调用函数的类对象(隐含 this 指针),仅需在参数列表中声明右侧操作数。

经典示例:时间类(Time)的加法重载
#include <iostream>
using namespace std;

class Time {
private:
    int hour;   // 小时
    int minute; // 分钟
    int second; // 秒
public:
    // 构造函数
    Time(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), second(s) {}

    // 加法运算符重载(类成员函数)
    const Time operator+(const Time& r);

    // 显示时间(辅助函数)
    void show() const {
        cout << hour << ":" << minute << ":" << second << endl;
    }
};

// 加法运算符重载实现(处理时间进位逻辑)
const Time Time::operator+(const Time& r) {
    Time tmp;
    // 秒数相加,处理进位
    tmp.second = this->second + r.second;
    if (tmp.second >= 60) {
        tmp.minute++;
        tmp.second -= 60;
    }
    // 分钟相加,处理进位
    tmp.minute = this->minute + r.minute;
    if (tmp.minute >= 60) {
        tmp.hour++;
        tmp.minute -= 60;
    }
    // 小时相加,处理24小时制
    tmp.hour = this->hour + r.hour;
    tmp.hour %= 24;

    return tmp;
}

// 测试代码
int main(void) {
    Time t1(2, 10, 20);
    Time t2(4, 12, 51);
    Time t = t1 + t2; // 等价于 t1.operator+(t2)
    t.show(); // 输出:6:23:11
    return 0;
}
语法重点
  • 左操作数隐含:t1 + t2 中,t1 是调用对象(this 指向 t1),t2 是参数列表中的右侧操作数。
  • 等价转换:t1 + t2t1.operator+(t2)t2 + t1t2.operator+(t1)

非类成员运算符重载

重载函数作为普通全局函数,需显式声明所有操作数,且至少有一个操作数是自定义类型(否则编译报错)。

经典示例:重载 << 实现时间类的流输出

默认情况下 cout << tt 为 Time 对象)不支持,需重载 << 运算符:

#include <iostream>
using namespace std;

class Time {
private:
    int hour;
    int minute;
    int second;
public:
    Time(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), second(s) {}

    // 提供公有接口,供全局重载函数访问私有成员
    int h() const { return hour; }
    int m() const { return minute; }
    int s() const { return second; }
};

// 非类成员运算符重载:重载 <<
ostream& operator<<(ostream& out, const Time& t) {
    out << t.h() << ":" << t.m() << ":" << t.s();
    return out; // 返回ostream&以支持连续输出(如cout << t1 << t2)
}

// 测试代码
int main(void) {
    Time t1(2, 10, 20);
    Time t2(4, 12, 51);
    cout << "t1: " << t1 << endl; // 输出:t1: 2:10:20
    cout << "t1 + t2: " << t1 << " + " << t2 << " = " << Time(t1.h()+t2.h(), t1.m()+t2.m(), t1.s()+t2.s()) << endl;
    return 0;
}
示例:重载 >> 实现时间类的流输入
// 非类成员运算符重载:重载 >>
istream& operator>>(istream& in, Time& t) {
    int h, m, s;
    in >> h >> m >> s;
    // 可在此处添加参数合法性校验(如h∈[0,23], m∈[0,59], s∈[0,59])
    t = Time(h, m, s); // 需确保Time类支持赋值操作(默认已支持)
    return in; // 返回istream&以支持连续输入(如cin >> t1 >> t2)
}

// 测试代码
int main(void) {
    Time t;
    cout << "请输入时间(时 分 秒):";
    cin >> t;
    cout << "你输入的时间:" << t << endl;
    return 0;
}
语法重点
  • 参数顺序:运算符的操作数需按「左→右」顺序填入参数列表(如 << 的左操作数是 ostream 对象,右操作数是 Time 对象,参数列表为 (ostream& out, const Time& t))。
  • 返回值:流运算符(<<>>)返回流对象引用(ostream&/istream&),以支持连续输入输出。

自加自减运算符重载(特殊单目运算符)

自加(++)、自减(--)是特殊的单目运算符,需区分「前缀」(++a)和「后缀」(a++),逻辑不同。

核心规则
类型 函数声明格式 返回值类型 逻辑特点
前缀++ Time& operator++() 对象引用(Time&) 先自增,再返回自身(支持链式赋值)
后缀++ const Time operator++(int) const 临时对象 先返回自身副本,再自增(不支持赋值)
前缀-- Time& operator--() 对象引用(Time&) 先自减,再返回自身
后缀-- const Time operator--(int) const 临时对象 先返回自身副本,再自减

注:后缀运算符的 int 参数仅为标记,无实际数据传递,编译器通过该参数区分前缀/后缀。

实现示例:Time 类的自加自减重载
class Time {
private:
    int hour;
    int minute;
    int second;
public:
    Time(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), second(s) {}

    // 前缀++:先自增,返回引用
    Time& operator++() {
        second++;
        if (second >= 60) {
            second = 0;
            minute++;
            if (minute >= 60) {
                minute = 0;
                hour++;
                hour %= 24;
            }
        }
        return *this;
    }

    // 后缀++:先返回副本,再自增(int为标记)
    const Time operator++(int) {
        Time tmp = *this; // 保存当前状态
        // 自增逻辑(与前缀一致)
        second++;
        if (second >= 60) {
            second = 0;
            minute++;
            if (minute >= 60) {
                minute = 0;
                hour++;
                hour %= 24;
            }
        }
        return tmp; // 返回自增前的副本
    }

    // 前缀--(类似前缀++)
    Time& operator--() {
        second--;
        if (second < 0) {
            second = 59;
            minute--;
            if (minute < 0) {
                minute = 59;
                hour--;
                if (hour < 0) hour = 23;
            }
        }
        return *this;
    }

    // 后缀--(类似后缀++)
    const Time operator--(int) {
        Time tmp = *this;
        second--;
        if (second < 0) {
            second = 59;
            minute--;
            if (minute < 0) {
                minute = 59;
                hour--;
                if (hour < 0) hour = 23;
            }
        }
        return tmp;
    }

    // 友元函数:直接访问私有成员,简化输出(后续友元章节详解)
    friend ostream& operator<<(ostream& out, const Time& t);
};

// 友元函数实现 << 重载
ostream& operator<<(ostream& out, const Time& t) {
    out << t.hour << ":" << t.minute << ":" << t.second;
    return out;
}

// 测试代码
int main(void) {
    Time t(23, 59, 59);
    cout << "初始时间:" << t << endl;

    Time t1 = ++t; // 前缀++:t先自增为0:0:0,t1 = t
    cout << "++t 后:t=" << t << ", t1=" << t1 << endl; // 0:0:0, 0:0:0

    Time t2 = t++; // 后缀++:t2 = t(0:0:0),t再自增为0:0:1
    cout << "t++ 后:t=" << t << ", t2=" << t2 << endl; // 0:0:1, 0:0:0

    return 0;
}

运算符重载的限制

C++ 对运算符重载有严格约束,避免滥用导致逻辑混乱:

  1. 归属约束:重载函数要么是类成员,要么是非类成员(全局函数),且非类成员重载需至少有一个自定义类型参数。
  2. 操作数与优先级约束:不能改变运算符的「操作数个数」和「优先级」(如 + 始终是双目运算符,优先级不变)。
  3. 运算符范围约束:只能重载 C++ 已有的运算符,不能创建新运算符(如不能自定义 @ 运算符)。
  4. 不可重载的运算符(涉及编译系统底层逻辑):
    • sizeof(求内存尺寸)
    • .(成员引用符)、.*(成员指针引用符)
    • ::(域解析符)
    • ?:(条件运算符)
    • typeid(RTTI 运算符)
    • 四种强制类型转换运算符:const_caststatic_castdynamic_castreinterpret_cast
  5. 只能作为类成员重载的运算符
    • =(赋值运算符)
    • ()(函数调用运算符)
    • [](下标运算符)
    • ->(成员访问运算符)

拓展知识

运算符重载的应用场景

  • 自定义数据类型的运算:如字符串拼接(string s1 + s2)、矩阵运算、日期计算。
  • 容器类操作:如 STL 中的 vector 重载 [] 实现下标访问,string 重载 == 实现字符串比较。
  • 简化代码逻辑:将复杂的类方法调用转化为直观的运算符操作(如 a + b 替代 a.add(b))。

常见错误与注意事项

  • 后缀自增返回非 const 对象:导致 (a++) = b 这样的非法赋值(C++ 标准不允许,编译报错),需返回 const 临时对象。
  • 返回局部对象的引用:如 Time& operator+() 中返回局部变量 tmp 的引用,会导致悬垂引用(局部变量销毁后引用失效)。
  • 忽略参数的 const 修饰:如 operator+(Time& r) 未加 const,导致常量对象无法作为参数(如 const Time t1; t1 + t2 编译报错)。

友元(Friend)

基本概念

类的封装性要求:非类成员只能访问类的 public 成员,无法直接访问 private/protected 成员。但在某些场景下(如外部函数需要直接操作类的私有数据),需要打破这种限制——友元就是为解决此问题设计的。

友元的核心思想:将外部函数/类声明为当前类的「朋友」,朋友可以直接访问当前类的所有成员(包括 private/protected),无需通过公有接口。

生活类比

电视机(TV 类)的频道、音量是私有成员,遥控器(Remoter 类)需要直接修改这些数据,因此将 Remoter 类声明为 TV 类的友元。

遥控器与电视机的友元关系示意图

友元的种类

友元函数

普通全局函数被声明为类的友元,可直接访问类的所有成员。

语法格式
#include <iostream>
using namespace std;

class Base {
private:
    int a;
protected:
    int b;
public:
    int c;
    // 声明友元函数:friend + 函数声明(无需属于类成员)
    friend void show(Base& r);
};

// 友元函数实现:无需加 friend 关键字,与普通函数一致
void show(Base& r) {
    // 直接访问 private/protected/public 成员
    r.a = 123; // private 成员
    r.b = 456; // protected 成员
    r.c = 789; // public 成员
    cout << "a: " << r.a << ", b: " << r.b << ", c: " << r.c << endl;
}

// 测试代码
int main(void) {
    Base obj;
    show(obj); // 输出:a: 123, b: 456, c: 789
    return 0;
}
语法要点
  • 友元函数与访问控制符(public/protected/private)无关:无论声明在类的哪个区域,效果相同。
  • 友元函数不是类成员:没有 this 指针,需通过参数传递类对象才能访问其成员。

友元类

整个类被声明为另一个类的友元,友元类的所有成员函数都能直接访问当前类的所有成员。

经典示例:遥控器类(Remoter)作为电视机类(TV)的友元
#include <iostream>
using namespace std;

// 电视机类
class TV {
private:
    int channel; // 频道(私有成员)
    int volume;  // 音量(私有成员)
public:
    // 声明 Remoter 为友元类:Remoter 的所有成员函数都能访问 TV 的私有成员
    friend class Remoter;
};

// 遥控器类
class Remoter {
public:
    // 调整频道
    void setChannel(TV& tv, int c) {
        // 直接访问 TV 的私有成员 channel
        if (c >= 1 && c <= 100) tv.channel = c;
        else cout << "频道非法!" << endl;
    }

    // 调整音量
    void setVolume(TV& tv, int v) {
        // 直接访问 TV 的私有成员 volume
        if (v >= 0 && v <= 100) tv.volume = v;
        else cout << "音量非法!" << endl;
    }

    // 显示当前状态
    void showStatus(TV& tv) {
        cout << "当前频道:" << tv.channel << ", 当前音量:" << tv.volume << endl;
    }
};

// 测试代码
int main(void) {
    TV myTV;
    Remoter myRemoter;

    myRemoter.setChannel(myTV, 10);
    myRemoter.setVolume(myTV, 30);
    myRemoter.showStatus(myTV); // 输出:当前频道:10, 当前音量:30

    myRemoter.setChannel(myTV, 101); // 输出:频道非法!
    return 0;
}

友元成员函数

仅将友元类的特定成员函数声明为当前类的友元,避免友元类的所有成员都拥有访问权限(更安全)。

语法格式(以万能遥控器为例)
#include <iostream>
using namespace std;

// 提前声明遥控器类(因为 TV 类中要引用 Remoter 的成员函数)
class Remoter;

// 电视机类
class TV {
private:
    int channel;
    int volume;
public:
    // 仅声明 Remoter 的 setChannel 和 setVolume 为友元成员函数
    friend void Remoter::setChannel(TV& tv, int c);
    friend void Remoter::setVolume(TV& tv, int v);
};

// 遥控器类(万能遥控器,可控制电视、灯光等)
class Remoter {
public:
    // 控制电视的频道(友元成员函数)
    void setChannel(TV& tv, int c) {
        if (c >= 1 && c <= 100) tv.channel = c;
    }

    // 控制电视的音量(友元成员函数)
    void setVolume(TV& tv, int v) {
        if (v >= 0 && v <= 100) tv.volume = v;
    }

    // 控制灯光(与 TV 无关,无 TV 类的访问权限)
    void turnOnLight() {
        cout << "灯光开启" << endl;
    }

    void turnOffLight() {
        cout << "灯光关闭" << endl;
    }

    // 显示电视状态(需通过友元成员函数间接访问)
    void showTVStatus(TV& tv) {
        // 直接访问 TV 的私有成员(因 setChannel/setVolume 是友元,但 showTVStatus 不是?不,此处需注意:友元声明是针对具体函数的,showTVStatus 未被声明为友元,无法直接访问 TV 的私有成员!)
        // 修正方案:要么将 showTVStatus 也声明为友元,要么在 TV 类中提供公有接口。
        cout << "当前频道:" << tv.channel << ", 当前音量:" << tv.volume << endl; // 编译报错!
    }
};

// 修正后的 TV 类(添加 showTVStatus 为友元)
class TV {
private:
    int channel;
    int volume;
public:
    friend void Remoter::setChannel(TV& tv, int c);
    friend void Remoter::setVolume(TV& tv, int v);
    friend void Remoter::showTVStatus(TV& tv); // 新增友元声明
};
语法要点
  • 提前声明:若友元成员函数所在的类(如 Remoter)在当前类(如 TV)之后定义,需提前声明友元类(class Remoter;)。
  • 精准授权:仅授权必要的成员函数,最小化权限泄露,兼顾灵活性与安全性。

友元的特性与争议

核心特性

  • 单向性:AB 的友元,不代表 BA 的友元(如遥控器能控制电视,电视不能控制遥控器)。
  • 非传递性:AB 的友元,BC 的友元,不代表 AC 的友元。
  • 非继承性:父类的友元不会自动成为子类的友元,子类需单独声明。

争议与使用原则

  • 争议:友元打破了类的封装性,可能导致代码安全性降低(如友元函数误修改类的私有数据)。
  • 使用原则:
    • 最小权限原则:优先使用友元成员函数,而非友元类,仅授权必要的操作。
    • 明确关系原则:仅在两个类存在明确的「使用关系」(use-a)时使用(如遥控器-电视、打印机-文档)。
    • 替代方案优先:若能通过公有接口实现需求,尽量不使用友元(如通过 getter/setter 访问私有成员)。

拓展知识

友元与运算符重载的结合

当重载非类成员运算符时,若需要访问类的私有成员,可将重载函数声明为类的友元(比提供 getter/setter 更简洁)。

示例:Time 类的 << 重载(友元版)

class Time {
private:
    int hour;
    int minute;
    int second;
public:
    Time(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), second(s) {}
    // 声明 << 重载函数为友元,直接访问私有成员
    friend ostream& operator<<(ostream& out, const Time& t);
};

// 友元函数实现 << 重载,直接访问私有成员
ostream& operator<<(ostream& out, const Time& t) {
    out << t.hour << ":" << t.minute << ":" << t.second;
    return out;
}

友元在实际开发中的应用

  • 运算符重载:如 STL 中的 string 类,重载 ==+ 等运算符时,通过友元函数访问私有字符数组。
  • 测试代码:单元测试中,测试函数需访问类的私有成员以验证内部状态,可将测试函数声明为友元。
  • 跨类协作:两个关系紧密的类(如 Buffer 缓冲区类和 Reader 读取类),通过友元实现高效数据交互。

整体总结

知识点 核心作用 关键约束/原则
运算符重载 让自定义类型支持运算符操作 不改变操作数个数、优先级;有限定运算符范围
友元 打破封装,实现跨类数据访问 最小权限、明确关系、替代方案优先
两者结合 简化重载函数,访问私有成员 友元仅用于必要场景,避免滥用

核心思想:C++ 的运算符重载和友元都是为了「兼顾封装性与灵活性」——既通过类保护数据安全,又在必要时提供便捷的交互方式,让代码更直观、高效。

posted @ 2025-12-26 08:43  Jaklin  阅读(4)  评论(0)    收藏  举报