当前位置: 首页 > news >正文

运算符重载

        C++的 运算符重载(Operator Overloading) 是一个很有趣又实用的特性,让你可以为自

定义类赋予类似内建类型的操作行为。
        

        什么是运算符重载

        运算符重载允许你“重新定义”已有运算符(如 +、-、== 等),使它们能够用于你自己的类

对象之间的操作。
        

        可以重载的运算符       

几乎所有运算符都可以重载 ,例如:
         算术:+ - * / %
         比较:== != < > <= >=
         赋值:=
         自增/自减:++ --
         输入输出:<< >>
         下标:[]
         函数调用:()
         指针访问:->
但这些 不能被重载
         .(成员访问符)
         *(成员指针访问)
         ::(作用域解析)
         sizeof
         typeid
         alignof
         decltype

        二元运算符重载的方式

        在 C++ 中,二元运算符(如 +, -, *, /, ==, != 等)可以有多种方式重载。主要分为两类:

        成员函数方式

        格式:

        class ClassName {
        public:
                ClassName operator+(const ClassName& rhs) const;
        };

         左操作数必须是 类的对象(或其引用)
         this 代表左操作数,参数是右操作数。
         适合 左操作数必须是类对象 的场景。
        

        示例 1(Operator1):成员函数重载 +

#include <iostream>
using namespace std;class Point
{
public:Point(int x = 0, int y = 0) : x(x), y(y) {}Point operator+(const Point& rhs) const {return Point(x + rhs.x, y + rhs.y);}void show() const {cout << "(" << x << ", " << y << ")" << endl;}
private:int x;int y;
};int main() {Point p1(1, 2), p2(3, 4);Point sum = p1 + p2;  // Ï൱ÓÚ p1.operator+(p2)sum.show();           // Êä³ö£º(4, 6)return 0;
}
        构造函数

        Point(int x = 0, int y = 0) : x(x), y(y) {}:这是一个带默认参数的构造函数。如果创建 Point 对象时不提供参数,x 和 y 会被初始化为 0;如果提供参数,则使用提供的值进行初始化,同时使用了初始化列表(减少系统开销;如果是const类型,则只能用初始化列表)。

        运算符重载

        Point operator+(const Point& rhs) const重载了 + 运算符,使得两个 Point 对象可以相加。const Point& rhs 是一个常量引用表示右操作数。函数内部通过 x + rhs.x 和 y + rhs.y 分别计算新点的横坐标和纵坐标,并返回一个新的 Point 对象const 关键字表示该函数不会修改调用对象的状态

        注解:

        在运算符重载函数 Point operator+(const Point& rhs) const 中,虽然没有显式用特定语句说明左右操作数的类型,但通过函数的定义、参数和返回值类型等信息可以明确左右操作数的类型。

        左操作数的类型

        左操作数是调用该运算符重载函数的对象由于此运算符重载函数是 Point 类的成员函数所以左操作数的类型必然是 Point 类的对象。在类的成员函数里,this 指针指向调用该函数的对象,这里左操作数就是 *this 所代表的对象

        例如,在代码 Point sum = p1 + p2; 中,p1 就是左操作数,它的类型是 Point

        右操作数的类型

        右操作数的类型运算符重载函数的参数类型确定。该函数的参数是 const Point& rhs,这表明右操作数是一个 Point 类对象的常量引用。使用常量引用的好处是既能避免对象的复制,提高效率,又能保证在函数内部不会修改右操作数对象的状态

        在 Point sum = p1 + p2; 中,p2 就是右操作数,它的类型同样是 Point

        友元函数方式

      格式:

        class ClassName {
                friend ClassName operator+(const ClassName& lhs, const ClassName& rhs);
        };

        可以 让左右操作数都不是类成员 (甚至左操作数可以是其他类型,比如 int +
Complex)。
         更灵活,适合实现 非对称重载 (如 int + Complex)。

        示例 2(Operator2):友元函数重载 +

#include <iostream>
using namespace std;class Complex {
public:Complex(double r = 0, double i = 0) : real(r), imag(i) {}// 友元函数重载 +friend Complex operator+(const Complex& lhs, const Complex& rhs);void show() const {cout << real << (imag >= 0 ? " + " : " - ") << abs(imag) << "i" << endl;
}
private:double real;double imag;
};Complex operator+(const Complex& lhs, const Complex& rhs) {return Complex(lhs.real + rhs.real, lhs.imag + rhs.imag);
}int main() {Complex c1(2.5, 3.5), c2(1.5, -1.0);Complex c3 = c1 + c2;  // 调用全局函数 operator+c3.show();             // 输出:4 + 2.5ireturn 0;
}
        友元函数声明

  friend Complex operator+(const Complex& lhs, const Complex& rhs);:声明了一个友元函数 operator+,用于重载 + 运算符。友元函数可以访问类的私有成员const Complex& lhs 和 const Complex& rhs 分别表示左操作数和右操作数,使用常量引用可以避免对象的复制,提高效率,同时保证函数内部不会修改操作数的状态。

        友元函数的实现
Complex operator+(const Complex& lhs, const Complex& rhs) {return Complex(lhs.real + rhs.real, lhs.imag + rhs.imag);
}

        这是 operator+ 友元函数的具体实现。它接受两个 Complex 对象的常量引用作为参数,分别将它们的实部和虚部相加,然后创建一个新的 Complex 对象并返回。

        成员函数和友元函数在运算符重载上的区别

        成员函数重载( Point 类的 + 重载)

class Point
{
public:// 其他成员...Point operator+(const Point& rhs) const {return Point(x + rhs.x, y + rhs.y);}// 其他成员...
};

        这种方式将运算符重载函数作为类的成员函数。在成员函数中,隐含了一个 this 指针,它指向调用该函数的对象,也就是运算符的左操作数所以成员函数只需要显式声明右操作数,左操作数通过 this 指针来隐式表示

        成员函数重载运算符时,由于其属于类的成员,编译器知道是哪个对象在调用该运算符函数。例如 p1 + p2,编译器会把它解释为 p1.operator+(p2),这里 p1 就是 this 指针指向的对象,p2 作为右操作数传递给 operator+ 函数。因此,在成员函数的定义中,只需要显式声明右操作数

        友元函数重载( Complex 类的 + 重载)

class Complex {
public:// 其他成员...friend Complex operator+(const Complex& lhs, const Complex& rhs);// 其他成员...
};Complex operator+(const Complex& lhs, const Complex& rhs) {return Complex(lhs.real + rhs.real, lhs.imag + rhs.imag);
}

        这种方式将运算符重载函数作为类的友元函数友元函数不是类的成员函数,它没有隐含的 this 指针所以需要显式声明左操作数和右操作数

        友元函数不属于类的成员没有 this 指针来隐式表示左操作数。所以在定义友元函数时,必须显式声明左操作数和右操作数,让编译器知道参与运算的两个对象。例如 c1 + c2,编译器会调用 operator+(c1, c2),这里 c1 是左操作数,c2 是右操作数。

        混合类型重载

        需要使用 非成员函数(通常是友元) 实现;

        示例 3(Operator3):支持 int + Complex

#include <iostream>
using namespace std;
class Complex {
public:Complex(double r = 0, double i = 0) : real(r), imag(i) {}// Complex + int:成员函数Complex operator+(int val) const {return Complex(real + val, imag);}// int + Complex:必须是友元或非成员函数friend Complex operator+(int val, const Complex& c) {return Complex(c.real + val, c.imag);}void show() const {cout << real << (imag >= 0 ? " + " : " - ") << abs(imag) << "i" << endl;}
private:double real;double imag;
};
int main() {Complex c(1.0, 2.0);Complex a = c + 5;  // 调用成员函数Complex b = 5 + c;  // 调用友元函数a.show();  // 输出:6 + 2ib.show();  // 输出:6 + 2ireturn 0;
}
        Complex + int 运算符重载(成员函数)
Complex operator+(int val) const {return Complex(real + val, imag);
}

        这是一个成员函数形式的运算符重载,用于实现 Complex 对象与整数(注意顺序)相加。

   const 关键字表明该函数不会修改调用它的 Complex 对象。

       函数内部创建一个新的 Complex 对象,其实部为原 Complex 对象的实部加上传入的整数 val,虚部保持不变。

        int + Complex 运算符重载(友元函数)
friend Complex operator+(int val, const Complex& c) {return Complex(c.real + val, c.imag);
}

        这里使用友元函数来实现 int + Complex 的加法运算。因为成员函数的运算符重载,左操作数必须是该类的对象,无法实现 int 作为左操作数的情况,所以需要使用友元函数。

   const Complex& c 是对 Complex 对象的常量引用,避免对象的复制,提高效率,同时保证函数内部不会修改该对象。

         函数返回一个新的 Complex 对象,其实部为传入的整数 val 加上 Complex 对象的实部,虚部保持不变。

        注:abs(~)是“~”的绝对值的意思,在<cstdlib>头文件下,注意只能处理整数类型绝对值。要是处理浮点类型,需要用<cmath>下的fabs(),用法和abs()一样。

        总结:二元运算符重载方式对比

        成员函数:
        优点:逻辑清晰,容易实现
        缺点:左操作数必须是类类型

        友元函数:
        优点:灵活,可以支持不同类型混合操作
        缺点:不易封装,需声明为friend

        非成员函数:
        优点:与友元函数类似,适合封装类外扩展逻辑
        缺点:无法访问类的私有成员,除非用friend。

        一元运算符重载的方式

        C++中的一元运算符重载有自增(++)、自减(--)、取负(-)、逻辑非(!)等。

        一元运算符的基本特性

        一元运算符只有一个操作数。
        可作为成员函数或友元函数(非成员)来重载。
        常见一元运算符包括:

         +(正号)
         -(负号)
         !(逻辑非)
         ~(按位取反)
         ++(自增,前缀/后缀)
         --(自减,前缀/后缀)
        

        一元运算符的重载方式

        方式一:成员函数

        class A {
        public:
                A operator!() const; // 重载 !,无参数
        };

        方式 2:非成员(通常是友元)

        class A {
                friend A operator!(const A& obj); // 必须一个参数
        };

        示例 1(Operator4):成员函数重载一元 -(取负)

#include <iostream>
using namespace std;class Number {
public:Number(int v) : value(v) {}Number operator-() const {  // 重载一元 -return Number(-value);}void show() const {cout << value << endl;}
private:int value;
};int main() {Number a(5);Number b = -a;  // 相当于 a.operator-()b.show();       // 输出:-5return 0;
}

        定义了一个 Number 类,并重载了一元负号运算符 -使得该类的对象可以像基本数据类型一样使用一元负号来取相反数

        一元负号运算符重载
Number operator-() const {return Number(-value);
}

        函数声明Number operator-() const 表示重载一元负号运算符 -operator- 是 C++ 中用于重载一元负号运算符的特定函数名const 关键字表明该函数不会修改调用它的对象的状态。

        函数实现:函数内部创建并返回一个新的 Number 对象,其 value 成员为当前对象 value 的相反数。具体来说,-value 计算出当前对象 value 的相反数,然后使用这个相反数作为参数调用 Number 类的构造函数创建一个新对象

        注:在这个一元负号运算符 - 重载的场景中,不存在左右操作数的概念

        运算符的分类

在 C++ 里,运算符可分为一元运算符、二元运算符和三元运算符:

  • 一元运算符:仅对一个操作数进行操作,像 ++(自增)、--(自减)、-(一元负号)等。
  • 二元运算符:对两个操作数进行操作,例如 +(加法)、-(减法)、*(乘法)等。
  • 三元运算符:C++ 里只有一个三元运算符 ?:,它需要三个操作数。
        一元负号运算符重载分析

        操作数的确定:在一元负号运算符重载函数 Number operator-() const 中,唯一的操作数就是调用该函数的对象本身。在 main 函数里,Number b = -a; 这行代码,a 就是操作数,-a 实际上调用的是 a.operator-(),也就是对 a 这个对象执行取负操作。

        注:在 C++ 的语法规则中,一元负号运算符 - 被定义为前缀运算符,也就是要放在操作数之前,不能写在后面。“++”和“--”可以自定义,后面讨论。

        函数定义:由于是一元运算符,函数不需要额外的参数来表示操作数const 关键字表明这个函数不会修改调用它的对象的状态。

        示例 2(Operator5):友元函数重载一元 !(逻辑非)

#include <iostream>
using namespace std;class BoolLike {
public:BoolLike(bool v) : value(v) {}friend BoolLike operator!(const BoolLike& b) {return BoolLike(!b.value);}void show() const {cout << (value ? "true" : "false") << endl;}
private:bool value;
};int main() {BoolLike x(true);BoolLike y = !x;  // 调用友元重载的 !y.show();         // 输出:falsereturn 0;
}
        逻辑非运算符 ! 的重载
friend BoolLike operator!(const BoolLike& b) {return BoolLike(!b.value);
}

        友元函数声明:使用 friend 关键字将 operator! 函数声明为 BoolLike 类的友元函数。友元函数可以访问类的私有成员,尽管它不是类的成员函数。

        函数参数const BoolLike& b 是一个常量引用,它表示要进行逻辑非运算的 BoolLike 对象。使用常量引用的好处是避免对象的复制,提高效率,同时保证函数内部不会修改传入对象的状态

        函数实现:函数内部通过 !b.value 对传入对象 b 的 value 成员取逻辑非,然后使用这个结果作为参数调用 BoolLike 类的构造函数创建并返回一个新的 BoolLike 对象

        示例 3(Operator6):自增运算符 ++(前缀和后缀)

#include <iostream>
using namespace std;class Counter {
public:Counter(int c = 0) : Count(c) {}// 前缀 ++Counter& operator++() {++Count;return *this;}// 后缀 ++(区分方式:int dummy 参数)Counter operator++(int) {//该参数无用,仅仅是为了前自增区别Counter temp = *this;Count++;return temp;}void show() const {cout << "Count: " << Count << endl;}
private:int Count;
};int main()
{Counter c(5);++c;      // 调用前缀c.show(); // Count: 6c++;      // 调用后缀c.show(); // Count: 7return 0;
}

        定义了一个名为Counter的类,用来实现一个计数器,并且对自增运算符++进行了重载,使其能支持前缀自增和后缀自增操作。

        

        前缀自增运算符重载
Counter& operator++() {++Count;return *this;
}

        该函数对前缀自增运算符++进行了重载。它先把Count成员变量的值加 1,然后返回当前对象的引用。这样就能实现链式调用,例如++(++c)

        它返回的是当前对象的引用 Counter&,这是为了支持链式调用,像 ++(++c) 这种形式。

        函数内部直接对 Count 进行自增操作,然后返回 *this,也就是当前对象的引用

        语义上,它会先把变量的值增加 1,然后返回增加后变量的引用。也就是说,当执行 ++x 时,x 的值会立即增加 1,并且表达式 ++x 的值就是增加后的 x

        后缀自增运算符重载
Counter operator++(int) {Counter temp = *this;Count++;return temp;
}

        此函数对后缀自增运算符++进行了重载。它借助一个int类型的占位参数来和前缀自增运算符区分开。在函数内部,先把当前对象的值保存到临时对象temp,接着将Count成员变量的值加 1,最后返回临时对象。

        它返回的是 Counter 类型的对象,而不是引用。这是因为它需要返回对象原来的值而不是增加后的值

        函数内部先创建一个临时对象 temp,把当前对象的值赋给它。接着对 Count 进行自增操作,最后返回临时对象 temp

        语义上,它会先返回变量原来的值,然后再把变量的值增加 1。即执行 x++ 时,表达式 x++ 的值是 x 原来的值,执行完这个表达式之后,x 的值才会增加 1。

        编译器如何区分前缀和后缀自增运算符

        在 C++ 中,编译器是通过函数参数列表区分前缀和后缀自增运算符的。

        前缀自增运算符重载函数没有额外的参数
        后缀自增运算符重载函数有一个额外的 int 类型的占位参数,通常被称为 “哑元参数”。

        代码运行说明
Counter c(5);
++c;      // 调用前缀
c.show(); // Count: 6c++;      // 调用后缀
c.show(); // Count: 7

        ++c 调用的是前缀自增运算符重载函数,先把 c 的 Count 值加 1 变成 6,然后输出 6。

  c++ 调用的是后缀自增运算符重载函数,先返回 c 原来的值(此时 Count 为 6),然后把 c 的 Count 值加 1 变成 7,最后输出 7。

        总结:一元运算符重载方式比较

        成员函数:    0个参数 ;          对象内部使用this;
        非成员函数:1个参数 ;          参数为对象引用,需声明为friend;
        后缀 ++/-- : 1个参数(int);用int作为参数与前缀区分

        常见用途:

         ++/--:迭代器、计数器类
        !:布尔类、状态类
        -:数学类(向量、复数等)
        ~:自定义位操作类

        [](下标运算符)和 ()(函数调用运算符)

        它们让对象看起来像数组或函数 ,在容器、仿函数、回调等场景中非常有用!
        

        重载 [] 运算符(下标访问)

        用法:让类对象能像数组一样通过 obj[index] 访问元素。

        作用范围当你在类的定义里对某个运算符(如 [])进行重载时,这个重载运算符就只适用于该类的对象。例如下面的IntArry类:

        封装一个数组:

#include <iostream>
using namespace std;
class IntArray {
public:IntArray() {for (int i = 0; i < 10; i++)data[i] = i * 10;}// 重载下标运算符(返回引用以支持赋值)int& operator[](int index) {return data[index];}// 常量对象支持只读访问const int& operator[](int index) const {return data[index];
}
private:int data[10];
};
int main() {IntArray arr;cout << "arr[3] = " << arr[3] << endl;arr[3] = 100; // 赋值cout << "arr[3] after set = " << arr[3] << endl;return 0;
}

        非常量版本的下标运算符重载

int& operator[](int index) {return data[index];
}

        该函数对下标运算符[]进行了重载。它接受一个整数参数index,表示要访问的数组元素的索引。函数返回数组data中对应索引位置元素的引用。返回引用的好处是可以支持对数组元素的赋值操作,例如arr[3] = 100;

        非常量对象调用

IntArray arr;
cout << "arr[3] = " << arr[3] << endl;
arr[3] = 100; 

        arr是一个非常量对象。当执行arr[3]时,编译器会调用非常量版本的operator[]函数,因为非常量对象可以调用非常量成员函数,并且这个函数返回的引用允许修改数组元素的值

        常量版本的下标运算符重载

const int& operator[](int index) const {return data[index];
}

        这是常量版本的下标运算符重载。它同样接受一个整数参数index,并返回数组data中对应索引位置元素的引用。不同的是,这个函数是一个常量成员函数(通过在函数声明末尾添加const关键字),意味着它可以被常量对象调用。而且返回的引用是const类型的,这确保了常量对象只能对数组元素进行只读访问,不能修改元素的值

        常量对象调用

const IntArray constArr;
cout << "constArr[3] = " << constArr[3] << endl;
// constArr[3] = 100; // 错误:不能修改常量对象的元素

        constArr是一个常量对象。当执行constArr[3]时,编译器会调用常量版本的operator[]函数。这是因为常量对象只能调用常量成员函数,以保证对象的状态不会被修改。如果尝试对constArr[3]进行赋值操作,编译器会报错,因为常量版本的operator[]返回的是const引用,不允许修改元素的值。

        编译器会依据调用对象是否为常量来区分应该调用常量版本还是非常量版本的运算符重载函数

        重载 () 运算符(函数调用)

#include <iostream>
using namespace std;
class Adder
{
public:int operator()(int a, int b) const{return a + b;}
};
int main()
{Adder add;cout << "add(10, 20) = " << add(10, 20) << endl;return 0;
}

        函数对象(也称为仿函数) 

        在 C++ 里,当一个类重载了函数调用运算符 () 时,这个类的对象就可以像函数一样被调用,这种对象就被称作函数对象或仿函数。

        特点:

        状态可保存:函数对象可以有自己的成员变量,这些成员变量可以用来保存对象的状态。这是函数对象与普通函数的一个重要区别,普通函数通常没有内部状态。

        类型可定制:函数对象是一种类型,因此可以作为模板参数传递。这使得函数对象在标准模板库(STL)中得到了广泛应用,例如在 std::sort 函数中可以使用自定义的函数对象来指定排序规则。

        Adder 类的定义

class Adder
{
public:int operator()(int a, int b) const{return a + b;}
};
        重载函数调用运算符

        int operator()(int a, int b) const:这里对函数调用运算符 () 进行了重载。在 C++ 里,当一个类重载了 () 运算符时,这个类的对象就可以像函数一样被调用,这样的对象被称作函数对象或者仿函数

        参数:该重载函数接受两个 int 类型的参数 a 和 b

        返回值:返回这两个参数的和,也就是 a + b

  const 修饰符const 关键字表明这个函数不会修改对象的状态,即不会改变 Adder 类对象的任何成员变量。

        main 函数
int main()
{Adder add;cout << "add(10, 20) = " << add(10, 20) << endl;return 0;
}

        像函数一样调用对象add(10, 20) 这里把 add 对象当作函数来调用。由于 Adder 类重载了 () 运算符,所以调用 add(10, 20) 实际上是调用了 Adder 类中定义的 operator() 函数,传入参数 10 和 20,然后返回它们的和。

        这段代码通过重载函数调用运算符 (),把 Adder 类的对象变成了一个函数对象。这样,对象 add 就可以像普通函数一样被调用,传入两个整数参数并返回它们的和。函数对象在 C++ 中有着广泛的应用,比如在标准模板库(STL)的算法中,经常会使用函数对象来定制算法的行为。

        自定义函数对象 + 状态保存

#include <iostream>
using namespace std;
class Multiplier
{
public:Multiplier(int f) : factor(f) {}int operator()(int x) const{return x * factor;}
private:int factor;
};
int main()
{Multiplier times2(2);Multiplier times3(3);cout << "times2(4) = " << times2(4) << endl; // 8cout << "times3(4) = " << times3(4) << endl; // 12return 0;
}

        重载函数调用运算符

int operator()(int x) const
{return x * factor;
}

        这里对函数调用运算符 () 进行了重载使得 Multiplier 类的对象可以像函数一样被调用

        参数:接受一个 int 类型的参数 x,表示要进行乘法运算的数。

        返回值:返回 x 与 factor 的乘积。

   const 修饰符const 关键字表明这个函数不会修改对象的状态,即不会改变 factor 的值

        main函数
int main()
{Multiplier times2(2);Multiplier times3(3);cout << "times2(4) = " << times2(4) << endl; // 8cout << "times3(4) = " << times3(4) << endl; // 12return 0;
}
        创建对象
Multiplier times2(2);
Multiplier times3(3);

        创建了两个 Multiplier 类的对象 times2 和 times3times2 的乘法因子 factor 被初始化为 2,times3 的乘法因子 factor 被初始化为 3。

        像函数一样调用对象
cout << "times2(4) = " << times2(4) << endl; // 8
cout << "times3(4) = " << times3(4) << endl; // 12

        times2(4) 和 times3(4) 把对象当作函数来调用调用 times2(4) 时,实际上是调用了 Multiplier 类中重载的 operator() 函数,传入参数 4,此时 factor 为 2,所以返回 4 * 2 = 8调用 times3(4) 时,factor 为 3,返回 4 * 3 = 12。最后将结果输出到控制台。

        小结

         [] :  用于  类似数组的元素访问, 常见的使用场景有:自定义容器类、矩阵类
         () :  用于  对象像函数一样调用, 常见的使用场景有:函数对象、回调

        这两个操作符在标准库中也非常常见,比如:

        std::vector<int> v; v[0];(重载 [])
        std::function<void()> f; f();(重载 ())

        重载 ->、类型转换符、或 new/delete

        这几个是 C++ 运算符重载中进阶又特别实用的内容。

        重载 -> 运算符(箭头运算符)

        用法:

        让对象像指针一样使用  obj -> 成员 
        常用于
封装指针或实现智能指针的行为

#include <iostream>
using namespace std;
class Person
{
public:void sayHello(){cout << "Hello from Person!" << endl;}
};
class SmartPointer
{
public:SmartPointer(Person* p) : ptr(p) {}
// 重载 -> 运算符,返回实际指针Person* operator->(){return ptr;}~SmartPointer(){delete ptr;}
private:Person* ptr;
};
int main()
{SmartPointer sp(new Person());sp->sayHello(); // 实际调用的是 Person 的成员函数return 0;
}

        SmartPointer 类的定义

class SmartPointer
{
public:SmartPointer(Person* p) : ptr(p) {}Person* operator->(){return ptr;}~SmartPointer(){delete ptr;}
private:Person* ptr;
};
        构造函数
SmartPointer(Person* p) : ptr(p) {}

        此构造函数接收一个 Person 类型的指针 p,并把它赋值给 ptr这意味着 SmartPointer 对象内部存储了一个指向 Person 对象的指针

        重载箭头运算符
Person* operator->()
{return ptr;
}

        这里重载了箭头运算符 ->。当 SmartPointer 对象使用 -> 运算符时,会调用这个重载函数。函数返回私有成员变量 ptr使得 SmartPointer 对象可以像指针一样访问 Person 对象的成员。这是智能指针实现的关键部分,通过重载 -> 运算符,SmartPointer 类模拟了指针的行为。

        这个重载函数返回 ptr,也就是返回 SmartPointer 对象内部存储的 Person 指针。通过重载 -> 运算符,SmartPointer 对象就能够像指针一样使用。

        析构函数
~SmartPointer()
{delete ptr;
}

        析构函数 ~SmartPointer 在 SmartPointer 对象生命周期结束时被调用。它的作用是释放 ptr 所指向的 Person 对象的内存,防止内存泄漏。这是智能指针自动管理内存的重要机制。

        main函数
int main()
{SmartPointer sp(new Person());sp->sayHello(); return 0;
}
        创建智能指针对象
SmartPointer sp(new Person());

        这行代码创建了一个 SmartPointer 类的对象 sp,并通过 new Person( ) 动态创建了一个 Person 对象将其地址传递给 SmartPointer 的构造函数。这样 sp 就管理了新创建的 Person 对象。

        调用成员函数
sp->sayHello();

        通过 sp-> 调用 sayHello 函数。由于 SmartPointer 类重载了 -> 运算符,sp-> 会调用 SmartPointer 类的 operator-> 函数,返回 ptr(下面解释为什么返回了一个指针就可以直接访问其内容),然后调用 Person 对象的 sayHello 函数,输出 "Hello from Person!"

        注:

        -> 运算符的特殊性

        在 C++ 中,-> 运算符具备 “递归调用” 的特性。当使用 obj->member 这样的表达式时,若 obj 是一个重载了 -> 运算符的对象,编译器会先调用 obj 的 operator->() 函数,得到一个指针。之后,编译器会检查这个返回的指针,如果这个指针类型还支持 -> 运算符,它会继续对这个指针应用 -> 运算符,直到得到一个普通指针或者对象,然后调用相应的成员函数或访问成员变量。

        复习相关指针的知识

        这个例子牵扯到不少指针相关内容,我们在此复习一下。

        指针(*)的使用

        在 C++ 里,指针是一种特殊的变量,其存储的是内存地址。

        声明指针变量

        在定义指针变量时,* 用来表明该变量是一个指针。例如:

Person* ptr;

        这里的 Person* 表明 ptr 是一个指向 Person 类型对象的指针。也就是说,ptr 存储的是 Person 对象在内存中的地址

        解引用指针

        当要访问指针所指向的对象时,可使用 * 进行解引用。例如:

Person p;
Person* ptr = &p; // 让 ptr 指向对象 p
(*ptr).sayHello(); // 解引用 ptr 并调用 Person 的 sayHello 方法

        (*ptr) 表示获取 ptr 所指向的对象,之后就能够调用该对象的成员函数或访问成员变量了。可以理解为负负得正

        箭头运算符(->)的使用

        箭头运算符(->其实是指针解引用和成员访问的一种简写形式。若有一个指向对象的指针 ptr使用 ptr->member 就等同于 (*ptr).member

Person p;
Person* ptr = &p;
ptr->sayHello(); // 等同于 (*ptr).sayHello();

        ptr->sayHello( ) 这种写法更加简洁,它先对 ptr 进行解引用,然后调用所指向对象的 sayHello 方法。

        注:!!!————

        不能定义空指针或未初始化的指针

上面的例子中:

        main函数错误示例
int main()
{Person *p;SmartPointer sp(p);sp->sayHello(); // 实际调用的是 Person 的成员函数return 0;
}

        代码中,Person *p; 仅仅声明了一个 Person 类型的指针 p,但并没有对其进行初始化,这就导致 p 是一个未初始化的指针,它指向的是一个不确定的内存地址。接着,SmartPointer sp(p); 把这个未初始化的指针传递给 SmartPointer 的构造函数,这会让 SmartPointer 对象 sp 管理一个指向不确定内存地址的指针。之后调用 sp->sayHello(); 时,由于 sp 内部的 ptr 指向的是不确定的内存,调用 sayHello 方法就会引发未定义行为,程序可能会崩溃或者产生其他不可预期的结果。

        正确的写法可以使用new和delete
int main()
{// 使用 new 动态分配内存Person *p = new Person();SmartPointer sp(p);sp->sayHello(); // 实际调用的是 Person 的成员函数return 0;
}

        delete在上面的析构函数中;

        在这个示例中,Person *p = new Person(); 动态分配了一个 Person 对象的内存,并且让指针 p 指向这个对象。接着,SmartPointer sp(p); 让 sp 管理这个合法的 Person 对象。当 sp 的生命周期结束时,析构函数 ~SmartPointer() 会调用 delete ptr; 来释放这块内存,从而避免了内存泄漏。

        正确的写法也可以使用栈上的对象

        此时智能指针的析构函数不能再调用 delete,因为栈上的对象会在其作用域结束时自动销毁。

#include <iostream>
using namespace std;class Person
{
public:void sayHello(){cout << "Hello from Person!" << endl;}
};class SmartPointer
{
public:SmartPointer(Person& p) : ptr(&p) {}// 重载 -> 运算符,返回实际指针Person* operator->(){return ptr;}// 析构函数不进行 delete 操作~SmartPointer() {}
private:Person* ptr;
};int main()
{// 在栈上创建 Person 对象Person p;SmartPointer sp(p);sp->sayHello(); // 实际调用的是 Person 的成员函数return 0;
}

        在这个示例中,Person p; 在栈上创建了一个 Person 对象。(这就避免出现空指针)SmartPointer 的构造函数接受一个 Person 对象的引用,然后将其地址存储在 ptr 中。析构函数不需要进行 delete 操作,因为栈上的对象会自动销毁。

        总结:不能定义空指针,但是可以直接定义一个类对象(像在栈上创建对象),这个对象会在内存里占据一块确定的空间,从而拥有一个合法的地址

        重载类型转换运算符(Type Conversion Operator)

        用法:定义类对象如何转换为其他类型(比如 int、double、string、自定义类型等)。

        类转换为 int 和 bool

#include <iostream>
using namespace std;
class MyNumber
{
public:MyNumber(int v) : value(v) {}
// 类型转换:对象转为 intoperator int() const{return value;}
// 类型转换:对象转为 booloperator bool() const{return value != 0;}
private:int value;
};int main()
{MyNumber num(42);int a = num; // 自动调用 operator int()bool b = num; // 自动调用 operator bool()cout << "a = " << a << ", b = " << b << endl;return 0;
}
        MyNumber 类的定义
class MyNumber
{
public:MyNumber(int v) : value(v) {}// 类型转换:对象转为 intoperator int() const{return value;}// 类型转换:对象转为 booloperator bool() const{return value != 0;}
private:int value;
};
        类型转换运算符

        operator int() const

        这是一个类型转换运算符,它允许将 MyNumber 类的对象隐式转换为 int 类型const 关键字表示这个函数不会修改对象的状态当 MyNumber 类的对象出现在需要 int 类型值的上下文中时,编译器会自动调用这个类型转换运算符。在这个函数中,它返回 value,也就是 MyNumber 对象内部存储的整数值。

        operator bool() const

        这是另一个类型转换运算符,它允许将 MyNumber 类的对象隐式转换为 bool 类型。同样,const 关键字表示这个函数不会修改对象的状态。当 MyNumber 类的对象出现在需要 bool 类型值的上下文中时,编译器会自动调用这个类型转换运算符。在这个函数中,如果 value 不等于 0,则返回 true,否则返回 false

        main函数
int main()
{MyNumber num(42);int a = num; // 自动调用 operator int()bool b = num; // 自动调用 operator bool()cout << "a = " << a << ", b = " << b << endl;return 0;
}
        创建对象
MyNumber num(42);

        这行代码使用 MyNumber 类的构造函数创建了一个 MyNumber 类的对象 num,并将其内部的 value 成员初始化为 42。

        类型转换

        int a = num;
        这里将 MyNumber 类的对象 num 赋值给 int 类型的变量 a。由于 MyNumber 类定义了 operator int() 类型转换运算符,编译器会自动调用这个运算符,将 num 转换为 int 类型的值,然后赋给 a。因此,a 的值为 42。

   bool b = num;
        这里将 MyNumber 类的对象 num 赋值给 bool 类型的变量 b。由于 MyNumber 类定义了 operator bool() 类型转换运算符,编译器会自动调用这个运算符,将 num 转换为 bool 类型的值,然后赋给 b。因为 num 的 value 为 42,不等于 0,所以 b 的值为 true

        注(延伸):

        在 C++ 里,即便 MyNumber 类的对象没有直接挨着 int 或者 bool只要处于需要对应类型值的上下文环境中,编译器就会自动调用相应的类型转换运算符

        类型转换的触发条件

        在 C++ 中,当一个自定义类型的对象出现在需要其他类型值的上下文中时,编译器会查找是否存在合适的类型转换运算符。

        赋值操作
MyNumber num(42);
int a = num;  // 赋值操作,需要 int 类型的值,调用 operator int()
bool b = num; // 赋值操作,需要 bool 类型的值,调用 operator bool()

        这里 num 是 MyNumber 类的对象,在赋值语句中分别需要 int 和 bool 类型的值,所以编译器会自动调用对应的类型转换运算符。

         函数参数传递
void printInt(int value) {std::cout << "The int value is: " << value << std::endl;
}void printBool(bool value) {std::cout << "The bool value is: " << (value ? "true" : "false") << std::endl;
}int main() {MyNumber num(42);printInt(num);  // 函数参数需要 int 类型,调用 operator int()printBool(num); // 函数参数需要 bool 类型,调用 operator bool()return 0;
}

        在调用 printInt 和 printBool 函数时,分别将 MyNumber 类的对象 num 作为参数传递。由于函数参数分别需要 int 和 bool 类型的值,编译器会自动调用相应的类型转换运算符

        条件判断
MyNumber num(42);
if (num) {std::cout << "The value is non-zero." << std::endl;
}

        在 if 语句的条件判断中,需要 bool 类型的值。因此,编译器会自动调用 MyNumber 类的 operator bool() 类型转换运算符,将 num 转换为 bool 类型的值进行判断。

        小结

        MyNumber 类对象是否直接挨着 int 或者 bool 并非调用类型转换运算符的关键条件。只要对象处于需要特定类型值的上下文环境中,编译器就会查找并调用合适的类型转换运算符,实现对象到目标类型的转换。

        重载 new 和 delete 运算符

        用法:允许你自定义内存分配和释放的行为(比如加日志、计数、对齐优化等)

        统计内存分配次数

#include <iostream>
using namespace std;
class MyClass
{
public:void* operator new(size_t size){cout << "[MyClass] Allocating " << size << " bytes." << endl;return malloc(size);}void operator delete(void* ptr){cout << "[MyClass] Deallocating memory." << endl;free(ptr);}
};
int main()
{MyClass* obj = new MyClass(); // 自动调用重载的 newdelete obj; // 自动调用重载的 deletereturn 0;
}
        MyClass类的定义
class MyClass
{
public:void* operator new(size_t size){cout << "[MyClass] Allocating " << size << " bytes." << endl;return malloc(size);}void operator delete(void* ptr){cout << "[MyClass] Deallocating memory." << endl;free(ptr);}
};
        重载 new 运算符
void* operator new(size_t size)
{cout << "[MyClass] Allocating " << size << " bytes." << endl;return malloc(size);
}

        返回类型void*(第一次见),表示返回一个指向分配内存的指针,这是因为 new 运算符的作用是分配一块内存,而返回的指针类型在使用时会根据具体的对象类型进行转换。例如,MyClass* obj = new MyClass(); 这里返回的 void* 指针会被隐式转换为 MyClass* 类型。

        参数size_t size size_t 是一个无符号整数类型size 表示需要分配的内存大小,这个大小是由编译器自动计算得出的,它取决于要创建的对象的类型。比如对于 MyClass 类的对象,size 就是 MyClass 对象所占用的内存字节数

        功能
        在这个重载的 new 运算符中,首先输出一条信息,显示正在为 MyClass 对象分配多少字节的内存,这在调试或者监控内存分配情况时非常有用。
        return malloc(size); 调用了 C 标准库函数 malloc 来分配指定大小的内存malloc 函数会在堆上分配 size 字节的内存,并返回指向这块内存的 void* 指针

        

        重载delete运算符
void operator delete(void* ptr)
{cout << "[MyClass] Deallocating memory." << endl;free(ptr);
}

        返回类型void,表示不返回任何值。

        参数void* ptr,表示需要释放的内存的指针。这个指针通常是之前通过 new 运算符分配内存时返回的指针。

        功能
        在这个重载的 delete 运算符中,首先输出一条信息,显示正在释放 MyClass 对象的内存。
        free(ptr); 调用了 C 标准库函数 free 来释放 ptr 所指向的内存free 函数会将之前通过 malloc 分配的内存归还给系统。

        main函数
int main()
{MyClass* obj = new MyClass(); // 自动调用重载的 newdelete obj; // 自动调用重载的 deletereturn 0;
}
        创建对象
MyClass* obj = new MyClass();

        这行代码使用 new 运算符创建一个 MyClass 类的对象。由于 MyClass 类重载了 new 运算符,编译器会自动调用重载的 operator new(size_t size) 函数来分配内存,并调用 MyClass 的构造函数(在这个代码中没有显式定义构造函数,使用默认构造函数)来初始化对象。最后,将分配的内存地址赋给指针 obj

        释放对象
delete obj;

        这行代码使用 delete 运算符释放 obj 所指向的 MyClass 对象的内存。由于 MyClass 类重载了 delete 运算符,编译器会自动调用重载的 operator delete(void* ptr) 函数来释放内存,并调用 MyClass 的析构函数(在这个代码中没有显式定义析构函数,使用默认析构函数)来清理对象。

        注:
        重载 new 和 delete 运算符时,要确保正确使用 malloc 和 free 或 new 和 delete 来进行内存的分配和释放,避免内存泄漏。
        内存管理一致性:在重载 new 和 delete 运算符时,要保证使用的内存分配和释放函数是一致的。比如这里使用 malloc 分配内存,就应该使用 free 释放内存;如果使用 new 分配内存,就应该使用 delete 释放内存,否则可能会导致内存泄漏或者未定义行为。

        注:

        new 运算符的使用与参数问题

        在 C++ 里,new 运算符有两种形式:运算符形式函数形式

        运算符形式的 new

        new MyClass() 属于运算符形式的 new,它是 C++ 内置的语法,用于创建对象。使用时直接在 new 后面跟上类名和构造函数的参数(如果有的话),例如:

MyClass* obj = new MyClass();

        这里 new 运算符会做两件事:

        调用 operator new 函数来分配内存。
        调用 MyClass 的构造函数来初始化这块内存中的对象

         函数形式的 new

void* operator new(size_t size) 

        对 operator new 函数的重载,它属于函数形式的 newoperator new 函数的参数 size_t size 由编译器自动计算并传递,这个 size 代表要分配的内存大小,也就是对象占用的字节数。当使用运算符形式的 new 时,编译器会自动调用对应的 operator new 函数来完成内存分配。

        关于类对象的问题

        当使用运算符形式的 new 创建对象时,并不一定要求 operator new 是在当前类中重载的。如果一个类没有重载 operator new,那么会使用全局的 operator new 函数;如果某个类重载了 operator new,那么在创建该类的对象时会调用其重载的版本。

class MyClass {
public:void* operator new(size_t size) {// 自定义内存分配逻辑return ::operator new(size); // 调用全局的 operator new}
};class AnotherClass {};int main() {MyClass* obj1 = new MyClass(); // 调用 MyClass 重载的 operator newAnotherClass* obj2 = new AnotherClass(); // 调用全局的 operator newreturn 0;
}

        malloc(size) 的返回值和执行动作

        malloc(size) 的执行动作

        malloc 是 C 标准库中的函数,用于在堆上分配指定大小的内存。

void* malloc(size_t size);

        当调用 malloc(size) 时,它会在堆内存中寻找一块连续的、大小为 size 字节的空闲内存块。如果找到了,就将这块内存标记为已使用,并返回指向这块内存起始地址的 void* 指针;如果没有足够的空闲内存可供分配,malloc 会返回 NULL

        malloc(size) 的返回值

        malloc(size) 返回的是一个 void* 类型的指针,这个指针指向分配的内存块的起始地址。在 operator new 函数中,将 malloc(size) 的返回值直接返回,使得 new 运算符可以获取到分配好的内存地址,然后在这块内存上调用对象的构造函数进行初始化。
        例如:

void* operator new(size_t size) {std::cout << "[MyClass] Allocating " << size << " bytes." << std::endl;void* ptr = std::malloc(size);if (ptr == nullptr) {throw std::bad_alloc(); // 内存分配失败,抛出异常}return ptr;
}

        综上所述,new 运算符在使用时直接跟类名和构造函数参数,编译器会自动处理内存分配和对象初始化malloc(size) 会在堆上分配指定大小的内存,并返回指向该内存起始地址的指针。

        小结

        ->  : 用途是让对象像指针一样访问成员,常见的场景有智能指针和自定义包装类等
       
        类型转换符:用途是控制对象如何(隐式/显式)转化为其他类型,常见的场景有逻辑判断、打印、类型适配等

        new/delete:控制对象如何分配/释放内存,常见场景有内存池、日志、调试工具等

        最后一个实例:

        复数类 Complex 重载 + 和 <<

#include <iostream>
using namespace std;
class Complex
{
public:Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载加法运算符 +Complex operator+(const Complex& other) const{return Complex(real + other.real, imag + other.imag);}
// 重载输出运算符 <<friend ostream& operator<<(ostream& os, const Complex& c){os << c.real;if (c.imag >= 0)os << " + " << c.imag << "i";elseos << " - " << -c.imag << "i";return os;}
private:double real;double imag;
};
int main()
{Complex c1(2.5, 3.0);Complex c2(1.5, -2.0);Complex c3 = c1 + c2;cout << "c1 = " << c1 << endl;cout << "c2 = " << c2 << endl;cout << "c3 = c1 + c2 = " << c3 << endl;return 0;
}

        Complex 类的定义

class Complex
{
public:Complex(double r = 0, double i = 0) : real(r), imag(i) {}// 重载加法运算符 +Complex operator+(const Complex& other) const{return Complex(real + other.real, imag + other.imag);}// 重载输出运算符 <<friend ostream& operator<<(ostream& os, const Complex& c){os << c.real;if (c.imag >= 0)os << " + " << c.imag << "i";elseos << " - " << -c.imag << "i";return os;}
private:double real;double imag;
};

        构造函数

Complex(double r = 0, double i = 0) : real(r), imag(i) {}

        这是 Complex 类的构造函数,它接受两个 double 类型的参数 r 和 i,分别用于初始化复数的实部 real 和虚部 imag。参数 r 和 i 都有默认值 0,这意味着在创建 Complex 对象时,如果不提供参数,将创建一个实部和虚部都为 0 的复数。

         重载加法运算符 +

Complex operator+(const Complex& other) const
{return Complex(real + other.real, imag + other.imag);
}

        函数签名Complex operator+(const Complex& other) const,返回类型为 Complex,表示返回一个新的 Complex 对象。参数 const Complex& other 是一个常量引用,指向另一个 Complex 对象,用于进行加法运算。const 关键字表示该函数不会修改调用对象的状态。

        功能:该函数实现了两个复数的加法运算。它创建一个新的 Complex 对象,其实部为当前对象的实部与 other 对象的实部之和,虚部为当前对象的虚部与 other 对象的虚部之和,并返回这个新对象。

        重载输出运算符 <<

friend ostream& operator<<(ostream& os, const Complex& c)
{os << c.real;if (c.imag >= 0)os << " + " << c.imag << "i";elseos << " - " << -c.imag << "i";return os;
}
        friend 关键字

  在 Complex 类中,将 operator<< 函数声明为友元函数,意味着这个函数虽然不是 Complex 类的成员函数,但它可以访问 Complex 类的私有成员(在这个例子中是 real 和 imag)。这是因为 ostream& operator<<(ostream& os, const Complex& c) 函数需要读取 Complex 对象的实部和虚部来进行输出操作,而私有成员在类外部通常是不能直接访问的,通过 friend 声明就打破了这种访问限制,让该函数可以合法地访问 Complex 类的私有成员。

        函数的返回类型和参数

        返回类型ostream&,即 ostream 类型的引用ostream 是 C++ 标准库中用于输出的流类cout 就是 ostream 的一个实例。返回 ostream 的引用是为了支持链式输出,例如 cout << a << b << c;,如果函数不返回 ostream 的引用,就无法实现这种连续输出多个值的功能。

        第一个参数ostream& os,这是一个 ostream 类型的引用,表示要将数据输出到的目标流对象。在实际使用中,通常是 cout,即标准输出流,也可以是其他 ostream 的派生类对象(如文件流 ofstream 等)。

        第二个参数const Complex& c,这是一个 Complex 类型的常量引用,指向要输出的复数对象。使用常量引用的原因是在函数内部只需要读取 Complex 对象的信息进行输出,不需要修改它,这样可以提高效率并避免意外修改对象的值。

        函数的具体实现:

        输出实部os << c.real;,这行代码将复数 c 的实部 real 输出到目标流 os 中。os 是 ostream 类型的引用,<< 运算符在这里是 ostream 类中已经重载好的用于输出基本数据类型(如 double)的版本,所以可以直接将 c.real 输出。

        根据虚部的正负输出虚部

if (c.imag >= 0)os << " + " << c.imag << "i";
elseos << " - " << -c.imag << "i";

        这段代码根据复数 c 的虚部 imag 的正负情况,将虚部以合适的格式输出到目标流 os 中。如果虚部大于等于 0,输出格式为 + <虚部值>i;如果虚部小于 0,将虚部取反后输出,格式为 - <虚部绝对值>i

        返回流对象return os;,这行代码将经过上述操作后的 ostream 对象 os 返回,使得可以继续对这个流对象进行其他输出操作,从而实现链式输出。

        功能:该函数实现了将 Complex 对象以 a + bi 或 a - bi 的形式输出到指定的输出流中。如果虚部为非负数,输出 a + bi 的形式;如果虚部为负数,输出 a - |b|i 的形式。

        这段代码通过重载加法运算符 + 和输出运算符 <<,实现了复数的加法运算和输出功能,使得 Complex 类的使用更加直观和方便。

提醒

        不要滥用运算符重载,比如不该重载 ++ 来表示“向后跳舞”这类违反直觉的行为。

        通常对于:
        二元运算符:如 +、-,可以作为成员 友元函数。
         流操作符(<<、>>):必须用 友元函数 来写

相关文章:

  • LeetCode(Hot.2)—— 49.字符异位词分组题解
  • 【win 1】win 右键菜单添加 idea pycharm vscode trae 打开文件夹
  • 笔试专题(十一)
  • 什么编程语言市场竞争小,但还易学?
  • Docker应用端口查看器docker-port-viewer
  • 基于springboot的老年医疗保健系统
  • HTML5 应用程序缓存:原理、实践与演进
  • 【Vue】模板语法与指令
  • 图灵奖得主LeCun:DeepSeek开源在产品层是一种竞争,但在基础方法层更像是一种合作;新一代AI将情感化
  • 【Linux】线程ID、线程管理、与线程互斥
  • 【概率论】条件期望
  • rebase和merge的区别
  • 【图片识别改名工具】图片文件区域OCR识别并自动重命名,批量识别指定区域根据指定识别文字批量改名,基于WPF和阿里云的技术方式实现
  • Ethan独立开发产品日报 | 2025-04-18
  • 汽车故障诊断工作原理:从需求到AUTOSAR诊断模块协作的浅析
  • Android 热点二维码简单示例
  • 0801ajax_mock-网络ajax请求1-react-仿低代码平台项目
  • 论文阅读:2024 ICLR Workshop. A STRONGREJECT for Empty Jailbreaks
  • 每日两道leetcode
  • SRS流媒体服务器
  • 十四届全国人大常委会第十五次会议将于4月27日至30日举行
  • 泸州市长余先河已任四川省委统战部常务副部长
  • 马上评丨“化学麻将”创新值得点赞,但要慎言推广
  • 网约车司机要乘客提前200米下车遭殴打,警方介入
  • 上传150个电视剧切条短视频到网上,博主被判赔2万元
  • 支持民营企业上市融资,上海将有什么新举措?