240414 类和对象
一、运算符重载
1. 简介
- 不能通过连接其他符号来创建新的操作符,比如 operator@
- 重载操作符必须有一个类类型参数,反例:int operator+(int i, int j)
- .* , :: (域作用限定符), sizeof , ?: (单目选择), . (访问对象成员) 这5个运算符不能重载
.* 操作符示例
class OBJ { public:void func() {cout << "void func()" << endl;} };typedef void(OBJ::*PtrFunc)(); //定义成员函数指针int main() {//函数指针//void (*ptr)();//成员函数要加&才能取到函数指针PtrFunc fp = &OBJ::func;OBJ tmp;//定义OBJ类型对象tmp//取出后用.*调用成员函数指针(tmp.*fp)();return 0; }
【注】fp 不是 tmp 的成员,*fp 才是
在C++中,当尝试将操作重载为全局函数时,会面临无法直接访问类的私有成员的问题。以下是几种解决策略:
1. 实现访问器和修改器(Getter 和 Setter)方法
为了安全地访问和修改类的私有成员变量,可以定义一对公共的成员函数,即访问器(getter)和修改器(setter)。这些函数将提供对私有成员的间接访问,同时保持封装的完整性。
2. 使用友元函数
可以将特定的全局函数声明为类的友元。这样,该全局函数将获得访问类内部私有和保护成员的权限,从而可以在不破坏封装的前提下,实现对私有成员的直接操作。
3. 重载为成员函数(标准实践)
在C++编程实践中,通常推荐将操作重载为类的成员函数。这种方式不仅允许直接访问类的私有成员,而且维护了类的封装性,符合面向对象设计的原则。此外,成员函数还可以利用this指针访问当前对象的成员,提高了代码的清晰度和可维护性。
重载函数优先在类里寻找
原因:作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
2. 赋值重载介绍
Date d1(2021, 2, 21);
//拷贝构造:基于一个已存在的对象创建初始化一个新的对象实例
Date d2(d1);
Date d3 = d1;Date d4(2024, 4, 13);
//赋值拷贝(赋值重载):一个已经存在的对象,拷贝赋值给另一个已经存在的对象
d1 = d4;
赋值操作的返回值是左操作数
为了连续赋值,可以将赋值重载函数调整如下:
Date operator=(const Date& d) {if (this != &d) {_y = d._y;_m = d._m;_d = d._d;}return *this;
}
3. 引用返回
(1)介绍
为了减少拷贝采用引用返回:
局部变量 d
在 func()
函数返回之前就已经创建,在函数结束时就会被销毁。因此,在 main()
函数中使用 ret
引用时,实际上是在使用一个不再存在的对象的引用。
【总结】
- 返回对象是一个局部或临时对象,出了当前 func 函数作用域就析构销毁了,那么不能用引用返回,用引用返回存在风险,因为引用对象在 func 函数栈帧已经销毁了
- 出了函数作用域,返回对象还没有析构,才能用引用返回减少拷贝,提高效率
Date& func() {static Date d(2024, 4, 14);return d; }int main() {Date& ret = func();ret.Print();return 0; }
返回对象生命周期到了,会析构,传值返回;生命周期没到,不会析构,传引用返回
(2)对比
int main() {Date d1(2024, 4, 1);Date d2(d1);Date d3(1000, 10, 10);d1 = d2 = d3;return 0;
}【1】
Date& operator=(const Date& d) // 引用返回
结果:
Date(const Date& d)
~Date()
~Date()
~Date()【2】
Date operator=(const Date& d) // 传值返回
结果:
Date(const Date& d)
Date(const Date& d)
Date(const Date& d)
~Date()
~Date()
~Date()
~Date()
~Date()
4. 赋值重载特性
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
【注】内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。跟拷贝构造类似,Date 或者 MyQueue 默认生成的赋值就够用了,但是类似 Stack/List 等都需要自己实现赋值重载。
赋值运算符不能重载成全局函数。
二、日期类
1. 日期比较大小
【注】可以复用已实现的重载函数
bool Date::operator<(const Date& d) {if (_y < d._y) {return true;}else if (_y == d._y) {if (_m < d._m) {return true;}else if (_m == d._m) {return _d < d._d;}}return false;
}bool Date::operator<=(const Date& d) {return *this < d || *this == d;
}bool Date::operator>(const Date& d) {return !(*this <= d);
}bool Date::operator>=(const Date& d) {return !(*this < d);
}bool Date::operator==(const Date& d) {return _y == d._y && _m == d._m && _d == d._d;
}bool Date::operator!=(const Date& d) {return !(*this == d);
}
2. 日期加减天数
(1)数学计算简例说明
先加到天上,天满进位月,月满进位年
2024 5 21
+60
= 2024 5 81
-31【注:5月份对应天数是31天】
= 2024 6 50
-30【注:6月份对应天数是30天】
= 2024 7 20
2024 5 21
-60
= 2024 5 -39
+30【注:4月份对应天数是31天】
= 2024 4 -9
+31【注:3月份对应天数是31天】
= 2024 3 22
(2)+与+=,-与-=
Date& Date::operator+=(int days) {if (days < 0) {return *this -= -days;}_d += days;while (_d > GetMonthDay(_y, _m)) {_d -= GetMonthDay(_y, _m);++_m;if (_m > 12) {++_y;_m = 1;}}return *this;
}Date Date::operator+(int days) {// tmp出作用域销毁,用传值返回 Date tmp = *this;// 拷贝构造/*tmp._d += days;while (tmp._d > GetMonthDay(tmp._y, tmp._m)) {tmp._d -= GetMonthDay(tmp._y, tmp._m);++tmp._m;if (tmp._m > 12) {++tmp._y;tmp._m -= 12;}}*/// 复用精简版tmp += days;return tmp;
}Date& Date::operator-=(int days) {if (days < 0) {return *this += -days;}_d -= days;while (_d < 0) {--_m;if (_m < 1) {--_y;_m = 12;}_d += GetMonthDay(_y, _m);}return *this;
}Date Date::operator-(int days) {Date tmp = *this;tmp -= days;return tmp;
}
3. 日期++/--
前置和后置++/--的本质区别在于返回值不同,下面以++为例:
使用场景:
- 当需要立即使用变量的更新后的值时,应该使用前置
++
- 当需要使用变量的当前值,并且同时希望在稍后某个点该变量的值增加 1 时,可以使用后置
++
// ++d
// 自定义类型中能用前置++尽量使用前置++
Date& Date::operator++() {*this += 1;return *this;
}// d++
// 为了和前置++区分构成重载,给后置++增加了一个int形参
Date Date::operator++(int) {Date tmp = *this;*this += 1;return tmp;
}++d1 => d1.operator++()
d1++ => d1.operator++(1)
// 这里形参名不重要,接收值不影响,也不需要用到
// 该参数仅仅是为了和前置++区分构成重载
【注】区分函数重载和运算符重载
函数重载:让函数名相同、参数不同的函数存在
运算符重载:让自定义类型可以用运算符,并且控制运算符的行为,增强可读性
多个同一运算符重载可以构成函数重载,比如下方示例:
Date operator-(int days) // 日期减天数
int operator-(const Date& d) // 日期减日期
4. 日期相减
int Date::operator-(const Date& d) {Date max = *this;Date min = d;int flag = 1;if (*this < d) {max = d;min = *this;flag = -1;}int cnt = 0;while (min != max) {++min;++cnt;}return cnt * flag;
}
5. 流插入、流提取
(1)std::ostream::operator<<
C++标准库中的一个成员函数,定义在 ostream
类中,用于实现向输出流插入(插入操作符,也称为“流插入器”)各种类型的数据,如字符串、整数、浮点数等。当你使用标准输出流 std::cout
时,实际上就是在调用这个操作符。
内置类型可以直接使用,因为库里面已经写好了
自动识别类型,本质是因为这些流插入重载构成函数重载
void Date::operator<<(ostream& out) {out << _y << " 年 " << _m << " 月 " << _d << " 日" << endl;
}
运算符重载中,参数顺序和操作数顺序是一样的
operator<< 想重载成成员函数可以,但使用起来不符合正常逻辑,建议重载成全局函数
Date ostream 顺序(不符合习惯)
d1.operator<<(cout); d1 << cout;
ostream Date 顺序
cout << d1;
写成成员函数能让 ostream 变为第一位吗?
不行,因为隐含的 Date* this 已经把第一个参数给占用了
为了解决链式流插入的问题,返回 ostream& 类型的 out// 链式流插入 ostream& operator<<(ostream& out, const Date& d) {out << d._y << " 年 " << d._m << " 月 " << d._d << " 日" << endl;return out; }
友元:
class Date {// 友元函数声明(针对私有成员变量访问)friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);
(2)std::ios_base::sync_with_stdio
std::ios_base::sync_with_stdio
主要用于控制C++标准I/O流(如 cin
, cout
, cerr
和 clog
)是否应该与C语言的标准I/O库(如 stdin
, stdout
, stderr
)同步。默认情况下,C++标准I/O流是与C标准I/O库同步的,这意味着它们共享底层文件描述符。
这个函数可以接受一个布尔值作为参数:
- 如果参数为
true
(这是默认行为),那么C++标准I/O流将与C标准I/O流同步。 - 如果参数为
false
,则会取消这种同步。
在某些情况下,取消同步是有益的。例如,在程序需要高性能输入输出时,取消同步可能会减少一些不必要的锁操作,从而提高性能。这是因为C++ I/O系统和C I/O系统在处理缓冲区时可能有不同的内部锁定机制,当它们同步时,为了保证线程安全,可能会有额外的开销。
在实际编程中,特别是在编写一些需要高效读写的竞赛代码或高性能应用时,开发者可能会选择调用
std::ios_base::sync_with_stdio(false)
来禁用同步以提升性能。此外,通常还会配合
std::cin.tie(nullptr);
使用,这会解除cin
和cout
之间的绑定,因为在默认情况下,cin
和cout
是绑定在一起的,即当cout
进行输出后,cin
在没有输入前会阻塞等待。
6. const 成员函数
【问题场景】
【解决办法】
const 修饰 *this,本质上改变 this 的类型
const Date* const this
const 对象可以调用Print( ) => 权限的平移
非 const 对象亦可调用Print( ) => 权限的缩小
对于常量
const d1
和非常量d2
,若operator<
未声明为const
成员函数,则表达式d1 < d2
将引发编译错误,而d2 < d1
可正常执行。编译错误发生在d1 < d2
是因为常量对象不能调用可能修改其状态的非const
成员函数,而d2 < d1
没有这个问题,因为d2
不是常量。
不修改成员的成员函数建议声明为 const
7. 取地址操作符重载*
class B {
public:// 我们不写编译器会自己实现,写了编译器就不会自己实现// 实践中一般不会自己实现,除非不想别人取到该类型对象的真实地址B* operator&() {cout << "B* operator&()" << endl;return this;}const B* operator&() const{cout << "const B* operator&() const" << endl;return this;}
private:int _B1 = 1;int _B2 = 2;int _B3 = 3;
};int main() {B b1;const B b2;cout << &b1 << endl;cout << &b2 << endl;return 0;
}