【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),这里隐藏了一个指向obj1this指针,所以看起来参数比操作数少一个。  

规则五:  .   ::   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】



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。