【C++修行之道】类和对象(四)运算符重载

走在努力路上的自己 2024-07-17 08:35:03 阅读 78

目录

一、 运算符重载

函数重载和运算符重载有什么关系?

二、.*运算符的作用

三、运算符重载的正常使用

四、重载成成员函数

五、赋值运算符重载

1.赋值运算符重载格式

传值返回和引用返回

有没有办法不生成拷贝?

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

六、前置++和后置++重载 


一、 运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号

函数原型:返回值类型 operator操作符(参数列表)。

注意:

不能通过连接其他符号来创建新的操作符:比如operator@ 重载操作符必须有一个类类型参数 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

函数重载和运算符重载有什么关系?

他们之间各论各的,没有关系

运算符重载:让自定义类型可以使用运算符,并且控制运算符的行为,增强可读性

函数重载:可以让函数名相同,参数不同的函数存在。

多个同一运算符的重载可以构成函数重载

 

二、.*运算符的作用

<code>class OB

{

public:

void func()

{

cout << "void func()" << endl;

}

};

typedef void(OB::* Ptrfunc)();// 成员函数指针类型

int main()

{

// 函数指针

void (*ptr)();

Ptrfunc fp = &OB::func;// 定义成员函数指针p指向函数func

// 成员函数规定要加&才能取到函数指针

OB temp;// 定义ob类对象temp

(temp.*fp)();// 调用成员函数

return 0;

}

<code>typedef void(OB::* Ptrfunc)(); // 成员函数指针类型

使用 typedef 定义了一个名为 Ptrfunc 的类型,这个类型是指向OB类中无参数、无返回值的成员函数的指针类型。OB::*的含义,它表示这是一个指向OB类成员函数的指针 Ptrfunc

Ptrfunc fp = &OB::func; // 定义成员函数指针fp指向函数func

在C++中,成员函数与普通函数在内存中的表示和存储方式有所不同。成员函数不仅包含函数的代码,还隐含地包含了一个指向类对象的this指针,这使得成员函数能够访问和修改对象的状态。在语法上,&类名::成员函数名是用来获取成员函数地址的标准方式如果不使用&运算符,编译器可能会将OB::func解析为对成员函数的调用运算符在这里的作用是明确告诉编译器:“我要的是这个成员函数的地址,而不是执行这个函数”。这样,编译器就能正确地生成获取成员函数地址的代码,而不是尝试调用该函数。

(temp.*fp)(); // 调用成员函数

.*:这是一个特殊的成员访问运算符,用于通过对象实例和成员函数指针来调用成员函数。当你有一个指向成员函数的指针,并且想要在某个特定的对象上调用这个函数时,就需要使用这个运算符。 在temp对象上,通过成员函数指针fp来调用它所指向的成员函数

三、运算符重载的正常使用

<code>class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

//private:

int _year;

int _month;

int _day;

};

// 重载成全局, 无法访问私有成员, 怎么解决?

// 1.提供这些成员get和set

// 2.友元

// 3.重载成成员函数(一般重载成这种)

//

// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?

// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数

bool operator==(const Date& d1, const Date& d2)

{

return d1._year == d2._year

&& d1._month == d2._month

&& d1._day == d2._day;

}

// d1 - d2

// d1 + d2 无意义

// d1 * d2 无意义

// 一个类要重载哪些运算符是看需求, 看重载有没有价值和意义

int main()

{

Date d3(2024, 4, 14);

Date d4(2024, 4, 15);

// 显示调用(可以正常使用)

operator==(d3, d4);

// 直接写,转换调用,编译器会转换成operator==(d3, d4)

d3 == d4;

return 0;

}

四、重载成成员函数

<code>class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

d3.Func(d4);

//bool Func(const Date& d)

//{

//return this->_year == d._year

//&& this->_month == d._month

//&& this->_day == d._day;

//}

// d3.operator==(d4);

bool operator==(const Date& d)

{

cout << "类中";

return _year == d._year

&& _month == d._month

&& _day == d._day;

// 隐藏了this指针

/*return this->_year == d._year

&& this->_month == d._month

&& this->_day == d._day;*/

}

//private:

int _year;

int _month;

int _day;

};

// 如果全局和类中都有运算符重载函数,编译器会选择调用类里的

bool operator==(const Date& d1, const Date& d2)

{

cout << "全局";

return d1._year == d2._year

&& d1._month == d2._month

&& d1._day == d2._day;

}

int main()

{

Date d3(2024, 4, 14);

Date d4(2024, 4, 15);

// 显式调用

d3.operator==(d4);

// 转换调用 等价于d3.operator==(d4);

d3 == d4;

return 0;

}

通过d3.operator==(d4)显式调用了类内的operator==函数。因为这里是直接通过对象d3来调用的,所以肯定是类内的版本被调用。d3 == d4这种简洁的写法在C++中会被自动转换为对operator==的调用。当有多个版本的operator==可用时(如本例中的类内和全局版本),C++会根据一定的规则(如作用域和参数匹配)来选择调用哪一个。在这个例子中,由于类内的版本是成员函数,且其参数与全局版本相同,所以编译器会优先选择类内的版本。 

五、赋值运算符重载

1.赋值运算符重载格式

参数类型:const T&,传递引用可以提高传参效率返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回*this :要复合连续赋值的含义

<code>class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

Date(const Date& d)

{

_year = d._year;

_month = d._month;

_day = d._day;

}

Date& operator=(const Date& d)

{

// 自己给自己赋值

if (this != &d)

{

_year = d._year;

_month = d._month;

_day = d._day;

}

// 需要返回值的原因:支持连续赋值

return *this;

}

private:

int _year;

int _month;

int _day;

};

int main()

{

Date d1(2024, 4, 14);

// 拷贝构造

// 一个已经存在的对象,拷贝给另一个要创建初始化的对象

Date d2(d1);

Date d3 = d1;

Date d4(2024, 5, 1);

// 赋值拷贝/赋值重载

// 一个已经存在的对象,拷贝赋值给另一个已经存在的对象

d1 = d4;

d1 = d2 = d4;

return 0;

}

 Date& operator=(const Date& d):这个函数重载了赋值运算符(=),允许我们使用=来将一个Date对象的值赋给另一个已经存在的Date对象。函数中首先检查自赋值的情况(即确保赋值操作的左右两边不是同一个对象),然后复制右边的对象的年、月和日到左边的对象,并返回左边对象的引用,以支持连续赋值操作。*this是对象本身,对象在main的作用域里创建,因此出main作用域才析构销毁。而出函数作用域不会销毁,所以此处才能return *this 

传值返回和引用返回

传值返回,返回的是对象的拷贝

引用返回,返回的是对象的别名

<code>class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

Date(const Date& d)

{

cout << "Date(const Date& d)" << endl;

_year = d._year;

_month = d._month;

_day = d._day;

}

void Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

~Date()

{

cout << "~Date()" << endl;

_year = -1;

_month = -1;

_day = -1;

}

private:

int _year;

int _month;

int _day;

};

Date(const Date& d):这是一个拷贝构造函数它接受一个Date对象的引用,并创建一个新的Date对象,其内容与传入的对象相同

有没有办法不生成拷贝?

使用引用返回

<code>Date func()

{

Date d(2024, 4, 14);

return d;

}

int main()

{

const Date& ref = func();

ref.Print();

return 0;

}

 在main函数中,首先通过调用func函数获取了一个对Date对象的常量引用ref。由于func返回的是一个临时对象,这个对象在表达式结束后就会被销毁。但是,由于ref是对这个临时对象的引用,所以这个临时对象的生命周期会被延长,直到ref的生命周期结束。这是C++11引入的引用折叠和生命周期延长规则的结果。

<code>Date& func()

{

Date d(2024, 4, 14);

//cout << &d << endl;

return d;

}

int fx()

{

int a = 1;

int b = 2;

int c = 3;

return a + b + c;

}

int main()

{

//Date& ref = func();

const Date& ref = func();

cout << &ref << endl;

fx();

return 0;

}

在main函数中,通过const Date& ref = func();获取了func函数返回的引用,并将其存储在常量引用ref中。由于func返回的是对局部变量的引用,这里的ref实际上引用了一个已经不存在的对象,因此这是不安全的 

<code>Date& func()

{

static Date d(2024, 4, 14);

return d;

}

// 出了作用域,返回对象还在没有析构,那就可以用引用返回,减少拷贝

// a、返回对象生命周期到了,会析构,传值返回

// b、返回对象生命周期没到,不会析构,传引用返回

int main()

{

const Date& ref = func();

//ref.Print();

return 0;

}

func函数返回一个对静态局部变量d的引用,该变量在函数第一次被调用时被初始化,并在程序的整个生命周期内持续存在。由于d是静态的,它不会在func函数返回后被销毁,因此可以安全地返回它的引用。 

在main函数中,调用了func函数并将返回的引用赋值给const Date& ref。由于返回的是引用,因此没有发生任何拷贝操作,这是效率更高的做法。

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

<code>class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

int _year;

int _month;

int _day;

};

// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数

Date& operator=(Date& left, const Date& right)

{

if (&left != &right)

{

left._year = right._year;

left._month = right._month;

left._day = right._day;

}

return left;

}

// 编译失败:

// error C2801: “operator =”必须是非静态成员

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

<code>class Time

{

public:

Time()

{

_hour = 1;

_minute = 1;

_second = 1;

}

Time& operator=(const Time& t)

{

if (this != &t)

{

_hour = t._hour;

_minute = t._minute;

_second = t._second;

}

return *this;

}

private:

int _hour;

int _minute;

int _second;

};

class Date

{

private:

// 基本类型(内置类型)

int _year = 1970;

int _month = 1;

int _day = 1;

// 自定义类型

Time _t;

};

int main()

{

Date d1;

Date d2;

d1 = d2;

return 0;

}

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实

现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。

typedef int DataType;

class Stack

{

public:

Stack(size_t capacity = 10)

{

_array = (DataType*)malloc(capacity * sizeof(DataType));

if (nullptr == _array)

{

perror("malloc申请空间失败");

return;

}

_size = 0;

_capacity = capacity;

}

void Push(const DataType& data)

{

// CheckCapacity();

_array[_size] = data;

_size++;

}

~Stack()

{

if (_array)

{

free(_array);

_array = nullptr;

_capacity = 0;

_size = 0;

}

}

private:

DataType* _array;

size_t _size;

size_t _capacity;

};

int main()

{

Stack s1;

s1.Push(1);

s1.Push(2);

s1.Push(3);

s1.Push(4);

Stack s2;

s2 = s1;

return 0;

}

注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

六、前置++和后置++重载 

<code>class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

// 前置++:返回+1之后的结果

// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率

Date& operator++()

{

_day += 1;

return *this;

}

// 后置++:

// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载

// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递

// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1

//       而temp是临时对象,因此只能以值的方式返回,不能返回引用

Date operator++(int)

{

Date temp(*this);

_day += 1;

return temp;

}

private:

int _year;

int _month;

int _day;

};

void test02()

{

Date d1(2024, 4, 14);

Date d2 = ++d1;

d1.Print();

d2.Print();

Date d3 = d1++;

d1.Print();

d3.Print();

/*d1.operator++(1);

d1.operator++(100);

d1.operator++(0);

d1.Print();*/

}

int main()

{

test02();

return 0;

}

今天就先到这了!!!

看到这里了还不给博主扣个:

⛳️ 点赞☀️收藏 ⭐️ 关注!

你们的点赞就是博主更新最大的动力!

有问题可以评论或者私信呢秒回哦。



声明

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