10. 使用类
使用类
运算符重载
运算符重载是 C++ 多态的一种形式,允许将重载的概念扩展到运算符上,赋予 C++ 运算符多种含义。
重载运算符的限制
只能重载现有的C++运算符。
重载后不能改变运算符的优先级
不能改变操作数的个数
不能重载
::,.,->,*(解引用),?:,sizeof至少有一个操作数是用户定义的类型
openator关键字
重载算数/关系运算符
成员函数:this充当左操作数。
全局函数:双参数。
注:+建议用全局+按值传参,+=建议成员
#include <iostream> using namespace std; class A { private: int m_val; public: // ---------- 构造函数 ---------- A(int val = 0) : m_val(val) { cout << "构造: " << m_val << endl; } // 拷贝构造(为了让你看到“拷贝”发生,方便理解) A(const A& other) : m_val(other.m_val) { cout << "拷贝构造: " << m_val << endl; } // 移动构造(为了让你看到“移动”发生,右值传参时走这里,零拷贝) A(A&& other) noexcept : m_val(other.m_val) { cout << "移动构造: " << m_val << endl; other.m_val = 0; // 偷走资源后,把别人置空 } // ---------- 核心法则 1: operator+= 必须写成 成员函数 ---------- // 理由:修改左操作数自身,返回自身引用(方便链式操作 a+=b+=c) A& operator+=(const A& other) { this->m_val += other.m_val; return *this; // 返回自己 } // 打印函数,方便看结果 void print() const { cout << "当前值: " << m_val << endl; } }; // ---------- 核心法则 2: operator+ 写成 全局函数,左操作数按值传递 ---------- // 理由: // 1. 左值传进来 -> 拷贝构造(安全,不影响原数据)。 // 2. 右值传进来 -> 移动构造(零开销,且不影响原数据,因为原数据本来就是临时的)。 // 3. 内部复用 +=,完全不需要友元(因为我们没碰私有成员,碰的是公有 +=)。 A operator+(A lhs, const A& rhs) { lhs += rhs; // 修改的是局部副本(lhs),原数据绝对安全 return lhs; // 返回局部对象(编译器自动触发 RVO 返回值优化,连移动都省了) } int main() { cout << "------ 场景 1: 左值 + 左值(拷贝,不破坏原数据)------" << endl; A a1(10); A a2(20); A a3 = a1 + a2; // a1 拷贝给 lhs,a2 引用传递。a1、a2 没变。 a3.print(); // 输出 30 a1.print(); // 输出 10(证明没被破坏) a2.print(); // 输出 20(证明没被破坏) cout << "\n------ 场景 2: 右值 + 右值(移动,零拷贝)------" << endl; A a4 = A(5) + A(7); // A(5) 移动给 lhs,A(7) 引用传递。没有任何深拷贝。 a4.print(); // 输出 12 cout << "\n------ 场景 3: 右值 + 左值(移动+引用,混合)------" << endl; A a5 = A(100) + a1; // A(100) 移动给 lhs,a1 引用传递。a1 依然没变。 a5.print(); // 输出 110 a1.print(); // 输出 10(依然没变) return 0; }
重载类型转换运算符(隐式转换)
class A { int a; public: // 将 A 隐式转换为 int operator int() const { return a; } }; A obj(10); int n = obj; // 这里调用了 operator int,n 变成 10提醒:隐式转换容易造成意料之外的编译错误,通常建议在前面加 explicit(C++11)禁止隐式调用
函数调用运算符 operator()(仿函数 / Functor)
class Add { public: int operator()(int x, int y) const { return x + y; } }; Add add; int result = add(3, 5); // 对象后面加括号,像函数一样! result = 8解引用/成员访问运算符 operator-> 和 operator*(智能指针专用) 学
std::shared_ptr和std::unique_ptr时的核心。重载这两个运算符,可以让一个类表现得像指针一样。class MyPtr { A* ptr; public: A* operator->() { return ptr; } A& operator*() { return *ptr; } }; // 这样你就可以用 MyPtr->a 直接访问内部 A 的成员了内存分配/释放运算符 operator new / operator delete(极客玩法) 重载全局或局部的 new 和 delete,自定义对象如何从堆中申请/释放内存(比如用内存池),这是游戏引擎或高频交易系统常用的底层优化手段。
友元
C++ 控制对类对象私有部分的访问。通常,公有类方法 提供唯一的访问途径。C++ 提供了 友元(friend) 机制来打破这种限制。
| 类型 | 说明 |
|---|---|
| 友元函数 | 非成员函数,可访问类的私有成员 |
| 友元类 | 整个类成为另一个类的友元 |
| 友元成员函数 | 某个类的成员函数成为另一个类的友元 |
友元函数不是成员函数,不能使用成员运算符.或->调用。友元函数与成员函数具有相同的私有权限。
友元函数的声明
class Time { private: int hours; int minutes; public: Time operator*(double n) const; friend Time operator*(double m, const Time & t); // 友元声明 };友元函数的定义
// 友元定义:不写 friend,不写 Time:: Time operator*(double m, const Time & t) { Time result; long totalminutes = t.hours * m * 60 + t.minutes * m; // 直接访问私有成员! result.hours = totalminutes / 60; result.minutes = totalminutes % 60; return result; }建议友元函数定义为以下模式
// 不访问私有数据,利用成员函数实现 Time operator*(double m, const Time & t) { return t * m; // 调用成员函数 t.operator*(m) }常用友元
重载<<运算符(支持链式调用)
// 友元声明(类中) friend std::ostream & operator<<(std::ostream & os, const Time & t); // 定义 std::ostream & operator<<(std::ostream & os, const Time & t) { os << t.hours << " hours, " << t.minutes << " minutes"; return os; // 返回 os 自身 }重载运算符:作为成员函数还是非成员函数
对于大多数运算符,C++提供了两种实现方式:
成员函数:一个通过 this 隐式传递,一个作为参数显式传递。
非成员函数(通常为友元):两个操作数都作为参数显式传递。
必须用成员函数
| 运算符 | 说明 |
|---|---|
= | 赋值运算符 |
() | 函数调用运算符 |
[] | 下标运算符 |
-> | 指针访问类成员运算符 |
这些运算符必须是成员函数,因为它们要求左操作数是一个类对象,且涉及 this 指针的特殊语义。
建议用非成员函数(友元)的场景
需要对操作数顺序有要求。
第一个操作数不是类对象。
需要对称性的运算符。如:
+、-、*、==等,a+b和b+a相同的调用。
优先级
如果运算符必须修改对象本身状态(如 +=、-=) → 通常用成员函数(因为修改的是 *this)
如果运算符不修改对象本身,且需要对称性(如 +、*) → 通常用非成员函数(友元)
如果第一个操作数不是类对象(如 ostream &) → 必须用非成员函数(友元)
矢量类实例
矢量(vector)是一个有大小和方向的量。
设计了两种等价表示方式
直角坐标:水平分量和垂直分量
极坐标:长度和方向(角度)
vector.hpp
#ifndef VECTOR_H_ #define VECTOR_H_ #include <iostream> namespace VECTOR { class Vector { public: enum Mode { RECT, POL }; // 两种模式 private: double x; // 水平分量 double y; // 垂直分量 double mag; // 长度 double ang; // 方向(弧度) Mode mode; // 状态成员:当前模式 // 私有方法:保持两种表示一致 void set_mag(); void set_ang(); void set_x(); void set_y(); public: Vector(); Vector(double n1, double n2, Mode form = RECT); void reset(double n1, double n2, Mode form = RECT); ~Vector(); // 内联访问函数(只读) double xval() const { return x; } double yval() const { return y; } double magval() const { return mag; } double angval() const { return ang; } void polar_mode(); // 切换到极坐标模式 void rect_mode(); // 切换到直角坐标模式 // 运算符重载 Vector operator+(const Vector & b) const; Vector operator-(const Vector & b) const; Vector operator-() const; // 取反 Vector operator*(double n) const; // 友元函数 friend Vector operator*(double n, const Vector & a); friend std::ostream & operator<<(std::ostream & os, const Vector & v); }; } // end namespace VECTOR #endifvector.cpp
#include <cmath> #include "vector.h" namespace VECTOR { const double Rad_to_deg = 45.0 / atan(1.0); // 约 57.2958 // -------- 私有方法:保持两种表示一致 -------- void Vector::set_mag() { mag = sqrt(x * x + y * y); } void Vector::set_ang() { ang = (x == 0.0 && y == 0.0) ? 0.0 : atan2(y, x); } void Vector::set_x() { x = mag * cos(ang); } void Vector::set_y() { y = mag * sin(ang); } // -------- 构造函数 -------- Vector::Vector() { x = y = mag = ang = 0.0; mode = RECT; } Vector::Vector(double n1, double n2, Mode form) { mode = form; if (form == RECT) { x = n1; y = n2; set_mag(); set_ang(); } else if (form == POL) { mag = n1; ang = n2 / Rad_to_deg; // 角度转为弧度 set_x(); set_y(); } else { // 非法模式:置零 x = y = mag = ang = 0.0; mode = RECT; } } // -------- 运算符重载 -------- Vector Vector::operator+(const Vector & b) const { return Vector(x + b.x, y + b.y); } Vector Vector::operator-(const Vector & b) const { return Vector(x - b.x, y - b.y); } Vector Vector::operator-() const { return Vector(-x, -y); } Vector Vector::operator*(double n) const { return Vector(x * n, y * n); } // -------- 友元函数 -------- Vector operator*(double n, const Vector & a) { return a * n; // 调用成员函数 operator*(double) } // 根据 mode 决定显示格式 std::ostream & operator<<(std::ostream & os, const Vector & v) { if (v.mode == Vector::RECT) { os << "(x,y) = (" << v.x << ", " << v.y << ")"; } else if (v.mode == Vector::POL) { os << "(m,a) = (" << v.mag << ", " << v.ang * Rad_to_deg << ")"; // 弧度转度显示 } else { os << "Vector object mode is invalid"; } return os; } } // end namespace VECTOR设计要点解析
两种表示方式同步更新
输入模式 构造函数操作 同步更新 RECT(直角坐标)设置 x、y调用 set_mag()、set_ang()计算极坐标POL(极坐标)设置 mag、ang调用 set_x()、set_y()计算直角坐标
类的自动转换和强制类型转换
C++ 处理内置类型转换时,兼容类型可以自动转换,不兼容类型需要强制转换。
类的设计可以让自定义类型之间或自定义类型与内置类型之间的转换变得有意义。
隐式类型转换的场景
#ifndef STONEWT_H_ #define STONEWT_H_ class Stonewt { private: enum { Lbs_per_stn = 14 }; // 1 英石 = 14 磅 int stone; // 整英石数 double pds_left; // 剩余磅数(小数) double pounds; // 总磅数 public: Stonewt(double lbs); // 从 double(总磅数)构造 Stonewt(int stn, double lbs); // 从(英石,磅)构造 Stonewt(); // 默认构造函数 ~Stonewt(); void show_lbs() const; // 显示总磅数 void show_stn() const; // 显示英石+磅 }; #endif对象初始化(Stonewt wolfe = 285.7;),编译器隐式的调用了构造函数将double类型转换为Stonewt类型。
赋值操作(myCat = 19.6;),编译器调用构造函数将(19.6)转为一个Stonewt类型的临时对象,再用编译器重写的赋值运算符,将这个临时对象赋值给myCat。实现了double类型到Stone类型的转化。
函数参数传递(display(422, 2);),编译器发现第一个参数是Stonewt &类型,隐式的调用构造函数。实现了double类型到Stonewt类型的转化。
函数返回值,同上。
注:隐式类型转换(从其它类型 → 类类型),靠的是“只接受一个参数”的构造函数。如果是两个参数,第二个参数是默认参数仍能隐式类型转换。
Stonewt wolfe = (285.7,5);右边被视为逗号运算,结果为5。
转换函数和友元函数
加法运算符的实现方法
成员函数
Stonewt Stonewt::operator+(const Stonewt & st) const { double pds = pounds + st.pounds; Stonewt sum(pds); return sum; }友元函数
Stonewt operator+(const Stonewt & st1, const Stonewt & st2) { double pds = st1.pounds + st2.pounds; Stonewt sum(pds); return sum; }两种方式只能选其一,否则会导致二义性。
当第一个操作数不是类对象时,只有友元函数能处理。成员函数要求调用对象必须是类对象。
二义性问题(转换函数捣乱) 如果类同时定义了转换函数(如 operator double())和加法运算符重载,编译器可能陷入二义性。
// 同时存在: Stonewt::operator double() const; // Stonewt → double Stonewt operator+(const Stonewt & a, const Stonewt & b); // 友元加法