【C 篇】C 类与对象深度解析(三)
接上篇: 【C 篇】C 类与对象深度解析(二):类的默认成员函数详解
在上一篇文章中,我们讨论了C 类的默认成员函数,包括构造函数、析构函数和拷贝构造函数。本篇我们将继续探索剩余的默认成员函数,这些是C 面向对象编程中不可或缺的高级特性。掌握这些功能将帮助您更加灵活地设计和实现C 类。❤️
4. 运算符重载基本概念
4.1 运算符重载的基本概念
运算符重载允许我们为类对象自定义运算符的行为,这样当我们对类对象使用这些运算符时,它们会按照我们定义的方式执行。如果没有定义对应的运算符重载,编译器将会报错,因为它不知道如何处理这些运算符。
- 运算符重载的定义:运算符重载是一个特殊的函数,名字是
operator
加上要重载的运算符。 - 参数数量:重载函数的参数数量取决于运算符的类型。一元运算符有一个参数,二元运算符有两个参数。
示例:重载==
运算符
#include<iostream>
using namespace std;
class Number {
public:
Number(int value = 0) : _value(value) {}
// 重载==运算符用于比较两个Number对象是否相等
bool operator==(const Number& n) const
{
return _value == n._value;
}
private:
int _value;
};
int main()
{
Number n1(10);
Number n2(10);
if (n1 == n2) {
cout << "两个数相等。" << endl;
} else {
cout << "两个数不相等。" << endl;
}
return 0;
}
解释:
operator==
:这个重载允许我们直接使用==
来比较两个Number
对象是否相等,而不需要手动检查它们的内部值。
4.2 重载运算符的规则
- 函数的名字:重载的函数名称必须是
operator
加上运算符,例如operator
、operator==
。 - 参数和返回类型:重载的运算符函数需要根据需要设置参数和返回类型。对于二元运算符,左侧对象传给第一个参数,右侧对象传给第二个参数。
示例:重载
运算符
#include<iostream>
using namespace std;
class Number {
public:
Number(int value = 0) : _value(value) {}
// 重载 运算符,用于两个Number对象相加
Number operator (const Number& n) const
{
return Number(_value n._value);
}
void Print() const
{
cout << "Value: " << _value << endl;
}
private:
int _value;
};
int main()
{
Number n1(10);
Number n2(20);
Number n3 = n1 n2; // 使用重载的 运算符
n3.Print(); // 输出: Value: 30
return 0;
}
解释:
operator
:这个运算符重载允许我们使用Number
对象,并返回一个新的Number
对象。
4.3 成员函数重载运算符
如上面的例子,当运算符重载定义为类的成员函数时,第一个运算对象会隐式地传递给
this
指针,因此成员函数的参数数量比操作数少一个。
示例:重载-
运算符
#include<iostream>
using namespace std;
class Number {
public:
Number(int value = 0) : _value(value) {}
// 重载-运算符,用于两个Number对象相减
Number operator-(const Number& n) const
{
return Number(_value - n._value);
}
void Print() const
{
cout << "Value: " << _value << endl;
}
private:
int _value;
};
int main()
{
Number n1(20);
Number n2(10);
Number n3 = n1 - n2; // 使用重载的-运算符
n3.Print(); // 输出: Value: 10
return 0;
}
解释:
operator-
:这个重载允许我们使用-
运算符来减去两个Number
对象的值,并返回一个新的Number
对象。
4.4 运算符重载的优先级与结合性
虽然我们可以改变运算符的行为,但其优先级和结合性与内置类型运算符保持一致。这意味着我们不能通过重载运算符来改变它们的运算顺序。
示例:重载*
运算符
#include<iostream>
using namespace std;
class Number {
public:
Number(int value = 0) : _value(value) {}
// 重载*运算符,用于两个Number对象相乘
Number operator*(const Number& n) const
{
return Number(_value * n._value);
}
void Print() const
{
cout << "Value: " << _value << endl;
}
private:
int _value;
};
int main()
{
Number n1(5);
Number n2(4);
Number n3 = n1 * n2; // 使用重载的*运算符
n3.Print(); // 输出: Value: 20
return 0;
}
解释:
operator*
:这个重载允许我们使用*
运算符来相乘两个Number
对象的值,并返回一个新的Number
对象,其优先级还是高于重载后的
4.5 运算符重载中的限制与特殊情况
4.5.1 不能创建新的操作符
在C 中,虽然可以重载大多数运算符,但不能创建新的操作符。也就是说,我们不能使用C 语法中没有的符号来创建新的运算符。例如,
operator@
是非法的,因为@
符号不是C 中的有效运算符。
解释:
- 只能重载C 已有的运算符,如
-
,*
,/
,==
等。不能创建诸如operator@
这样的运算符,因为@
不属于C 的运算符集合。
示例:尝试创建一个新的操作符(会报错)
代码语言:javascript复制#include<iostream>
using namespace std;
class Number {
public:
Number(int value = 0) : _value(value) {}
// 错误:不能定义新的运算符
Number operator@(const Number& n) const
{
return Number(_value n._value);
}
private:
int _value;
};
int main()
{
Number n1(10);
Number n2(20);
// n1 @ n2; // 错误:@ 不是一个合法的C 运算符
return 0;
}
结果:
- 编译器会报错,因为
@
不是C 中的有效运算符,不能通过operator@
进行重载。
4.5.2 无法重载的运算符
在C 中,有五个运算符是不能重载的,这些运算符的行为在语言中是固定的,不能改变。
这些运算符包括:
.
(成员访问运算符).*
(成员指针访问运算符)见以下补充::
(作用域解析运算符)sizeof
(大小计算运算符)?:
(三元条件运算符)
解释:
- 这些运算符的行为在C 中是固定的,无法通过重载改变它们的语义或使用方式。
示例:尝试重载不能重载的运算符(会报错)
代码语言:javascript复制#include<iostream>
using namespace std;
class Number {
public:
Number(int value = 0) : _value(value) {}
// 错误:不能重载sizeof运算符
int operator sizeof() const
{
return _value;
}
private:
int _value;
};
int main()
{
Number n1(10);
// int size = sizeof(n1); // 错误:无法重载sizeof运算符
return 0;
}
结果:
- 编译器会报错,因为
sizeof
是一个固定的运算符,无法通过重载改变其行为。
4.5.3 前置和后置递增运算符的重载
在C 中,递增运算符
可以有两种形式:前置递增和后置递增。它们的功能类似,但实现方式不同。
- 前置递增:先递增,然后返回递增后的值。
- 后置递增:先返回当前值,然后递增。
为了区分前置和后置递增运算符,C 规定在重载后置递增运算符时,必须增加一个int
参数。这只是一个区分符,并没有实际用途。
示例:重载前置和后置递增运算符
前置直接操作对象,传引用返回,而后置返回副本,用传值返回
代码语言:javascript复制#include<iostream>
using namespace std;
class Number {
public:
Number(int value = 0) : _value(value) {}
// 重载前置
Number& operator ()
{
_value;
return *this;
}
// 重载后置
Number operator (int)
{
Number tmp = *this; // 保存当前值
_value; // 递增
return tmp; // 返回原始值
}
void Print() const
{
cout << "Value: " << _value << endl;
}
private:
int _value;
};
int main()
{
Number n1(5);
n1; // 前置
n1.Print(); // 输出: Value: 6
n1 ; // 后置
n1.Print(); // 输出: Value: 7
return 0;
}
解释:
operator ()
:这是前置递增的实现。先递增,然后返回递增后的对象自身。operator (int)
:这是后置递增的实现。先保存当前对象的副本,然后递增,并返回副本。
结果:
- 前置递增直接修改并返回对象自身,而后置递增返回递增前的副本,之后再进行递增。
这里我们直接使用的普通 类型来实现 1操作,在之后实现了 =运算符重载后可以实现复用,这在最后类和对象的实践篇:日期类的实现会讲到
补充: .*
(成员指针访问运算符)
介绍:
.*
是 C 中的成员指针访问运算符,用于通过对象访问指向该对象成员的指针。这个运算符主要用在需要通过指针访问对象的成员函数或成员变量的场景中。
在 C 中,.*
和 ->*
运算符提供了类似于.
和 ->
的功能,但用于成员指针操作。因为.*
这种运算符在使用上非常特殊,因此不能进行重载。
示例:使用 .*
运算符访问成员函数
假设我们有一个类 MyClass
,其中包含一个成员函数 Func
,我们可以通过成员指针访问并调用这个函数。
#include<iostream>
using namespace std;
class MyClass {
public:
void Func() {
cout << "MyClass::Func() 被调用了" << endl;
}
};
int main() {
MyClass obj; // 创建类对象
void (MyClass::*pf)() = &MyClass::Func; // 定义成员函数指针,并指向MyClass的Func函数
// 通过对象和成员函数指针调用函数
(obj.*pf)(); // 使用 .* 运算符调用成员函数
return 0;
}
解释:
void (MyClass::*pf)() = &MyClass::Func;
:这行代码定义了一个指向MyClass
成员函数的指针pf
,并将其初始化为指向Func
函数的地址。(obj.*pf)();
:使用.*
运算符,通过obj
对象调用pf
所指向的成员函数Func
。
示例:使用 .*
运算符访问成员变量
同样的方式可以用于访问成员变量,通过成员指针操作符,我们可以通过一个对象来访问其成员变量。
代码语言:javascript复制#include<iostream>
using namespace std;
class MyClass {
public:
int value; // 成员变量
};
int main() {
MyClass obj; // 创建类对象
obj.value = 42; // 直接访问成员变量
int MyClass::*pValue = &MyClass::value; // 定义成员变量指针,并指向MyClass的value成员
// 通过对象和成员变量指针访问成员变量
cout << "Value: " << obj.*pValue << endl; // 使用 .* 运算符访问成员变量
return 0;
}
解释:
int MyClass::*pValue = &MyClass::value;
:这行代码定义了一个指向MyClass
成员变量的指针pValue
,并将其初始化为指向value
变量的地址。obj.*pValue
:使用.*
运算符,通过obj
对象访问pValue
所指向的成员变量value
。
不能重载 .*
运算符
由于 .*
运算符的特殊性,它不能被重载。.*
的行为在 C 语言中已经固定,主要用于通过对象访问其成员指针所指向的成员。
示例:尝试重载 .*
(会报错)
#include<iostream>
using namespace std;
class MyClass {
public:
int value;
// 错误:不能重载 .* 运算符
int operator.*() const {
return value;
}
};
int main() {
MyClass obj;
obj.value = 42;
// 编译错误:无法重载 .* 运算符
// cout << obj.* << endl;
return 0;
}
结果:
- 尝试重载
.*
运算符会导致编译错误,因为这个运算符在 C 中是固定的,不能改变其行为。
5 赋值运算符重载
赋值运算符重载是一个特殊的运算符重载,用于将一个对象的状态复制到另一个已经存在的对象中。需要注意的是,赋值运算符重载与拷贝构造函数是不同的,拷贝构造用于初始化一个新对象,而赋值运算符则用于给已经存在的对象赋值。
5.1 赋值运算符重载必须定义为成员函数
赋值运算符重载是C 的一个特殊运算符重载,必须作为类的成员函数来定义。这是因为赋值运算符总是需要操作当前对象(
this
指针),因此它不能作为全局函数来实现。
示例:定义赋值运算符重载
代码语言:javascript复制#include<iostream>
using namespace std;
class MyClass {
public:
MyClass(int value = 0) : _value(value) {}
// 赋值运算符重载,参数是const类型的引用
MyClass& operator=(const MyClass& other) {
if (this != &other) { // 防止自我赋值
_value = other._value;
}
return *this; // 返回当前对象的引用,以支持链式赋值
}
void Print() const {
cout << "Value: " << _value << endl;
}
private:
int _value;
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = obj1; // 调用赋值运算符重载
obj2.Print(); // 输出: Value: 10
return 0;
}
解释:
operator=
:这是赋值运算符的重载函数。this
指针指向当前对象,other
是被赋值的对象。if (this != &other)
:检查当前对象是否与传入对象是同一个对象,如果是同一个对象,则跳过赋值操作,以避免自我赋值引起的问题。return *this;
:返回当前对象的引用,这允许连续的赋值操作,例如a = b = c;
。
5.2 有返回值,建议写成当前类类型的引用
赋值运算符重载函数通常返回当前对象的引用,这样可以安全支持链式赋值操作,即多个对象之间连续赋值的语句。
示例:支持链式赋值
代码语言:javascript复制#include<iostream>
using namespace std;
class MyClass {
public:
MyClass(int value = 0) : _value(value) {}
// 赋值运算符重载,返回当前对象的引用
MyClass& operator=(const MyClass& other) {
if (this != &other) {
_value = other._value;
}
return *this;
}
void Print() const {
cout << "Value: " << _value << endl;
}
private:
int _value;
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
MyClass obj3(30);
obj1 = obj2 = obj3; // 链式赋值
obj1.Print(); // 输出: Value: 30
obj2.Print(); // 输出: Value: 30
return 0;
}
解释:
- 在链式赋值中,
obj2 = obj3
会首先执行,operator=
返回obj2
的引用,然后obj1 = obj2
执行,这样obj1
最终也得到了obj3
的值。
5.3 编译器自动生成的默认赋值运算符
如果我们没有显式定义赋值运算符重载,编译器会自动生成一个默认的赋值运算符。
这个默认的赋值运算符执行的是浅拷贝操作:对于内置类型成员变量,逐个字节地复制值;对于自定义类型成员变量,则调用它们的赋值运算符重载。
示例:使用编译器生成的默认赋值运算符
代码语言:javascript复制#include<iostream>
using namespace std;
class MyClass {
public:
MyClass(int value = 0) : _value(value) {}
// 未显式定义赋值运算符重载,编译器会自动生成
void Print() const {
cout << "Value: " << _value << endl;
}
private:
int _value;
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = obj1; // 使用编译器生成的默认赋值运算符
obj2.Print(); // 输出: Value: 10
return 0;
}
解释:
- 在这个例子中,编译器生成了一个默认的赋值运算符,它对内置类型的成员变量执行浅拷贝操作。
5.4 显式实现赋值运算符重载的必要性
在一些情况下,例如类中包含指针成员或其他动态资源,浅拷贝可能会导致问题。这时,我们需要显式实现赋值运算符重载来进行深拷贝,以确保对象的独立性。
示例:显式实现赋值运算符进行深拷贝
代码语言:javascript复制#include<iostream>
using namespace std;
class MyClass {
public:
MyClass(int value = 0) : _value(new int(value)) {}
// 拷贝构造函数
MyClass(const MyClass& other) {
_value = new int(*other._value);
}
// 赋值运算符重载
MyClass& operator=(const MyClass& other) {
if (this != &other) {
delete _value; // 删除旧的动态内存
_value = new int(*other._value); // 分配新的动态内存并复制值
}
return *this;
}
~MyClass() {
delete _value; // 析构函数中释放动态内存
}
void Print() const {
cout << "Value: " << *_value << endl;
}
private:
int* _value; // 指针类型成员变量
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = obj1; // 使用自定义的赋值运算符进行深拷贝
obj1.Print(); // 输出: Value: 10
obj2.Print(); // 输出: Value: 10
return 0;
}
解释:
- 在这个例子中,
MyClass
类中包含一个指针成员变量_value
,我们需要自定义赋值运算符以确保进行深拷贝,即在赋值时为_value
分配新的内存,并将值复制到新分配的内存中。
5.5 赋值运算符与析构函数的关系
如果一个类显式定义了析构函数来释放动态资源,那么它通常也需要显式定义赋值运算符重载,以避免浅拷贝带来的资源管理问题。
示例:显式实现析构函数和赋值运算符重载
代码语言:javascript复制#include<iostream>
using namespace std;
class MyClass {
public:
MyClass(int value = 0) : _value(new int(value)) {}
// 拷贝构造函数
MyClass(const MyClass& other) {
_value = new int(*other._value);
}
// 赋值运算符重载
MyClass& operator=(const MyClass& other) {
if (this != &other) {
delete _value;
_value = new int(*other._value);
}
return *this;
}
// 析构函数
~MyClass() {
delete _value;
}
void Print() const {
cout << "Value: " << *_value << endl;
}
private:
int* _value;
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = obj1; // 使用自定义的赋值运算符进行深拷贝
obj1.Print(); // 输出: Value: 10
obj2.Print(); // 输出: Value: 10
return 0;
}
解释:
MyClass
包含一个指针成员变量_value
,我们通过显式实现赋值运算符和析构函数来管理动态内存,确保不会因为浅拷贝导致资源泄漏或多次释放同一块内存。
总结
赋值运算符重载在管理动态资源、确保对象独立性以及支持链式赋值时非常有用。通过理解赋值运算符的特性和如何正确实现它,我们可以编写更健壮的C 程序,避免浅拷贝带来的问题。
6. 取地址运算符重载
在C 中,取地址运算符&
用于获取对象的内存地址。在大多数情况下,编译器自动生成的取地址运算符重载已经足够使用。然而,在某些特殊场景下,我们可能希望控制或限制对象地址的获取方式,这时候我们就可以手动重载取地址运算符。
6.1 const成员函数
const成员函数是指用
const
修饰的成员函数。它主要用于确保成员函数不会修改类的成员变量,从而保证函数的只读特性。
- 用法:将
const
修饰符放在成员函数的参数列表之后。 - 效果:
const
实际修饰的是成员函数中隐含的this
指针,表示在该成员函数中不能对类的任何成员进行修改。
示例代码:const成员函数
代码语言:javascript复制#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
// const成员函数,表示不会修改类成员变量
void Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
// 非const对象可以调用const成员函数
Date d1(2024, 7, 5);
d1.Print();
// const对象也可以调用const成员函数
const Date d2(2024, 8, 5);
d2.Print();
return 0;
}
解释:
void Print() const
:const
修饰了Print
函数,表示它不会修改Date
类的成员变量。this
指针的类型在这个函数中变为const Date* const
,意味着它指向的对象及指针本身都不能被修改。- 权限的缩小:
const
对象只能调用const
成员函数,而非const
对象可以调用任意成员函数,这体现了一种权限的缩小。
6.2 取地址运算符重载
取地址运算符
&
通常用于获取对象的地址。通过重载该运算符,可以自定义获取对象地址的方式,甚至可以禁止获取地址或返回一个伪造的地址。
普通取地址运算符重载
普通取地址运算符用于非const
对象,重载后可以控制返回对象的地址。
示例代码:普通取地址运算符重载
代码语言:javascript复制#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
// 重载普通取地址运算符
Date* operator&() {
// return this; // 返回对象的真实地址
return nullptr; // 返回空指针,伪装地址
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2024, 7, 5);
Date* p1 = &d1; // 使用重载的取地址运算符
if (p1 == nullptr) {
cout << "未获取到真实地址" << endl;
} else {
cout << "对象的地址为: " << p1 << endl;
}
return 0;
}
解释:
Date* operator&()
:这是普通的取地址运算符重载。可以根据需求决定是否返回对象的真实地址,也可以返回nullptr
或其他伪造地址,以达到某些特定需求(如禁止获取对象地址)的目的。
const取地址运算符重载
const
取地址运算符用于const
对象,重载后可以控制如何返回const
对象的地址。
示例代码:const取地址运算符重载
代码语言:javascript复制#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
// 重载const取地址运算符
const Date* operator&() const {
// return this; // 返回对象的真实地址
return nullptr; // 返回空指针,伪装地址
}
private:
int _year;
int _month;
int _day;
};
int main() {
const Date d1(2024, 8, 5);
const Date* p1 = &d1; // 使用重载的取地址运算符
if (p1 == nullptr) {
cout << "未获取到真实地址" << endl;
} else {
cout << "对象的地址为: " << p1 << endl;
}
return 0;
}
解释:
const Date* operator&() const
:这是const
取地址运算符重载。它同样可以控制是否返回const
对象的真实地址或者伪装地址。
总结
- 默认行为:在大多数情况下,编译器自动生成的取地址运算符已经足够使用,不需要手动重载。
- 特殊需求:在一些特殊场景(如禁止获取对象地址)下,可以通过重载取地址运算符来自定义行为。
- const修饰:通过
const
修饰符可以控制成员函数的只读特性,确保在函数中不修改类成员变量。同时,const
取地址运算符重载可以用于const
对象,确保其地址获取方式受到控制。
写在最后
运算符重载使C 类对象能像基本数据类型一样操作,赋予类更直观的行为。通过重载
、
-
等运算符,我们可以实现对象间的运算和比较。赋值运算符尤其重要,确保对象在涉及动态资源时安全地复制。const
成员函数则提供了数据保护,避免意外修改。总的来说,运算符重载让代码更加简洁优雅,增强了程序的灵活性。