💯前言
回顾上篇文章👉【C++】类的默认成员函数:深入剖析与应用(上)中对构造函数、拷贝构造函数和析构函数的讨论,强调这些默认成员函数在类的创建、初始化和销毁过程中的重要性。
✍引出本篇将继续探讨剩余的重要默认成员函数,以更全面地理解类的内部机制。⭐深入理解和掌握这些默认成员函数,对于每一位 C++ 开发者来说都至关重要。
💯赋值运算符重载
⭐运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
#include <iostream>// 1. 以一个简单的复数类为例,展示运算符重载// 复数类
class Complex {
public:// 实部和虚部double real;double imag;// 构造函数Complex(double r = 0, double i = 0) : real(r), imag(i) {}// 2. 重载加法运算符 +Complex operator+(const Complex& other) const {// 返回一个新的复数,实部为两个复数实部之和,虚部为两个复数虚部之和return Complex(real + other.real, imag + other.imag);}// 3. 重载输出流运算符 <<,这里需要声明为友元函数,因为它的第一个参数是流对象,不是类的成员friend std::ostream& operator<<(std::ostream& os, const Complex& c);
};// 4. 定义输出流运算符 << 的函数体
std::ostream& operator<<(std::ostream& os, const Complex& c) {os << c.real;if (c.imag >= 0) {os << " + ";} else {os << " - ";}os << std::abs(c.imag) << "i";return os;
}int main() {Complex c1(3, 4);Complex c2(1, -2);// 5. 使用重载的加法运算符Complex sum = c1 + c2;std::cout << "c1 + c2 = " << sum << std::endl;return 0;
}
❗注意 :
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
🏆代码解释:
#include <iostream>// 1. 以一个简单的自定义类为例 class MyClass { public:int value;// 构造函数MyClass(int val = 0) : value(val) {}// 2. 重载加法运算符 +MyClass operator+(const MyClass& other) const {return MyClass(value + other.value);} };int main() {MyClass obj1(5);MyClass obj2(3);// 3. 使用重载的加法运算符MyClass result = obj1 + obj2;std::cout << "Result value: " << result.value << std::endl;return 0; }
👆在上述代码中:
规则一:不能通过连接其他符号来创建新的操作符
- 代码中只对已有的加法运算符
+
进行了重载,不能像规则中提到的那样创建一个operator@
这样的新运算符。规则二:重载操作符必须有一个类类型参数
- 在重载加法运算符的函数
MyClass operator+(const MyClass& other) const
中,有一个参数是类类型const MyClass& other
,满足该规则。规则三:用于内置类型的运算符,其含义不能改变
- 代码中没有尝试改变内置整型的加法运算符含义,比如不能让内置的
int + int
做其他奇怪的操作。规则四:作为类成员函数重载时,其形参看起来比操作数数目少 1,因为成员函数的第一个参数为隐藏的 this
- 当使用
obj1 + obj2
时,实际上是调用obj1.operator+(obj2)
,这里隐藏了一个指向obj1
的this
指针,所以看起来参数比操作数少一个。规则五: . :: sizeof ?: . 这五个运算符不能重载*
- 代码中没有尝试对这五个运算符进行重载,符合规则。
综上所述,通过这段代码展示了运算符重载的一些规则的实际应用情况
⭐赋值运算符重载
1.赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
#include <iostream>template <typename T> class MyClass { public:T value;MyClass(T val = 0) : value(val) {}// 重载赋值运算符MyClass& operator=(const MyClass& other) {if (this!= &other) {value = other.value;}return *this;} };int main() {MyClass<int> obj1(5);MyClass<int> obj2(3);// 测试赋值运算符obj1 = obj2;std::cout << "obj1.value: " << obj1.value << std::endl;// 测试连续赋值MyClass<int> obj3(7);obj3 = obj2 = obj1;std::cout << "obj2.value: " << obj2.value << std::endl;std::cout << "obj3.value: " << obj3.value << std::endl;return 0; }
👆在上述代码中:
MyClass& operator=(const MyClass& other)
重载了赋值运算符,参数类型为const T&
,传递引用避免了不必要的对象拷贝,提高了传参效率。返回值类型为
MyClass&
,返回引用可以提高返回的效率,使得支持连续赋值成为可能。在函数内部首先检测是否自己给自己赋值,即通过
if (this!= &other)
进行判断,如果是自己给自己赋值则直接返回,避免不必要的操作。最后返回
*this
,满足连续赋值的含义,例如obj3 = obj2 = obj1
,先计算obj2 = obj1
,然后obj3
再赋值为这个结果。
2.赋值运算符只能重载成类的成员函数不能重载成全局函数
🏆代码解释:
#include <iostream>class MyClass { public:int value;MyClass(int val = 0) : value(val) {}// 成员函数形式重载赋值运算符MyClass& operator=(const MyClass& other) {if (this!= &other) {value = other.value;}return *this;} };int main() {MyClass obj1(5);MyClass obj2(3);// 使用成员函数重载的赋值运算符obj1 = obj2;std::cout << "obj1.value after assignment: " << obj1.value << std::endl;// 尝试用全局函数重载赋值运算符(这是错误的做法,编译会报错)// MyClass operator=(MyClass lhs, const MyClass& rhs) {// lhs.value = rhs.value;// return lhs;// }return 0; }
👆在上述代码中:
在
MyClass
类内部,以成员函数的形式正确地重载了赋值运算符operator=
。在main
函数中,可以成功地使用这个重载的赋值运算符进行对象赋值操作。如果尝试像注释部分那样以全局函数的形式重载赋值运算符,编译器会报错。这是因为赋值运算符的重载有特殊的规则,它通常只能作为类的成员函数进行重载,以确保正确地处理对象的内部状态和资源管理等问题。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
❗注意 :
内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
🏆代码解释:
#include <iostream>class AnotherClass { public:int data;AnotherClass(int val = 0) : data(val) {}AnotherClass& operator=(const AnotherClass& other) {if (this!= &other) {data = other.data;}return *this;} };class MyClass { public:int num;AnotherClass obj;MyClass(int n = 0, int val = 0) : num(n), obj(val) {} };int main() {MyClass obj1(5, 10);MyClass obj2(3, 15);// 编译器生成的默认赋值运算符被调用,逐字节拷贝obj1 = obj2;std::cout << "obj1.num: " << obj1.num << std::endl;std::cout << "obj1.obj.data: " << obj1.obj.data << std::endl;return 0; }
👆在上述代码中:
MyClass
类中有一个内置类型成员变量num
和一个自定义类型成员变量obj
(属于AnotherClass
类)。- 当没有显式实现
MyClass
的赋值运算符重载时,编译器会生成一个默认的赋值运算符重载,以值的方式逐字节拷贝。对于内置类型成员变量num
,直接进行赋值操作。对于自定义类型成员变量obj
,会调用AnotherClass
类中的赋值运算符重载来完成赋值。- 在
main
函数中,通过obj1 = obj2
演示了这个过程,可以看到赋值后的结果。
⭐前置++和后置++重载
#include <iostream>class Counter {
private:int count;
public:Counter(int c = 0) : count(c) {}// 前置++重载Counter& operator++() {++count;return *this;}// 后置++重载Counter operator++(int) {Counter temp(*this);++count;return temp;}int getCount() const {return count;}
};int main() {Counter c(5);std::cout << "Initial count: " << c.getCount() << std::endl;// 前置++++c;std::cout << "After pre-increment: " << c.getCount() << std::endl;// 后置++Counter c2 = c++;std::cout << "After post-increment (original object): " << c.getCount() << std::endl;std::cout << "Value of new object after post-increment: " << c2.getCount() << std::endl;return 0;
}
👆在上述代码中:
- 定义了一个
Counter
类,其中包含一个私有成员变量count
。- 前置
++
运算符重载函数operator++()
直接增加count
的值,并返回修改后的对象引用,实现了先增加再返回的操作。- 后置
++
运算符重载函数operator++(int)
创建了一个当前对象的副本,然后增加count
的值,最后返回副本,实现了先返回再增加的操作。注意这里的参数int
只是一个占位符,用于区分前置和后置运算符重载。- 在
main
函数中,演示了前置++
和后置++
的不同行为
💯const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
我们来看以下代码:
#include <iostream>class Date {
public:Date(int year, int month, int day) : _year(year), _month(month), _day(day) {}// 非 const 成员函数void Print() {std::cout << "Print()" << std::endl;std::cout << "year:" << _year << std::endl;std::cout << "month:" << _month << std::endl;std::cout << "day:" << _day << std::endl << std::endl;}// const 成员函数void Print() const {std::cout << "Print()const" << std::endl;std::cout << "year:" << _year << std::endl;std::cout << "month:" << _month << std::endl;std::cout << "day:" << _day << std::endl << std::endl;}private:int _year; // 年int _month; // 月int _day; // 日
};void Test() {Date d1(2022, 1, 13);d1.Print();const Date d2(2022, 1, 13);d2.Print();
}int main() {Test();return 0;
}
分析如下:
const Date d2(2022, 1, 13);
是一个 const 对象,尝试调用非 const 成员函数Print()
会导致编译错误,因为 const 对象只能调用 const 成员函数。Date d1(2022, 1, 13);
是非 const 对象,可以调用非 const 成员函数Print()
,也可以调用 const 成员函数Print() const
。- 在 const 成员函数
Print() const
中,如果尝试调用非 const 成员函数,会导致编译错误,因为 const 成员函数不能调用非 const 成员函数。- 在非 const 成员函数
Print()
中,可以调用 const 成员函数Print() const
,因为非 const 成员函数可以调用 const 成员函数。
💯取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
#include <iostream>class Date {
public:Date* operator&() {return this;}const Date* operator&() const {return this;}private:int _year; // 年int _month; // 月int _day; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!
💯总结
C++ 类的默认成员函数有
构造函数、拷贝构造函数、析构函数👉【C++】类的默认成员函数:深入剖析与应用(上)
赋值运算符重载、const 成员函数及取地址操作符重载等。它们分别在对象创建、复制、销毁、赋值、只读访问及特定地址获取等场景发挥重要作用。
掌握这些对编写高质量 C++ 代码至关重要。🌟🌟🌟
以后我将深入研究继承、多态、模板等特性,并将默认成员函数与这些特性结合,以解决更复杂编程问题!欢迎关注我👉【A Charmer】