本篇的目的是给C++的类和对象(中)进行补充,主要介绍剩下的默认成员函数。
赋值运算符重载
运算符重载
在了解赋值运算符重载之前,我们需要先了解一下运算符重载。C++为程序员提供了许多的运算符,我们才能够方便地处理基本类型的数据。对于自定义类型来说,我们无法直接使用这些运算符,不过在C++中,通过运算符重载可以让自定义类型直接使用运算符。
在C语言中,我们如果想要对自定义类型的数据进行处理,就需要定义对应的函数。在C++中,为了增强代码的可读性以及让程序设计更有效率,引入了运算符重载,运算符重载是具有特殊函数名的函数。例如,当我们定义了两个日期类的对象后,我们想比较两个对象哪个在前,我们可以编写一个成员函数,如下:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool less(const Date& x)
{
if (_year < x._year)
return true;
else if (_year == x._year && _month < x._month)
return true;
else if (_year == x._year && _month == x._month && _day < x._day)
return true;
else
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 1, 19);
Date d2(2024);
cout << d2.less(d1) << endl; // d1 < d2
return 0;
}
定义一个函数虽然能够解决问题,不过如果可以直接使用运算符进行比较大小,那便捷程度不是函数能比的。在C++中,我们可以通过运算符重载使得比较两个自定义类型和比较两个基本类型一样方便,即实现 d1 < d2 。
运算符重载是具有特殊函数名的函数,函数名为:关键字 operator 后接需要重载的运算符符号,例如:
bool Date::operator<(const Date& d)
{
if (this->_year < d._year)
return true;
else if (this->_year == d._year && this->_month < d._month)
return true;
else if (this->_year == d._year && this->_month == d._month && this->_day < d._day)
return true;
else
return false;
}
重载后,我们进行比较时可以直接使用<,也可以通过函数名进行调用,这两个是等价的。
int main()
{
Date d1(2025, 1, 19);
Date d2(2024);
cout << d2.less(d1) << endl; // d1 < d2
d2 < d1;
d2.operator(d1);
return 0;
}
只要懂得如何命名,我们几乎可以对所有的运算符进行重载,使运算符能够满足我们自定以类型的使用。不过,对于运算符重载,也有需要注意的地方:
首先,不能创建新的操作符。 比如我们想创建一个@运算,然后进行运算符重载operator@,这是不被允许的。
其次,对于重载的操作符,必须有一个自定义类型的参数。换句话说,我们不能更改内置类型的操作符的含义。即形如 int operator+(int x, double y) 这种是不被允许的。
接着,对于运算符重载而言,函数有 n 个参数,代表重载的运算符就有 n 个操作数,就是 n目操作符。而作为类成员函数重载时,因为形参有隐含的 this 指针,所以形参个数看起来少了一个。同时,重载后的运算符会按照操作数的顺序进行传参,即 d1 < d2 中,this 指针指向 d1,d 指针指向 d2。而且,对于类成员函数来说,第一个参数始终是隐含的 this 指针。
最后,有5个操作符我们是不能重载的,这需要记住
.* :: sizeof ?: .
赋值运算符重载
对于赋值运算符,它的重载方法和其他运算符的重载方法一致,特殊的是赋值运算符是默认成员函数之一。
赋值运算符的重载格式:首先,参数类型需要是对象的引用,当然你也可以选择传递对象,不过为了提高效率,传递引用当然更香。其次返回值类型也是对象的引用,目的是在满足连续赋值的情况下提高返回效率,返回时可以返回 *this。此外,可以的话,最好检查是否对自己赋值,对于日期类而言,自己对自己赋值并没有什么大不了的,但是如果类中需要赋值的存在一块动态内存呢?那个时候如果自己对自己赋值,你需要创建一块空间,复制,然后销毁原空间,指向新空间,关键这一系列操作下来没有什么意义,反倒还浪费了空间。
由于是默认成员函数,所以当我们在类中不自己定义时,系统会自动生成一个。因为这个特性,所以无论形参是什么,当我们对赋值运算符重载时,必须定义成成员函数,否则,如果类中没有赋值运算符重载,类外有一个,在类中自动生成了这个函数后,调用时可能会产生调用歧义。不过,幸运的是,当在类外定义这个函数时编译器会报错。
那么,系统自动生成的赋值运算符重载是什么样的呢?和拷贝构造相似,对于基本类型,直接赋值,对于自定义类型,会调用该类型的赋值运算符重载。因此我们可以根据实际情况,决定我们是否要自己定义赋值运算符重载。不过,如果成员变量中存在 const 成员或者引用的话,无论我们不自己定义运算符重载函数,系统会删除自己生成的赋值运算符重载。即:
学习了赋值运算符重载后,我们有两种方法通过已有对象对对象赋值,一是拷贝构造函数,二是通过赋值运算符重载函数,那么C++程序中什么时候会调用拷贝构造函数,什么时候是赋值呢?更具体一点,下面的代码运行逻辑是什么呢?
int main()
{
Date d1(2025, 1, 19);
Date d2 = d1; //调用什么函数?
Date d3(2025, 1, 19);
Date d4(2024);
d3 = d4; //这又调用了什么?
return 0;
}
形式上虽然是将 d1 赋值给 d2,但实际上,这行代码直接调用拷贝构造函数创建了新对象,因此,当两个已存在的对象间进行赋值,程序会调用赋值运算符重载函数;而当我们使用一个已存在的对象创建新的对象或者初始化新的对象时,程序会调用拷贝构造函数创建新对象。
前置++和后置++
前置++和后置++的区别便是返回值不同,对于这两个操作符,我们又该如何进行重载呢?C++标准规定了,对于默认的 operator++(),实现的是前置++,而要实现后置,需要添加一个参数 int ,来做标记即 operator++(int)。因为我默认在类中实现这两个函数,所以参数个数少了一个。
不过由于前置++返回加法之后的值,所以我们可以令前置++的返回值是对象的引用,而后置++返回加法之前的值,这代表我们必须创建一个临时变量来存储之前的状态,而返回值也必须是一个对象。所以就效率而言,前置++明显是更快的,因此在使用自定义类型的++时,如果没有特殊需求,最好使用前置++。样例如下:
//前置++重载
Date& Date::operator++()
{
*this += 1;
return *this;
}
//后置++重载
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
//前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
//后置--
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
const 成员
首先我们来看个例子:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
bool less(const Date& x)
{
if (_year < x._year)
return true;
else if (_year == x._year && _month < x._month)
return true;
else if (_year == x._year && _month == x._month && _day < x._day)
return true;
else
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 1, 19);
const Date d2(2024);
d1.print();
d2.print();
return 0;
}
编译后发现编译器为我们报了个错误:
报错的原因是在函数调用的过程中将this指针的权限放大了,既然如此将函数中的参数的权限缩小就行了,但是我们在形参中并不能显式地声明 this 形参,那该怎么做呢?C++为我们找了一个地方,即在函数后加上const:
void print() const
{
cout << _year << '-' << _month << '-' << _day << endl;
}
这样子,函数的 this 形参就是 const Date* 了, 在函数之后加上 const,修饰的并不是函数的返回值,修饰的是函数隐含的 this 指针所指象的对象,即 *this 不能改变。那么修饰后普通对象(非 const )可以调用吗?答案是肯定的,因为普通对象的 this 指针是 Date* this ,对于指针来说,赋值时权限可以缩小或者平移,就是不能放大。
说到权限这块,只有引用和指针才会涉及到权限,因为通过引用和指针可以改变变量的值。我们可以看下面的示例:
const int a = 10;
int b = a;
const int a = 10;
int& b = a;
const int a = 10;
int* ptr = a;
其中,第一个仅仅是对变量 b 的赋值,无论 b 怎么改变,a 都不受影响。 但是对于引用和指针来说,可以通过他俩令 a 改变,因此第一个可以编译通过,而二三就不行了,因为权限放大了。
取地址运算符重载
最后的两个默认成员函数就是取地址运算符重载了,它们是:
Date* operator&()
{
cout << "Date* operator&()" << endl;
return this;
}
const Date* operator&() const
{
cout << "const Date* operator&() const" << endl;
return this;
}
一个是为普通对象取地址,另一个是对 const 对象取地址。对于这两个函数,一般来说系统自动生成的函数就够用了,除非我们有什么特殊需求,需要自己定义这两个函数,例如不想让人取得对象的地址,否则可以不用管。 因为我们平常就只是取地址罢了。
到此,类和对象的中篇就结束了,我们主要了解了六个默认成员函数,对于这六个函数,还有更多的细节等着我们去发掘,不过并不是在博客里,而是在我们使用C++的过程中。现在,我们该准备去了解更多关于类和对象的知识了。