运算符重载
C++的 运算符重载(Operator Overloading) 是一个很有趣又实用的特性,让你可以为自
什么是运算符重载
运算符重载允许你“重新定义”已有运算符(如 +、-、== 等),使它们能够用于你自己的类
可以重载的运算符
二元运算符重载的方式
在 C++ 中,二元运算符(如 +, -, *, /, ==, != 等)可以有多种方式重载。主要分为两类:
成员函数方式
格式:
class ClassName {
public:
ClassName operator+(const ClassName& rhs) const;
};
示例 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);
};
示例 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
和 times3
。times2
的乘法因子 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
函数的重载,它属于函数形式的 new
。operator 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
类的使用更加直观和方便。
提醒
不要滥用运算符重载,比如不该重载 ++ 来表示“向后跳舞”这类违反直觉的行为。