重载运算符
问题引入
int a=10
int b=20
int c=a+b
对于内置数据类型编译器知道如何运算(+)
然后对于自定义类型
People p1+People p2 编译器是处理不了的,因为People类内部没有定义 操作符 +,
在People类内定义成员函数
#include <iostream>
#include<string>
using namespace std;
class People {
public:
string name;
int age;
People(string n, int a) :name(n), age(a) {};
People& plus(const People& p) {
this->name = p.name + this->name;
this->age = p.age + this->age;
return *this;
}
};
void test() {
People p1("a",19);
People p2("b", 11);
People p3=p1.plus(p2);
cout<<p3.name<<endl;
cout << p3.age << endl;
}
int main() {
test();
return EXIT_SUCCESS;
}
结果
ba
30
People p1.plus(p2)
People p1.operator+(p2)
把成员函数名字从plus换成operator+,然后调用的时候可以直用 p1+p2,operator省略了
operator=运算符的重载
#include<iostream>
#include<string>
using namespace::std;
class MyStr {
private:
string name;
int id;
public:
MyStr() {
cout << "默认构造" << endl;
}
MyStr(int id, string name) {
cout << "有参构造" << endl;
this->id = id;
this->name =name;
}
MyStr(const MyStr& str) {
cout << "拷贝构造" << endl;
this->id = str.id;
this->name = str.name;
}
MyStr& operator=(const MyStr& str) {
cout << "operator=" << endl;
if (this != &str) {
this->id = str.id;
this->name = str.name;
}
return *this;
}
~MyStr() {
cout << "析构函数" << endl;
}
};
int main() {
MyStr str1(1,"zxy");
cout << "~~~~~~~~~~~~~~~~~~~~" << endl;
MyStr str2;
str2 = str1;
cout << "~~~~~~~~~~~~~~~~" << endl;
MyStr str3 = str2;
return 0;
}
结果:
MyStr str1(1,"zxy");
MyStr str2;
//赋值 重载=运算符
str2 = str1;
//初始化 调用拷贝构造
MyStr str3 = str2;
参数方面
MyStr& operator=(const MyStr& str) {
cout << "operator=" << endl;
if (this != &str) {
this->id = str.id;
this->name = str.name;
}
return *this;
}
传入const 修饰的引用
1 const 修饰形参使函数不但能接受非const 实参,也能接受const 实参,传入的形参由于被const修饰,所以不能被通过形参名修改
2 引用传递,少调用一次拷贝构造
3一般我们不希望传入的实参被修改
返回值
一般是被赋值对象的引用 return *this; (MyStr&)
优点
1 返回时少调用一次拷贝构造,提高效率
2 可以实现连续的赋值
a=b=c
如果返回的类型是值,不是引用,(a=b)=c那么在执行 a=b时 执行的是=运算符重载,return 后会产生一个匿名对象,这是一个右值,返回这个匿名对象(临时值),执行a=b后,得到一个临时右值,再执行=c,就会出错,右值不能当做左值用。引用既可以当左值,也可以当右值
调用的时机
1 为一个类对象A赋值,str1=str2;用已经存在的str2初始化str1 (str2=str1)
str2调用=操作符重载
2用其他类型(如内置类型)的值为其赋值,(隐式类型转化,前提类A提供其他类型B到类型A的构造函数 A(B))
当为一个类对象赋值(注意:可以用本类对象为其赋值(如上面例1),也可以用其它类型(如内置类型)的值为其赋值,关于这一点,见后面的例2)时,会由该对象调用该类的赋值运算符重载函数。
MyStr str2;
(声明加初始化,调用无参构造函数)
str1 = str2;(用str2给str1赋值)
MyStr str3 = str2;
用str2初始化str3.调用拷贝构造
提供默认赋值运算符重载函数的时机
当程序没有显示提供一个用本类或者本类的引用为参数的赋值运算符重载函数时,编译器会自动提供一个赋值运算符重载。
。注意我们的限定条件,不是说只要程序中有了显式的赋值运算符重载函数,编译器就一定不再提供默认的版本,而是说只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。可见,所谓默认,就是“以本类或本类的引用为参数”的意思。
#include<iostream>
#include<string>
using namespace::std;
class A {
private: int count;
public: A() {
cout << "默认构造" << endl;
}
A(int c) :count(c) {
cout << "有参构造" << endl;
}
//参数是int类型的赋值运算符重载
A& operator=(int count) {
cout << "参数是int类型的赋值运算符重载" << endl;
this->count = count;
return *this;
}
// 拷贝构造
A(const A& a) {
this->count = a.count;
cout << "拷贝构造" << endl;
}
~A() {
cout << this << "析构函数" << endl;
}
};
int main() {
A a1(1);
A a2, a3;
//a2=a1 依然调用编译器默认提供的赋值运算符重载
a2 = a1;
a3 = 1;
return 0;
}
虽然我们提供了一个参数是int类型的赋值运算符重载,但是a2=a1,编译器依然提供了默认赋值运算符重载
以类本身或本类引用为参数的赋值运算符重载,才是编译器默认的赋值运算符重载函数
当以上代码注释掉 参数类型为int的赋值运算符重载后,a3=1,调用有参的构造函数
结论:1 当程序没有显示提供一个以类本身或本身引用为参数的赋值运算符重载时,编译器会默认提供一个
2 当用一个非类A的值对一个类A进行赋值时,如果同时存在匹配的赋值运算符重载和构造函数优先调用赋值运算符重载(如上面的参数是int类型的运算符重载),如果没有退而求其次,调用匹配的构造函数进行赋值操作。
3 A a3=1; 必须要有int=》A类的类型转换的构造函数,有参数为int的赋值运算符重载也没用
#include<iostream>
#include<string>
using namespace::std;
class A {
private: int count=0;
public: A() {
cout << "默认构造" << endl;
}
A& operator=(const int a) {
this->count = a;
cout << "=运算符重载" << endl;
return *this;
}
~A() {
cout << this << "析构函数" << endl;
}
};
int main() {
//错误代码
A a3 = 1;
return 0;
}
显示提供赋值运算符重载的时机
1 用非类A的值为类A对象进行赋值时(当然要是存在相应的构造函数也可不提供)
2用类A的值为类A的对象进行赋值时,且类A的成员变量含有指针,需要开辟内存,为了避免浅拷贝,所以必须显示的重载赋值运算符
深拷贝vs浅拷贝
赋值运算符重载和拷贝构造函数都会涉及到这个问题,只要类A的成员变量里面有指针,内置类型(如int)不会有问题
浅拷贝就是系统默认提供的拷贝构造函数或者赋值运算符重载只是简单的把类对象a的数据成员的地址给了b的数据成员,b的数据成员指向了a的数据成员的地址,没有开辟新的空间(b.name=a.name) MyStr b=a;
带来的问题
1 修改a.name时,b.name也会随之变化
2 对 对象a,b析构时,会对同一个内存释放两次,导致程序崩溃
解决方法
1 重写拷贝构造或者赋值运算符重载
2使用std::shared_ptr可以完美解决问题
深拷贝例子
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person {
public:
Person() {}
Person(char* name, int age) {
cout << "调用有参构造" << endl;
m_name = (char*)malloc(strlen(name) + 1);
strcpy(m_name, name);
m_age = age;
}
Person(const Person& a) {
cout<<"调用拷贝构造"<<endl;
m_age = a.m_age;
m_name = (char*)malloc(strlen(a.m_name) + 1);
}
~Person() {
cout << "析构函数调用" << endl;
if (m_name != NULL) {
free(m_name);
m_name = NULL;
}
}
char* m_name = NULL;
int m_age = 0;
};
void test01() {
Person p1((char*)"张三", 18);
Person p2(p1);
}
int main()
{
test01();
system("pause");
return EXIT_SUCCESS;
}
调用有参构造
调用拷贝构造
析构函数调用
析构函数调用
请按任意键继续.
失败代码
#include <iostream>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象
}
system("pause");
return 0;
}
Student
~Student 780776
~Student 780776
不然会造成两个name指针指向同一个内存对象,然后函数调用结束,系统会对同一块内存进行两次析构,导致非法内
赋值运算符重载函数只能是类的非静态成员函数
不能是静态成员函数,也不可能是友元函数
有人说赋值运算符重载往往需要返回*this,然而友元函数和静态成员函数没有this指针,但是可以这么写
static friend MyStr& operator=(const MyStr str1,const MyStr str2)
{
……
return str1;
}
不能是静态成员函数是因为,静态成员函数只能操作静态成员,不能操作非静态成员,这显然是不行的
不能是友元函数的原因,假设类没有显示提供以类本身或者类本身引用为参数的赋值运算符操作函数,但是我们假设c++允许我们提供一个友元函数作为赋值运算符重载函数,但是友元函数不属于类,所以类内也会默认提供一个赋值运算符重载函数。当执行str2=str1时
编译器不知道调用哪个,会产生二义性。
所以c++强制规定赋值运算符重载函数必须是非静态非友元成员函数,所以c++就可以提供默认版本,避免二义性
赋值运算符重载函数不能被继承
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
int X;
A() {}
A& operator =(const int x)
{
X = x;
return *this;
}
};
class B :public A
{
public:
B(void) :A() {}
};
int main()
{
A a;
B b;
a = 45;
//b = 67;
(A)b = 67;
return 0;
}
b=67,不能通过编译,说明基类的=operator函数没有被继承
因为派生类往往会添加自己的数据成员和成员函数,如果允许派生类继承基类的赋值运算符重载函数,而且派生类没有提供,派生类就会调用基类的赋值运算符重载函数,但是基类的这个函数不能处理派生类后添加的数据成员和成员函数,就会有问题。
所以,C++规定,赋值运算符重载函数不能被继承。 上面代码中, (A)b = 67; 一句可以编译通过,原因是我们将B类对象b强制转换成了A类对象。
赋值运算符重载要避免自赋值
如何判断赋值者和被赋值者是不同的对象
通过比较他们的地址
if(this!=&str){
..........
}
原因1:
为了效率,自己给自己赋值没有意义,所有先判断if(this!=&str),如false直接return *this
原因2:
如果类的数据成员含有指针,那么更会带来问题,假设p=_p,p所指向的通常是new出来的对象,我们赋值前通常要delete掉p的空间,在重新为p开辟空间,然后进行赋值。如果赋值前没有释放掉p的空间,会导致非法内存,如果是自赋值,那么p和_p是同一指针,在赋值操作前对p的delete操作,将导致p所指的数据同时被销毁。那么重新赋值时,拿什么来赋?
所以,对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this。
运算符重载的一些例子
左移运算符重载
cout<<xxxxx<<endl;
这种形式来讲全局函数更适合左移运算符的重载
#include<iostream>
#include<string>
using namespace::std;
//cout<<A;
class A {
friend ostream& operator<<(ostream& c, A a);
private:int count;
public:
// 默认构造函数
A() {
cout << "默认构造函数" <<this <<endl;
}
//有参构造函数
A(int c):count(c) {
cout << "有参构造函数" << this << endl;
}
//拷贝构造函数
A(const A& a) {
this->count = a.count;
cout << "拷贝构造函数" << this << endl;
}
//赋值运算符重载
A& operator=(const A& a) {
cout<<"赋值运算符重载"<<this << endl;
this->count = a.count;
return *this;
}
~A() {
cout << "A的析构函数" <<this<< endl;
}
};
//全局函数左移操作符重载、
ostream& operator<<(ostream& c, A a) {
c << a.count;
return c;
}
void test() {
A a(1);
cout <<a << endl;
}
int main() {
test();
system("pause");
return 0;
}
前置++,后置++运算符的重载
#include<iostream>
#include<string>
using namespace::std;
//cout<<A;
class A {
friend ostream& operator<<(ostream& c, A a);
friend void test();
private: int count=0;
public:
// 默认构造函数
A() {
cout << "默认构造函数" <<this <<endl;
}
//有参构造函数
A(int c):count(c) {
cout << "有参构造函数" << this << endl;
}
//拷贝构造函数
A(const A& a) {
this->count = a.count;
cout << "拷贝构造函数" << this << endl;
}
//赋值运算符重载
A& operator=(const A& a) {
cout<<"赋值运算符重载"<<this << endl;
this->count = a.count;
return *this;
}
~A() {
cout << " A的析构函数" <<this<< endl;
}
//前置++
A& operator++() {
this->count++;
return *this;
}
//后置++
A operator++(int) {
A tmp = *this;
++*this;
return tmp;
}
};
//全局函数左移操作符重载、
ostream& operator<<(ostream& c,A a) {
c << a.count;
return c;
}
void test() {
A a;
cout<<a<< endl;
cout << "前置++" << " " << ++a << endl;
cout << "后置++" << " " << a++ << endl;
}
int main() {
test();
system("pause");
return 0;
}