🎈归属专栏:浅尝C++
🚗个人主页:Jammingpro
🐟记录一句:在Linux与C++中来回横跳,哪个学累了,就去学另外一个~~
文章前言:本篇文章简要介绍C++的运算符重载,同时接着上一篇文章,继续介绍类的余下3个成员函数,即赋值运算符重载、普通对象取地址运算符重载、const对象取地址运算符重载。
文章目录
- 赋值运算符重载
- 运算符重载
- 赋值运算符重载
- 前置后置++及--重载
- 普通对象及const对象取地址运算符重载
我们知道,如果一个类什么都没有,那么它就是一个空类。空类真的什么都没有吗?答案是否定的。任何类在什么都不写时,编译器会自动生成下图的6个默认成员函数,上一篇文章已经介绍了构造函数、析构函数及拷贝构造函数,这一篇文章将介绍余下的3个默认成员函数。👇
ps:默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
注意:
①不能通过连接其他符号来创建新的操作符:比如operator@ (必须是C++中已经存在的运算符,其他符号不能重载)
②重载操作符必须有一个类类型参数 (即调用重载运算符的变量,其中一个必须是类类型)
③用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
④作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this(第一个参数是类对象自身,不需要用户显示传入,编译器会自动传入该对象的this指针)。
⑤.*
、::
、sizeof
、?:
、.
注意以上5个运算符不能重载。
下面我们实现一个复数类,并在类内重载==
运算符👇
#include <iostream>
using namespace std;
class Complex
{
public:
Complex(double r, double i)
{
_real = r;
_imag = i;
}
bool operator==(const Complex& obj)
{
return _real == obj._real && _imag == obj._imag;
}
private:
double _real;
double _imag;
};
int main()
{
Complex c1(1, 2);
Complex c2(1, 2);
if(c1 == c2)
cout << "they are equal" << endl;
else
cout << "too bad" << endl;
return 0;
}
可不可以不在类内实现呢?当然可以,我们可以写成下方所示的全局函数👇
ps:下面代码涉及到友元的概念,如果还不知道友元是什么,可以将复数的属性设为公有的(public),并将带friend关键字的行删除,这样操作代码仍可以正常运行。
#include <iostream>
class Complex
{
public:
Complex(double r, double i)
{
_real = r;
_imag = i;
}
private:
friend bool operator==(const Complex& c1, const Complex& c2);
double _real;
double _imag;
};
bool operator==(const Complex& c1, const Complex& c2)
{
return c1._real == c2._real && c1._imag == c2._imag;
}
int main()
{
Complex c1(1, 2);
Complex c2(1, 2);
if(c1 == c2)
cout << "they are equal" << endl;
else
cout << "too bad" << endl;
return 0;
}
赋值运算符重载
赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要符合连续赋值的含义
下面的复数类显示给出了赋值运算符重载,在下面代码注释中,将给出上面重载格式的详细描述👇
#include <iostream>
using namespace std;
class Complex
{
public:
//复数类的构造函数
Complex(double r, double i)
{
_real =r;
_imag = i;
}
//复数类的拷贝构造函数
Complex(const Complex& c)
{
_real = c._real;
_imag = c._imag;
}
//赋值运算符重载
//返回参数使用引用是为了提高效率,避免调用拷贝函数构造临时对象。这里也可以使用非引用返回
//参数使用const是为了避免赋值过程中被修改,引用传递是为了提高效率
Complex& operator=(const Complex& c)
{
//防止自己给自己赋值
if(this != &c)
{
_real = c._real;
_imag = c._imag;
return *this;
//返回*this,即返回当前赋值运算符左值对象自身
}
};
private:
double _real;
double _imag;
}
赋值运算符只能重载成类的成员函数不能重载成全局函数
下面这段代码将赋值运算符重载为全局函数,下面代码在编译时将报错。
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
#include <iostream>
using namespace std;
class Complex
{
public:
Complex(double r, double i)
{
_real = r;
_imag = i;
}
Complex(const Complex& c)
{
_real = c._real;
_imag = c._imag;
}
private:
friend Complex& operator=(const Complex& c1, const Complex& c2);
double _real;
double _imag;
};
Complex& operator=(const Complex& c1, const Complex& c2)
{
if (&c1 != &c2)
{
c1._real = c2._real;
c1._imag = c2._imag;
return c1;
}
}
int main()
{
Complex c1(1, 2);
Complex c2 = c1;
}
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝(即浅拷贝)。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像复数类这样的类是没必要的。跟拷贝构造函数一样,如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。像下面的Stack
类就需要自己实现重载赋值运算符。
class Stack
{
public:
Stack(int n)
{
m_mem = new char[n];
m_size = 0;
m_capacity = n;
}
Stack& operator=(const Stack& s)
{
m_mem = new char[s.m_capacity];
strcpy(m_mem, s.m_mem);
m_size = s.m_size;
m_capacity = s.m_capacity;
return *this;
}
private:
char* m_mem;
int m_size;
int m_capacity;
};
下面图片演示的是编译器自动生成的重载赋值函数,其为值拷贝(即浅拷贝)。导致两个Stack对象指向同一内存空间,当两个Stack对象生命周期结束后,将分别释放该内存空间,一个内存空间被重复释放,将导致错误。
前置后置++及–重载
既然上面将运算符重载讲解的差不多了,这里就顺带叙述以下前置后置++和–。前置和后置的区别在于:前置运算符重载无需参数,后置运算符重载需要一个占位参数。具体讲解内容将在下面的代码注释中给出👇
class Counter
{
public:
Counter(int n)
{
storage = n;
}
//前置++运算符重载,参数列表无参数
Counter& operator++()
{
storage++;
return *this;
}
//后置++运算符,参数列表需要一个占位参数
//后置运算符返回的是++之前的值,因此要先保存一份原来的值,而保存下来的值存储在临时变量中,因此不能返回引用
Counter operator++(int)
{
Counter copy = *this;
++storage;
return copy;
}
//前后置--与上面同理
private:
int storage;
}
从上面代码可以知道,后置运算符的效率低于前置运算符,因为后置运算符需要创建临时对象保存当前对象+1或-1前的状态。同时,由于保存对象原状态的变量为临时变量,函数执行结束后生命周期结束,因此只能使用值返回,不能使用引用返回。创建值返回的临时变量需要再调用1次宝贝构造函数。后置运算符共调用了2次拷贝构造函数创建2个临时对象,因此效率较低。
普通对象及const对象取地址运算符重载
这一部分的代码实现与上述运算符重载类似,这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载。直接上代码👇
class Complex
{
public:
Complex* operator&()
{
return this;
}
const Complex* operator&() const//->这里函数后面的const相当于给this指针加上const属性,防止this指针指向的对象内容被修改
{
return this;
}
private:
double _real;
double _imag;
}
上面出现了const关键字,我们这通过一些问题对const关键字做一些介绍:
①const对象可以调用非const成员函数吗?
const对象是不可以修改的,非const成员函数可以修改对象中的成员,显然const对象不可以调用非const成员函数。
②非const对象可以调用const成员函数吗?
非const对象的成员数值可以修改,既然可以修改,那么就可以调用const和非const的成员函数了。
③const成员函数内可以调用其它的非const成员函数吗?
const成员函数是为了不修改对象本身,而非const成员函数可以修改对象成员,因此,const成员函数内不可以调用其它的非const成员函数
④非const成员函数内可以调用其它的const成员函数吗?
非const成员函数内可以修改对象成员,因此它可以调其他任何的const和非const成员函数。
文章结语:这篇文章对C++中的运算符重载,同时接着上一篇文章,继续介绍类的余下3个成员函数,即赋值运算符重载、普通对象取地址运算符重载、const对象取地址运算符重载。
🎈欢迎进入浅尝C++专栏,查看更多文章。
如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d