运算符重载
C++为了增强代码的可读性引入了运算符重载,对已有的运算符重新进行定义,赋予其另一种功能,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
运算符重载的定义
函数名字:operator后面接需要重载的运算符符号。
函数原型:Type operator操作符 (参数列表)。
注意
■ 1、不能通过连接其他符号来创建新的操作符:比如operator@。
■ 2、重载操作符必须有一个类类型,没有类型那么根本没有意义。
■ 3、用于内置类型的操作符,其含义不能改变,例如:内置的整数+,不能改变其含义。
■ 4、作为类成员的运算符重载函数时,其形参看起来比操作数数目少1个,其中成员函数的操作符有一个默认的形参this,限定为第一个形参。对于全局的运算符重载函数的参数和操作数目的个数相等。
■ 5、.*、::、sizeof、?:、. 这5种运算符不能重载。
重载输入输出运算符
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year, int month, int day): _year(year),_month(month), _day(day){}
private:
int _year;
int _month;
int _day;
};
//返回引用可以实现链式编程,ostream和istream用非常量,在输入或输出时要改变状态,用引用是因为我们无法直接复制一个ostream和istream对象。
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout<<d._year<<"-"<<d._month<<"-"<<d._day;
return _cout;
}
返回引用可以实现链式编程,Date声明为非常量因为我们本来的目的就是将数据读入到这个对象当中。
istream& operator>>(istream& _cin, Date& d)
{
_cin>>d._year;
_cin>>d._month;
_cin>>d._day;
return _cin;
}
//输入运算符必须处理输入可能失败的情况,如果输入失败应该将参数恢复成原始的状态。
int main()
{
Date d;
cin>>d;
cout<<d<<endl;
return 0;
}
输入输出运算符必须是非成员函数
与iostream 标准库兼容的输入输出运算符必须是普通的非成员函数,而不是类的成员函数。否则,它们的左侧运算对象将是我们的类的一个对象:
Date date;
date<<cout; //如果operator<<是Date的成员
我们无法给标准库中的类添加任何成员。IO运算符通常需要读写类的非共有数据成员,所以IO运算符一般被声明为友元。
重载赋值运算符
赋值符常常初学者的混淆。这是毫无疑问的,因为’=’在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。当对象没有初始化而赋值时会引起拷贝构造函数的调用,当已经初始化了而赋值时会调用赋值运算重载函数的调用。
赋值运算符主要有以下几点
■ 1、返回值(返回*this)。
■ 2、检测是否自己给自己赋值。
■ 3、一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝(浅拷贝)。当有指针时,调用析构函数会引发内存问题。
代码实例1:
class Person {
public:
Person(string name, int age) :_name(name),_age(age){}
//当准备给两个相同对象赋值时,检查是否自己给自己赋值
Person& operator=(const Person& person) {
if (this != &person) {
_name = person._name;
_age = person._age;
}
return *this;
}
private:
string _name;
int _age;
};
void test() {
Person person1("小明", 20);
Person person2 = person1; //调用拷贝构造
//如果一个对象还没有被创建,则必须初始化,也就是调用构造函数
//上述例子由于person2还没有初始化,所以会调用构造函数
//由于person2是从已有的person1来创建的,所以只有一个选择
//就是调用拷贝构造函数
person2 = person1; //调用operator=函数
//由于person2已经创建,不需要再调用构造函数,这时候调用的是重载的赋值运算符
}
代码实例2:
//一个类默认创建 默认构造、析构、拷贝构造 operator=赋值运算符 进行简单的值传递(浅拷贝)
class Person {
public:
Person() = default;//合成默认构造函数
Person(const char *name,int age ) {
this->_name = new char[strlen(name)+1];
strcpy(this->_name,name);
_age = age;
}
//重载赋值运算符
Person& operator=(const Person& person) {
//注意:由于当前对象已经创建完毕,那么就有可能pName指向堆内存
//这个时候如果直接赋值,会导致内存没有及时释放,所以要先将堆内存释放
if (this->_name != NULL) {
delete[] this->_name;
this->_name=NULL;
}
//重新申请空间,进行深拷贝
this->_name = new char[strlen(person._name)+1];
strcpy(this->_name,person._name);
this->_age = person._age;
return *this;
}
//调用析构函数
~Person() {
if (this->_name != NULL) {
delete[] this->_name;
this->_name = NULL;
}
}
private:
char* _name;
int _age;
};
有关深浅拷贝问题参考我的另一篇博客:C++入门–构造函数、拷贝构造函数、析构函数
重载自增自减(++/–)运算符
普通的重载形式无法同时定义前置和后置运算符。前置和后置版本使用的是同一个符号,意味着其重载版本所用的名字将是相同的,并且运算对象的数量和类型也相同。后置版本接受一个额外的int类型的形参(operator++(int))。当我们使用后置运算符时,编译器为这个形式提供一个值为0的参数。
注意:
为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。而后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个临时值而非引用。
实例代码:
class Integer {
friend ostream& operator<<(ostream& cout, Integer& it);
public:
Integer() {
num = 0;
}
//前置++
Integer& operator++() {
++this->num;
return *this;
}
//后置++
Integer operator++(int) {
Integer temp = *this;
++this->num;
return temp;
}
//前置--
Integer& operator--() {
--this->num;
return *this;
}
//后置--
Integer& operator--(int) {
Integer temp = *this;
--this->num;
return temp;
}
private:
int num;
};
ostream& operator<<(ostream& cout,Integer &it) {
cout << it.num;
return cout;
}
结论:调用代码时候,要优先选择前缀形式,除非确实需要后缀形式返回的原值,前缀和后缀形式语义上是等价的,前缀形式少创建了一个临时对象,所以效率经常会略高一些。
重载下标运算符:[]
表示容器的类通常可以通过元素的容器中的位置访问元素,这些类一般会定义下标运算符 operator [ ] 。下标运算符必须是成员函数,并且下标运算符返回所访问元素的引用,这样做为了能够作为左值修改元素的值。
实例代码:
class StrVec{
public:
std::string& operator[](std::size_t n){
return elements[n];
}
const std::string& operator[](std::size_t n) const {
return elements[n];
}//当定义常量对象时,只能调用这个函数
private:
std::string *elements;
};
如果一个类包含下标运算符,则它通常会定义两个版本,一个返回普通引用,另一个是类的常量成员并且返回常量引用。当我们对常量对象取下标时,不能为其赋值。
重载成员访问(->、*)运算符[智能指针]
如果new出来的对象,就要让程序员自己去释放,但是实际开发中常常会忘记释放对象引起内存泄漏。因此我们可以用智能指针来托管这个对象,所以对象的释放就不用程序员操心了,但是有了智能指针想要和指针一样就要重载-> 和 * 运算符。
代码实例:
class Person {
public:
Person(int age) {
m_Age = age;
}
void showAge() {
cout << "年龄为:" << this->m_Age << endl;
}
~Person() {
cout << "Person析构函数调用" << endl;
}
private:
int m_Age;
};
//智能指针
//用来托管自定义类型的对象,让对象进行自动释放
class smartPointer {
public:
smartPointer(Person *person) {
this->person = person;
}
//重载->让智能指针对象Person* p 一样去使用
Person* operator->() {
return this->person;
}
Person& operator*() {
return *this->person;
}
~smartPointer() {
cout << "智能指针析构了" << endl;
if (this->person != NULL) {
delete this->person;
this->person = NULL;
}
}
private:
Person* person;
};
void test01() {
smartPointer sp(new Person(10));//sp开辟到了栈上,自动释放
sp->showAge(); // sp->->showAge(); 编译器优化了 写法
}
符号重载总结
■1、=、[]、()和 -> 操作符只能通过成员函数进行重载。
■2、<<和>>只能通过全局函数配合友元函数进行重载。
■3、不要重载||和&&因为无法实现短路规则。
常规建议
运算符 | 建议使用 |
---|---|
所有的一元运算符 | 成员 |
=、[]、()、->、* | 必须是成员 |
+=、-=、/=、*=、^=、&=、!=、%= | 成员 |
其他二元运算符 | 非成员 |