C++ 运算符重载、友元与对象模型深入

一、引言:让对象也能像普通变量一样运算

在前面的学习中,我们已经掌握了 C++ 的基础语法、函数、指针、面向对象、继承与多态。我们可以轻松定义类、创建对象、封装数据、实现多态接口。但你是否想过:

为什么两个 int 可以直接 a + b,而我们自己写的对象 Student、Point、Complex 不能直接相加、比较、输出?

因为 C++ 内置的运算符(+ - * / == != <<>> \[\] () = ++ -- 等)默认只支持基本数据类型,并不认识我们自定义的对象。

为了让对象也能自然、直观、优雅地参与运算,C++ 提供了 运算符重载(Operator Overloading) 机制。它是面向对象编程中提升代码可读性、简洁性的关键技术。

与此同时,为了在特定场景下让外部函数或类能够访问类的私有成员,C++ 提供了 友元(Friend)

而为了真正理解对象为什么能调用成员、this 指针是什么、对象占多少内存、多态底层如何实现,我们必须深入 C++ 对象模型

本篇文章将系统、深入、完整讲解:

  • 运算符重载的意义与语法
  • 常用运算符重载:+ - == != ++ -- <<>> \[\] = ()
  • 友元函数与友元类
  • C++ 对象内存模型
  • this 指针底层原理
  • 常函数、常对象、mutable
  • 运算符重载的工程规范与陷阱

本篇内容偏向底层与进阶,是从 "会用 C++" 到 "精通 C++" 的必经之路。


二、运算符重载:让对象支持数学运算

(一)什么是运算符重载

运算符重载:对 C++ 已有运算符重新定义,使其支持自定义对象的运算。

本质: 运算符本质是 函数 。 例如: a + b 等价于 operator+(a, b)

重载后,我们可以写出这样的代码:

cpp

运行

复制代码
Point p1(1,2), p2(3,4);
Point p3 = p1 + p2;

(二)运算符重载语法

cpp

运行

复制代码
返回值类型 operator 运算符 (参数) {
    // 实现逻辑
}

例如重载加号:

cpp

运行

来源:mzjlrdb.cn/AQTRO

来源:mzjlrdb.cn/TBEYS

来源:mzjlrdb.cn/DVRHR

来源:mzjlrdb.cn/WRMBX

来源:mzjlrdb.cn/BQGIS

来源:mzjlrdb.cn/DGUPS

来源:mzjlrdb.cn/VTZUC

来源:mzjlrdb.cn/UBASF

来源:mzjlrdb.cn/ZPZGP

来源:mzjlrdb.cn/FIWMV

复制代码
Point operator+(Point &other) {
    return Point(x + other.x, y + other.y);
}

(三)运算符重载两种实现方式

  1. 成员函数重载(this 指针作为左操作数)
  2. 全局函数重载(需要用友元访问私有成员)

三、常用运算符重载实战

1. 加号运算符 +

cpp

运行

复制代码
class Point {
private:
    int x, y;
public:
    Point(int x=0, int y=0) : x(x), y(y) {}
    Point operator+(Point &p) {
        return Point(x+p.x, y+p.y);
    }
};

2. 等号 ==、不等!=

cpp

运行

复制代码
bool operator==(Point &p) {
    return x==p.x && y==p.y;
}
bool operator!=(Point &p) {
    return !(*this == p);
}

3. 自增 ++(前置、后置)

cpp

运行

复制代码
// 前置 ++p
Point& operator++() {
    x++; y++; return *this;
}
// 后置 p++
Point operator++(int) {
    Point temp = *this; x++; y++; return temp;
}

4. 左移运算符 <<(输出对象,必须全局 + 友元)

cpp

运行

复制代码
friend ostream& operator<<(ostream &out, Point &p) {
    out << p.x << "," << p.y;
    return out;
}

使用:

cpp

运行

复制代码
cout << p1 << endl;

5. 赋值运算符 =(重点解决浅拷贝问题)

如果对象中有指针,必须重载 =,实现深拷贝

cpp

运行

复制代码
class Test {
private:
    int *p;
public:
    Test(int val) { p = new int(val); }
    // 重载赋值
    Test& operator=(Test &t) {
        if (this == &t) return *this; // 防止自赋值
        delete p; // 释放旧内存
        p = new int(*t.p); // 深拷贝
        return *this;
    }
    ~Test() { delete p; }
};

6. 下标运算符 \[\]

cpp

运行

复制代码
int& operator[](int index) {
    return arr[index];
}

7. 函数调用运算符 ()

用于实现仿函数,是 STL 核心基础。


四、不能重载的运算符

以下 5 个运算符不能重载(C++ 固定规则):

  1. :: 域解析
  2. . 成员访问
  3. .* 成员指针解引用
  4. sizeof
  5. ?: 三目运算符

五、友元:打破封装的 "特殊授权"

(一)什么是友元

友元允许 外部函数 / 外部类 访问当前类的 private/protected 成员。

友元的作用:

  • 实现运算符重载(如 <<>>)
  • 提高某些场景效率
  • 方便类之间协作

注意:友元破坏封装,不要滥用!

(二)友元函数

cpp

运行

复制代码
class A {
private: int x;
public:
    friend void show(A &a); // 友元函数
};
void show(A &a) {
    cout << a.x; // 可访问私有
}

(三)友元类

cpp

运行

复制代码
class B;
class A {
    friend class B; // B是A的友元类
private: int x;
};
class B {
public:
    void f(A &a) { cout << a.x; }
};

六、C++ 对象模型深入(底层核心)

(一)对象占多少内存?

对象内存 = 所有非静态成员变量大小之和

  • 成员函数 不占对象内存
  • 静态成员变量 不属于对象,属于类
  • 空对象大小:1 字节(占位)

示例:

cpp

运行

来源:gy.baqiaoyijia.cn

来源:u1.baqiaoyijia.cn

来源:2k.baqiaoyijia.cn

来源:b0.baqiaoyijia.cn

来源:d9.baqiaoyijia.cn

来源:ya.baqiaoyijia.cn

来源:2c.baqiaoyijia.cn

来源:v9.baqiaoyijia.cn

来源:6d.baqiaoyijia.cn

来源:6o.baqiaoyijia.cn

复制代码
class A {
    int a; double b;
};
cout << sizeof(A); // 16(内存对齐)

(二)多态对象内存布局

如果类中有虚函数:

  • 对象前 4/8 字节多一个 vptr 虚指针
  • 指向 vtable 虚函数表

多态调用底层: p->speak() → 通过对象找到 vptr → 通过 vptr 找到 vtable → 在表中找到函数地址 → 调用

(三)this 指针底层

  • 每个非静态成员函数 都隐含一个参数:this
  • this 指向当前调用对象的地址
  • 成员函数访问变量本质是:this->x

七、常函数、常对象与 mutable

(一)常函数

函数后加 const:

cpp

运行

复制代码
void show() const {
    // 不能修改成员变量
}

常函数中 this 是 const 指针

(二)常对象

cpp

运行

复制代码
const Person p;

只能调用常函数。

(三)mutable

被 mutable 修饰的成员变量,在常函数中也可以修改


八、运算符重载工程规范

  1. 保持运算符原有语义(+ 不要做减法)
  2. 对称运算符建议全局重载(+ - * / == != << >>)
  3. 赋值运算符 = 必须成员重载
  4. 自增自减返回值要符合惯例
  5. 对象含指针必须重载 = 并深拷贝
  6. 不要重载逻辑运算符 && ||(会丢失短路特性)
  7. 链式运算符必须返回引用(<< = ++)

九、经典综合案例:复数类(Complex)

cpp

运行

复制代码
#include <iostream>
using namespace std;

class Complex {
private:
    double real, imag;
public:
    Complex(double r=0, double i=0) : real(r), imag(i) {}

    // +
    Complex operator+(Complex &c) {
        return Complex(real+c.real, imag+c.imag);
    }

    // ==
    bool operator==(Complex &c) {
        return real==c.real && imag==c.imag;
    }

    // 前置++
    Complex& operator++() {
        real++; imag++; return *this;
    }

    // 友元<<
    friend ostream& operator<<(ostream &out, Complex &c);
};

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

int main() {
    Complex c1(1,2), c2(3,4);
    Complex c3 = c1 + c2;
    cout << c3 << endl;
    ++c3;
    cout << c3 << endl;
    return 0;
}

十、常见错误与陷阱

  1. 重载 << 写成成员函数 → 无法链式调用
  2. 自增后置不返回临时对象 → 逻辑错误
  3. 赋值运算符不深拷贝 → 重复释放崩溃
  4. 赋值运算符不判断自赋值 → 崩溃
  5. 常函数修改成员 → 编译失败
  6. 滥用友元 → 破坏封装、难以维护

十一、本章总结

本篇文章系统、深入、完整讲解了 C++ 高级面向对象核心:

  1. 运算符重载:让对象支持 + - * / == != ++ -- <<>> \[\] = ()
  2. 友元函数 / 友元类:授权访问私有成员
  3. 对象内存模型:成员不占对象内存、虚指针、虚表
  4. this 指针底层:指向当前对象
  5. 常函数、常对象、mutable
  6. 深拷贝是重载赋值运算符的核心

掌握本篇,你将能够:

  • 写出优雅、直观、接近自然语言的面向对象代码
  • 理解 C++ 对象底层实现,彻底搞懂多态
  • 写出工程级、高健壮性的类
  • 为后续学习 STL 打下坚实基础

下一篇预告

第 9 篇《C++ 模板与泛型编程:STL 核心思想》(4000 字以上)

将带你学习:

  • 函数模板
  • 类模板
  • 泛型编程思想
  • 模板特化、分离编译
  • 这是 STL(标准模板库)的基石