【C++】类的默认成员函数:深入剖析与应用(下)
CSDN 2024-10-22 12:05:03 阅读 60
💯前言
回顾上篇文章👉【C++】类的默认成员函数:深入剖析与应用(上)中对构造函数、拷贝构造函数和析构函数的讨论,强调这些默认成员函数在类的创建、初始化和销毁过程中的重要性。
✍引出本篇将继续探讨剩余的重要默认成员函数,以更全面地理解类的内部机制。
⭐深入理解和掌握这些默认成员函数,对于每一位 C++ 开发者来说都至关重要。
💯赋值运算符重载
⭐运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
<code>#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;
}
👆在上述代码中:
<code>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;
}
👆在上述代码中:
在<code>MyClass类内部,以成员函数的形式正确地重载了赋值运算符
operator=
。在main
函数中,可以成功地使用这个重载的赋值运算符进行对象赋值操作。
如果尝试像注释部分那样以全局函数的形式重载赋值运算符,编译器会报错。这是因为赋值运算符的重载有特殊的规则,它通常只能作为类的成员函数进行重载,以确保正确地处理对象的内部状态和资源管理等问题。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
❗注意 :
内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
🏆代码解释:
<code>#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;
}
👆在上述代码中:
<code>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;
}
👆在上述代码中:
定义了一个<code>Counter类,其中包含一个私有成员变量
count
。前置++
运算符重载函数operator++()
直接增加count
的值,并返回修改后的对象引用,实现了先增加再返回的操作。后置++
运算符重载函数operator++(int)
创建了一个当前对象的副本,然后增加count
的值,最后返回副本,实现了先返回再增加的操作。注意这里的参数int
只是一个占位符,用于区分前置和后置运算符重载。在main
函数中,演示了前置++
和后置++
的不同行为
💯const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
我们来看以下代码:
<code>#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;
}
分析如下:
<code>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】
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。