文章目录
运算符重载
定义:运算符重载:就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
定义重载的运算符就像定义函数,只是该函数的名字是operator@,这个@符号就代表着被重载的运算符。其中函数的参数取决于两个因素:运算符是一个参数的还是两个参数
如果是全局函数那么一元就是一个参数,两元就是两个参数。
如果是成员函数,一元的成员函数不需要参数,两元的成员函数需要一个参数。
(例如下面的+法运算符)
加号运算符的重载(+)
例如:有个person类,类中有两个成员变量:a,b。然后创建的对象Person1中,a,b的值分别是1,1;创建的对象Person2中,a,b的值分别是2,2。那么想重新定义一种加法:创建新的对象Person3使得a,b的值等于另外两个对象的变量的和。即Person3 = Person1+Person2.即(3,3)。
成员函数重载
class Person
{
public:
Person(int a,int b):m_a(a),m_b(b){}
int m_a;
int m_b;
public:
Person personAddPerson(Person& p)
{
Person temp(0, 0);
temp.m_a = this->m_a + p.m_a;
temp.m_b = this->m_b + p.m_b;
return temp;
}
Person operator+(Person& p)//系统给的名字
{
Person temp(0, 0);
temp.m_a = this->m_a + p.m_a;
temp.m_b = this->m_b + p.m_b;
return temp;
}
};
int main()
{
Person p1(10, 10);
Person p2(10, 10);
Person p3 = p1.personAddPerson(p2);
cout << p3.m_a << endl;//20
Person p4 = p1 + p2;
cout << p4.m_a << endl;//20
return 0;
}
personAddPerson这个函数很好理解,就是创建了一个成员函数,然后分别让对象p1和p2的两个成员变量相加。然后调用的时候如同p3的调用结果。 而
operator+
这个成员函数是编译器帮忙规定的,也可以使用**本质:Person p3 = p1.operator+(p2)**来调用,但是编译器就可以帮忙简化为: Person p3 = p1+p2;
全局函数重载
(这个时候需要两个参数)
Person operator+(Person& p1, Person& p2)
{
Person temp(0,0);
temp.m_a = p1.m_a + p2.m_a;
temp.m_b = p1.m_b + p2.m_b;
return temp;
}
在写完了这个全局函数以后,如果想要调用这个函数。原先:本质:Person p3 = operator+(p1,p2);现在简化为:Person p3 = p1+p2;
这就是运算符重载
运算符的重载也是可以发生函数的重载的。
比如:将上面的operator+的全局函数的参数改成一个是Person类型,一个是int类型,那么也是可以调用成功的,至于怎么用就看自己的操作代码。
左移运算符重载(<<)
创建的对象是不能直接输出的。
class Person
{
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
int m_A;
int m_B;
};
int main()
{
Person p1(10, 20);
//cout << p1 ;
return 0;
}
如果直接想将p1这个对象打印,这种代码运行是肯定会报错的。
试图利用成员函数做“<<”符号的重载。
//原先加法的是“+”: a+b 也就相当于 a.operator+(b)
//现在是“<<”:p.operator<<(cout) 相当于 p<<cout 这样写与平常的cout<<p是相反的,所以要实现<<符号的重载还是使用全局函数比较好。
全局函数重载
那么就需要两个参数了,一个就是cout,一个就是对象p1。
cout的数据类型是输出流对象,是用输出流类创建出来的对象。
class Person
{
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
int m_A;
int m_B;
};
void operator<<(ostream& cout,Person& p1)
{
cout << "m_A = " << p1.m_A <<" m_B=" << p1.m_B << endl;
}
int main()
{
Person p1(10, 20);
cout << p1;
return 0;
}
//输出的结果是:m_A = 10 m_B=20
使用全局函数就可以控制参数的前后位置。比如上代码中,必须保证cout在<<的前面,p1在<<的后面,那么在设置全局函数的时候也要保证cout参数在前面,p1参数在后面。
但是这样发现那就没有办法:cout<<p1<<endl;
如果想写这样的链式编程的话,就会报错,必须保证cout<<p1的返回结果是ostream类型的cout对象。(把本体返回)
如果将cout对象返回了以后,一切就回归正常,那么程序也正常的执行换行,与cout<<endl;一样。
ostream operator<<(ostream& cout,Person& p1)
{
cout << "m_A = " << p1.m_A <<" m_B=" << p1.m_B << endl;
}
如果将成员变量权限改成私有,那么就使用友元(将全局函数作为友元,friend后面直接加函数就可)
class Person
{
friend ostream& operator<<(ostream& cout, Person& p1);
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
private:
int m_A;
int m_B;
};
ostream& operator<<(ostream& cout,Person& p1)//注意参数和返回值都是引用
{
cout << "m_A = " << p1.m_A << " m_B=" << p1.m_B << endl;
return cout;
}
int main()
{
Person p1(10, 20);
cout << p1 << endl;
cout << endl;
return 0;
}
递增运算符重载(++)
前置++和后置++的符号都是++,那么怎么做区分呢?
前置:void operator(){}
后置:void operator(int){}
后置的函数时有int(占位参数)的。
重载代码的一步步实现:
class MyInter//自己创建的int类
{
public:
MyInter()
{
m_num = 0;
}
private:
int m_num;
};
void test01()
{
MyInter myInt;
//cout << myInt << endl;
}
首先创建一个自己的类。然后发现cout<<myInt<<endl是会报错的,因为编译器不认识MyInter类,所以需要对<<进行重载(与上相同)
既然要打印MyInter类中的私有成员变量,那么就需要将这个重载函数设置为友元。
class MyInter//自己创建的int类
{
friend ostream& operator<<(ostream& cout, MyInter& myInt);//设置友元,使函数可以访问私有权限的成员变量
public:
MyInter()
{
m_num = 0;
}
private:
int m_num;
};
ostream& operator<<(ostream& cout,MyInter& myInt)
{
cout << myInt.m_num << endl;
return cout;
}
void test01()
{
MyInter myInt;
cout << myInt << endl;
}
int main()
{
test01();
return 0;
}
前置++
这个时候在test01中如果要求实现myInt的前置++操作,那么直接++myInt编译器是不认识的,所以需要对前置++进行重载。使用成员函数的方法进行重载。
class MyInter//自己创建的int类
{
friend ostream& operator<<(ostream& cout, MyInter& myInt);//设置友元,使函数可以访问私有权限的成员变量
public:
MyInter()
{
m_num = 0;
}
void operator++()
{
this->m_num++;
}
private:
int m_num;
};
这是时候在输出++myInt,输出的结果就是1.
但是cout << ++myInt << endl
这个时候就报错的了,因为系统不认识。所以经过++运算后的结果(返回值)应该是它本身,毕竟下一次++也是在自己身上进行++的。所以返回自己就是return *this;然后函数的返回值也应该改成自身的类型:MyInter &。
class MyInter//自己创建的int类
{
friend ostream& operator<<(ostream& cout, MyInter& myInt);//设置友元,使函数可以访问私有权限的成员变量
public:
MyInter()
{
m_num = 0;
}
MyInter& operator++()
{
this->m_num++;
return *this;
}
private:
int m_num;
};
这样进行的
cout<<++(++myInt)<<endl;
cout<<myInt<<endl;
两个结果都是2./但是,如果将上面的重载函数的返回值改成数值而不是引用了呢?MyInter& —>MyInter:
输出的结果分别是2,1 .很容易想啊,如果改成数值了以后,在第一次自增完了以后传过去的不再是myInt,而是值1,所以输出的值虽然是2,但是myInt的值是1.
后置++
需要先记录初始状态,
class MyInter//自己创建的int类
{
friend ostream& operator<<(ostream& cout, MyInter& myInt);//设置友元,使函数可以访问私有权限的成员变量
public:
MyInter()
{
m_num = 0;
}
//前置++重载
MyInter& operator++()
{
this->m_num++;
return *this;
}
//后置++重载
MyInter operator++(int)
{
MyInter temp = *this;//先记录初始状态
this->m_num++;
return temp;
}
//不能返回以引用(MyInter&)的形式返回,因为这个引用temp是临时的,只在函数中创建的,出了函数就释放了。
//所以后置++的返回值和前置++的返回值是不一样的,后置++返回的是一个值。
private:
int m_num;
};
所以后置++返回的是值,所以后置++不能进行链式的运算。(myInt++)++这样的写法是错误的。第一次后置++的返回值已经是值了,不是引用了。
总结代码:(有点小错待解决)
class MyInter//自己创建的int类
{
friend ostream& operator<<(ostream& cout, MyInter& myInt);//设置友元,使函数可以访问私有权限的成员变量
public:
MyInter()
{
m_num = 0;
}
//前置++重载
MyInter& operator++()
{
this->m_num++;
return *this;
}
//后置++重载
MyInter operator++(int)
{
MyInter temp = *this;//先记录初始状态
this->m_num++;
return temp;
}
//不能返回以引用(MyInter&)的形式返回,因为这个引用temp是临时的,只在函数中创建的,出了函数就释放了。
//所以后置++的返回值和前置++的返回值是不一样的,后置++返回的是一个值。
private:
int m_num;
};
ostream& operator<<(ostream& cout,MyInter& myInt)
{
cout << myInt.m_num < endl;
return cout;
}
void test01()
{
MyInter myInt;
++myInt;
cout << ++myInt << endl;
}
void test02()
{
MyInter myInt;
myInt++;
//cout << myInt++;
//这里会报错,显示需要MyInter类型的数据,但是我提供的是个值。但后置++返回的本来就是值啊。
}
int main()
{
test02();
return 0;
}
补充:前置的效率要高于后置,因为前置没有创建新的对象,后置用值的方式返回,需要创建新的对象(拷贝构造)。 将现在对象的所有的信息全部复制一遍,然后传给新的对象,然后最后再返回这个对象的值。其实本质上的效率差不了太多,如果数据量大的话,可以优先使用前置++。
指针运算符重载
class Person
{
public:
Person(int age)
{
cout << "Person的有参构造调用" << endl;
this->m_age = age;
}
void showAge()
{
cout << "年龄为: " << this->m_age << endl;
}
~Person()
{
cout << "Person的析构调用" << endl;
}
int m_age;
};
int main()
{
//Person p(100);//将对象开辟到栈上了,这个时候对象是由编译器free的。
Person* p = new Person(18);//将对象开辟到堆上(返回的是一个指针),这个时候是由自己free的。
p->showAge();//与 (*p).showAge() 是相同的。
delete(p);//只有加上这一句才会调用析构
return 0;
}
见上面的经典代码,发现如果在堆上创建对象的话是需要自己将创建的对象释放掉的,所以为了防止自己忘了将对象释放掉,那么可以设计一种智能指针,管理new出来的对象的释放。(这种只能指针也是一种类)
class Person
{
public:
Person(int age)
{
cout << "Person的有参构造调用" << endl;
this->m_age = age;
}
void showAge()
{
cout << "年龄为: " << this->m_age << endl;
}
~Person()
{
cout << "Person的析构调用" << endl;
}
int m_age;
};
class SmartPoint
{
public:
SmartPoint(Person* person)
{
this->m_person = person;
}
~SmartPoint()
{
if (this->m_person!=NULL)
{
delete this->m_person;
this->m_person = NULL;
}
}
private:
Person* m_person;
};
int main()
{
SmartPoint sp(new Person(18));//这里的sp对象是创建在栈上的,所以是可以自动的执行自己的析构代码,
//在析构代码中进行对象中成员m_person的判断,并将这个成员给释放掉。所以有了这个智能指针类以后,不需要自己将person对象释放了,
//只要将智能指针对象创建出来,就可以自动得调用它的析构(将person对象释放)。
return 0;
}
但是如果想进行操作sp->showAge();这种写法是不可以的,因为sp的有关类中没有showAge函数,showAge()函数是它的成员函数的相关类中的,而且如果想调用->需要保证调用对象是个指针,而这个sp是一个spartpoint类型的的对象。如果想强制性的使得操作写法成立,那么就需要重新定义->.(需要在类中做重载)
正常来说,应该是指针才能调用->,所以重载的函数的返回值就应该是个指针, 而且还应该是Person* 的指针,这样直接将自己维护的指针返回就可以了,m_person的类型就是Person*。
在智能指针类中添加构造:
class SmartPoint
{
public:
SmartPoint(Person* person)
{
this->m_person = person;
}
~SmartPoint()
{
if (this->m_person!=NULL)
{
delete this->m_person;
this->m_person = NULL;
}
}
Person* operator->()
{
return m_person;
}
private:
Person* m_person;
};
这样就可以直接调用:
int main()
{
SmartPoint sp(new Person(18));
sp->showAge();
return 0;
}
如果想让这个对象看作是一个指针,还应该可以进行解引用:
(*sp).showAge()也应该是可以的,所以还应该对 *进行重载。
class SmartPoint
{
public:
SmartPoint(Person* person)
{
this->m_person = person;
}
~SmartPoint()
{
if (this->m_person!=NULL)
{
delete this->m_person;
this->m_person = NULL;
}
}
Person* operator->()//重载->运算符
{
return m_person;
}
Person& operator*()//重载*运算符,返回指针的自身
{
return *(m_person);//对这个指针进行解引用返回
}
private:
Person* m_person;
};
所有代码:
class Person
{
public:
Person(int age)
{
cout << "Person的有参构造调用" << endl;
this->m_age = age;
}
void showAge()
{
cout << "年龄为: " << this->m_age << endl;
}
~Person()
{
cout << "Person的析构调用" << endl;
}
int m_age;
};
class SmartPoint
{
public:
SmartPoint(Person* person)
{
this->m_person = person;
}
~SmartPoint()
{
if (this->m_person!=NULL)
{
delete this->m_person;
this->m_person = NULL;
}
}
Person* operator->()//重载->运算符
{
return m_person;
}
Person& operator*()//重载*运算符,返回指针的自身
{
return *(m_person);//对这个指针进行解引用返回
}
private:
Person* m_person;
};
int main()
{
SmartPoint sp(new Person(18));
sp->showAge();
(*sp).showAge();
return 0;
}
总结:就是原先有个person类,然后在类中写了个展示年龄的函数,然后创建了个有参构造。然后创建了个智能指针的类,成员函数是一个person*的变量。因为创建智能指针对象的时候是在栈上创建的,所以可以直接将对象delete掉,同时在析构函数中还可以加一句,顺便将创建的person类的对象也同时释放掉,这就解决了容易忘记的情况。这个时候又想能不能使用智能指针的对象来调用person中的函数?智能指针对象中有个指针就是person的指针,但是又不能直接sp->showAge(),因为这个对象sp不是person *类型的,所以可以重载->,使得只要是smartpoint的对象使用->,就可以返回自己所一直维护的指针m_person.
第一个功能体现出的是它的智能功能,第二个功能体现出的是它的指针功能。这个类不但可以帮助任务free对象,而且还可以当作指针使用。
注意一点:将对象转换成指针的时候:sp->showAge()实际上应该是sp->->showAge(),sp->返回的是person*的m_age,还应该进行一步的 ->,但是这里编译器进行优化了,两个->这样反而是错误的。
(实际用途不多)
赋值运算符重载(=)
class Person
{
public :
int m_Age;
};
int main()
{
Person p1;
p1.m_Age = 10;
//Person p2(p1);//这样的写法是拷贝构造函数。
//Person p2 = p1;//这样的写法也是拷贝构造函数。
Person p2;
p2 = p1;//如果这样写,那么就不会拷贝构造函数了,而是会进行赋值操作
cout << p2.m_Age << endl;//结果是10
return 0;
}
从上面的函数看出来:对象和对象能够直接赋值?赋值运算符为什么能区分对象呢?
因为:之前说过编译器默认给一个类至少添加3个函数:默认构造,析构 ,拷贝构造(值拷贝),但其实是4个,还有一个是**operator=**值拷贝),这个本身也是一个函数。
class Person
{
public :
Person(const char* name, int age)
{
this->m_Name = new char[strlen(name) + 1];//在堆上开辟了空间,需要释放。
strcpy(this->m_Name, name);
this->m_Age = age;
}
int m_Age;
char* m_Name;
~Person()//在Person的析构中将m_name的空间释放。(在释放Person类的对象的时候顺便将m_name也释放)
{
if (this->m_Name != NULL)
{
delete [] this->m_Name;
this-> m_Name = NULL;
}
}
};
int main()
{
Person p1("tom", 10);
Person p2("jerry", 20);
p2 = p1;//如果这样写,那么就不会拷贝构造函数了,而是会进行赋值操作
cout << p2.m_Age << endl;//结果是10
return 0;
}
ps补充:先打断一下,写代码的时候发现不能直接将“tom“当作单数传到char* name,必须在前面加const。以后写有参构造的时候要注意一下。
首先,这样写肯定是会报错的。因为p2的信息已经和p1的是一样了,浅拷贝问题(当程序结束自动调用析构函数的时候会删除两个相同的字符串指针)堆区的内容重复的释放。
这个时候可以选择重载一下=操作符。在之前有个类似的情况,其他代码一样,在创建p2的时候是Person p2(p1),当时的问题也是浅拷贝问题,原因是调用的是编译器的拷贝构造函数所以造成了浅拷贝问题,解决方法就是自己重新写个拷贝构造函数Person(const Person&p),进行深拷贝(直接拷贝对象)。我们现在这里用的是赋值操作,但是也是会出现浅拷贝问题。
接下来重载一下+运算符就ok:
在p2 = p1中,p1就是传进去的参数。
class Person
{
public :
Person(const char* name, int age)
{
this->m_Name = new char[strlen(name) + 1];//在堆上开辟了空间,需要释放。
strcpy(this->m_Name, name);
this->m_Age = age;
}
int m_Age;
char* m_Name;
~Person()//在Person的析构中将m_name的空间释放。(在释放Person类的对象的时候顺便将m_name也释放)
{
if (this->m_Name != NULL)
{
delete [] this->m_Name;
this-> m_Name = NULL;
}
}
void operator=(const Person& p)//对=重载,加const是为了防止不小心将传进来的对象修改
{
//先判断原来的堆区是否有内容,如果有内容先释放。后来测试的时候发现不释放也是可以的。
if (this->m_Name != NULL)
{
delete[]this->m_Name;
this->m_Name = NULL;
}
//进行深拷贝工作:
this->m_Name = new char[strlen(p.m_Name) + 1];//在堆上创建空间
strcpy(this->m_Name , p.m_Name);
this->m_Age = p.m_Age;
}
};
int main()
{
Person p1("tom", 10);
Person p2("jerry", 20);
p2 = p1;//如果这样写,那么就不会拷贝构造函数了,而是会进行赋值操作
cout << p2.m_Age << endl;//结果是10
return 0;
}
如果进行:
int a = 10;
int b = 20;
int c = b = a;
cout << a << b << c<< endl;
//最后的结果都是10
因为将a 的值赋给了b,又将b 的值赋给了c;所以最后的结果都是10.
但是如果三个数据都是对象的话运行就会报错。(p3 = p2 =p1)
因为先执行p2 = p1,返回的结果是void类型的(见上面自己的重载的代码),将一个void数据赋给p3肯定是会出错的。
改进:p2 在调用完 = 号以后的返回值应该还是p2才可以(这样等会p3执行=的时候返回值才可以是p3 的数据类型),所以应该返回本体类型而不是void。
Person& operator=(const Person& p)//对=重载,加const是为了防止不小心将传进来的对象修改
{
//先判断原来的堆区是否有内容,如果有内容先释放。
if (this->m_Name != NULL)
{
delete[]this->m_Name;
this->m_Name = NULL;
}
//进行深拷贝工作:
this->m_Name = new char[strlen(p.m_Name) + 1];//在堆上创建空间
strcpy(this->m_Name , p.m_Name);
this->m_Age = p.m_Age;
return *this;
}
这是为了严谨才这么做的。
这里的=运算符重载是因为有属性指向堆区了,在释放的时候导致堆区的内容进行重释放了,所以才要对=运算符进行重载。但是如果创建的属性都是栈上的,那么也不需要堆=运算符进行重载。
这个时候如果写:Person p4 = p3,那么在写的时候编译器还是会崩的,因为这里的不是赋值运算,而是调用的系统的拷贝构造运算,而刚才仅仅是将=运算符进行重载,没有处理拷贝构造函数,导致如果调用拷贝构造函数函数会出现浅拷贝的问题。
//这里就不需要像上面那样对堆里之前有没有东西进行判断了,因为拷贝构造都是从无到有的,不需要判断,直接深拷贝。
Person(const Person&p)
{
this->m_Name = new char[strlen(p.m_Name) + 1];//在堆上创建空间
strcpy(this->m_Name , p.m_Name);
this->m_Age = p.m_Age;
}
关系运算符的重载(==)
class Person
{
public:
Person(string name ,int age)
{
this->m_Age = age;
this->m_Name = name;
}
string m_Name;
int m_Age;
};
int main()
{
Person p1("TOM", 18);
Person p2("TOM", 18);
if (p1 == p2)
{
cout << "一样大" << endl;
}
return 0;
}
这样写是肯定会报错的,因为两个对象之间不能这样直接比较。
所以要对==进行重载,由于 ==是双目运算符,所以需要传进来一个参数p2
class Person
{
public:
Person(string name ,int age)
{
this->m_Age = age;
this->m_Name = name;
}
bool operator==(Person& p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return true;
}
else
return false;
}
string m_Name;
int m_Age;
};
int main()
{
Person p1("TOM", 18);
Person p2("TOM", 18);
if (p1 == p2)
{
cout << "一样大" << endl;
}
return 0;
}
这样输出的结果就是:”一样大“
!=运算符也是同样的道理:
class Person
{
public:
Person(string name ,int age)
{
this->m_Age = age;
this->m_Name = name;
}
bool operator==(Person& p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return true;
}
else
return false;
}
bool operator!=(Person& p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return false;
}
else
return true;
}
string m_Name;
int m_Age;
};
int main()
{
Person p1("TOM", 18);
Person p2("TOM", 17);
if (p1 != p2)
{
cout << "不一样大" << endl;
}
return 0;
}
这样输出的结果就是”不一样大“
这个关系运算符的实用性比较强
函数调用运算符的重载(小括号)
就是重载小括号
class MyPrint
{
public :
void operator()(string text)
{
cout << text << endl;
}
};
class MyAdd
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
void test01()
{
MyPrint myprint;
myprint("hello world");//仿函数
//这里的myprint不是一个函数名,而是一个对象。通常叫这个对象为 函数对象。
//这种仿函数是十分灵活的:
MyAdd myadd;
cout << myadd(3, 4) << endl;
//仿函数可以结合匿名对象使用
//匿名对象:类+(),这个对象在执行完这一句之后就释放了,很适合与仿函数结合。
//这样就不用再自己创建对象了,起名字还费劲
cout << MyAdd()(1, 1) << endl;
}
int main()
{
test01();
return 0;
}
所以,仿函数就是对()进行重载。
不要重载 &&,||
具有短路特性:首先会计算左边的值,如果左边的值完全能够决定结果,那么就不需要计算右边的值了。
这种短路特性是没法模拟的。
总结
1,<< 和 >>只能通过全局函数配合友元进行重载(因为cout在左边,正常的参数都应该在右边)
2,= , [ ] , ( ) 和 -> 运算符都只能通过成员函数进行重载。
3,不要重载&&和||操作符,因为无法实现短路规则。
中括号[ ]的重载略。